getting file size for all dict files to be downloaded. coming to be 400mb or so.
[worddb.git] / libs / openid / consumer / discover.py
blob2301c5b2830fff04557ffa8d6d2e5f1e70bd1c28
1 # -*- test-case-name: openid.test.test_discover -*-
2 """Functions to discover OpenID endpoints from identifiers.
3 """
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',
16 import urlparse
18 from openid import oidutil, fetchers, urinorm
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
28 from openid.consumer import html_parse
30 OPENID_1_0_NS = 'http://openid.net/xmlns/1.0'
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'
33 OPENID_1_1_TYPE = 'http://openid.net/signon/1.1'
34 OPENID_1_0_TYPE = 'http://openid.net/signon/1.0'
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
39 class OpenIDServiceEndpoint(object):
40 """Object representing an OpenID service endpoint.
42 @ivar identity_url: the verified identifier.
43 @ivar canonicalID: For XRI, the persistent identifier.
44 """
46 # OpenID service type URIs, listed in order of preference. The
47 # ordering of this list affects yadis and XRI service discovery.
48 openid_type_uris = [
49 OPENID_IDP_2_0_TYPE,
51 OPENID_2_0_TYPE,
52 OPENID_1_1_TYPE,
53 OPENID_1_0_TYPE,
56 def __init__(self):
57 self.claimed_id = None
58 self.server_url = None
59 self.type_uris = []
60 self.local_id = None
61 self.canonicalID = None
62 self.used_yadis = False # whether this came from an XRDS
63 self.display_identifier = None
65 def usesExtension(self, extension_uri):
66 return extension_uri in self.type_uris
68 def preferredNamespace(self):
69 if (OPENID_IDP_2_0_TYPE in self.type_uris or
70 OPENID_2_0_TYPE in self.type_uris):
71 return OPENID_2_0_MESSAGE_NS
72 else:
73 return OPENID_1_0_MESSAGE_NS
75 def supportsType(self, type_uri):
76 """Does this endpoint support this type?
78 I consider C{/server} endpoints to implicitly support C{/signon}.
79 """
80 return (
81 (type_uri in self.type_uris) or
82 (type_uri == OPENID_2_0_TYPE and self.isOPIdentifier())
85 def getDisplayIdentifier(self):
86 """Return the display_identifier if set, else return the claimed_id.
87 """
88 if self.display_identifier is not None:
89 return self.display_identifier
90 if self.claimed_id is None:
91 return None
92 else:
93 return urlparse.urldefrag(self.claimed_id)[0]
95 def compatibilityMode(self):
96 return self.preferredNamespace() != OPENID_2_0_MESSAGE_NS
98 def isOPIdentifier(self):
99 return OPENID_IDP_2_0_TYPE in self.type_uris
101 def parseService(self, yadis_url, uri, type_uris, service_element):
102 """Set the state of this object based on the contents of the
103 service element."""
104 self.type_uris = type_uris
105 self.server_url = uri
106 self.used_yadis = True
108 if not self.isOPIdentifier():
109 # XXX: This has crappy implications for Service elements
110 # that contain both 'server' and 'signon' Types. But
111 # that's a pathological configuration anyway, so I don't
112 # think I care.
113 self.local_id = findOPLocalIdentifier(service_element,
114 self.type_uris)
115 self.claimed_id = yadis_url
117 def getLocalID(self):
118 """Return the identifier that should be sent as the
119 openid.identity parameter to the server."""
120 # I looked at this conditional and thought "ah-hah! there's the bug!"
121 # but Python actually makes that one big expression somehow, i.e.
122 # "x is x is x" is not the same thing as "(x is x) is x".
123 # That's pretty weird, dude. -- kmt, 1/07
124 if (self.local_id is self.canonicalID is None):
125 return self.claimed_id
126 else:
127 return self.local_id or self.canonicalID
129 def fromBasicServiceEndpoint(cls, endpoint):
130 """Create a new instance of this class from the endpoint
131 object passed in.
133 @return: None or OpenIDServiceEndpoint for this endpoint object"""
134 type_uris = endpoint.matchTypes(cls.openid_type_uris)
136 # If any Type URIs match and there is an endpoint URI
137 # specified, then this is an OpenID endpoint
138 if type_uris and endpoint.uri is not None:
139 openid_endpoint = cls()
140 openid_endpoint.parseService(
141 endpoint.yadis_url,
142 endpoint.uri,
143 endpoint.type_uris,
144 endpoint.service_element)
145 else:
146 openid_endpoint = None
148 return openid_endpoint
150 fromBasicServiceEndpoint = classmethod(fromBasicServiceEndpoint)
152 def fromHTML(cls, uri, html):
153 """Parse the given document as HTML looking for an OpenID <link
154 rel=...>
156 @rtype: [OpenIDServiceEndpoint]
158 discovery_types = [
159 (OPENID_2_0_TYPE, 'openid2.provider', 'openid2.local_id'),
160 (OPENID_1_1_TYPE, 'openid.server', 'openid.delegate'),
163 link_attrs = html_parse.parseLinkAttrs(html)
164 services = []
165 for type_uri, op_endpoint_rel, local_id_rel in discovery_types:
166 op_endpoint_url = html_parse.findFirstHref(
167 link_attrs, op_endpoint_rel)
168 if op_endpoint_url is None:
169 continue
171 service = cls()
172 service.claimed_id = uri
173 service.local_id = html_parse.findFirstHref(
174 link_attrs, local_id_rel)
175 service.server_url = op_endpoint_url
176 service.type_uris = [type_uri]
178 services.append(service)
180 return services
182 fromHTML = classmethod(fromHTML)
185 def fromXRDS(cls, uri, xrds):
186 """Parse the given document as XRDS looking for OpenID services.
188 @rtype: [OpenIDServiceEndpoint]
190 @raises XRDSError: When the XRDS does not parse.
192 @since: 2.1.0
194 return extractServices(uri, xrds, cls)
196 fromXRDS = classmethod(fromXRDS)
199 def fromDiscoveryResult(cls, discoveryResult):
200 """Create endpoints from a DiscoveryResult.
202 @type discoveryResult: L{DiscoveryResult}
204 @rtype: list of L{OpenIDServiceEndpoint}
206 @raises XRDSError: When the XRDS does not parse.
208 @since: 2.1.0
210 if discoveryResult.isXRDS():
211 method = cls.fromXRDS
212 else:
213 method = cls.fromHTML
214 return method(discoveryResult.normalized_uri,
215 discoveryResult.response_text)
217 fromDiscoveryResult = classmethod(fromDiscoveryResult)
220 def fromOPEndpointURL(cls, op_endpoint_url):
221 """Construct an OP-Identifier OpenIDServiceEndpoint object for
222 a given OP Endpoint URL
224 @param op_endpoint_url: The URL of the endpoint
225 @rtype: OpenIDServiceEndpoint
227 service = cls()
228 service.server_url = op_endpoint_url
229 service.type_uris = [OPENID_IDP_2_0_TYPE]
230 return service
232 fromOPEndpointURL = classmethod(fromOPEndpointURL)
235 def __str__(self):
236 return ("<%s.%s "
237 "server_url=%r "
238 "claimed_id=%r "
239 "local_id=%r "
240 "canonicalID=%r "
241 "used_yadis=%s "
243 % (self.__class__.__module__, self.__class__.__name__,
244 self.server_url,
245 self.claimed_id,
246 self.local_id,
247 self.canonicalID,
248 self.used_yadis))
252 def findOPLocalIdentifier(service_element, type_uris):
253 """Find the OP-Local Identifier for this xrd:Service element.
255 This considers openid:Delegate to be a synonym for xrd:LocalID if
256 both OpenID 1.X and OpenID 2.0 types are present. If only OpenID
257 1.X is present, it returns the value of openid:Delegate. If only
258 OpenID 2.0 is present, it returns the value of xrd:LocalID. If
259 there is more than one LocalID tag and the values are different,
260 it raises a DiscoveryFailure. This is also triggered when the
261 xrd:LocalID and openid:Delegate tags are different.
263 @param service_element: The xrd:Service element
264 @type service_element: ElementTree.Node
266 @param type_uris: The xrd:Type values present in this service
267 element. This function could extract them, but higher level
268 code needs to do that anyway.
269 @type type_uris: [str]
271 @raises DiscoveryFailure: when discovery fails.
273 @returns: The OP-Local Identifier for this service element, if one
274 is present, or None otherwise.
275 @rtype: str or unicode or NoneType
277 # XXX: Test this function on its own!
279 # Build the list of tags that could contain the OP-Local Identifier
280 local_id_tags = []
281 if (OPENID_1_1_TYPE in type_uris or
282 OPENID_1_0_TYPE in type_uris):
283 local_id_tags.append(nsTag(OPENID_1_0_NS, 'Delegate'))
285 if OPENID_2_0_TYPE in type_uris:
286 local_id_tags.append(nsTag(XRD_NS_2_0, 'LocalID'))
288 # Walk through all the matching tags and make sure that they all
289 # have the same value
290 local_id = None
291 for local_id_tag in local_id_tags:
292 for local_id_element in service_element.findall(local_id_tag):
293 if local_id is None:
294 local_id = local_id_element.text
295 elif local_id != local_id_element.text:
296 format = 'More than one %r tag found in one service element'
297 message = format % (local_id_tag,)
298 raise DiscoveryFailure(message, None)
300 return local_id
302 def normalizeURL(url):
303 """Normalize a URL, converting normalization failures to
304 DiscoveryFailure"""
305 try:
306 normalized = urinorm.urinorm(url)
307 except ValueError, why:
308 raise DiscoveryFailure('Normalizing identifier: %s' % (why[0],), None)
309 else:
310 return urlparse.urldefrag(normalized)[0]
312 def arrangeByType(service_list, preferred_types):
313 """Rearrange service_list in a new list so services are ordered by
314 types listed in preferred_types. Return the new list."""
316 def enumerate(elts):
317 """Return an iterable that pairs the index of an element with
318 that element.
320 For Python 2.2 compatibility"""
321 return zip(range(len(elts)), elts)
323 def bestMatchingService(service):
324 """Return the index of the first matching type, or something
325 higher if no type matches.
327 This provides an ordering in which service elements that
328 contain a type that comes earlier in the preferred types list
329 come before service elements that come later. If a service
330 element has more than one type, the most preferred one wins.
332 for i, t in enumerate(preferred_types):
333 if preferred_types[i] in service.type_uris:
334 return i
336 return len(preferred_types)
338 # Build a list with the service elements in tuples whose
339 # comparison will prefer the one with the best matching service
340 prio_services = [(bestMatchingService(s), orig_index, s)
341 for (orig_index, s) in enumerate(service_list)]
342 prio_services.sort()
344 # Now that the services are sorted by priority, remove the sort
345 # keys from the list.
346 for i in range(len(prio_services)):
347 prio_services[i] = prio_services[i][2]
349 return prio_services
351 def getOPOrUserServices(openid_services):
352 """Extract OP Identifier services. If none found, return the
353 rest, sorted with most preferred first according to
354 OpenIDServiceEndpoint.openid_type_uris.
356 openid_services is a list of OpenIDServiceEndpoint objects.
358 Returns a list of OpenIDServiceEndpoint objects."""
360 op_services = arrangeByType(openid_services, [OPENID_IDP_2_0_TYPE])
362 openid_services = arrangeByType(openid_services,
363 OpenIDServiceEndpoint.openid_type_uris)
365 return op_services or openid_services
367 def discoverYadis(uri):
368 """Discover OpenID services for a URI. Tries Yadis and falls back
369 on old-style <link rel='...'> discovery if Yadis fails.
371 @param uri: normalized identity URL
372 @type uri: str
374 @return: (claimed_id, services)
375 @rtype: (str, list(OpenIDServiceEndpoint))
377 @raises DiscoveryFailure: when discovery fails.
379 # Might raise a yadis.discover.DiscoveryFailure if no document
380 # came back for that URI at all. I don't think falling back
381 # to OpenID 1.0 discovery on the same URL will help, so don't
382 # bother to catch it.
383 response = yadisDiscover(uri)
385 yadis_url = response.normalized_uri
386 body = response.response_text
387 try:
388 openid_services = OpenIDServiceEndpoint.fromXRDS(yadis_url, body)
389 except XRDSError:
390 # Does not parse as a Yadis XRDS file
391 openid_services = []
393 if not openid_services:
394 # Either not an XRDS or there are no OpenID services.
396 if response.isXRDS():
397 # if we got the Yadis content-type or followed the Yadis
398 # header, re-fetch the document without following the Yadis
399 # header, with no Accept header.
400 return discoverNoYadis(uri)
402 # Try to parse the response as HTML.
403 # <link rel="...">
404 openid_services = OpenIDServiceEndpoint.fromHTML(yadis_url, body)
406 return (yadis_url, getOPOrUserServices(openid_services))
408 def discoverXRI(iname):
409 endpoints = []
410 try:
411 canonicalID, services = xrires.ProxyResolver().query(
412 iname, OpenIDServiceEndpoint.openid_type_uris)
414 if canonicalID is None:
415 raise XRDSError('No CanonicalID found for XRI %r' % (iname,))
417 flt = filters.mkFilter(OpenIDServiceEndpoint)
418 for service_element in services:
419 endpoints.extend(flt.getServiceEndpoints(iname, service_element))
420 except XRDSError:
421 oidutil.log('xrds error on ' + iname)
423 for endpoint in endpoints:
424 # Is there a way to pass this through the filter to the endpoint
425 # constructor instead of tacking it on after?
426 endpoint.canonicalID = canonicalID
427 endpoint.claimed_id = canonicalID
428 endpoint.display_identifier = iname
430 # FIXME: returned xri should probably be in some normal form
431 return iname, getOPOrUserServices(endpoints)
434 def discoverNoYadis(uri):
435 http_resp = fetchers.fetch(uri)
436 if http_resp.status not in (200, 206):
437 raise DiscoveryFailure(
438 'HTTP Response status from identity URL host is not 200. '
439 'Got status %r' % (http_resp.status,), http_resp)
441 claimed_id = http_resp.final_url
442 openid_services = OpenIDServiceEndpoint.fromHTML(
443 claimed_id, http_resp.body)
444 return claimed_id, openid_services
446 def discoverURI(uri):
447 parsed = urlparse.urlparse(uri)
448 if parsed[0] and parsed[1]:
449 if parsed[0] not in ['http', 'https']:
450 raise DiscoveryFailure('URI scheme is not HTTP or HTTPS', None)
451 else:
452 uri = 'http://' + uri
454 uri = normalizeURL(uri)
455 claimed_id, openid_services = discoverYadis(uri)
456 claimed_id = normalizeURL(claimed_id)
457 return claimed_id, openid_services
459 def discover(identifier):
460 if xri.identifierScheme(identifier) == "XRI":
461 return discoverXRI(identifier)
462 else:
463 return discoverURI(identifier)