~gary/python-openid/python-openid-2.2.1-patched

« back to all changes in this revision

Viewing changes to openid/consumer/discover.py

  • Committer: Launchpad Patch Queue Manager
  • Date: 2007-11-30 02:46:28 UTC
  • mfrom: (1.1.1 pyopenid-2.0)
  • Revision ID: launchpad@pqm.canonical.com-20071130024628-qktwsew3383iawmq
[rs=SteveA] upgrade to python-openid-2.0.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# -*- test-case-name: openid.test.test_discover -*-
2
 
 
3
 
from urljr import fetchers
4
 
 
5
 
from openid import oidutil
6
 
 
7
 
# If the Yadis library is available, use it. Otherwise, only use
8
 
# old-style discovery.
9
 
try:
10
 
    import yadis
11
 
except ImportError:
12
 
    yadis_available = False
13
 
 
14
 
    oidutil.log('Consumer operating without Yadis support '
15
 
                '(failed to import Yadis library)')
16
 
 
17
 
    class DiscoveryFailure(RuntimeError):
18
 
        """A failure to discover an OpenID server.
19
 
 
20
 
        When the C{yadis} package is available, this is
21
 
        C{yadis.discover.DiscoveryFailure}."""
22
 
else:
23
 
    yadis_available = True
24
 
    from yadis.etxrd import nsTag, XRDSError
25
 
    from yadis.services import applyFilter as extractServices
26
 
    from yadis.discover import discover as yadisDiscover
27
 
    from yadis.discover import DiscoveryFailure
28
 
    from yadis import xrires, filters
29
 
 
30
 
from openid.consumer.parse import openIDDiscover as parseOpenIDLinkRel
31
 
from openid.consumer.parse import ParseError
 
2
"""Functions to discover OpenID endpoints from identifiers.
 
3
"""
 
4
 
 
5
__all__ = [
 
6
    'DiscoveryFailure',
 
7
    'OPENID_1_0_NS',
 
8
    'OPENID_1_0_TYPE',
 
9
    'OPENID_1_1_TYPE',
 
10
    'OPENID_2_0_TYPE',
 
11
    'OPENID_IDP_2_0_TYPE',
 
12
    'OpenIDServiceEndpoint',
 
13
    'discover',
 
14
    ]
 
15
 
 
16
import urlparse
 
17
 
 
18
from openid import oidutil, fetchers, urinorm
 
19
 
 
20
from openid import yadis
 
21
from openid.yadis.etxrd import nsTag, XRDSError, XRD_NS_2_0
 
22
from openid.yadis.services import applyFilter as extractServices
 
23
from openid.yadis.discover import discover as yadisDiscover
 
24
from openid.yadis.discover import DiscoveryFailure
 
25
from openid.yadis import xrires, filters
 
26
from openid.yadis import xri
 
27
 
 
28
from openid.consumer import html_parse
32
29
 
33
30
OPENID_1_0_NS = 'http://openid.net/xmlns/1.0'
34
 
OPENID_1_2_TYPE = 'http://openid.net/signon/1.2'
 
31
OPENID_IDP_2_0_TYPE = 'http://specs.openid.net/auth/2.0/server'
 
32
OPENID_2_0_TYPE = 'http://specs.openid.net/auth/2.0/signon'
35
33
OPENID_1_1_TYPE = 'http://openid.net/signon/1.1'
36
34
OPENID_1_0_TYPE = 'http://openid.net/signon/1.0'
37
35
 
 
36
from openid.message import OPENID1_NS as OPENID_1_0_MESSAGE_NS
 
37
from openid.message import OPENID2_NS as OPENID_2_0_MESSAGE_NS
 
38
 
38
39
class OpenIDServiceEndpoint(object):
39
40
    """Object representing an OpenID service endpoint.
40
41
 
41
42
    @ivar identity_url: the verified identifier.
42
43
    @ivar canonicalID: For XRI, the persistent identifier.
43
44
    """
 
45
 
 
46
    # OpenID service type URIs, listed in order of preference.  The
 
47
    # ordering of this list affects yadis and XRI service discovery.
