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
- .disable_entity_expansion ⇒ 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
29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/openid/yadis/services.rb', line 29 def self.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) do |service_element| endpoints += flt.get_service_endpoints(normalized_uri, service_element) end endpoints end |
.disable_entity_expansion ⇒ Object
103 104 105 106 107 108 109 |
# File 'lib/openid/yadis/xrds.rb', line 103 def self.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(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.
70 71 72 73 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 70 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 = where_is_yadis?(resp) if result.xrds_uri and result.used_yadis_location? begin resp = OpenID.fetch(result.xrds_uri) rescue StandardError 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 result end |
.each_service(xrds_tree, &block) ⇒ Object
aka iterServices in Python
130 131 132 133 |
# File 'lib/openid/yadis/xrds.rb', line 130 def self.each_service(xrds_tree, &block) xrd = get_yadis_xrd(xrds_tree) xrd.each_element("Service", &block) end |
.expand_service(service_element) ⇒ Object
143 144 145 146 147 148 149 150 151 |
# File 'lib/openid/yadis/xrds.rb', line 143 def self.(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
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 |
# File 'lib/openid/yadis/accept.rb', line 6 def self.generate_accept_header(*elements) parts = [] elements.each do |element| if element.is_a?(String) qs = "1.0" mtype = element else mtype, q = element q = q.to_f raise ArgumentError.new("Invalid preference factor: #{q}") if q > 1 or q <= 0 qs = format("%0.1f", q) end parts << [qs, mtype] end parts.sort! chunks = [] parts.each do |q, mtype| chunks << if q == "1.0" mtype else format("%s; q=%s", mtype, q) end end chunks.join(", ") end |
.get_acceptable(accept_header, have_types) ⇒ Object
127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/openid/yadis/accept.rb', line 127 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 = parse_accept_header(accept_header) preferred = match_types(accepted, have_types) preferred.collect { |mtype, _| mtype } end |
.get_canonical_id(iname, xrd_tree) ⇒ Object
24 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 |
# File 'lib/openid/yadis/xrds.rb', line 24 def self.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 do |el| xrd_list << el end xrd_list.reverse! cid_elements = [] unless xrd_list.empty? xrd_list[0].elements.each do |e| next unless e.respond_to?(:name) cid_elements << e if e.name == "CanonicalID" end end cid_element = cid_elements[0] return unless cid_element canonical_id = XRI.make_xri(cid_element.text) child_id = canonical_id.downcase xrd_list[1..-1].each do |xrd| parent_sought = child_id[0...child_id.rindex("!")] parent = XRI.make_xri(xrd.elements["CanonicalID"].text) if parent_sought != parent.downcase raise XRDSFraud.new(format( "%s can not come from %s", parent_sought, parent, )) end child_id = parent_sought end root = XRI.(iname) unless XRI.(root, child_id) raise XRDSFraud.new(format("%s can not come from root %s", child_id, root)) end canonical_id end |
.get_service_endpoints(input_url, flt = nil) ⇒ Object
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# File 'lib/openid/yadis/services.rb', line 7 def self.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 => e raise DiscoveryFailure.new(e.to_s, nil) end [result.normalized_uri, endpoints] end |
.get_yadis_xrd(xrds_tree) ⇒ Object
118 119 120 121 122 123 124 125 126 127 |
# File 'lib/openid/yadis/xrds.rb', line 118 def self.get_yadis_xrd(xrds_tree) REXML::XPath.each( xrds_tree.root, "/xrds:XRDS/xrd:XRD[last()]", XRDS_NAMESPACES, ) do |el| return el end raise XRDSError.new("No XRD element found.") end |
.html_yadis_location(html) ⇒ Object
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 44 45 46 47 48 |
# File 'lib/openid/yadis/parsehtml.rb', line 9 def self.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 if ["/head", "body", "/body"].member?(el.tag_name) if el.tag_name == "head" && !(el.to_s[-2] == "/") in_head = true # tag ends with a /: a short tag end next unless in_head if el.tag_name == "script" && !(el.to_s[-2] == "/") parser.getTag("/script") # tag ends with a /: a short tag end return if el.tag_name == "html" next unless el.tag_name == "meta" and (equiv = el.attr_hash["http-equiv"]) if %w[x-xrds-location x-yadis-location].member?(equiv.downcase) && el.attr_hash.member?("content") return CGI.unescapeHTML(el.attr_hash["content"]) end end rescue HTMLTokenizerError # just stop parsing if there's an error end end |
.is_xrds?(xrds_tree) ⇒ Boolean
111 112 113 114 115 116 |
# File 'lib/openid/yadis/xrds.rb', line 111 def self.is_xrds?(xrds_tree) xrds_root = xrds_tree.root (!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.
143 144 145 146 147 148 149 150 |
# File 'lib/openid/yadis/filters.rb', line 143 def self.make_filter(parts) # Convert the parts into a list, and pass to mk_compound_filter parts = [BasicServiceEndpoint] if parts.nil? return mk_compound_filter(parts) if parts.is_a?(Array) mk_compound_filter([parts]) end |
.match_types(accept_types, have_types) ⇒ Object
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 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/openid/yadis/accept.rb', line 75 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)] default = if accept_types.nil? or accept_types == [] # Accept all of them 1 else 0 end match_main = {} match_sub = {} accept_types.each do |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 end accepted_list = [] order_maintainer = 0 have_types.each do |mtype| main, sub = mtype.split("/", 2) q = if match_sub.member?([main, sub]) match_sub[[main, sub]] else match_main.fetch(main, default) end if q != 0 accepted_list << [1 - q, order_maintainer, q, mtype] order_maintainer += 1 end end accepted_list.sort! 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
157 158 159 160 161 162 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 |
# File 'lib/openid/yadis/filters.rb', line 157 def self.mk_compound_filter(parts) raise TypeError, "#{parts.inspect} is not iterable" unless parts.respond_to?(:each) # Separate into a list of callables and a list of filter objects transformers = [] filters = [] parts.each do |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 end filters << TransformFilterMaker.new(transformers) if transformers.length > 0 return filters[0] if filters.length == 1 CompoundFilter.new(filters) end |
.parse_accept_header(value) ⇒ Object
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 |
# File 'lib/openid/yadis/accept.rb', line 36 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 do |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 do |ext| unless ext.index("=").nil? k, v = ext.split("=", 2) q = v.to_f if k == "q" end end q = 1.0 if q.nil? accept << [q, main, sub] end accept.sort! accept.reverse! accept.collect { |q, main, sub| [main, sub, q] } end |
.parseXRDS(text) ⇒ Object
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/openid/yadis/xrds.rb', line 87 def self.parseXRDS(text) disable_entity_expansion do raise XRDSError.new("Not an XRDS document.") if text.nil? begin d = REXML::Document.new(text) rescue RuntimeError raise XRDSError.new("Not an XRDS document. Failed to parse XML.") end return d if is_xrds?(d) raise XRDSError.new("Not an XRDS document.") end end |
.prio_sort(elements) ⇒ Object
Sort a list of elements that have priority attributes.
154 155 156 157 158 |
# File 'lib/openid/yadis/xrds.rb', line 154 def self.prio_sort(elements) elements.sort do |a, b| a.attribute("priority").to_s.to_i <=> b.attribute("priority").to_s.to_i end end |
.services(xrds_tree) ⇒ Object
135 136 137 138 139 140 141 |
# File 'lib/openid/yadis/xrds.rb', line 135 def self.services(xrds_tree) s = [] each_service(xrds_tree) do |service| s << service end 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 yadis_loc end |