Ticket #260: discover.py

File discover.py, 4.4 kB (added by http://stiod.pip.verisignlabs.com/, 6 months ago)

Save as openid/yadis/discover.py

Line 
1 # -*- test-case-name: openid.test.test_yadis_discover -*-
2 __all__ = ['discover', 'DiscoveryResult', 'DiscoveryFailure']
3
4 from cStringIO import StringIO
5
6 from openid import fetchers
7
8 from openid.yadis.constants import \
9      YADIS_HEADER_NAME, YADIS_CONTENT_TYPE, YADIS_ACCEPT_HEADER
10 from openid.yadis.parsehtml import MetaNotFound, findHTMLMeta
11
12 class DiscoveryFailure(Exception):
13     """Raised when a YADIS protocol error occurs in the discovery process"""
14     identity_url = None
15
16     def __init__(self, message, http_response):
17         Exception.__init__(self, message)
18         self.http_response = http_response
19
20 class DiscoveryResult(object):
21     """Contains the result of performing Yadis discovery on a URI"""
22
23     # The URI that was passed to the fetcher
24     request_uri = None
25
26     # The result of following redirects from the request_uri
27     normalized_uri = None
28
29     # The URI from which the response text was returned (set to
30     # None if there was no XRDS document found)
31     xrds_uri = None
32
33     # The content-type returned with the response_text
34     content_type = None
35
36     # The document returned from the xrds_uri
37     response_text = None
38
39     def __init__(self, request_uri):
40         """Initialize the state of the object
41
42         sets all attributes to None except the request_uri
43         """
44         self.request_uri = request_uri
45
46     def usedYadisLocation(self):
47         """Was the Yadis protocol's indirection used?"""
48         return self.normalized_uri != self.xrds_uri
49
50     def isXRDS(self):
51         """Is the response text supposed to be an XRDS document?"""
52         return (self.usedYadisLocation() or
53                 self.content_type == YADIS_CONTENT_TYPE)
54
55 def discover(uri):
56     """Discover services for a given URI.
57
58     @param uri: The identity URI as a well-formed http or https
59         URI. The well-formedness and the protocol are not checked, but
60         the results of this function are undefined if those properties
61         do not hold.
62
63     @return: DiscoveryResult object
64
65     @raises Exception: Any exception that can be raised by fetching a URL with
66         the given fetcher.
67     @raises DiscoveryFailure: When the HTTP response does not have a 200 code.
68     """
69     result = DiscoveryResult(uri)
70     resp = fetchers.fetch(uri, headers={'Accept': YADIS_ACCEPT_HEADER})
71     if resp.status != 200 and resp.status != 206:
72         raise DiscoveryFailure(
73             'HTTP Response status from identity URL host is not 200. '
74             'Got status %r' % (resp.status,), resp)
75
76     # Note the URL after following redirects
77     result.normalized_uri = resp.final_url
78
79     # Attempt to find out where to go to discover the document
80     # or if we already have it
81     result.content_type = resp.headers.get('content-type')
82
83     result.xrds_uri = whereIsYadis(resp)
84
85     if result.xrds_uri and result.usedYadisLocation():
86         resp = fetchers.fetch(result.xrds_uri)
87         if resp.status != 200 and resp.status != 206:
88             exc = DiscoveryFailure(
89                 'HTTP Response status from Yadis host is not 200. '
90                 'Got status %r' % (resp.status,), resp)
91             exc.identity_url = result.normalized_uri
92             raise exc
93         result.content_type = resp.headers.get('content-type')
94
95     result.response_text = resp.body
96     return result
97
98
99
100 def whereIsYadis(resp):
101     """Given a HTTPResponse, return the location of the Yadis document.
102
103     May be the URL just retrieved, another URL, or None, if I can't
104     find any.
105
106     [non-blocking]
107
108     @returns: str or None
109     """
110     # Attempt to find out where to go to discover the document
111     # or if we already have it
112     content_type = resp.headers.get('content-type')
113
114     # According to the spec, the content-type header must be an exact
115     # match, or else we have to look for an indirection.
116     if (content_type and
117         content_type.split(';', 1)[0].lower() == YADIS_CONTENT_TYPE):
118         return resp.final_url
119     else:
120         # Try the header
121         yadis_loc = resp.headers.get(YADIS_HEADER_NAME.lower())
122
123         if not yadis_loc:
124             # Parse as HTML if the header is missing.
125             #
126             # XXX: do we want to do something with content-type, like
127             # have a whitelist or a blacklist (for detecting that it's
128             # HTML)?
129             try:
130                 yadis_loc = findHTMLMeta(StringIO(resp.body))
131             except MetaNotFound:
132                 pass
133
134         return yadis_loc
135