44
48
    openid_type_uris = [
45
 
        OPENID_1_2_TYPE,
 
49
        OPENID_IDP_2_0_TYPE,
 
50
 
 
51
        OPENID_2_0_TYPE,
46
52
        OPENID_1_1_TYPE,
47
53
        OPENID_1_0_TYPE,
48
54
        ]
49
55
 
50
56
    def __init__(self):
51
 
        self.identity_url = None
 
57
        self.claimed_id = None
52
58
        self.server_url = None
53
59
        self.type_uris = []
54
 
        self.delegate = None
 
60
        self.local_id = None
55
61
        self.canonicalID = None
56
62
        self.used_yadis = False # whether this came from an XRDS
57
63
 
58
64
    def usesExtension(self, extension_uri):
59
65
        return extension_uri in self.type_uris
60
66
 
 
67
    def preferredNamespace(self):
 
68
        if (OPENID_IDP_2_0_TYPE in self.type_uris or
 
69
            OPENID_2_0_TYPE in self.type_uris):
 
70
            return OPENID_2_0_MESSAGE_NS
 
71
        else:
 
72
            return OPENID_1_0_MESSAGE_NS
 
73
 
 
74
    def supportsType(self, type_uri):
 
75
        """Does this endpoint support this type?
 
76
 
 
77
        I consider C{/server} endpoints to implicitly support C{/signon}.
 
78
        """
 
79
        return (
 
80
            (type_uri in self.type_uris) or 
 
81
            (type_uri == OPENID_2_0_TYPE and self.isOPIdentifier())
 
82
            )
 
83
 
 
84
    def compatibilityMode(self):
 
85
        return self.preferredNamespace() != OPENID_2_0_MESSAGE_NS
 
86
 
 
87
    def isOPIdentifier(self):
 
88
        return OPENID_IDP_2_0_TYPE in self.type_uris
 
89
 
61
90
    def parseService(self, yadis_url, uri, type_uris, service_element):
62
91
        """Set the state of this object based on the contents of the
63
92
        service element."""
64
93
        self.type_uris = type_uris
65
 
        self.identity_url = yadis_url
66
94
        self.server_url = uri
67
 
        self.delegate = findDelegate(service_element)
68
95
        self.used_yadis = True
69
96
 
70
 
    def getServerID(self):
 
97
        if not self.isOPIdentifier():
 
98
            # XXX: This has crappy implications for Service elements
 
99
            # that contain both 'server' and 'signon' Types.  But
 
100
            # that's a pathological configuration anyway, so I don't
 
101
            # think I care.
 
102
            self.local_id = findOPLocalIdentifier(service_element,
 
103
                                                  self.type_uris)
 
104
            self.claimed_id = yadis_url
 
105
 
 
106
    def getLocalID(self):
71
107
        """Return the identifier that should be sent as the
72
 
        openid.identity_url parameter to the server."""
73
 
        if self.delegate is None:
74
 
            return self.canonicalID or self.identity_url
 
108
        openid.identity parameter to the server."""
 
109
        # I looked at this conditional and thought "ah-hah! there's the bug!"
 
110
        # but Python actually makes that one big expression somehow, i.e.
 
111
        # "x is x is x" is not the same thing as "(x is x) is x".
 
112
        # That's pretty weird, dude.  -- kmt, 1/07
 
113
        if (self.local_id is self.canonicalID is None):
 
114
            return self.claimed_id
75
115
        else:
76
 
            return self.delegate
 
116
            return self.local_id or self.canonicalID
77
117
 
78
118
    def fromBasicServiceEndpoint(cls, endpoint):
79
119
        """Create a new instance of this class from the endpoint
102
142
        """Parse the given document as HTML looking for an OpenID <link
103
143
        rel=...>
104
144
 
105
 
        @raises: openid.consumer.parse.ParseError
106
 
        """
107
 
        delegate_url, server_url = parseOpenIDLinkRel(html)
 
145
        @rtype: [OpenIDServiceEndpoint]
 
