module OpenID::Yadis
Constants
- XRDS_NAMESPACES
- XRDS_NS
- XRD_NS_2_0
- YADIS_ACCEPT_HEADER
A value suitable for using as an accept header when performing YADIS discovery, unless the application has special requirements
- YADIS_CONTENT_TYPE
- YADIS_HEADER_NAME
Public Class Methods
# File lib/openid/yadis/services.rb, line 27 def Yadis.apply_filter(normalized_uri, xrd_data, flt=nil) # Generate an iterable of endpoint objects given this input data, # presumably from the result of performing the Yadis protocol. flt = Yadis.make_filter(flt) et = Yadis.parseXRDS(xrd_data) endpoints = [] each_service(et) { |service_element| endpoints += flt.get_service_endpoints(normalized_uri, service_element) } return endpoints end
# File lib/openid/yadis/xrds.rb, line 110 def Yadis::disable_entity_expansion _previous_ = REXML::Document::entity_expansion_limit REXML::Document::entity_expansion_limit = 0 yield ensure REXML::Document::entity_expansion_limit = _previous_ end
Discover services for a given URI.
uri: The identity URI as a well-formed http or https URI. The well-formedness and the protocol are not checked, but the results of this function are undefined if those properties do not hold.
returns a DiscoveryResult object
Raises DiscoveryFailure when the HTTP response does not have a 200 code.
# File lib/openid/yadis/discovery.rb, line 74 def self.discover(uri) result = DiscoveryResult.new(uri) begin resp = OpenID.fetch(uri, nil, {'Accept' => YADIS_ACCEPT_HEADER}) rescue Exception raise DiscoveryFailure.new("Failed to fetch identity URL #{uri} : #{$!}", $!) end if resp.code != "200" and resp.code != "206" raise DiscoveryFailure.new( "HTTP Response status from identity URL host is not \"200\"." "Got status #{resp.code.inspect} for #{resp.final_url}", resp) end # Note the URL after following redirects result.normalized_uri = resp.final_url # Attempt to find out where to go to discover the document or if # we already have it result.content_type = resp['content-type'] result.xrds_uri = self.where_is_yadis?(resp) if result.xrds_uri and result.used_yadis_location? begin resp = OpenID.fetch(result.xrds_uri) rescue raise DiscoveryFailure.new("Failed to fetch Yadis URL #{result.xrds_uri} : #{$!}", $!) end if resp.code != "200" and resp.code != "206" exc = DiscoveryFailure.new( "HTTP Response status from Yadis host is not \"200\". " + "Got status #{resp.code.inspect} for #{resp.final_url}", resp) exc.identity_url = result.normalized_uri raise exc end result.content_type = resp['content-type'] end result.response_text = resp.body return result end
aka iterServices in Python
# File lib/openid/yadis/xrds.rb, line 135 def Yadis::each_service(xrds_tree, &block) xrd = get_yadis_xrd(xrds_tree) xrd.each_element('Service', &block) end
# File lib/openid/yadis/xrds.rb, line 148 def Yadis::expand_service(service_element) es = service_element.elements uris = es.each('URI') { |u| } uris = prio_sort(uris) types = es.each('Type/text()') # REXML::Text objects are not strings. types = types.collect { |t| t.to_s } uris.collect { |uri| [types, uri.text, service_element] } end
Generate an accept header value
- str or (str, float)
-
-> str
# File lib/openid/yadis/accept.rb, line 8 def self.generate_accept_header(*elements) parts = [] elements.each { |element| if element.is_a?(String) qs = "1.0" mtype = element else mtype, q = element q = q.to_f if q > 1 or q <= 0 raise ArgumentError.new("Invalid preference factor: #{q}") end qs = sprintf("%0.1f", q) end parts << [qs, mtype] } parts.sort! chunks = [] parts.each { |q, mtype| if q == '1.0' chunks << mtype else chunks << sprintf("%s; q=%s", mtype, q) end } return chunks.join(', ') end
# File lib/openid/yadis/accept.rb, line 132 def self.get_acceptable(accept_header, have_types) # Parse the accept header and return a list of available types # in preferred order. If a type is unacceptable, it will not be # in the resulting list. # # This is a convenience wrapper around matchTypes and # parse_accept_header # # (str, [str]) -> [str] accepted = self.parse_accept_header(accept_header) preferred = self.match_types(accepted, have_types) return preferred.collect { |mtype, _| mtype } end
# File lib/openid/yadis/xrds.rb, line 25 def Yadis::get_canonical_id(iname, xrd_tree) # Return the CanonicalID from this XRDS document. # # @param iname: the XRI being resolved. # @type iname: unicode # # @param xrd_tree: The XRDS output from the resolver. # # @returns: The XRI CanonicalID or None. # @returntype: unicode or None xrd_list = [] REXML::XPath::match(xrd_tree.root, '/xrds:XRDS/xrd:XRD', XRDS_NAMESPACES).each { |el| xrd_list << el } xrd_list.reverse! cid_elements = [] if !xrd_list.empty? xrd_list[0].elements.each { |e| if !e.respond_to?('name') next end if e.name == 'CanonicalID' cid_elements << e end } end cid_element = cid_elements[0] if !cid_element return nil end canonicalID = XRI.make_xri(cid_element.text) childID = canonicalID.downcase xrd_list[1..-1].each { |xrd| parent_sought = childID[0...childID.rindex('!')] parent = XRI.make_xri(xrd.elements["CanonicalID"].text) if parent_sought != parent.downcase raise XRDSFraud.new(sprintf("%s can not come from %s", parent_sought, parent)) end childID = parent_sought } root = XRI.root_authority(iname) if not XRI.provider_is_authoritative(root, childID) raise XRDSFraud.new(sprintf("%s can not come from root %s", childID, root)) end return canonicalID end
# File lib/openid/yadis/services.rb, line 8 def Yadis.get_service_endpoints(input_url, flt=nil) # Perform the Yadis protocol on the input URL and return an # iterable of resulting endpoint objects. # # @param flt: A filter object or something that is convertable # to a filter object (using mkFilter) that will be used to # generate endpoint objects. This defaults to generating # BasicEndpoint objects. result = Yadis.discover(input_url) begin endpoints = Yadis.apply_filter(result.normalized_uri, result.response_text, flt) rescue XRDSError => err raise DiscoveryFailure.new(err.to_s, nil) end return [result.normalized_uri, endpoints] end
# File lib/openid/yadis/xrds.rb, line 125 def Yadis::get_yadis_xrd(xrds_tree) REXML::XPath.each(xrds_tree.root, '/xrds:XRDS/xrd:XRD[last()]', XRDS_NAMESPACES) { |el| return el } raise XRDSError.new("No XRD element found.") end
# File lib/openid/yadis/parsehtml.rb, line 6 def Yadis.html_yadis_location(html) parser = HTMLTokenizer.new(html) # to keep track of whether or not we are in the head element in_head = false begin while el = parser.getTag('head', '/head', 'meta', 'body', '/body', 'html', 'script') # we are leaving head or have reached body, so we bail return nil if ['/head', 'body', '/body'].member?(el.tag_name) if el.tag_name == 'head' unless el.to_s[-2] == ?/ # tag ends with a /: a short tag in_head = true end end next unless in_head if el.tag_name == 'script' unless el.to_s[-2] == ?/ # tag ends with a /: a short tag parser.getTag('/script') end end return nil if el.tag_name == 'html' if el.tag_name == 'meta' and (equiv = el.attr_hash['http-equiv']) if ['x-xrds-location','x-yadis-location'].member?(equiv.downcase) && el.attr_hash.member?('content') return CGI::unescapeHTML(el.attr_hash['content']) end end end rescue HTMLTokenizerError # just stop parsing if there's an error end end
# File lib/openid/yadis/xrds.rb, line 118 def Yadis::is_xrds?(xrds_tree) xrds_root = xrds_tree.root return (!xrds_root.nil? and xrds_root.name == 'XRDS' and xrds_root.namespace == XRDS_NS) end
Convert a filter-convertable thing into a filter
parts should be a filter, an endpoint, a callable, or a list of any of these.
# File lib/openid/yadis/filters.rb, line 145 def self.make_filter(parts) # Convert the parts into a list, and pass to mk_compound_filter if parts.nil? parts = [BasicServiceEndpoint] end if parts.is_a?(Array) return mk_compound_filter(parts) else return mk_compound_filter([parts]) end end
# File lib/openid/yadis/accept.rb, line 80 def self.match_types(accept_types, have_types) # Given the result of parsing an Accept: header, and the # available MIME types, return the acceptable types with their # quality markdowns. # # For example: # # >>> acceptable = parse_accept_header('text/html, text/plain; q=0.5') # >>> matchTypes(acceptable, ['text/plain', 'text/html', 'image/jpeg']) # [('text/html', 1.0), ('text/plain', 0.5)] # # Type signature: ([(str, str, float)], [str]) -> [(str, float)] if accept_types.nil? or accept_types == [] # Accept all of them default = 1 else default = 0 end match_main = {} match_sub = {} accept_types.each { |main, sub, q| if main == '*' default = [default, q].max next elsif sub == '*' match_main[main] = [match_main.fetch(main, 0), q].max else match_sub[[main, sub]] = [match_sub.fetch([main, sub], 0), q].max end } accepted_list = [] order_maintainer = 0 have_types.each { |mtype| main, sub = mtype.split('/', 2) if match_sub.member?([main, sub]) q = match_sub[[main, sub]] else q = match_main.fetch(main, default) end if q != 0 accepted_list << [1 - q, order_maintainer, q, mtype] order_maintainer += 1 end } accepted_list.sort! return accepted_list.collect { |_, _, q, mtype| [mtype, q] } end
Create a filter out of a list of filter-like things
Used by ::make_filter
parts should be a list of things that can be passed to ::make_filter
# File lib/openid/yadis/filters.rb, line 163 def self.mk_compound_filter(parts) if !parts.respond_to?('each') raise TypeError, "#{parts.inspect} is not iterable" end # Separate into a list of callables and a list of filter objects transformers = [] filters = [] parts.each { |subfilter| if !subfilter.is_a?(Array) # If it's not an iterable if subfilter.respond_to?('get_service_endpoints') # It's a full filter filters << subfilter elsif subfilter.respond_to?('from_basic_service_endpoint') # It's an endpoint object, so put its endpoint conversion # attribute into the list of endpoint transformers transformers << subfilter.method('from_basic_service_endpoint') elsif subfilter.respond_to?('call') # It's a proc, so add it to the list of endpoint # transformers transformers << subfilter else raise @@filter_type_error end else filters << mk_compound_filter(subfilter) end } if transformers.length > 0 filters << TransformFilterMaker.new(transformers) end if filters.length == 1 return filters[0] else return CompoundFilter.new(filters) end end
# File lib/openid/yadis/xrds.rb, line 90 def Yadis::parseXRDS(text) disable_entity_expansion do if text.nil? raise XRDSError.new("Not an XRDS document.") end begin d = REXML::Document.new(text) rescue RuntimeError raise XRDSError.new("Not an XRDS document. Failed to parse XML.") end if is_xrds?(d) return d else raise XRDSError.new("Not an XRDS document.") end end end
# File lib/openid/yadis/accept.rb, line 39 def self.parse_accept_header(value) # Parse an accept header, ignoring any accept-extensions # # returns a list of tuples containing main MIME type, MIME # subtype, and quality markdown. # # str -> [(str, str, float)] chunks = value.split(',', -1).collect { |v| v.strip } accept = [] chunks.each { |chunk| parts = chunk.split(";", -1).collect { |s| s.strip } mtype = parts.shift if mtype.index('/').nil? # This is not a MIME type, so ignore the bad data next end main, sub = mtype.split('/', 2) q = nil parts.each { |ext| if !ext.index('=').nil? k, v = ext.split('=', 2) if k == 'q' q = v.to_f end end } q = 1.0 if q.nil? accept << [q, main, sub] } accept.sort! accept.reverse! return accept.collect { |q, main, sub| [main, sub, q] } end
Sort a list of elements that have priority attributes.
# File lib/openid/yadis/xrds.rb, line 159 def Yadis::prio_sort(elements) elements.sort { |a,b| a.attribute('priority').to_s.to_i <=> b.attribute('priority').to_s.to_i } end
# File lib/openid/yadis/xrds.rb, line 140 def Yadis::services(xrds_tree) s = [] each_service(xrds_tree) { |service| s << service } return s end
Given a HTTPResponse, return the location of the Yadis document.
May be the URL just retrieved, another URL, or None, if I can't find any.
- non-blocking
# File lib/openid/yadis/discovery.rb, line 124 def self.where_is_yadis?(resp) # Attempt to find out where to go to discover the document or if # we already have it content_type = resp['content-type'] # According to the spec, the content-type header must be an # exact match, or else we have to look for an indirection. if (!content_type.nil? and !content_type.to_s.empty? and content_type.split(';', 2)[0].downcase == YADIS_CONTENT_TYPE) return resp.final_url else # Try the header yadis_loc = resp[YADIS_HEADER_NAME.downcase] if yadis_loc.nil? # Parse as HTML if the header is missing. # # XXX: do we want to do something with content-type, like # have a whitelist or a blacklist (for detecting that it's # HTML)? yadis_loc = Yadis.html_yadis_location(resp.body) end end return yadis_loc end