Package openid :: Package consumer :: Module discover
[frames] | no frames]

Source Code for Module openid.consumer.discover

  1  # -*- test-case-name: openid.test.test_discover -*- 
  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 
 29   
 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' 
 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   
39 -class OpenIDServiceEndpoint(object):
40 """Object representing an OpenID service endpoint. 41 42 @ivar identity_url: the verified identifier. 43 @ivar canonicalID: For XRI, the persistent identifier. 44 """ 45 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, 50 51 OPENID_2_0_TYPE, 52 OPENID_1_1_TYPE, 53 OPENID_1_0_TYPE, 54 ] 55
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
64 - def usesExtension(self, extension_uri):
65 return extension_uri in self.type_uris
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
90 - def parseService(self, yadis_url, uri, type_uris, service_element):
91 """Set the state of this object based on the contents of the 92 service element.""" 93 self.type_uris = type_uris 94 self.server_url = uri 95 self.used_yadis = True 96 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):
107 """Return the identifier that should be sent as the 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 115 else: 116 return self.local_id or self.canonicalID
117
118 - def fromBasicServiceEndpoint(cls, endpoint):
119 """Create a new instance of this class from the endpoint 120 object passed in. 121 122 @return: None or OpenIDServiceEndpoint for this endpoint object""" 123 type_uris = endpoint.matchTypes(cls.openid_type_uris) 124 125 # If any Type URIs match and there is an endpoint URI 126 # specified, then this is an OpenID endpoint 127 if type_uris and endpoint.uri is not None: 128 openid_endpoint = cls() 129 openid_endpoint.parseService( 130 endpoint.yadis_url, 131 endpoint.uri, 132 endpoint.type_uris, 133 endpoint.service_element) 134 else: 135 openid_endpoint = None 136 137 return openid_endpoint
138 139 fromBasicServiceEndpoint = classmethod(fromBasicServiceEndpoint) 140
141 - def fromHTML(cls, uri, html):
142 """Parse the given document as HTML looking for an OpenID <link 143 rel=...> 144 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 """ 180 service = cls() 181 service.server_url = op_endpoint_url 182 service.type_uris = [OPENID_IDP_2_0_TYPE] 183 return service
184 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
317
318 -def discoverYadis(uri):
319 """Discover OpenID services for a URI. Tries Yadis and falls back 320 on old-style <link rel='...'> discovery if Yadis fails. 321 322 @param uri: normalized identity URL 323 @type uri: str 324 325 @return: (claimed_id, services) 326 @rtype: (str, list(OpenIDServiceEndpoint)) 327 328 @raises DiscoveryFailure: when discovery fails. 329 """ 330 # Might raise a yadis.discover.DiscoveryFailure if no document 331 # came back for that URI at all. I don't think falling back 332 # to OpenID 1.0 discovery on the same URL will help, so don't 333 # bother to catch it. 334 response = yadisDiscover(uri) 335 336 yadis_url = response.normalized_uri 337 try: 338 openid_services = extractServices( 339 response.normalized_uri, response.response_text, 340 OpenIDServiceEndpoint) 341 except XRDSError: 342 # Does not parse as a Yadis XRDS file 343 openid_services = [] 344 345 if not openid_services: 346 # Either not an XRDS or there are no OpenID services. 347 348 if response.isXRDS(): 349 # if we got the Yadis content-type or followed the Yadis 350 # header, re-fetch the document without following the Yadis 351 # header, with no Accept header. 352 return discoverNoYadis(uri) 353 else: 354 body = response.response_text 355 356 # Try to parse the response as HTML to get OpenID 1.0/1.1 357 # <link rel="..."> 358 openid_services = OpenIDServiceEndpoint.fromHTML(yadis_url, body) 359 360 return (yadis_url, getOPOrUserServices(openid_services))
361
362 -def discoverXRI(iname):
363 endpoints = [] 364 try: 365 canonicalID, services = xrires.ProxyResolver().query( 366 iname, OpenIDServiceEndpoint.openid_type_uris) 367 368 if canonicalID is None: 369 raise XRDSError('No CanonicalID found for XRI %r' % (iname,)) 370 371 flt = filters.mkFilter(OpenIDServiceEndpoint) 372 for service_element in services: 373 endpoints.extend(flt.getServiceEndpoints(iname, service_element)) 374 except XRDSError: 375 oidutil.log('xrds error on ' + iname) 376 377 for endpoint in endpoints: 378 # Is there a way to pass this through the filter to the endpoint 379 # constructor instead of tacking it on after? 380 endpoint.canonicalID = canonicalID 381 endpoint.claimed_id = canonicalID 382 383 # FIXME: returned xri should probably be in some normal form 384 return iname, getOPOrUserServices(endpoints)
385 386
387 -def discoverNoYadis(uri):
388 http_resp = fetchers.fetch(uri) 389 if http_resp.status != 200: 390 raise DiscoveryFailure( 391 'HTTP Response status from identity URL host is not 200. ' 392 'Got status %r' % (http_resp.status,), http_resp) 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)
417