146
        """
 
147
        discovery_types = [
 
148
            (OPENID_2_0_TYPE, 'openid2.provider', 'openid2.local_id'),
 
149
            (OPENID_1_1_TYPE, 'openid.server', 'openid.delegate'),
 
150
            ]
 
151
 
 
152
        link_attrs = html_parse.parseLinkAttrs(html)
 
153
        services = []
 
154
        for type_uri, op_endpoint_rel, local_id_rel in discovery_types:
 
155
            op_endpoint_url = html_parse.findFirstHref(
 
156
                link_attrs, op_endpoint_rel)
 
157
            if op_endpoint_url is None:
 
158
                continue
 
159
 
 
160
            service = cls()
 
161
            service.claimed_id = uri
 
162
            service.local_id = html_parse.findFirstHref(
 
163
                link_attrs, local_id_rel)
 
164
            service.server_url = op_endpoint_url
 
165
            service.type_uris = [type_uri]
 
166
 
 
167
            services.append(service)
 
168
 
 
169
        return services
 
170
 
 
171
    fromHTML = classmethod(fromHTML)
 
172
 
 
173
    def fromOPEndpointURL(cls, op_endpoint_url):
 
174
        """Construct an OP-Identifier OpenIDServiceEndpoint object for
 
175
        a given OP Endpoint URL
 
176
 
 
177
        @param op_endpoint_url: The URL of the endpoint
 
178
        @rtype: OpenIDServiceEndpoint
 
179
        """
108
180
        service = cls()
109
 
        service.identity_url = uri
110
 
        service.delegate = delegate_url
111
 
        service.server_url = server_url
112
 
        service.type_uris = [OPENID_1_0_TYPE]
 
181
        service.server_url = op_endpoint_url
 
182
        service.type_uris = [OPENID_IDP_2_0_TYPE]
113
183
        return service
114
184
 
115
 
    fromHTML = classmethod(fromHTML)
116
 
 
117
 
def findDelegate(service_element):
118
 
    """Extract a openid:Delegate value from a Yadis Service element
119
 
    represented as an ElementTree Element object. If no delegate is
120
 
    found, returns None."""
121
 
    # XXX: should this die if there is more than one delegate element?
122
 
    delegate_tag = nsTag(OPENID_1_0_NS, 'Delegate')
123
 
 
124
 
    delegates = service_element.findall(delegate_tag)
125
 
    for delegate_element in delegates:
126
 
        delegate = delegate_element.text
127
 
        break
128
 
    else:
129
 
        delegate = None
130
 
 
131
 
    return delegate
 
185
    fromOPEndpointURL = classmethod(fromOPEndpointURL)
 
186
 
 
187
 
 
188
    def __str__(self):
 
189
        return ("<%s.%s "
 
190
                "server_url=%r "
 
191
                "claimed_id=%r "
 
192
                "local_id=%r "
 
193
                "canonicalID=%r "
 
194
                "used_yadis=%s "
 
195
                ">"
 
196
                 % (self.__class__.__module__, self.__class__.__name__,
 
197
                    self.server_url,
 
198
                    self.claimed_id,
 
199
                    self.local_id,
 
200
                    self.canonicalID,
 
201
                    self.used_yadis))
 
202
 
 
203
 
 
204
 
 
205
def findOPLocalIdentifier(service_element, type_uris):
 
206
    """Find the OP-Local Identifier for this xrd:Service element.
 
207
 
 
208
    This considers openid:Delegate to be a synonym for xrd:LocalID if
 
209
    both OpenID 1.X and OpenID 2.0 types are present. If only OpenID
 
210
    1.X is present, it returns the value of openid:Delegate. If only
 
211
    OpenID 2.0 is present, it returns the value of xrd:LocalID. If
 
212
    there is more than one LocalID tag and the values are different,
 
213
    it raises a DiscoveryFailure. This is also triggered when the
 
214
    xrd:LocalID and openid:Delegate tags are different.
 
215
 
 
216
    @param service_element: The xrd:Service element
 
217
    @type service_element: ElementTree.Node
 
218
 
 
219
    @param type_uris: The xrd:Type values present in this service
 
220
        element. This function could extract them, but higher level
 
221
        code needs to do that anyway.
 
222
    @type type_uris: [str]
 
223
 
 
224
    @raises DiscoveryFailure: when discovery fails.
 
225
 
 
226
    @returns: The OP-Local Identifier for this service element, if one
 
227
        is present, or None otherwise.
 
228
    @rtype: str or unicode or NoneType
 
