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

apply_filter(normalized_uri, xrd_data, flt=nil) click to toggle source
# 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
disable_entity_expansion() { || ... } click to toggle source
# 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(uri) click to toggle source

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
each_service(xrds_tree, &block) click to toggle source

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
expand_service(service_element) click to toggle source
# 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_accept_header(*elements) click to toggle source

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
get_acceptable(accept_header, have_types) click to toggle source
# 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) click to toggle source
# 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
get_service_endpoints(input_url, flt=nil) click to toggle source
# 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) click to toggle source
# 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
html_yadis_location(html) click to toggle source
# 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) click to toggle source
# 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
make_filter(parts) click to toggle source

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
match_types(accept_types, have_types) click to toggle source
# 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) click to toggle source

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
parseXRDS(text) click to toggle source
# 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
parse_accept_header(value) click to toggle source
# 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
prio_sort(elements) click to toggle source

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
services(xrds_tree) click to toggle source
# File lib/openid/yadis/xrds.rb, line 140
def Yadis::services(xrds_tree)
  s = []
  each_service(xrds_tree) { |service|
    s << service
  }
  return s
end
where_is_yadis?(resp) click to toggle source

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