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