229
    """
 
230
    # XXX: Test this function on its own!
 
231
 
 
232
    # Build the list of tags that could contain the OP-Local Identifier
 
233
    local_id_tags = []
 
234
    if (OPENID_1_1_TYPE in type_uris or
 
235
        OPENID_1_0_TYPE in type_uris):
 
236
        local_id_tags.append(nsTag(OPENID_1_0_NS, 'Delegate'))
 
237
 
 
238
    if OPENID_2_0_TYPE in type_uris:
 
239
        local_id_tags.append(nsTag(XRD_NS_2_0, 'LocalID'))
 
240
 
 
241
    # Walk through all the matching tags and make sure that they all
 
242
    # have the same value
 
243
    local_id = None
 
244
    for local_id_tag in local_id_tags:
 
245
        for local_id_element in service_element.findall(local_id_tag):
 
246
            if local_id is None:
 
247
                local_id = local_id_element.text
 
248
            elif local_id != local_id_element.text:
 
249
                format = 'More than one %r tag found in one service element'
 
250
                message = format % (local_id_tag,)
 
251
                raise DiscoveryFailure(message, None)
 
252
 
 
253
    return local_id
 
254
 
 
255
def normalizeURL(url):
 
256
    """Normalize a URL, converting normalization failures to
 
257
    DiscoveryFailure"""
 
258
    try:
 
259
        return urinorm.urinorm(url)
 
260
    except ValueError, why:
 
261
        raise DiscoveryFailure('Normalizing identifier: %s' % (why[0],), None)
 
262
 
 
263
def arrangeByType(service_list, preferred_types):
 
264
    """Rearrange service_list in a new list so services are ordered by
 
265
    types listed in preferred_types.  Return the new list."""
 
266
 
 
267
    def enumerate(elts):
 
268
        """Return an iterable that pairs the index of an element with
 
269
        that element.
 
270
 
 
271
        For Python 2.2 compatibility"""
 
272
        return zip(range(len(elts)), elts)
 
273
 
 
274
    def bestMatchingService(service):
 
275
        """Return the index of the first matching type, or something
 
276
        higher if no type matches.
 
277
 
 
278
        This provides an ordering in which service elements that
 
279
        contain a type that comes earlier in the preferred types list
 
280
        come before service elements that come later. If a service
 
281
        element has more than one type, the most preferred one wins.
 
282
        """
 
283
        for i, t in enumerate(preferred_types):
 
284
            if preferred_types[i] in service.type_uris:
 
285
                return i
 
286
 
 
287
        return len(preferred_types)
 
288
 
 
289
    # Build a list with the service elements in tuples whose
 
290
    # comparison will prefer the one with the best matching service
 
291
    prio_services = [(bestMatchingService(s), orig_index, s)
 
292
                     for (orig_index, s) in enumerate(service_list)]
 
293
    prio_services.sort()
 
294
 
 
295
    # Now that the services are sorted by priority, remove the sort
 
296
    # keys from the list.
 
297
    for i in range(len(prio_services)):
 
298
        prio_services[i] = prio_services[i][2]
 
299
 
 
300
    return prio_services
 
301
 
 
302
def getOPOrUserServices(openid_services):
 
303
    """Extract OP Identifier services.  If none found, return the
 
304
    rest, sorted with most preferred first according to
 
305
    OpenIDServiceEndpoint.openid_type_uris.
 
306
 
 
307
    openid_services is a list of OpenIDServiceEndpoint objects.
 
308
 
 
309
    Returns a list of OpenIDServiceEndpoint objects."""
 
310
 
 
311
    op_services = arrangeByType(openid_services, [OPENID_IDP_2_0_TYPE])
 
312
 
 
313
    openid_services = arrangeByType(openid_services,
 
314
                                    OpenIDServiceEndpoint.openid_type_uris)
 
315
 
 
316
    return op_services or openid_services
132
317
 
133
318
def discoverYadis(uri):
134
319
    """Discover OpenID services for a URI. Tries Yadis and falls back
137
322
    @param uri: normalized identity URL
138
323
    @type uri: str
139
324
 
140
 
    @return: (identity_url, services)
 
325
    @return: (claimed_id, services)
141
326
    @rtype: (str, list(OpenIDServiceEndpoint))
