Module: OpenID::Yadis
- Defined in:
- lib/openid/yadis/xri.rb,
lib/openid/yadis/xrds.rb,
lib/openid/yadis/accept.rb,
lib/openid/yadis/xrires.rb,
lib/openid/yadis/filters.rb,
lib/openid/yadis/services.rb,
lib/openid/yadis/constants.rb,
lib/openid/yadis/discovery.rb,
lib/openid/yadis/parsehtml.rb
Defined Under Namespace
Modules: XRI Classes: BasicServiceEndpoint, CompoundFilter, DiscoveryResult, TransformFilterMaker, XRDSError, XRDSFraud
Constant Summary collapse
- XRD_NS_2_0 =
'xri://$xrd*($v*2.0)'
- XRDS_NS =
'xri://$xrds'
- XRDS_NAMESPACES =
{ 'xrds' => XRDS_NS, 'xrd' => XRD_NS_2_0, }
- YADIS_HEADER_NAME =
'X-XRDS-Location'
- YADIS_CONTENT_TYPE =
'application/xrds+xml'
- YADIS_ACCEPT_HEADER =
A value suitable for using as an accept header when performing YADIS discovery, unless the application has special requirements
generate_accept_header( ['text/html', 0.3], ['application/xhtml+xml', 0.5], [YADIS_CONTENT_TYPE, 1.0] )
- @@filter_type_error =
Exception raised when something is not able to be turned into a filter
TypeError.new( 'Expected a filter, an endpoint, a callable or a list of any of these.')
Class Method Summary collapse
- .apply_filter(normalized_uri, xrd_data, flt = nil) ⇒ Object
-
.discover(uri) ⇒ Object
Discover services for a given URI.
-
.each_service(xrds_tree, &block) ⇒ Object
aka iterServices in Python.
- .expand_service(service_element) ⇒ Object
-
.generate_accept_header(*elements) ⇒ Object
Generate an accept header value.
- .get_acceptable(accept_header, have_types) ⇒ Object
- .get_canonical_id(iname, xrd_tree) ⇒ Object
- .get_service_endpoints(input_url, flt = nil) ⇒ Object
- .get_yadis_xrd(xrds_tree) ⇒ Object
- .html_yadis_location(html) ⇒ Object
- .is_xrds?(xrds_tree) ⇒ Boolean
-
.make_filter(parts) ⇒ Object
Convert a filter-convertable thing into a filter.
- .match_types(accept_types, have_types) ⇒ Object
-
.mk_compound_filter(parts) ⇒ Object
Create a filter out of a list of filter-like things.
- .parse_accept_header(value) ⇒ Object
- .parseXRDS(text) ⇒ Object
-
.prio_sort(elements) ⇒ Object
Sort a list of elements that have priority attributes.
- .services(xrds_tree) ⇒ Object
-
.where_is_yadis?(resp) ⇒ Boolean
Given a HTTPResponse, return the location of the Yadis document.
Class Method Details
.apply_filter(normalized_uri, xrd_data, flt = nil) ⇒ Object
27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# 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 |
.discover(uri) ⇒ Object
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.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# 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 |
.each_service(xrds_tree, &block) ⇒ Object
aka iterServices in Python
125 126 127 128 |
# File 'lib/openid/yadis/xrds.rb', line 125 def Yadis::each_service(xrds_tree, &block) xrd = get_yadis_xrd(xrds_tree) xrd.each_element('Service', &block) end |
.expand_service(service_element) ⇒ Object
138 139 140 141 142 143 144 145 146 |
# File 'lib/openid/yadis/xrds.rb', line 138 def Yadis::(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_accept_header(*elements) ⇒ Object
Generate an accept header value
- str or (str, float)
-
-> str
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# 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 |
.get_acceptable(accept_header, have_types) ⇒ Object
132 133 134 135 136 137 138 139 140 141 142 143 144 |
# 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 |
.get_canonical_id(iname, xrd_tree) ⇒ Object
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# 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.(iname) if not XRI.(root, childID) raise XRDSFraud.new(sprintf("%s can not come from root %s", childID, root)) end return canonicalID end |
.get_service_endpoints(input_url, flt = nil) ⇒ Object
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# 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 |
.get_yadis_xrd(xrds_tree) ⇒ Object
115 116 117 118 119 120 121 122 |
# File 'lib/openid/yadis/xrds.rb', line 115 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 |
.html_yadis_location(html) ⇒ Object
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# 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 |
.is_xrds?(xrds_tree) ⇒ Boolean
108 109 110 111 112 113 |
# File 'lib/openid/yadis/xrds.rb', line 108 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 |
.make_filter(parts) ⇒ Object
Convert a filter-convertable thing into a filter
parts should be a filter, an endpoint, a callable, or a list of any of these.
145 146 147 148 149 150 151 152 153 154 155 156 |
# 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 |
.match_types(accept_types, have_types) ⇒ Object
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# 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 |
.mk_compound_filter(parts) ⇒ Object
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
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# 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 |
.parse_accept_header(value) ⇒ Object
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# 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 |
.parseXRDS(text) ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/openid/yadis/xrds.rb', line 90 def Yadis::parseXRDS(text) if text.nil? raise XRDSError.new("Not an XRDS document.") end begin d = REXML::Document.new(text) rescue RuntimeError => why 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 |
.prio_sort(elements) ⇒ Object
Sort a list of elements that have priority attributes.
149 150 151 152 153 |
# File 'lib/openid/yadis/xrds.rb', line 149 def Yadis::prio_sort(elements) elements.sort { |a,b| a.attribute('priority').to_s.to_i <=> b.attribute('priority').to_s.to_i } end |
.services(xrds_tree) ⇒ Object
130 131 132 133 134 135 136 |
# File 'lib/openid/yadis/xrds.rb', line 130 def Yadis::services(xrds_tree) s = [] each_service(xrds_tree) { |service| s << service } return s end |
.where_is_yadis?(resp) ⇒ Boolean
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
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# 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 |