C0 code coverage information
Generated on Fri Jul 11 15:55:30 -0700 2008 with rcov 0.7.0
Code reported as executed by Ruby looks like this...
and this: this line is also marked as covered.
Lines considered as run by rcov, but not reported by Ruby, look like this,
and this: these lines were inferred by rcov (using simple heuristics).
Finally, here's a line marked as not executed.
1 # Functions to discover OpenID endpoints from identifiers.
2
3 require 'uri'
4 require 'openid/util'
5 require 'openid/fetchers'
6 require 'openid/urinorm'
7 require 'openid/message'
8 require 'openid/yadis/discovery'
9 require 'openid/yadis/xrds'
10 require 'openid/yadis/xri'
11 require 'openid/yadis/services'
12 require 'openid/yadis/filters'
13 require 'openid/consumer/html_parse'
14 require 'openid/yadis/xrires'
15
16 module OpenID
17
18 OPENID_1_0_NS = 'http://openid.net/xmlns/1.0'
19 OPENID_IDP_2_0_TYPE = 'http://specs.openid.net/auth/2.0/server'
20 OPENID_2_0_TYPE = 'http://specs.openid.net/auth/2.0/signon'
21 OPENID_1_1_TYPE = 'http://openid.net/signon/1.1'
22 OPENID_1_0_TYPE = 'http://openid.net/signon/1.0'
23
24 OPENID_1_0_MESSAGE_NS = OPENID1_NS
25 OPENID_2_0_MESSAGE_NS = OPENID2_NS
26
27 # Object representing an OpenID service endpoint.
28 class OpenIDServiceEndpoint
29
30 # OpenID service type URIs, listed in order of preference. The
31 # ordering of this list affects yadis and XRI service discovery.
32 OPENID_TYPE_URIS = [
33 OPENID_IDP_2_0_TYPE,
34
35 OPENID_2_0_TYPE,
36 OPENID_1_1_TYPE,
37 OPENID_1_0_TYPE,
38 ]
39
40 # the verified identifier.
41 attr_accessor :claimed_id
42
43 # For XRI, the persistent identifier.
44 attr_accessor :canonical_id
45
46 attr_accessor :server_url, :type_uris, :local_id, :used_yadis
47
48 def initialize
49 @claimed_id = nil
50 @server_url = nil
51 @type_uris = []
52 @local_id = nil
53 @canonical_id = nil
54 @used_yadis = false # whether this came from an XRDS
55 @display_identifier = nil
56 end
57
58 def display_identifier
59 return @display_identifier if @display_identifier
60
61 return @claimed_id if @claimed_id.nil?
62
63 begin
64 parsed_identifier = URI.parse(@claimed_id)
65 rescue URI::InvalidURIError
66 raise ProtocolError, "Claimed identifier #{claimed_id} is not a valid URI"
67 end
68
69 return @claimed_id if not parsed_identifier.fragment
70
71 disp = parsed_identifier
72 disp.fragment = nil
73
74 return disp.to_s
75 end
76
77 def display_identifier=(display_identifier)
78 @display_identifier = display_identifier
79 end
80
81 def uses_extension(extension_uri)
82 return @type_uris.member?(extension_uri)
83 end
84
85 def preferred_namespace
86 if (@type_uris.member?(OPENID_IDP_2_0_TYPE) or
87 @type_uris.member?(OPENID_2_0_TYPE))
88 return OPENID_2_0_MESSAGE_NS
89 else
90 return OPENID_1_0_MESSAGE_NS
91 end
92 end
93
94 def supports_type(type_uri)
95 # Does this endpoint support this type?
96 #
97 # I consider C{/server} endpoints to implicitly support C{/signon}.
98 (
99 @type_uris.member?(type_uri) or
100 (type_uri == OPENID_2_0_TYPE and is_op_identifier())
101 )
102 end
103
104 def compatibility_mode
105 return preferred_namespace() != OPENID_2_0_MESSAGE_NS
106 end
107
108 def is_op_identifier
109 return @type_uris.member?(OPENID_IDP_2_0_TYPE)
110 end
111
112 def parse_service(yadis_url, uri, type_uris, service_element)
113 # Set the state of this object based on the contents of the
114 # service element.
115 @type_uris = type_uris
116 @server_url = uri
117 @used_yadis = true
118
119 if !is_op_identifier()
120 # XXX: This has crappy implications for Service elements that
121 # contain both 'server' and 'signon' Types. But that's a
122 # pathological configuration anyway, so I don't think I care.
123 @local_id = OpenID.find_op_local_identifier(service_element,
124 @type_uris)
125 @claimed_id = yadis_url
126 end
127 end
128
129 def get_local_id
130 # Return the identifier that should be sent as the
131 # openid.identity parameter to the server.
132 if @local_id.nil? and @canonical_id.nil?
133 return @claimed_id
134 else
135 return (@local_id or @canonical_id)
136 end
137 end
138
139 def self.from_basic_service_endpoint(endpoint)
140 # Create a new instance of this class from the endpoint object
141 # passed in.
142 #
143 # @return: nil or OpenIDServiceEndpoint for this endpoint object"""
144
145 type_uris = endpoint.match_types(OPENID_TYPE_URIS)
146
147 # If any Type URIs match and there is an endpoint URI specified,
148 # then this is an OpenID endpoint
149 if (!type_uris.nil? and !type_uris.empty?) and !endpoint.uri.nil?
150 openid_endpoint = self.new
151 openid_endpoint.parse_service(
152 endpoint.yadis_url,
153 endpoint.uri,
154 endpoint.type_uris,
155 endpoint.service_element)
156 else
157 openid_endpoint = nil
158 end
159
160 return openid_endpoint
161 end
162
163 def self.from_html(uri, html)
164 # Parse the given document as HTML looking for an OpenID <link
165 # rel=...>
166 #
167 # @rtype: [OpenIDServiceEndpoint]
168
169 discovery_types = [
170 [OPENID_2_0_TYPE, 'openid2.provider', 'openid2.local_id'],
171 [OPENID_1_1_TYPE, 'openid.server', 'openid.delegate'],
172 ]
173
174 link_attrs = OpenID.parse_link_attrs(html)
175 services = []
176 discovery_types.each { |type_uri, op_endpoint_rel, local_id_rel|
177
178 op_endpoint_url = OpenID.find_first_href(link_attrs, op_endpoint_rel)
179
180 if !op_endpoint_url
181 next
182 end
183
184 service = self.new
185 service.claimed_id = uri
186 service.local_id = OpenID.find_first_href(link_attrs, local_id_rel)
187 service.server_url = op_endpoint_url
188 service.type_uris = [type_uri]
189
190 services << service
191 }
192
193 return services
194 end
195
196 def self.from_xrds(uri, xrds)
197 # Parse the given document as XRDS looking for OpenID services.
198 #
199 # @rtype: [OpenIDServiceEndpoint]
200 #
201 # @raises L{XRDSError}: When the XRDS does not parse.
202 return Yadis::apply_filter(uri, xrds, self)
203 end
204
205 def self.from_discovery_result(discoveryResult)
206 # Create endpoints from a DiscoveryResult.
207 #
208 # @type discoveryResult: L{DiscoveryResult}
209 #
210 # @rtype: list of L{OpenIDServiceEndpoint}
211 #
212 # @raises L{XRDSError}: When the XRDS does not parse.
213 if discoveryResult.is_xrds()
214 meth = self.method('from_xrds')
215 else
216 meth = self.method('from_html')
217 end
218
219 return meth.call(discoveryResult.normalized_uri,
220 discoveryResult.response_text)
221 end
222
223 def self.from_op_endpoint_url(op_endpoint_url)
224 # Construct an OP-Identifier OpenIDServiceEndpoint object for
225 # a given OP Endpoint URL
226 #
227 # @param op_endpoint_url: The URL of the endpoint
228 # @rtype: OpenIDServiceEndpoint
229 service = self.new
230 service.server_url = op_endpoint_url
231 service.type_uris = [OPENID_IDP_2_0_TYPE]
232 return service
233 end
234
235 def to_s
236 return sprintf("<%s server_url=%s claimed_id=%s " +
237 "local_id=%s canonical_id=%s used_yadis=%s>",
238 self.class, @server_url, @claimed_id,
239 @local_id, @canonical_id, @used_yadis)
240 end
241 end
242
243 def self.find_op_local_identifier(service_element, type_uris)
244 # Find the OP-Local Identifier for this xrd:Service element.
245 #
246 # This considers openid:Delegate to be a synonym for xrd:LocalID
247 # if both OpenID 1.X and OpenID 2.0 types are present. If only
248 # OpenID 1.X is present, it returns the value of
249 # openid:Delegate. If only OpenID 2.0 is present, it returns the
250 # value of xrd:LocalID. If there is more than one LocalID tag and
251 # the values are different, it raises a DiscoveryFailure. This is
252 # also triggered when the xrd:LocalID and openid:Delegate tags are
253 # different.
254
255 # XXX: Test this function on its own!
256
257 # Build the list of tags that could contain the OP-Local
258 # Identifier
259 local_id_tags = []
260 if type_uris.member?(OPENID_1_1_TYPE) or
261 type_uris.member?(OPENID_1_0_TYPE)
262 # local_id_tags << Yadis::nsTag(OPENID_1_0_NS, 'openid', 'Delegate')
263 service_element.add_namespace('openid', OPENID_1_0_NS)
264 local_id_tags << "openid:Delegate"
265 end
266
267 if type_uris.member?(OPENID_2_0_TYPE)
268 # local_id_tags.append(Yadis::nsTag(XRD_NS_2_0, 'xrd', 'LocalID'))
269 service_element.add_namespace('xrd', Yadis::XRD_NS_2_0)
270 local_id_tags << "xrd:LocalID"
271 end
272
273 # Walk through all the matching tags and make sure that they all
274 # have the same value
275 local_id = nil
276 local_id_tags.each { |local_id_tag|
277 service_element.each_element(local_id_tag) { |local_id_element|
278 if local_id.nil?
279 local_id = local_id_element.text
280 elsif local_id != local_id_element.text
281 format = 'More than one %s tag found in one service element'
282 message = sprintf(format, local_id_tag)
283 raise DiscoveryFailure.new(message, nil)
284 end
285 }
286 }
287
288 return local_id
289 end
290
291 def self.normalize_url(url)
292 # Normalize a URL, converting normalization failures to
293 # DiscoveryFailure
294 begin
295 normalized = URINorm.urinorm(url)
296 rescue URI::Error => why
297 raise DiscoveryFailure.new("Error normalizing #{url}: #{why.message}", nil)
298 else
299 defragged = URI::parse(normalized)
300 defragged.fragment = nil
301 return defragged.normalize.to_s
302 end
303 end
304
305 def self.best_matching_service(service, preferred_types)
306 # Return the index of the first matching type, or something higher
307 # if no type matches.
308 #
309 # This provides an ordering in which service elements that contain
310 # a type that comes earlier in the preferred types list come
311 # before service elements that come later. If a service element
312 # has more than one type, the most preferred one wins.
313 preferred_types.each_with_index { |value, index|
314 if service.type_uris.member?(value)
315 return index
316 end
317 }
318
319 return preferred_types.length
320 end
321
322 def self.arrange_by_type(service_list, preferred_types)
323 # Rearrange service_list in a new list so services are ordered by
324 # types listed in preferred_types. Return the new list.
325
326 # Build a list with the service elements in tuples whose
327 # comparison will prefer the one with the best matching service
328 prio_services = []
329
330 service_list.each_with_index { |s, index|
331 prio_services << [best_matching_service(s, preferred_types), index, s]
332 }
333
334 prio_services.sort!
335
336 # Now that the services are sorted by priority, remove the sort
337 # keys from the list.
338 (0...prio_services.length).each { |i|
339 prio_services[i] = prio_services[i][2]
340 }
341
342 return prio_services
343 end
344
345 def self.get_op_or_user_services(openid_services)
346 # Extract OP Identifier services. If none found, return the rest,
347 # sorted with most preferred first according to
348 # OpenIDServiceEndpoint.openid_type_uris.
349 #
350 # openid_services is a list of OpenIDServiceEndpoint objects.
351 #
352 # Returns a list of OpenIDServiceEndpoint objects.
353
354 op_services = arrange_by_type(openid_services, [OPENID_IDP_2_0_TYPE])
355
356 openid_services = arrange_by_type(openid_services,
357 OpenIDServiceEndpoint::OPENID_TYPE_URIS)
358
359 if !op_services.empty?
360 return op_services
361 else
362 return openid_services
363 end
364 end
365
366 def self.discover_yadis(uri)
367 # Discover OpenID services for a URI. Tries Yadis and falls back
368 # on old-style <link rel='...'> discovery if Yadis fails.
369 #
370 # @param uri: normalized identity URL
371 # @type uri: str
372 #
373 # @return: (claimed_id, services)
374 # @rtype: (str, list(OpenIDServiceEndpoint))
375 #
376 # @raises DiscoveryFailure: when discovery fails.
377
378 # Might raise a yadis.discover.DiscoveryFailure if no document
379 # came back for that URI at all. I don't think falling back to
380 # OpenID 1.0 discovery on the same URL will help, so don't bother
381 # to catch it.
382 response = Yadis.discover(uri)
383
384 yadis_url = response.normalized_uri
385 body = response.response_text
386
387 begin
388 openid_services = OpenIDServiceEndpoint.from_xrds(yadis_url, body)
389 rescue Yadis::XRDSError
390 # Does not parse as a Yadis XRDS file
391 openid_services = []
392 end
393
394 if openid_services.empty?
395 # Either not an XRDS or there are no OpenID services.
396
397 if response.is_xrds
398 # if we got the Yadis content-type or followed the Yadis
399 # header, re-fetch the document without following the Yadis
400 # header, with no Accept header.
401 return self.discover_no_yadis(uri)
402 end
403
404 # Try to parse the response as HTML.
405 # <link rel="...">
406 openid_services = OpenIDServiceEndpoint.from_html(yadis_url, body)
407 end
408
409 return [yadis_url, self.get_op_or_user_services(openid_services)]
410 end
411
412 def self.discover_xri(iname)
413 endpoints = []
414
415 begin
416 canonical_id, services = Yadis::XRI::ProxyResolver.new().query(
417 iname, OpenIDServiceEndpoint::OPENID_TYPE_URIS)
418
419 if canonical_id.nil?
420 raise Yadis::XRDSError.new(sprintf('No CanonicalID found for XRI %s', iname))
421 end
422
423 flt = Yadis.make_filter(OpenIDServiceEndpoint)
424
425 services.each { |service_element|
426 endpoints += flt.get_service_endpoints(iname, service_element)
427 }
428 rescue Yadis::XRDSError => why
429 Util.log('xrds error on ' + iname + ': ' + why.to_s)
430 end
431
432 endpoints.each { |endpoint|
433 # Is there a way to pass this through the filter to the endpoint
434 # constructor instead of tacking it on after?
435 endpoint.canonical_id = canonical_id
436 endpoint.claimed_id = canonical_id
437 endpoint.display_identifier = iname
438 }
439
440 # FIXME: returned xri should probably be in some normal form
441 return [iname, self.get_op_or_user_services(endpoints)]
442 end
443
444 def self.discover_no_yadis(uri)
445 http_resp = OpenID.fetch(uri)
446 if http_resp.code != "200" and http_resp.code != "206"
447 raise DiscoveryFailure.new(
448 "HTTP Response status from identity URL host is not \"200\". "\
449 "Got status #{http_resp.code.inspect}", http_resp)
450 end
451
452 claimed_id = http_resp.final_url
453 openid_services = OpenIDServiceEndpoint.from_html(
454 claimed_id, http_resp.body)
455 return [claimed_id, openid_services]
456 end
457
458 def self.discover_uri(uri)
459 # Hack to work around URI parsing for URls with *no* scheme.
460 if uri.index("://").nil?
461 uri = 'http://' + uri
462 end
463
464 begin
465 parsed = URI::parse(uri)
466 rescue URI::InvalidURIError => why
467 raise DiscoveryFailure.new("URI is not valid: #{why.message}", nil)
468 end
469
470 if !parsed.scheme.nil? and !parsed.scheme.empty?
471 if !['http', 'https'].member?(parsed.scheme)
472 raise DiscoveryFailure.new(
473 "URI scheme #{parsed.scheme} is not HTTP or HTTPS", nil)
474 end
475 end
476
477 uri = self.normalize_url(uri)
478 claimed_id, openid_services = self.discover_yadis(uri)
479 claimed_id = self.normalize_url(claimed_id)
480 return [claimed_id, openid_services]
481 end
482
483 def self.discover(identifier)
484 if Yadis::XRI::identifier_scheme(identifier) == :xri
485 normalized_identifier, services = discover_xri(identifier)
486 else
487 return discover_uri(identifier)
488 end
489 end
490 end
Generated using the rcov code coverage analysis tool for Ruby version 0.7.0.