142
327
 
143
 
    @raises: DiscoveryFailure
 
328
    @raises DiscoveryFailure: when discovery fails.
144
329
    """
145
330
    # Might raise a yadis.discover.DiscoveryFailure if no document
146
331
    # came back for that URI at all.  I don't think falling back
148
333
    # bother to catch it.
149
334
    response = yadisDiscover(uri)
150
335
 
151
 
    identity_url = response.normalized_uri
 
336
    yadis_url = response.normalized_uri
152
337
    try:
153
338
        openid_services = extractServices(
154
339
            response.normalized_uri, response.response_text,
170
355
 
171
356
        # Try to parse the response as HTML to get OpenID 1.0/1.1
172
357
        # <link rel="...">
173
 
        try:
174
 
            service = OpenIDServiceEndpoint.fromHTML(identity_url, body)
175
 
        except ParseError:
176
 
            pass # Parsing failed, so return an empty list
177
 
        else:
178
 
            openid_services = [service]
179
 
 
180
 
    return (identity_url, openid_services)
181
 
 
 
358
        openid_services = OpenIDServiceEndpoint.fromHTML(yadis_url, body)
 
359
 
 
360
    return (yadis_url, getOPOrUserServices(openid_services))
182
361
 
183
362
def discoverXRI(iname):
184
363
    endpoints = []
185
364
    try:
186
365
        canonicalID, services = xrires.ProxyResolver().query(
187
366
            iname, OpenIDServiceEndpoint.openid_type_uris)
 
367
 
 
368
        if canonicalID is None:
 
369
            raise XRDSError('No CanonicalID found for XRI %r' % (iname,))
 
370
 
188
371
        flt = filters.mkFilter(OpenIDServiceEndpoint)
189
372
        for service_element in services:
190
373
            endpoints.extend(flt.getServiceEndpoints(iname, service_element))
195
378
        # Is there a way to pass this through the filter to the endpoint
196
379
        # constructor instead of tacking it on after?
197
380
        endpoint.canonicalID = canonicalID
 
381
        endpoint.claimed_id = canonicalID
198
382
 
199
383
    # FIXME: returned xri should probably be in some normal form
200
 
    return iname, endpoints
 
384
    return iname, getOPOrUserServices(endpoints)
201
385
 
202
386
 
203
387
def discoverNoYadis(uri):
206
390
        raise DiscoveryFailure(
207
391
            'HTTP Response status from identity URL host is not 200. '
208
392
            'Got status %r' % (http_resp.status,), http_resp)
209
 
    identity_url = http_resp.final_url
210
 
 
211
 
    # Try to parse the response as HTML to get OpenID 1.0/1.1
212
 
    # <link rel="...">
213
 
    try:
214
 
        service = OpenIDServiceEndpoint.fromHTML(identity_url, http_resp.body)
215
 
    except ParseError:
216
 
        openid_services = []
217
 
    else:
218
 
        openid_services = [service]
219
 
 
220
 
    return identity_url, openid_services
221
 
 
222
 
if yadis_available:
223
 
    discover = discoverYadis
224
 
else:
225
 
    discover = discoverNoYadis
 
393
 
 
394
    claimed_id = http_resp.final_url
 
395
    openid_services = OpenIDServiceEndpoint.fromHTML(
 
396
        claimed_id, http_resp.body)
 
397
    return claimed_id, openid_services
 
398
 
 
399
def discoverURI(uri):
 
400
    parsed = urlparse.urlparse(uri)
 
401
    if parsed[0] and parsed[1]:
 
402
        if parsed[0] not in ['http', 'https']:
 
403
            raise DiscoveryFailure('URI scheme is not HTTP or HTTPS', None)
 
404
    else:
 
405
        uri = 'http://' + uri
 
406
 
 
407
    uri = normalizeURL(uri)
 
408
    claimed_id, openid_services = discoverYadis(uri)
 
409
    claimed_id = normalizeURL(claimed_id)
 
410
    return claimed_id, openid_services
 
411
 
 
412
def discover(identifier):
 
413
    if xri.identifierScheme(identifier) == "XRI":
 
414
        return discoverXRI(identifier)
 
415
    else:
 
416
        return discoverURI(identifier)