diff options
Diffstat (limited to 'vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/discovery.rb')
-rw-r--r-- | vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/discovery.rb | 498 |
1 files changed, 498 insertions, 0 deletions
diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/discovery.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/discovery.rb new file mode 100644 index 000000000..01fe03656 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/discovery.rb @@ -0,0 +1,498 @@ +# Functions to discover OpenID endpoints from identifiers. + +require 'uri' +require 'openid/util' +require 'openid/fetchers' +require 'openid/urinorm' +require 'openid/message' +require 'openid/yadis/discovery' +require 'openid/yadis/xrds' +require 'openid/yadis/xri' +require 'openid/yadis/services' +require 'openid/yadis/filters' +require 'openid/consumer/html_parse' +require 'openid/yadis/xrires' + +module OpenID + + OPENID_1_0_NS = 'http://openid.net/xmlns/1.0' + OPENID_IDP_2_0_TYPE = 'http://specs.openid.net/auth/2.0/server' + OPENID_2_0_TYPE = 'http://specs.openid.net/auth/2.0/signon' + OPENID_1_1_TYPE = 'http://openid.net/signon/1.1' + OPENID_1_0_TYPE = 'http://openid.net/signon/1.0' + + OPENID_1_0_MESSAGE_NS = OPENID1_NS + OPENID_2_0_MESSAGE_NS = OPENID2_NS + + # Object representing an OpenID service endpoint. + class OpenIDServiceEndpoint + + # OpenID service type URIs, listed in order of preference. The + # ordering of this list affects yadis and XRI service discovery. + OPENID_TYPE_URIS = [ + OPENID_IDP_2_0_TYPE, + + OPENID_2_0_TYPE, + OPENID_1_1_TYPE, + OPENID_1_0_TYPE, + ] + + # the verified identifier. + attr_accessor :claimed_id + + # For XRI, the persistent identifier. + attr_accessor :canonical_id + + attr_accessor :server_url, :type_uris, :local_id, :used_yadis + + def initialize + @claimed_id = nil + @server_url = nil + @type_uris = [] + @local_id = nil + @canonical_id = nil + @used_yadis = false # whether this came from an XRDS + @display_identifier = nil + end + + def display_identifier + return @display_identifier if @display_identifier + + return @claimed_id if @claimed_id.nil? + + begin + parsed_identifier = URI.parse(@claimed_id) + rescue URI::InvalidURIError + raise ProtocolError, "Claimed identifier #{claimed_id} is not a valid URI" + end + + return @claimed_id if not parsed_identifier.fragment + + disp = parsed_identifier + disp.fragment = nil + + return disp.to_s + end + + def display_identifier=(display_identifier) + @display_identifier = display_identifier + end + + def uses_extension(extension_uri) + return @type_uris.member?(extension_uri) + end + + def preferred_namespace + if (@type_uris.member?(OPENID_IDP_2_0_TYPE) or + @type_uris.member?(OPENID_2_0_TYPE)) + return OPENID_2_0_MESSAGE_NS + else + return OPENID_1_0_MESSAGE_NS + end + end + + def supports_type(type_uri) + # Does this endpoint support this type? + # + # I consider C{/server} endpoints to implicitly support C{/signon}. + ( + @type_uris.member?(type_uri) or + (type_uri == OPENID_2_0_TYPE and is_op_identifier()) + ) + end + + def compatibility_mode + return preferred_namespace() != OPENID_2_0_MESSAGE_NS + end + + def is_op_identifier + return @type_uris.member?(OPENID_IDP_2_0_TYPE) + end + + def parse_service(yadis_url, uri, type_uris, service_element) + # Set the state of this object based on the contents of the + # service element. + @type_uris = type_uris + @server_url = uri + @used_yadis = true + + if !is_op_identifier() + # XXX: This has crappy implications for Service elements that + # contain both 'server' and 'signon' Types. But that's a + # pathological configuration anyway, so I don't think I care. + @local_id = OpenID.find_op_local_identifier(service_element, + @type_uris) + @claimed_id = yadis_url + end + end + + def get_local_id + # Return the identifier that should be sent as the + # openid.identity parameter to the server. + if @local_id.nil? and @canonical_id.nil? + return @claimed_id + else + return (@local_id or @canonical_id) + end + end + + def self.from_basic_service_endpoint(endpoint) + # Create a new instance of this class from the endpoint object + # passed in. + # + # @return: nil or OpenIDServiceEndpoint for this endpoint object""" + + type_uris = endpoint.match_types(OPENID_TYPE_URIS) + + # If any Type URIs match and there is an endpoint URI specified, + # then this is an OpenID endpoint + if (!type_uris.nil? and !type_uris.empty?) and !endpoint.uri.nil? + openid_endpoint = self.new + openid_endpoint.parse_service( + endpoint.yadis_url, + endpoint.uri, + endpoint.type_uris, + endpoint.service_element) + else + openid_endpoint = nil + end + + return openid_endpoint + end + + def self.from_html(uri, html) + # Parse the given document as HTML looking for an OpenID <link + # rel=...> + # + # @rtype: [OpenIDServiceEndpoint] + + discovery_types = [ + [OPENID_2_0_TYPE, 'openid2.provider', 'openid2.local_id'], + [OPENID_1_1_TYPE, 'openid.server', 'openid.delegate'], + ] + + link_attrs = OpenID.parse_link_attrs(html) + services = [] + discovery_types.each { |type_uri, op_endpoint_rel, local_id_rel| + + op_endpoint_url = OpenID.find_first_href(link_attrs, op_endpoint_rel) + + if !op_endpoint_url + next + end + + service = self.new + service.claimed_id = uri + service.local_id = OpenID.find_first_href(link_attrs, local_id_rel) + service.server_url = op_endpoint_url + service.type_uris = [type_uri] + + services << service + } + + return services + end + + def self.from_xrds(uri, xrds) + # Parse the given document as XRDS looking for OpenID services. + # + # @rtype: [OpenIDServiceEndpoint] + # + # @raises L{XRDSError}: When the XRDS does not parse. + return Yadis::apply_filter(uri, xrds, self) + end + + def self.from_discovery_result(discoveryResult) + # Create endpoints from a DiscoveryResult. + # + # @type discoveryResult: L{DiscoveryResult} + # + # @rtype: list of L{OpenIDServiceEndpoint} + # + # @raises L{XRDSError}: When the XRDS does not parse. + if discoveryResult.is_xrds() + meth = self.method('from_xrds') + else + meth = self.method('from_html') + end + + return meth.call(discoveryResult.normalized_uri, + discoveryResult.response_text) + end + + def self.from_op_endpoint_url(op_endpoint_url) + # Construct an OP-Identifier OpenIDServiceEndpoint object for + # a given OP Endpoint URL + # + # @param op_endpoint_url: The URL of the endpoint + # @rtype: OpenIDServiceEndpoint + service = self.new + service.server_url = op_endpoint_url + service.type_uris = [OPENID_IDP_2_0_TYPE] + return service + end + + def to_s + return sprintf("<%s server_url=%s claimed_id=%s " + + "local_id=%s canonical_id=%s used_yadis=%s>", + self.class, @server_url, @claimed_id, + @local_id, @canonical_id, @used_yadis) + end + end + + def self.find_op_local_identifier(service_element, type_uris) + # Find the OP-Local Identifier for this xrd:Service element. + # + # This considers openid:Delegate to be a synonym for xrd:LocalID + # if both OpenID 1.X and OpenID 2.0 types are present. If only + # OpenID 1.X is present, it returns the value of + # openid:Delegate. If only OpenID 2.0 is present, it returns the + # value of xrd:LocalID. If there is more than one LocalID tag and + # the values are different, it raises a DiscoveryFailure. This is + # also triggered when the xrd:LocalID and openid:Delegate tags are + # different. + + # XXX: Test this function on its own! + + # Build the list of tags that could contain the OP-Local + # Identifier + local_id_tags = [] + if type_uris.member?(OPENID_1_1_TYPE) or + type_uris.member?(OPENID_1_0_TYPE) + # local_id_tags << Yadis::nsTag(OPENID_1_0_NS, 'openid', 'Delegate') + service_element.add_namespace('openid', OPENID_1_0_NS) + local_id_tags << "openid:Delegate" + end + + if type_uris.member?(OPENID_2_0_TYPE) + # local_id_tags.append(Yadis::nsTag(XRD_NS_2_0, 'xrd', 'LocalID')) + service_element.add_namespace('xrd', Yadis::XRD_NS_2_0) + local_id_tags << "xrd:LocalID" + end + + # Walk through all the matching tags and make sure that they all + # have the same value + local_id = nil + local_id_tags.each { |local_id_tag| + service_element.each_element(local_id_tag) { |local_id_element| + if local_id.nil? + local_id = local_id_element.text + elsif local_id != local_id_element.text + format = 'More than one %s tag found in one service element' + message = sprintf(format, local_id_tag) + raise DiscoveryFailure.new(message, nil) + end + } + } + + return local_id + end + + def self.normalize_xri(xri) + # Normalize an XRI, stripping its scheme if present + m = /^xri:\/\/(.*)/.match(xri) + xri = m[1] if m + return xri + end + + def self.normalize_url(url) + # Normalize a URL, converting normalization failures to + # DiscoveryFailure + begin + normalized = URINorm.urinorm(url) + rescue URI::Error => why + raise DiscoveryFailure.new("Error normalizing #{url}: #{why.message}", nil) + else + defragged = URI::parse(normalized) + defragged.fragment = nil + return defragged.normalize.to_s + end + end + + def self.best_matching_service(service, preferred_types) + # Return the index of the first matching type, or something higher + # if no type matches. + # + # This provides an ordering in which service elements that contain + # a type that comes earlier in the preferred types list come + # before service elements that come later. If a service element + # has more than one type, the most preferred one wins. + preferred_types.each_with_index { |value, index| + if service.type_uris.member?(value) + return index + end + } + + return preferred_types.length + end + + def self.arrange_by_type(service_list, preferred_types) + # Rearrange service_list in a new list so services are ordered by + # types listed in preferred_types. Return the new list. + + # Build a list with the service elements in tuples whose + # comparison will prefer the one with the best matching service + prio_services = [] + + service_list.each_with_index { |s, index| + prio_services << [best_matching_service(s, preferred_types), index, s] + } + + prio_services.sort! + + # Now that the services are sorted by priority, remove the sort + # keys from the list. + (0...prio_services.length).each { |i| + prio_services[i] = prio_services[i][2] + } + + return prio_services + end + + def self.get_op_or_user_services(openid_services) + # Extract OP Identifier services. If none found, return the rest, + # sorted with most preferred first according to + # OpenIDServiceEndpoint.openid_type_uris. + # + # openid_services is a list of OpenIDServiceEndpoint objects. + # + # Returns a list of OpenIDServiceEndpoint objects. + + op_services = arrange_by_type(openid_services, [OPENID_IDP_2_0_TYPE]) + + openid_services = arrange_by_type(openid_services, + OpenIDServiceEndpoint::OPENID_TYPE_URIS) + + if !op_services.empty? + return op_services + else + return openid_services + end + end + + def self.discover_yadis(uri) + # Discover OpenID services for a URI. Tries Yadis and falls back + # on old-style <link rel='...'> discovery if Yadis fails. + # + # @param uri: normalized identity URL + # @type uri: str + # + # @return: (claimed_id, services) + # @rtype: (str, list(OpenIDServiceEndpoint)) + # + # @raises DiscoveryFailure: when discovery fails. + + # Might raise a yadis.discover.DiscoveryFailure if no document + # came back for that URI at all. I don't think falling back to + # OpenID 1.0 discovery on the same URL will help, so don't bother + # to catch it. + response = Yadis.discover(uri) + + yadis_url = response.normalized_uri + body = response.response_text + + begin + openid_services = OpenIDServiceEndpoint.from_xrds(yadis_url, body) + rescue Yadis::XRDSError + # Does not parse as a Yadis XRDS file + openid_services = [] + end + + if openid_services.empty? + # Either not an XRDS or there are no OpenID services. + + if response.is_xrds + # if we got the Yadis content-type or followed the Yadis + # header, re-fetch the document without following the Yadis + # header, with no Accept header. + return self.discover_no_yadis(uri) + end + + # Try to parse the response as HTML. + # <link rel="..."> + openid_services = OpenIDServiceEndpoint.from_html(yadis_url, body) + end + + return [yadis_url, self.get_op_or_user_services(openid_services)] + end + + def self.discover_xri(iname) + endpoints = [] + iname = self.normalize_xri(iname) + + begin + canonical_id, services = Yadis::XRI::ProxyResolver.new().query( + iname, OpenIDServiceEndpoint::OPENID_TYPE_URIS) + + if canonical_id.nil? + raise Yadis::XRDSError.new(sprintf('No CanonicalID found for XRI %s', iname)) + end + + flt = Yadis.make_filter(OpenIDServiceEndpoint) + + services.each { |service_element| + endpoints += flt.get_service_endpoints(iname, service_element) + } + rescue Yadis::XRDSError => why + Util.log('xrds error on ' + iname + ': ' + why.to_s) + end + + endpoints.each { |endpoint| + # Is there a way to pass this through the filter to the endpoint + # constructor instead of tacking it on after? + endpoint.canonical_id = canonical_id + endpoint.claimed_id = canonical_id + endpoint.display_identifier = iname + } + + # FIXME: returned xri should probably be in some normal form + return [iname, self.get_op_or_user_services(endpoints)] + end + + def self.discover_no_yadis(uri) + http_resp = OpenID.fetch(uri) + if http_resp.code != "200" and http_resp.code != "206" + raise DiscoveryFailure.new( + "HTTP Response status from identity URL host is not \"200\". "\ + "Got status #{http_resp.code.inspect}", http_resp) + end + + claimed_id = http_resp.final_url + openid_services = OpenIDServiceEndpoint.from_html( + claimed_id, http_resp.body) + return [claimed_id, openid_services] + end + + def self.discover_uri(uri) + # Hack to work around URI parsing for URls with *no* scheme. + if uri.index("://").nil? + uri = 'http://' + uri + end + + begin + parsed = URI::parse(uri) + rescue URI::InvalidURIError => why + raise DiscoveryFailure.new("URI is not valid: #{why.message}", nil) + end + + if !parsed.scheme.nil? and !parsed.scheme.empty? + if !['http', 'https'].member?(parsed.scheme) + raise DiscoveryFailure.new( + "URI scheme #{parsed.scheme} is not HTTP or HTTPS", nil) + end + end + + uri = self.normalize_url(uri) + claimed_id, openid_services = self.discover_yadis(uri) + claimed_id = self.normalize_url(claimed_id) + return [claimed_id, openid_services] + end + + def self.discover(identifier) + if Yadis::XRI::identifier_scheme(identifier) == :xri + normalized_identifier, services = discover_xri(identifier) + else + return discover_uri(identifier) + end + end +end |