1
1
# -*- test-case-name: openid.test.test_discover -*-
3
from urljr import fetchers
5
from openid import oidutil
7
# If the Yadis library is available, use it. Otherwise, only use
12
yadis_available = False
14
oidutil.log('Consumer operating without Yadis support '
15
'(failed to import Yadis library)')
17
class DiscoveryFailure(RuntimeError):
18
"""A failure to discover an OpenID server.
20
When the C{yadis} package is available, this is
21
C{yadis.discover.DiscoveryFailure}."""
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
30
from openid.consumer.parse import openIDDiscover as parseOpenIDLinkRel
31
from openid.consumer.parse import ParseError
2
"""Functions to discover OpenID endpoints from identifiers.
11
'OPENID_IDP_2_0_TYPE',
12
'OpenIDServiceEndpoint',
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
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'
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):
39
40
"""Object representing an OpenID service endpoint.
41
42
@ivar identity_url: the verified identifier.
42
43
@ivar canonicalID: For XRI, the persistent identifier.
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 = [
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 = []
55
61
self.canonicalID = None
56
62
self.used_yadis = False # whether this came from an XRDS
58
64
def usesExtension(self, extension_uri):
59
65
return extension_uri in self.type_uris
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
72
return OPENID_1_0_MESSAGE_NS
74
def supportsType(self, type_uri):
75
"""Does this endpoint support this type?
77
I consider C{/server} endpoints to implicitly support C{/signon}.
80
(type_uri in self.type_uris) or
81
(type_uri == OPENID_2_0_TYPE and self.isOPIdentifier())
84
def compatibilityMode(self):
85
return self.preferredNamespace() != OPENID_2_0_MESSAGE_NS
87
def isOPIdentifier(self):
88
return OPENID_IDP_2_0_TYPE in self.type_uris
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
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
102
self.local_id = findOPLocalIdentifier(service_element,
104
self.claimed_id = yadis_url
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
116
return self.local_id or self.canonicalID
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
105
@raises: openid.consumer.parse.ParseError
107
delegate_url, server_url = parseOpenIDLinkRel(html)
145
@rtype: [OpenIDServiceEndpoint]
148
(OPENID_2_0_TYPE, 'openid2.provider', 'openid2.local_id'),
149
(OPENID_1_1_TYPE, 'openid.server', 'openid.delegate'),
152
link_attrs = html_parse.parseLinkAttrs(html)
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:
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]
167
services.append(service)
171
fromHTML = classmethod(fromHTML)
173
def fromOPEndpointURL(cls, op_endpoint_url):
174
"""Construct an OP-Identifier OpenIDServiceEndpoint object for
175
a given OP Endpoint URL
177
@param op_endpoint_url: The URL of the endpoint
178
@rtype: OpenIDServiceEndpoint
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]
115
fromHTML = classmethod(fromHTML)
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')
124
delegates = service_element.findall(delegate_tag)
125
for delegate_element in delegates:
126
delegate = delegate_element.text
185
fromOPEndpointURL = classmethod(fromOPEndpointURL)
196
% (self.__class__.__module__, self.__class__.__name__,
205
def findOPLocalIdentifier(service_element, type_uris):
206
"""Find the OP-Local Identifier for this xrd:Service element.
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.
216
@param service_element: The xrd:Service element
217
@type service_element: ElementTree.Node
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]
224
@raises DiscoveryFailure: when discovery fails.
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
230
# XXX: Test this function on its own!
232
# Build the list of tags that could contain the OP-Local Identifier
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'))
238
if OPENID_2_0_TYPE in type_uris:
239
local_id_tags.append(nsTag(XRD_NS_2_0, 'LocalID'))
241
# Walk through all the matching tags and make sure that they all
242
# have the same value
244
for local_id_tag in local_id_tags:
245
for local_id_element in service_element.findall(local_id_tag):
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)
255
def normalizeURL(url):
256
"""Normalize a URL, converting normalization failures to
259
return urinorm.urinorm(url)
260
except ValueError, why:
261
raise DiscoveryFailure('Normalizing identifier: %s' % (why[0],), None)
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."""
268
"""Return an iterable that pairs the index of an element with
271
For Python 2.2 compatibility"""
272
return zip(range(len(elts)), elts)
274
def bestMatchingService(service):
275
"""Return the index of the first matching type, or something
276
higher if no type matches.
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.
283
for i, t in enumerate(preferred_types):
284
if preferred_types[i] in service.type_uris:
287
return len(preferred_types)
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)]
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]
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.
307
openid_services is a list of OpenIDServiceEndpoint objects.
309
Returns a list of OpenIDServiceEndpoint objects."""
311
op_services = arrangeByType(openid_services, [OPENID_IDP_2_0_TYPE])
313
openid_services = arrangeByType(openid_services,
314
OpenIDServiceEndpoint.openid_type_uris)
316
return op_services or openid_services
133
318
def discoverYadis(uri):
134
319
"""Discover OpenID services for a URI. Tries Yadis and falls back
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
211
# Try to parse the response as HTML to get OpenID 1.0/1.1
214
service = OpenIDServiceEndpoint.fromHTML(identity_url, http_resp.body)
218
openid_services = [service]
220
return identity_url, openid_services
223
discover = discoverYadis
225
discover = discoverNoYadis
394
claimed_id = http_resp.final_url
395
openid_services = OpenIDServiceEndpoint.fromHTML(
396
claimed_id, http_resp.body)
397
return claimed_id, openid_services
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)
405
uri = 'http://' + uri
407
uri = normalizeURL(uri)
408
claimed_id, openid_services = discoverYadis(uri)
409
claimed_id = normalizeURL(claimed_id)
410
return claimed_id, openid_services
412
def discover(identifier):
413
if xri.identifierScheme(identifier) == "XRI":
414
return discoverXRI(identifier)
416
return discoverURI(identifier)