diff options
Diffstat (limited to 'vendor/gems/ruby-openid-2.1.4/lib')
43 files changed, 8811 insertions, 0 deletions
diff --git a/vendor/gems/ruby-openid-2.1.4/lib/hmac/hmac.rb b/vendor/gems/ruby-openid-2.1.4/lib/hmac/hmac.rb new file mode 100644 index 000000000..e8bfa42bc --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/hmac/hmac.rb @@ -0,0 +1,112 @@ +# Copyright (C) 2001 Daiki Ueno <ueno@unixuser.org> +# This library is distributed under the terms of the Ruby license. + +# This module provides common interface to HMAC engines. +# HMAC standard is documented in RFC 2104: +# +# H. Krawczyk et al., "HMAC: Keyed-Hashing for Message Authentication", +# RFC 2104, February 1997 +# +# These APIs are inspired by JCE 1.2's javax.crypto.Mac interface. +# +# <URL:http://java.sun.com/security/JCE1.2/spec/apidoc/javax/crypto/Mac.html> + +module HMAC + class Base + def initialize(algorithm, block_size, output_length, key) + @algorithm = algorithm + @block_size = block_size + @output_length = output_length + @status = STATUS_UNDEFINED + @key_xor_ipad = '' + @key_xor_opad = '' + set_key(key) unless key.nil? + end + + private + def check_status + unless @status == STATUS_INITIALIZED + raise RuntimeError, + "The underlying hash algorithm has not yet been initialized." + end + end + + public + def set_key(key) + # If key is longer than the block size, apply hash function + # to key and use the result as a real key. + key = @algorithm.digest(key) if key.size > @block_size + key_xor_ipad = "\x36" * @block_size + key_xor_opad = "\x5C" * @block_size + for i in 0 .. key.size - 1 + key_xor_ipad[i] ^= key[i] + key_xor_opad[i] ^= key[i] + end + @key_xor_ipad = key_xor_ipad + @key_xor_opad = key_xor_opad + @md = @algorithm.new + @status = STATUS_INITIALIZED + end + + def reset_key + @key_xor_ipad.gsub!(/./, '?') + @key_xor_opad.gsub!(/./, '?') + @key_xor_ipad[0..-1] = '' + @key_xor_opad[0..-1] = '' + @status = STATUS_UNDEFINED + end + + def update(text) + check_status + # perform inner H + md = @algorithm.new + md.update(@key_xor_ipad) + md.update(text) + str = md.digest + # perform outer H + md = @algorithm.new + md.update(@key_xor_opad) + md.update(str) + @md = md + end + alias << update + + def digest + check_status + @md.digest + end + + def hexdigest + check_status + @md.hexdigest + end + alias to_s hexdigest + + # These two class methods below are safer than using above + # instance methods combinatorially because an instance will have + # held a key even if it's no longer in use. + def Base.digest(key, text) + begin + hmac = self.new(key) + hmac.update(text) + hmac.digest + ensure + hmac.reset_key + end + end + + def Base.hexdigest(key, text) + begin + hmac = self.new(key) + hmac.update(text) + hmac.hexdigest + ensure + hmac.reset_key + end + end + + private_class_method :new, :digest, :hexdigest + end + + STATUS_UNDEFINED, STATUS_INITIALIZED = 0, 1 +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/hmac/sha1.rb b/vendor/gems/ruby-openid-2.1.4/lib/hmac/sha1.rb new file mode 100644 index 000000000..d2f0088a3 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/hmac/sha1.rb @@ -0,0 +1,11 @@ +require 'hmac/hmac' +require 'digest/sha1' + +module HMAC + class SHA1 < Base + def initialize(key = nil) + super(Digest::SHA1, 64, 20, key) + end + public_class_method :new, :digest, :hexdigest + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/hmac/sha2.rb b/vendor/gems/ruby-openid-2.1.4/lib/hmac/sha2.rb new file mode 100644 index 000000000..0412ba408 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/hmac/sha2.rb @@ -0,0 +1,25 @@ +require 'hmac/hmac' +require 'digest/sha2' + +module HMAC + class SHA256 < Base + def initialize(key = nil) + super(Digest::SHA256, 64, 32, key) + end + public_class_method :new, :digest, :hexdigest + end + + class SHA384 < Base + def initialize(key = nil) + super(Digest::SHA384, 128, 48, key) + end + public_class_method :new, :digest, :hexdigest + end + + class SHA512 < Base + def initialize(key = nil) + super(Digest::SHA512, 128, 64, key) + end + public_class_method :new, :digest, :hexdigest + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid.rb new file mode 100644 index 000000000..0a5643451 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid.rb @@ -0,0 +1,20 @@ +# Copyright 2006-2007 JanRain, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you +# may not use this file except in compliance with the License. You may +# obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +module OpenID + VERSION = "2.1.4" +end + +require "openid/consumer" +require 'openid/server' diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/association.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/association.rb new file mode 100644 index 000000000..fd2cd5991 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/association.rb @@ -0,0 +1,249 @@ +require "openid/kvform" +require "openid/util" +require "openid/cryptutil" +require "openid/message" + +module OpenID + + def self.get_secret_size(assoc_type) + if assoc_type == 'HMAC-SHA1' + return 20 + elsif assoc_type == 'HMAC-SHA256' + return 32 + else + raise ArgumentError("Unsupported association type: #{assoc_type}") + end + end + + # An Association holds the shared secret between a relying party and + # an OpenID provider. + class Association + attr_reader :handle, :secret, :issued, :lifetime, :assoc_type + + FIELD_ORDER = + [:version, :handle, :secret, :issued, :lifetime, :assoc_type,] + + # Load a serialized Association + def self.deserialize(serialized) + parsed = Util.kv_to_seq(serialized) + parsed_fields = parsed.map{|k, v| k.to_sym} + if parsed_fields != FIELD_ORDER + raise ProtocolError, 'Unexpected fields in serialized association'\ + " (Expected #{FIELD_ORDER.inspect}, got #{parsed_fields.inspect})" + end + version, handle, secret64, issued_s, lifetime_s, assoc_type = + parsed.map {|field, value| value} + if version != '2' + raise ProtocolError, "Attempted to deserialize unsupported version "\ + "(#{parsed[0][1].inspect})" + end + + self.new(handle, + Util.from_base64(secret64), + Time.at(issued_s.to_i), + lifetime_s.to_i, + assoc_type) + end + + # Create an Association with an issued time of now + def self.from_expires_in(expires_in, handle, secret, assoc_type) + issued = Time.now + self.new(handle, secret, issued, expires_in, assoc_type) + end + + def initialize(handle, secret, issued, lifetime, assoc_type) + @handle = handle + @secret = secret + @issued = issued + @lifetime = lifetime + @assoc_type = assoc_type + end + + # Serialize the association to a form that's consistent across + # JanRain OpenID libraries. + def serialize + data = { + :version => '2', + :handle => handle, + :secret => Util.to_base64(secret), + :issued => issued.to_i.to_s, + :lifetime => lifetime.to_i.to_s, + :assoc_type => assoc_type, + } + + Util.assert(data.length == FIELD_ORDER.length) + + pairs = FIELD_ORDER.map{|field| [field.to_s, data[field]]} + return Util.seq_to_kv(pairs, strict=true) + end + + # The number of seconds until this association expires + def expires_in(now=nil) + if now.nil? + now = Time.now.to_i + else + now = now.to_i + end + time_diff = (issued.to_i + lifetime) - now + if time_diff < 0 + return 0 + else + return time_diff + end + end + + # Generate a signature for a sequence of [key, value] pairs + def sign(pairs) + kv = Util.seq_to_kv(pairs) + case assoc_type + when 'HMAC-SHA1' + CryptUtil.hmac_sha1(@secret, kv) + when 'HMAC-SHA256' + CryptUtil.hmac_sha256(@secret, kv) + else + raise ProtocolError, "Association has unknown type: "\ + "#{assoc_type.inspect}" + end + end + + # Generate the list of pairs that form the signed elements of the + # given message + def make_pairs(message) + signed = message.get_arg(OPENID_NS, 'signed') + if signed.nil? + raise ProtocolError, 'Missing signed list' + end + signed_fields = signed.split(',', -1) + data = message.to_post_args + signed_fields.map {|field| [field, data.fetch('openid.'+field,'')] } + end + + # Return whether the message's signature passes + def check_message_signature(message) + message_sig = message.get_arg(OPENID_NS, 'sig') + if message_sig.nil? + raise ProtocolError, "#{message} has no sig." + end + calculated_sig = get_message_signature(message) + return calculated_sig == message_sig + end + + # Get the signature for this message + def get_message_signature(message) + Util.to_base64(sign(make_pairs(message))) + end + + def ==(other) + (other.class == self.class and + other.handle == self.handle and + other.secret == self.secret and + + # The internals of the time objects seemed to differ + # in an opaque way when serializing/unserializing. + # I don't think this will be a problem. + other.issued.to_i == self.issued.to_i and + + other.lifetime == self.lifetime and + other.assoc_type == self.assoc_type) + end + + # Add a signature (and a signed list) to a message. + def sign_message(message) + if (message.has_key?(OPENID_NS, 'sig') or + message.has_key?(OPENID_NS, 'signed')) + raise ArgumentError, 'Message already has signed list or signature' + end + + extant_handle = message.get_arg(OPENID_NS, 'assoc_handle') + if extant_handle and extant_handle != self.handle + raise ArgumentError, "Message has a different association handle" + end + + signed_message = message.copy() + signed_message.set_arg(OPENID_NS, 'assoc_handle', self.handle) + message_keys = signed_message.to_post_args.keys() + + signed_list = [] + message_keys.each { |k| + if k.starts_with?('openid.') + signed_list << k[7..-1] + end + } + + signed_list << 'signed' + signed_list.sort! + + signed_message.set_arg(OPENID_NS, 'signed', signed_list.join(',')) + sig = get_message_signature(signed_message) + signed_message.set_arg(OPENID_NS, 'sig', sig) + return signed_message + end + end + + class AssociationNegotiator + attr_reader :allowed_types + + def self.get_session_types(assoc_type) + case assoc_type + when 'HMAC-SHA1' + ['DH-SHA1', 'no-encryption'] + when 'HMAC-SHA256' + ['DH-SHA256', 'no-encryption'] + else + raise ProtocolError, "Unknown association type #{assoc_type.inspect}" + end + end + + def self.check_session_type(assoc_type, session_type) + if !get_session_types(assoc_type).include?(session_type) + raise ProtocolError, "Session type #{session_type.inspect} not "\ + "valid for association type #{assoc_type.inspect}" + end + end + + def initialize(allowed_types) + self.allowed_types=(allowed_types) + end + + def copy + Marshal.load(Marshal.dump(self)) + end + + def allowed_types=(allowed_types) + allowed_types.each do |assoc_type, session_type| + self.class.check_session_type(assoc_type, session_type) + end + @allowed_types = allowed_types + end + + def add_allowed_type(assoc_type, session_type=nil) + if session_type.nil? + session_types = self.class.get_session_types(assoc_type) + else + self.class.check_session_type(assoc_type, session_type) + session_types = [session_type] + end + for session_type in session_types do + @allowed_types << [assoc_type, session_type] + end + end + + def allowed?(assoc_type, session_type) + @allowed_types.include?([assoc_type, session_type]) + end + + def get_allowed_type + @allowed_types.empty? ? nil : @allowed_types[0] + end + end + + DefaultNegotiator = + AssociationNegotiator.new([['HMAC-SHA1', 'DH-SHA1'], + ['HMAC-SHA1', 'no-encryption'], + ['HMAC-SHA256', 'DH-SHA256'], + ['HMAC-SHA256', 'no-encryption']]) + + EncryptedNegotiator = + AssociationNegotiator.new([['HMAC-SHA1', 'DH-SHA1'], + ['HMAC-SHA256', 'DH-SHA256']]) +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer.rb new file mode 100644 index 000000000..afe025a00 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer.rb @@ -0,0 +1,395 @@ +require "openid/consumer/idres.rb" +require "openid/consumer/checkid_request.rb" +require "openid/consumer/associationmanager.rb" +require "openid/consumer/responses.rb" +require "openid/consumer/discovery_manager" +require "openid/consumer/discovery" +require "openid/message" +require "openid/yadis/discovery" +require "openid/store/nonce" + +module OpenID + # OpenID support for Relying Parties (aka Consumers). + # + # This module documents the main interface with the OpenID consumer + # library. The only part of the library which has to be used and + # isn't documented in full here is the store required to create an + # Consumer instance. + # + # = OVERVIEW + # + # The OpenID identity verification process most commonly uses the + # following steps, as visible to the user of this library: + # + # 1. The user enters their OpenID into a field on the consumer's + # site, and hits a login button. + # + # 2. The consumer site discovers the user's OpenID provider using + # the Yadis protocol. + # + # 3. The consumer site sends the browser a redirect to the OpenID + # provider. This is the authentication request as described in + # the OpenID specification. + # + # 4. The OpenID provider's site sends the browser a redirect back to + # the consumer site. This redirect contains the provider's + # response to the authentication request. + # + # The most important part of the flow to note is the consumer's site + # must handle two separate HTTP requests in order to perform the + # full identity check. + # + # = LIBRARY DESIGN + # + # This consumer library is designed with that flow in mind. The + # goal is to make it as easy as possible to perform the above steps + # securely. + # + # At a high level, there are two important parts in the consumer + # library. The first important part is this module, which contains + # the interface to actually use this library. The second is + # openid/store/interface.rb, which describes the interface to use if + # you need to create a custom method for storing the state this + # library needs to maintain between requests. + # + # In general, the second part is less important for users of the + # library to know about, as several implementations are provided + # which cover a wide variety of situations in which consumers may + # use the library. + # + # The Consumer class has methods corresponding to the actions + # necessary in each of steps 2, 3, and 4 described in the overview. + # Use of this library should be as easy as creating an Consumer + # instance and calling the methods appropriate for the action the + # site wants to take. + # + # This library automatically detects which version of the OpenID + # protocol should be used for a transaction and constructs the + # proper requests and responses. Users of this library do not need + # to worry about supporting multiple protocol versions; the library + # supports them implicitly. Depending on the version of the + # protocol in use, the OpenID transaction may be more secure. See + # the OpenID specifications for more information. + # + # = SESSIONS, STORES, AND STATELESS MODE + # + # The Consumer object keeps track of two types of state: + # + # 1. State of the user's current authentication attempt. Things + # like the identity URL, the list of endpoints discovered for + # that URL, and in case where some endpoints are unreachable, the + # list of endpoints already tried. This state needs to be held + # from Consumer.begin() to Consumer.complete(), but it is only + # applicable to a single session with a single user agent, and at + # the end of the authentication process (i.e. when an OP replies + # with either <tt>id_res</tt>. or <tt>cancel</tt> it may be + # discarded. + # + # 2. State of relationships with servers, i.e. shared secrets + # (associations) with servers and nonces seen on signed messages. + # This information should persist from one session to the next + # and should not be bound to a particular user-agent. + # + # These two types of storage are reflected in the first two + # arguments of Consumer's constructor, <tt>session</tt> and + # <tt>store</tt>. <tt>session</tt> is a dict-like object and we + # hope your web framework provides you with one of these bound to + # the user agent. <tt>store</tt> is an instance of Store. + # + # Since the store does hold secrets shared between your application + # and the OpenID provider, you should be careful about how you use + # it in a shared hosting environment. If the filesystem or database + # permissions of your web host allow strangers to read from them, do + # not store your data there! If you have no safe place to store + # your data, construct your consumer with nil for the store, and it + # will operate only in stateless mode. Stateless mode may be + # slower, put more load on the OpenID provider, and trusts the + # provider to keep you safe from replay attacks. + # + # Several store implementation are provided, and the interface is + # fully documented so that custom stores can be used as well. See + # the documentation for the Consumer class for more information on + # the interface for stores. The implementations that are provided + # allow the consumer site to store the necessary data in several + # different ways, including several SQL databases and normal files + # on disk. + # + # = IMMEDIATE MODE + # + # In the flow described above, the user may need to confirm to the + # OpenID provider that it's ok to disclose his or her identity. The + # provider may draw pages asking for information from the user + # before it redirects the browser back to the consumer's site. This + # is generally transparent to the consumer site, so it is typically + # ignored as an implementation detail. + # + # There can be times, however, where the consumer site wants to get + # a response immediately. When this is the case, the consumer can + # put the library in immediate mode. In immediate mode, there is an + # extra response possible from the server, which is essentially the + # server reporting that it doesn't have enough information to answer + # the question yet. + # + # = USING THIS LIBRARY + # + # Integrating this library into an application is usually a + # relatively straightforward process. The process should basically + # follow this plan: + # + # Add an OpenID login field somewhere on your site. When an OpenID + # is entered in that field and the form is submitted, it should make + # a request to the your site which includes that OpenID URL. + # + # First, the application should instantiate a Consumer with a + # session for per-user state and store for shared state using the + # store of choice. + # + # Next, the application should call the <tt>begin</tt> method of + # Consumer instance. This method takes the OpenID URL as entered by + # the user. The <tt>begin</tt> method returns a CheckIDRequest + # object. + # + # Next, the application should call the redirect_url method on the + # CheckIDRequest object. The parameter <tt>return_to</tt> is the + # URL that the OpenID server will send the user back to after + # attempting to verify his or her identity. The <tt>realm</tt> + # parameter is the URL (or URL pattern) that identifies your web + # site to the user when he or she is authorizing it. Send a + # redirect to the resulting URL to the user's browser. + # + # That's the first half of the authentication process. The second + # half of the process is done after the user's OpenID Provider sends + # the user's browser a redirect back to your site to complete their + # login. + # + # When that happens, the user will contact your site at the URL + # given as the <tt>return_to</tt> URL to the redirect_url call made + # above. The request will have several query parameters added to + # the URL by the OpenID provider as the information necessary to + # finish the request. + # + # Get a Consumer instance with the same session and store as before + # and call its complete() method, passing in all the received query + # arguments and URL currently being handled. + # + # There are multiple possible return types possible from that + # method. These indicate the whether or not the login was + # successful, and include any additional information appropriate for + # their type. + class Consumer + attr_accessor :session_key_prefix + + # Initialize a Consumer instance. + # + # You should create a new instance of the Consumer object with + # every HTTP request that handles OpenID transactions. + # + # session: the session object to use to store request information. + # The session should behave like a hash. + # + # store: an object that implements the interface in Store. + def initialize(session, store) + @session = session + @store = store + @session_key_prefix = 'OpenID::Consumer::' + end + + # Start the OpenID authentication process. See steps 1-2 in the + # overview for the Consumer class. + # + # user_url: Identity URL given by the user. This method performs a + # textual transformation of the URL to try and make sure it is + # normalized. For example, a user_url of example.com will be + # normalized to http://example.com/ normalizing and resolving any + # redirects the server might issue. + # + # anonymous: A boolean value. Whether to make an anonymous + # request of the OpenID provider. Such a request does not ask for + # an authorization assertion for an OpenID identifier, but may be + # used with extensions to pass other data. e.g. "I don't care who + # you are, but I'd like to know your time zone." + # + # Returns a CheckIDRequest object containing the discovered + # information, with a method for building a redirect URL to the + # server, as described in step 3 of the overview. This object may + # also be used to add extension arguments to the request, using + # its add_extension_arg method. + # + # Raises DiscoveryFailure when no OpenID server can be found for + # this URL. + def begin(openid_identifier, anonymous=false) + manager = discovery_manager(openid_identifier) + service = manager.get_next_service(&method(:discover)) + + if service.nil? + raise DiscoveryFailure.new("No usable OpenID services were found "\ + "for #{openid_identifier.inspect}", nil) + else + begin_without_discovery(service, anonymous) + end + end + + # Start OpenID verification without doing OpenID server + # discovery. This method is used internally by Consumer.begin() + # after discovery is performed, and exists to provide an interface + # for library users needing to perform their own discovery. + # + # service: an OpenID service endpoint descriptor. This object and + # factories for it are found in the openid/consumer/discovery.rb + # module. + # + # Returns an OpenID authentication request object. + def begin_without_discovery(service, anonymous) + assoc = association_manager(service).get_association + checkid_request = CheckIDRequest.new(assoc, service) + checkid_request.anonymous = anonymous + + if service.compatibility_mode + rt_args = checkid_request.return_to_args + rt_args[Consumer.openid1_return_to_nonce_name] = Nonce.mk_nonce + rt_args[Consumer.openid1_return_to_claimed_id_name] = + service.claimed_id + end + + self.last_requested_endpoint = service + return checkid_request + end + + # Called to interpret the server's response to an OpenID + # request. It is called in step 4 of the flow described in the + # Consumer overview. + # + # query: A hash of the query parameters for this HTTP request. + # Note that in rails, this is <b>not</b> <tt>params</tt> but + # <tt>params.reject{|k,v|request.path_parameters[k]}</tt> + # because <tt>controller</tt> and <tt>action</tt> and other + # "path parameters" are included in params. + # + # current_url: Extract the URL of the current request from your + # application's web request framework and specify it here to have it + # checked against the openid.return_to value in the response. Do not + # just pass <tt>args['openid.return_to']</tt> here; that will defeat the + # purpose of this check. (See OpenID Authentication 2.0 section 11.1.) + # + # If the return_to URL check fails, the status of the completion will be + # FAILURE. + + # + # Returns a subclass of Response. The type of response is + # indicated by the status attribute, which will be one of + # SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED. + def complete(query, current_url) + message = Message.from_post_args(query) + mode = message.get_arg(OPENID_NS, 'mode', 'invalid') + begin + meth = method('complete_' + mode) + rescue NameError + meth = method(:complete_invalid) + end + response = meth.call(message, current_url) + cleanup_last_requested_endpoint + if [SUCCESS, CANCEL].member?(response.status) + cleanup_session + end + return response + end + + protected + + def session_get(name) + @session[session_key(name)] + end + + def session_set(name, val) + @session[session_key(name)] = val + end + + def session_key(suffix) + @session_key_prefix + suffix + end + + def last_requested_endpoint + session_get('last_requested_endpoint') + end + + def last_requested_endpoint=(endpoint) + session_set('last_requested_endpoint', endpoint) + end + + def cleanup_last_requested_endpoint + @session[session_key('last_requested_endpoint')] = nil + end + + def discovery_manager(openid_identifier) + DiscoveryManager.new(@session, openid_identifier, @session_key_prefix) + end + + def cleanup_session + discovery_manager(nil).cleanup(true) + end + + + def discover(identifier) + OpenID.discover(identifier) + end + + def negotiator + DefaultNegotiator + end + + def association_manager(service) + AssociationManager.new(@store, service.server_url, + service.compatibility_mode, negotiator) + end + + def handle_idres(message, current_url) + IdResHandler.new(message, current_url, @store, last_requested_endpoint) + end + + def complete_invalid(message, unused_return_to) + mode = message.get_arg(OPENID_NS, 'mode', '<No mode set>') + return FailureResponse.new(last_requested_endpoint, + "Invalid openid.mode: #{mode}") + end + + def complete_cancel(unused_message, unused_return_to) + return CancelResponse.new(last_requested_endpoint) + end + + def complete_error(message, unused_return_to) + error = message.get_arg(OPENID_NS, 'error') + contact = message.get_arg(OPENID_NS, 'contact') + reference = message.get_arg(OPENID_NS, 'reference') + + return FailureResponse.new(last_requested_endpoint, + error, contact, reference) + end + + def complete_setup_needed(message, unused_return_to) + if message.is_openid1 + return complete_invalid(message, nil) + else + setup_url = message.get_arg(OPENID2_NS, 'user_setup_url') + return SetupNeededResponse.new(last_requested_endpoint, setup_url) + end + end + + def complete_id_res(message, current_url) + if message.is_openid1 + setup_url = message.get_arg(OPENID1_NS, 'user_setup_url') + if !setup_url.nil? + return SetupNeededResponse.new(last_requested_endpoint, setup_url) + end + end + + begin + idres = handle_idres(message, current_url) + rescue OpenIDError => why + return FailureResponse.new(last_requested_endpoint, why.message) + else + return SuccessResponse.new(idres.endpoint, message, + idres.signed_fields) + end + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/associationmanager.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/associationmanager.rb new file mode 100644 index 000000000..51c0f3d25 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/associationmanager.rb @@ -0,0 +1,340 @@ +require "openid/dh" +require "openid/util" +require "openid/kvpost" +require "openid/cryptutil" +require "openid/protocolerror" +require "openid/association" + +module OpenID + class Consumer + + # A superclass for implementing Diffie-Hellman association sessions. + class DiffieHellmanSession + class << self + attr_reader :session_type, :secret_size, :allowed_assoc_types, + :hashfunc + end + + def initialize(dh=nil) + if dh.nil? + dh = DiffieHellman.from_defaults + end + @dh = dh + end + + # Return the query parameters for requesting an association + # using this Diffie-Hellman association session + def get_request + args = {'dh_consumer_public' => CryptUtil.num_to_base64(@dh.public)} + if (!@dh.using_default_values?) + args['dh_modulus'] = CryptUtil.num_to_base64(@dh.modulus) + args['dh_gen'] = CryptUtil.num_to_base64(@dh.generator) + end + + return args + end + + # Process the response from a successful association request and + # return the shared secret for this association + def extract_secret(response) + dh_server_public64 = response.get_arg(OPENID_NS, 'dh_server_public', + NO_DEFAULT) + enc_mac_key64 = response.get_arg(OPENID_NS, 'enc_mac_key', NO_DEFAULT) + dh_server_public = CryptUtil.base64_to_num(dh_server_public64) + enc_mac_key = Util.from_base64(enc_mac_key64) + return @dh.xor_secret(self.class.hashfunc, + dh_server_public, enc_mac_key) + end + end + + # A Diffie-Hellman association session that uses SHA1 as its hash + # function + class DiffieHellmanSHA1Session < DiffieHellmanSession + @session_type = 'DH-SHA1' + @secret_size = 20 + @allowed_assoc_types = ['HMAC-SHA1'] + @hashfunc = CryptUtil.method(:sha1) + end + + # A Diffie-Hellman association session that uses SHA256 as its hash + # function + class DiffieHellmanSHA256Session < DiffieHellmanSession + @session_type = 'DH-SHA256' + @secret_size = 32 + @allowed_assoc_types = ['HMAC-SHA256'] + @hashfunc = CryptUtil.method(:sha256) + end + + # An association session that does not use encryption + class NoEncryptionSession + class << self + attr_reader :session_type, :allowed_assoc_types + end + @session_type = 'no-encryption' + @allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256'] + + def get_request + return {} + end + + def extract_secret(response) + mac_key64 = response.get_arg(OPENID_NS, 'mac_key', NO_DEFAULT) + return Util.from_base64(mac_key64) + end + end + + # An object that manages creating and storing associations for an + # OpenID provider endpoint + class AssociationManager + def self.create_session(session_type) + case session_type + when 'no-encryption' + NoEncryptionSession.new + when 'DH-SHA1' + DiffieHellmanSHA1Session.new + when 'DH-SHA256' + DiffieHellmanSHA256Session.new + else + raise ArgumentError, "Unknown association session type: "\ + "#{session_type.inspect}" + end + end + + def initialize(store, server_url, compatibility_mode=false, + negotiator=nil) + @store = store + @server_url = server_url + @compatibility_mode = compatibility_mode + @negotiator = negotiator || DefaultNegotiator + end + + def get_association + if @store.nil? + return nil + end + + assoc = @store.get_association(@server_url) + if assoc.nil? || assoc.expires_in <= 0 + assoc = negotiate_association + if !assoc.nil? + @store.store_association(@server_url, assoc) + end + end + + return assoc + end + + def negotiate_association + assoc_type, session_type = @negotiator.get_allowed_type + begin + return request_association(assoc_type, session_type) + rescue ServerError => why + supported_types = extract_supported_association_type(why, assoc_type) + if !supported_types.nil? + # Attempt to create an association from the assoc_type and + # session_type that the server told us it supported. + assoc_type, session_type = supported_types + begin + return request_association(assoc_type, session_type) + rescue ServerError => why + Util.log("Server #{@server_url} refused its suggested " \ + "association type: session_type=#{session_type}, " \ + "assoc_type=#{assoc_type}") + return nil + end + end + end + end + + protected + def extract_supported_association_type(server_error, assoc_type) + # Any error message whose code is not 'unsupported-type' should + # be considered a total failure. + if (server_error.error_code != 'unsupported-type' or + server_error.message.is_openid1) + Util.log("Server error when requesting an association from "\ + "#{@server_url}: #{server_error.error_text}") + return nil + end + + # The server didn't like the association/session type that we + # sent, and it sent us back a message that might tell us how to + # handle it. + Util.log("Unsupported association type #{assoc_type}: "\ + "#{server_error.error_text}") + + # Extract the session_type and assoc_type from the error message + assoc_type = server_error.message.get_arg(OPENID_NS, 'assoc_type') + session_type = server_error.message.get_arg(OPENID_NS, 'session_type') + + if assoc_type.nil? or session_type.nil? + Util.log("Server #{@server_url} responded with unsupported "\ + "association session but did not supply a fallback.") + return nil + elsif !@negotiator.allowed?(assoc_type, session_type) + Util.log("Server sent unsupported session/association type: "\ + "session_type=#{session_type}, assoc_type=#{assoc_type}") + return nil + else + return [assoc_type, session_type] + end + end + + # Make and process one association request to this endpoint's OP + # endpoint URL. Returns an association object or nil if the + # association processing failed. Raises ServerError when the + # remote OpenID server returns an error. + def request_association(assoc_type, session_type) + assoc_session, args = create_associate_request(assoc_type, session_type) + + begin + response = OpenID.make_kv_post(args, @server_url) + return extract_association(response, assoc_session) + rescue HTTPStatusError => why + Util.log("Got HTTP status error when requesting association: #{why}") + return nil + rescue Message::KeyNotFound => why + Util.log("Missing required parameter in response from "\ + "#{@server_url}: #{why}") + return nil + + rescue ProtocolError => why + Util.log("Protocol error processing response from #{@server_url}: "\ + "#{why}") + return nil + end + end + + # Create an association request for the given assoc_type and + # session_type. Returns a pair of the association session object + # and the request message that will be sent to the server. + def create_associate_request(assoc_type, session_type) + assoc_session = self.class.create_session(session_type) + args = { + 'mode' => 'associate', + 'assoc_type' => assoc_type, + } + + if !@compatibility_mode + args['ns'] = OPENID2_NS + end + + # Leave out the session type if we're in compatibility mode + # *and* it's no-encryption. + if !@compatibility_mode || + assoc_session.class.session_type != 'no-encryption' + args['session_type'] = assoc_session.class.session_type + end + + args.merge!(assoc_session.get_request) + message = Message.from_openid_args(args) + return assoc_session, message + end + + # Given an association response message, extract the OpenID 1.X + # session type. Returns the association type for this message + # + # This function mostly takes care of the 'no-encryption' default + # behavior in OpenID 1. + # + # If the association type is plain-text, this function will + # return 'no-encryption' + def get_openid1_session_type(assoc_response) + # If it's an OpenID 1 message, allow session_type to default + # to nil (which signifies "no-encryption") + session_type = assoc_response.get_arg(OPENID1_NS, 'session_type') + + # Handle the differences between no-encryption association + # respones in OpenID 1 and 2: + + # no-encryption is not really a valid session type for + # OpenID 1, but we'll accept it anyway, while issuing a + # warning. + if session_type == 'no-encryption' + Util.log("WARNING: #{@server_url} sent 'no-encryption'"\ + "for OpenID 1.X") + + # Missing or empty session type is the way to flag a + # 'no-encryption' response. Change the session type to + # 'no-encryption' so that it can be handled in the same + # way as OpenID 2 'no-encryption' respones. + elsif session_type == '' || session_type.nil? + session_type = 'no-encryption' + end + + return session_type + end + + def self.extract_expires_in(message) + # expires_in should be a base-10 string. + expires_in_str = message.get_arg(OPENID_NS, 'expires_in', NO_DEFAULT) + if !(/\A\d+\Z/ =~ expires_in_str) + raise ProtocolError, "Invalid expires_in field: #{expires_in_str}" + end + expires_in_str.to_i + end + + # Attempt to extract an association from the response, given the + # association response message and the established association + # session. + def extract_association(assoc_response, assoc_session) + # Extract the common fields from the response, raising an + # exception if they are not found + assoc_type = assoc_response.get_arg(OPENID_NS, 'assoc_type', + NO_DEFAULT) + assoc_handle = assoc_response.get_arg(OPENID_NS, 'assoc_handle', + NO_DEFAULT) + expires_in = self.class.extract_expires_in(assoc_response) + + # OpenID 1 has funny association session behaviour. + if assoc_response.is_openid1 + session_type = get_openid1_session_type(assoc_response) + else + session_type = assoc_response.get_arg(OPENID2_NS, 'session_type', + NO_DEFAULT) + end + + # Session type mismatch + if assoc_session.class.session_type != session_type + if (assoc_response.is_openid1 and session_type == 'no-encryption') + # In OpenID 1, any association request can result in a + # 'no-encryption' association response. Setting + # assoc_session to a new no-encryption session should + # make the rest of this function work properly for + # that case. + assoc_session = NoEncryptionSession.new + else + # Any other mismatch, regardless of protocol version + # results in the failure of the association session + # altogether. + raise ProtocolError, "Session type mismatch. Expected "\ + "#{assoc_session.class.session_type}, got "\ + "#{session_type}" + end + end + + # Make sure assoc_type is valid for session_type + if !assoc_session.class.allowed_assoc_types.member?(assoc_type) + raise ProtocolError, "Unsupported assoc_type for session "\ + "#{assoc_session.class.session_type} "\ + "returned: #{assoc_type}" + end + + # Delegate to the association session to extract the secret + # from the response, however is appropriate for that session + # type. + begin + secret = assoc_session.extract_secret(assoc_response) + rescue Message::KeyNotFound, ArgumentError => why + raise ProtocolError, "Malformed response for "\ + "#{assoc_session.class.session_type} "\ + "session: #{why.message}" + end + + + return Association.from_expires_in(expires_in, assoc_handle, secret, + assoc_type) + end + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/checkid_request.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/checkid_request.rb new file mode 100644 index 000000000..eb5d3979d --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/checkid_request.rb @@ -0,0 +1,186 @@ +require "openid/message" +require "openid/util" + +module OpenID + class Consumer + # An object that holds the state necessary for generating an + # OpenID authentication request. This object holds the association + # with the server and the discovered information with which the + # request will be made. + # + # It is separate from the consumer because you may wish to add + # things to the request before sending it on its way to the + # server. It also has serialization options that let you encode + # the authentication request as a URL or as a form POST. + class CheckIDRequest + attr_accessor :return_to_args, :message + attr_reader :endpoint + + # Users of this library should not create instances of this + # class. Instances of this class are created by the library + # when needed. + def initialize(assoc, endpoint) + @assoc = assoc + @endpoint = endpoint + @return_to_args = {} + @message = Message.new(endpoint.preferred_namespace) + @anonymous = false + end + + attr_reader :anonymous + + # Set whether this request should be made anonymously. If a + # request is anonymous, the identifier will not be sent in the + # request. This is only useful if you are making another kind of + # request with an extension in this request. + # + # Anonymous requests are not allowed when the request is made + # with OpenID 1. + def anonymous=(is_anonymous) + if is_anonymous && @message.is_openid1 + raise ArgumentError, ("OpenID1 requests MUST include the "\ + "identifier in the request") + end + @anonymous = is_anonymous + end + + # Add an object that implements the extension interface for + # adding arguments to an OpenID message to this checkid request. + # + # extension_request: an OpenID::Extension object. + def add_extension(extension_request) + extension_request.to_message(@message) + end + + # Add an extension argument to this OpenID authentication + # request. You probably want to use add_extension and the + # OpenID::Extension interface. + # + # Use caution when adding arguments, because they will be + # URL-escaped and appended to the redirect URL, which can easily + # get quite long. + def add_extension_arg(namespace, key, value) + @message.set_arg(namespace, key, value) + end + + # Produce a OpenID::Message representing this request. + # + # Not specifying a return_to URL means that the user will not be + # returned to the site issuing the request upon its completion. + # + # If immediate mode is requested, the OpenID provider is to send + # back a response immediately, useful for behind-the-scenes + # authentication attempts. Otherwise the OpenID provider may + # engage the user before providing a response. This is the + # default case, as the user may need to provide credentials or + # approve the request before a positive response can be sent. + def get_message(realm, return_to=nil, immediate=false) + if !return_to.nil? + return_to = Util.append_args(return_to, @return_to_args) + elsif immediate + raise ArgumentError, ('"return_to" is mandatory when using '\ + '"checkid_immediate"') + elsif @message.is_openid1 + raise ArgumentError, ('"return_to" is mandatory for OpenID 1 '\ + 'requests') + elsif @return_to_args.empty? + raise ArgumentError, ('extra "return_to" arguments were specified, '\ + 'but no return_to was specified') + end + + + message = @message.copy + + mode = immediate ? 'checkid_immediate' : 'checkid_setup' + message.set_arg(OPENID_NS, 'mode', mode) + + realm_key = message.is_openid1 ? 'trust_root' : 'realm' + message.set_arg(OPENID_NS, realm_key, realm) + + if !return_to.nil? + message.set_arg(OPENID_NS, 'return_to', return_to) + end + + if not @anonymous + if @endpoint.is_op_identifier + # This will never happen when we're in OpenID 1 + # compatibility mode, as long as is_op_identifier() + # returns false whenever preferred_namespace returns + # OPENID1_NS. + claimed_id = request_identity = IDENTIFIER_SELECT + else + request_identity = @endpoint.get_local_id + claimed_id = @endpoint.claimed_id + end + + # This is true for both OpenID 1 and 2 + message.set_arg(OPENID_NS, 'identity', request_identity) + + if message.is_openid2 + message.set_arg(OPENID2_NS, 'claimed_id', claimed_id) + end + end + + if @assoc + message.set_arg(OPENID_NS, 'assoc_handle', @assoc.handle) + assoc_log_msg = "with assocication #{@assoc.handle}" + else + assoc_log_msg = 'using stateless mode.' + end + + Util.log("Generated #{mode} request to #{@endpoint.server_url} "\ + "#{assoc_log_msg}") + return message + end + + # Returns a URL with an encoded OpenID request. + # + # The resulting URL is the OpenID provider's endpoint URL with + # parameters appended as query arguments. You should redirect + # the user agent to this URL. + # + # OpenID 2.0 endpoints also accept POST requests, see + # 'send_redirect?' and 'form_markup'. + def redirect_url(realm, return_to=nil, immediate=false) + message = get_message(realm, return_to, immediate) + return message.to_url(@endpoint.server_url) + end + + # Get html for a form to submit this request to the IDP. + # + # form_tag_attrs is a hash of attributes to be added to the form + # tag. 'accept-charset' and 'enctype' have defaults that can be + # overridden. If a value is supplied for 'action' or 'method', + # it will be replaced. + def form_markup(realm, return_to=nil, immediate=false, + form_tag_attrs=nil) + message = get_message(realm, return_to, immediate) + return message.to_form_markup(@endpoint.server_url, form_tag_attrs) + end + + # Get a complete HTML document that autosubmits the request to the IDP + # with javascript. This method wraps form_markup - see that method's + # documentation for help with the parameters. + def html_markup(realm, return_to=nil, immediate=false, + form_tag_attrs=nil) + Util.auto_submit_html(form_markup(realm, + return_to, + immediate, + form_tag_attrs)) + end + + # Should this OpenID authentication request be sent as a HTTP + # redirect or as a POST (form submission)? + # + # This takes the same parameters as redirect_url or form_markup + def send_redirect?(realm, return_to=nil, immediate=false) + if @endpoint.compatibility_mode + return true + else + url = redirect_url(realm, return_to, immediate) + return url.length <= OPENID1_URL_LIMIT + end + end + end + end +end 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 diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/discovery_manager.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/discovery_manager.rb new file mode 100644 index 000000000..8f838117d --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/discovery_manager.rb @@ -0,0 +1,123 @@ +module OpenID + class Consumer + + # A set of discovered services, for tracking which providers have + # been attempted for an OpenID identifier + class DiscoveredServices + attr_reader :current + + def initialize(starting_url, yadis_url, services) + @starting_url = starting_url + @yadis_url = yadis_url + @services = services.dup + @current = nil + end + + def next + @current = @services.shift + end + + def for_url?(url) + [@starting_url, @yadis_url].member?(url) + end + + def started? + !@current.nil? + end + + def empty? + @services.empty? + end + end + + # Manages calling discovery and tracking which endpoints have + # already been attempted. + class DiscoveryManager + def initialize(session, url, session_key_suffix=nil) + @url = url + + @session = session + @session_key_suffix = session_key_suffix || 'auth' + end + + def get_next_service + manager = get_manager + if !manager.nil? && manager.empty? + destroy_manager + manager = nil + end + + if manager.nil? + yadis_url, services = yield @url + manager = create_manager(yadis_url, services) + end + + if !manager.nil? + service = manager.next + store(manager) + else + service = nil + end + + return service + end + + def cleanup(force=false) + manager = get_manager(force) + if !manager.nil? + service = manager.current + destroy_manager(force) + else + service = nil + end + return service + end + + protected + + def get_manager(force=false) + manager = load + if force || manager.nil? || manager.for_url?(@url) + return manager + else + return nil + end + end + + def create_manager(yadis_url, services) + manager = get_manager + if !manager.nil? + raise StandardError, "There is already a manager for #{yadis_url}" + end + if services.empty? + return nil + end + manager = DiscoveredServices.new(@url, yadis_url, services) + store(manager) + return manager + end + + def destroy_manager(force=false) + if !get_manager(force).nil? + destroy! + end + end + + def session_key + 'OpenID::Consumer::DiscoveredServices::' + @session_key_suffix + end + + def store(manager) + @session[session_key] = manager + end + + def load + @session[session_key] + end + + def destroy! + @session[session_key] = nil + end + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/html_parse.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/html_parse.rb new file mode 100644 index 000000000..579874caf --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/html_parse.rb @@ -0,0 +1,134 @@ +require "openid/yadis/htmltokenizer" + +module OpenID + + # Stuff to remove before we start looking for tags + REMOVED_RE = / + # Comments + <!--.*?--> + + # CDATA blocks + | <!\[CDATA\[.*?\]\]> + + # script blocks + | <script\b + + # make sure script is not an XML namespace + (?!:) + + [^>]*>.*?<\/script> + + /mixu + + def OpenID.openid_unescape(s) + s.gsub('&','&').gsub('<','<').gsub('>','>').gsub('"','"') + end + + def OpenID.unescape_hash(h) + newh = {} + h.map{|k,v| + newh[k]=openid_unescape(v) + } + newh + end + + + def OpenID.parse_link_attrs(html) + stripped = html.gsub(REMOVED_RE,'') + parser = HTMLTokenizer.new(stripped) + + links = [] + # to keep track of whether or not we are in the head element + in_head = false + in_html = false + saw_head = false + + begin + while el = parser.getTag('head', '/head', 'link', 'body', '/body', + 'html', '/html') + + # we are leaving head or have reached body, so we bail + return links if ['/head', 'body', '/body', '/html'].member?(el.tag_name) + + # enforce html > head > link + if el.tag_name == 'html' + in_html = true + end + next unless in_html + if el.tag_name == 'head' + if saw_head + return links #only allow one head + end + saw_head = true + unless el.to_s[-2] == 47 # tag ends with a /: a short tag + in_head = true + end + end + next unless in_head + + return links if el.tag_name == 'html' + + if el.tag_name == 'link' + links << unescape_hash(el.attr_hash) + end + + end + rescue Exception # just stop parsing if there's an error + end + return links + end + + def OpenID.rel_matches(rel_attr, target_rel) + # Does this target_rel appear in the rel_str? + # XXX: TESTME + rels = rel_attr.strip().split() + rels.each { |rel| + rel = rel.downcase + if rel == target_rel + return true + end + } + + return false + end + + def OpenID.link_has_rel(link_attrs, target_rel) + # Does this link have target_rel as a relationship? + + # XXX: TESTME + rel_attr = link_attrs['rel'] + return (rel_attr and rel_matches(rel_attr, target_rel)) + end + + def OpenID.find_links_rel(link_attrs_list, target_rel) + # Filter the list of link attributes on whether it has target_rel + # as a relationship. + + # XXX: TESTME + matchesTarget = lambda { |attrs| link_has_rel(attrs, target_rel) } + result = [] + + link_attrs_list.each { |item| + if matchesTarget.call(item) + result << item + end + } + + return result + end + + def OpenID.find_first_href(link_attrs_list, target_rel) + # Return the value of the href attribute for the first link tag in + # the list that has target_rel as a relationship. + + # XXX: TESTME + matches = find_links_rel(link_attrs_list, target_rel) + if !matches or matches.empty? + return nil + end + + first = matches[0] + return first['href'] + end +end + diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/idres.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/idres.rb new file mode 100644 index 000000000..ab924e294 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/idres.rb @@ -0,0 +1,523 @@ +require "openid/message" +require "openid/protocolerror" +require "openid/kvpost" +require "openid/consumer/discovery" +require "openid/urinorm" + +module OpenID + class TypeURIMismatch < ProtocolError + attr_reader :type_uri, :endpoint + + def initialize(type_uri, endpoint) + @type_uri = type_uri + @endpoint = endpoint + end + end + + class Consumer + @openid1_return_to_nonce_name = 'rp_nonce' + @openid1_return_to_claimed_id_name = 'openid1_claimed_id' + + # Set the name of the query parameter that this library will use + # to thread a nonce through an OpenID 1 transaction. It will be + # appended to the return_to URL. + def self.openid1_return_to_nonce_name=(query_arg_name) + @openid1_return_to_nonce_name = query_arg_name + end + + # See openid1_return_to_nonce_name= documentation + def self.openid1_return_to_nonce_name + @openid1_return_to_nonce_name + end + + # Set the name of the query parameter that this library will use + # to thread the requested URL through an OpenID 1 transaction (for + # use when verifying discovered information). It will be appended + # to the return_to URL. + def self.openid1_return_to_claimed_id_name=(query_arg_name) + @openid1_return_to_claimed_id_name = query_arg_name + end + + # See openid1_return_to_claimed_id_name= + def self.openid1_return_to_claimed_id_name + @openid1_return_to_claimed_id_name + end + + # Handles an openid.mode=id_res response. This object is + # instantiated and used by the Consumer. + class IdResHandler + attr_reader :endpoint, :message + + def initialize(message, current_url, store=nil, endpoint=nil) + @store = store # Fer the nonce and invalidate_handle + @message = message + @endpoint = endpoint + @current_url = current_url + @signed_list = nil + + # Start the verification process + id_res + end + + def signed_fields + signed_list.map {|x| 'openid.' + x} + end + + protected + + # This method will raise ProtocolError unless the request is a + # valid id_res response. Once it has been verified, the methods + # 'endpoint', 'message', and 'signed_fields' contain the + # verified information. + def id_res + check_for_fields + verify_return_to + verify_discovery_results + check_signature + check_nonce + end + + def server_url + @endpoint.nil? ? nil : @endpoint.server_url + end + + def openid_namespace + @message.get_openid_namespace + end + + def fetch(field, default=NO_DEFAULT) + @message.get_arg(OPENID_NS, field, default) + end + + def signed_list + if @signed_list.nil? + signed_list_str = fetch('signed', nil) + if signed_list_str.nil? + raise ProtocolError, 'Response missing signed list' + end + + @signed_list = signed_list_str.split(',', -1) + end + @signed_list + end + + def check_for_fields + # XXX: if a field is missing, we should not have to explicitly + # check that it's present, just make sure that the fields are + # actually being used by the rest of the code in + # tests. Although, which fields are signed does need to be + # checked somewhere. + basic_fields = ['return_to', 'assoc_handle', 'sig', 'signed'] + basic_sig_fields = ['return_to', 'identity'] + + case openid_namespace + when OPENID2_NS + require_fields = basic_fields + ['op_endpoint'] + require_sigs = basic_sig_fields + + ['response_nonce', 'claimed_id', 'assoc_handle',] + when OPENID1_NS + require_fields = basic_fields + ['identity'] + require_sigs = basic_sig_fields + else + raise RuntimeError, "check_for_fields doesn't know about "\ + "namespace #{openid_namespace.inspect}" + end + + require_fields.each do |field| + if !@message.has_key?(OPENID_NS, field) + raise ProtocolError, "Missing required field #{field}" + end + end + + require_sigs.each do |field| + # Field is present and not in signed list + if @message.has_key?(OPENID_NS, field) && !signed_list.member?(field) + raise ProtocolError, "#{field.inspect} not signed" + end + end + end + + def verify_return_to + begin + msg_return_to = URI.parse(URINorm::urinorm(fetch('return_to'))) + rescue URI::InvalidURIError + raise ProtocolError, ("return_to is not a valid URI") + end + + verify_return_to_args(msg_return_to) + if !@current_url.nil? + verify_return_to_base(msg_return_to) + end + end + + def verify_return_to_args(msg_return_to) + return_to_parsed_query = {} + if !msg_return_to.query.nil? + CGI.parse(msg_return_to.query).each_pair do |k, vs| + return_to_parsed_query[k] = vs[0] + end + end + query = @message.to_post_args + return_to_parsed_query.each_pair do |rt_key, rt_val| + msg_val = query[rt_key] + if msg_val.nil? + raise ProtocolError, "Message missing return_to argument '#{rt_key}'" + elsif msg_val != rt_val + raise ProtocolError, ("Parameter '#{rt_key}' value "\ + "#{msg_val.inspect} does not match "\ + "return_to's value #{rt_val.inspect}") + end + end + @message.get_args(BARE_NS).each_pair do |bare_key, bare_val| + rt_val = return_to_parsed_query[bare_key] + if not return_to_parsed_query.has_key? bare_key + # This may be caused by your web framework throwing extra + # entries in to your parameters hash that were not GET or + # POST parameters. For example, Rails has been known to + # add "controller" and "action" keys; another server adds + # at least a "format" key. + raise ProtocolError, ("Unexpected parameter (not on return_to): "\ + "'#{bare_key}'=#{rt_val.inspect})") + end + if rt_val != bare_val + raise ProtocolError, ("Parameter '#{bare_key}' value "\ + "#{bare_val.inspect} does not match "\ + "return_to's value #{rt_val.inspect}") + end + end + end + + def verify_return_to_base(msg_return_to) + begin + app_parsed = URI.parse(URINorm::urinorm(@current_url)) + rescue URI::InvalidURIError + raise ProtocolError, "current_url is not a valid URI: #{@current_url}" + end + + [:scheme, :host, :port, :path].each do |meth| + if msg_return_to.send(meth) != app_parsed.send(meth) + raise ProtocolError, "return_to #{meth.to_s} does not match" + end + end + end + + # Raises ProtocolError if the signature is bad + def check_signature + if @store.nil? + assoc = nil + else + assoc = @store.get_association(server_url, fetch('assoc_handle')) + end + + if assoc.nil? + check_auth + else + if assoc.expires_in <= 0 + # XXX: It might be a good idea sometimes to re-start the + # authentication with a new association. Doing it + # automatically opens the possibility for + # denial-of-service by a server that just returns expired + # associations (or really short-lived associations) + raise ProtocolError, "Association with #{server_url} expired" + elsif !assoc.check_message_signature(@message) + raise ProtocolError, "Bad signature in response from #{server_url}" + end + end + end + + def check_auth + Util.log("Using 'check_authentication' with #{server_url}") + begin + request = create_check_auth_request + rescue Message::KeyNotFound => why + raise ProtocolError, "Could not generate 'check_authentication' "\ + "request: #{why.message}" + end + + response = OpenID.make_kv_post(request, server_url) + + process_check_auth_response(response) + end + + def create_check_auth_request + signed_list = @message.get_arg(OPENID_NS, 'signed', NO_DEFAULT).split(',') + + # check that we got all the signed arguments + signed_list.each {|k| + @message.get_aliased_arg(k, NO_DEFAULT) + } + + ca_message = @message.copy + ca_message.set_arg(OPENID_NS, 'mode', 'check_authentication') + + return ca_message + end + + # Process the response message from a check_authentication + # request, invalidating associations if requested. + def process_check_auth_response(response) + is_valid = response.get_arg(OPENID_NS, 'is_valid', 'false') + + invalidate_handle = response.get_arg(OPENID_NS, 'invalidate_handle') + if !invalidate_handle.nil? + Util.log("Received 'invalidate_handle' from server #{server_url}") + if @store.nil? + Util.log('Unexpectedly got "invalidate_handle" without a store!') + else + @store.remove_association(server_url, invalidate_handle) + end + end + + if is_valid != 'true' + raise ProtocolError, ("Server #{server_url} responds that the "\ + "'check_authentication' call is not valid") + end + end + + def check_nonce + case openid_namespace + when OPENID1_NS + nonce = + @message.get_arg(BARE_NS, Consumer.openid1_return_to_nonce_name) + + # We generated the nonce, so it uses the empty string as the + # server URL + server_url = '' + when OPENID2_NS + nonce = @message.get_arg(OPENID2_NS, 'response_nonce') + server_url = self.server_url + else + raise StandardError, 'Not reached' + end + + if nonce.nil? + raise ProtocolError, 'Nonce missing from response' + end + + begin + time, extra = Nonce.split_nonce(nonce) + rescue ArgumentError => why + raise ProtocolError, "Malformed nonce: #{nonce.inspect}" + end + + if !@store.nil? && !@store.use_nonce(server_url, time, extra) + raise ProtocolError, ("Nonce already used or out of range: "\ + "#{nonce.inspect}") + end + end + + def verify_discovery_results + begin + case openid_namespace + when OPENID1_NS + verify_discovery_results_openid1 + when OPENID2_NS + verify_discovery_results_openid2 + else + raise StandardError, "Not reached: #{openid_namespace}" + end + rescue Message::KeyNotFound => why + raise ProtocolError, "Missing required field: #{why.message}" + end + end + + def verify_discovery_results_openid2 + to_match = OpenIDServiceEndpoint.new + to_match.type_uris = [OPENID_2_0_TYPE] + to_match.claimed_id = fetch('claimed_id', nil) + to_match.local_id = fetch('identity', nil) + to_match.server_url = fetch('op_endpoint') + + if to_match.claimed_id.nil? && !to_match.local_id.nil? + raise ProtocolError, ('openid.identity is present without '\ + 'openid.claimed_id') + elsif !to_match.claimed_id.nil? && to_match.local_id.nil? + raise ProtocolError, ('openid.claimed_id is present without '\ + 'openid.identity') + + # This is a response without identifiers, so there's really no + # checking that we can do, so return an endpoint that's for + # the specified `openid.op_endpoint' + elsif to_match.claimed_id.nil? + @endpoint = + OpenIDServiceEndpoint.from_op_endpoint_url(to_match.server_url) + return + end + + if @endpoint.nil? + Util.log('No pre-discovered information supplied') + discover_and_verify(to_match.claimed_id, [to_match]) + else + begin + verify_discovery_single(@endpoint, to_match) + rescue ProtocolError => why + Util.log("Error attempting to use stored discovery "\ + "information: #{why.message}") + Util.log("Attempting discovery to verify endpoint") + discover_and_verify(to_match.claimed_id, [to_match]) + end + end + + if @endpoint.claimed_id != to_match.claimed_id + @endpoint = @endpoint.dup + @endpoint.claimed_id = to_match.claimed_id + end + end + + def verify_discovery_results_openid1 + claimed_id = + @message.get_arg(BARE_NS, Consumer.openid1_return_to_claimed_id_name) + + if claimed_id.nil? + if @endpoint.nil? + raise ProtocolError, ("When using OpenID 1, the claimed ID must "\ + "be supplied, either by passing it through "\ + "as a return_to parameter or by using a "\ + "session, and supplied to the IdResHandler "\ + "when it is constructed.") + else + claimed_id = @endpoint.claimed_id + end + end + + to_match = OpenIDServiceEndpoint.new + to_match.type_uris = [OPENID_1_1_TYPE] + to_match.local_id = fetch('identity') + # Restore delegate information from the initiation phase + to_match.claimed_id = claimed_id + + to_match_1_0 = to_match.dup + to_match_1_0.type_uris = [OPENID_1_0_TYPE] + + if !@endpoint.nil? + begin + begin + verify_discovery_single(@endpoint, to_match) + rescue TypeURIMismatch + verify_discovery_single(@endpoint, to_match_1_0) + end + rescue ProtocolError => why + Util.log('Error attempting to use stored discovery information: ' + + why.message) + Util.log('Attempting discovery to verify endpoint') + else + return @endpoint + end + end + + # Either no endpoint was supplied or OpenID 1.x verification + # of the information that's in the message failed on that + # endpoint. + discover_and_verify(to_match.claimed_id, [to_match, to_match_1_0]) + end + + # Given an endpoint object created from the information in an + # OpenID response, perform discovery and verify the discovery + # results, returning the matching endpoint that is the result of + # doing that discovery. + def discover_and_verify(claimed_id, to_match_endpoints) + Util.log("Performing discovery on #{claimed_id}") + _, services = OpenID.discover(claimed_id) + if services.length == 0 + # XXX: this might want to be something other than + # ProtocolError. In Python, it's DiscoveryFailure + raise ProtocolError, ("No OpenID information found at "\ + "#{claimed_id}") + end + verify_discovered_services(claimed_id, services, to_match_endpoints) + end + + + def verify_discovered_services(claimed_id, services, to_match_endpoints) + # Search the services resulting from discovery to find one + # that matches the information from the assertion + failure_messages = [] + for endpoint in services + for to_match_endpoint in to_match_endpoints + begin + verify_discovery_single(endpoint, to_match_endpoint) + rescue ProtocolError => why + failure_messages << why.message + else + # It matches, so discover verification has + # succeeded. Return this endpoint. + @endpoint = endpoint + return + end + end + end + + Util.log("Discovery verification failure for #{claimed_id}") + failure_messages.each do |failure_message| + Util.log(" * Endpoint mismatch: " + failure_message) + end + + # XXX: is DiscoveryFailure in Python OpenID + raise ProtocolError, ("No matching endpoint found after "\ + "discovering #{claimed_id}") + end + + def verify_discovery_single(endpoint, to_match) + # Every type URI that's in the to_match endpoint has to be + # present in the discovered endpoint. + for type_uri in to_match.type_uris + if !endpoint.uses_extension(type_uri) + raise TypeURIMismatch.new(type_uri, endpoint) + end + end + + # Fragments do not influence discovery, so we can't compare a + # claimed identifier with a fragment to discovered information. + defragged_claimed_id = + case Yadis::XRI.identifier_scheme(endpoint.claimed_id) + when :xri + endpoint.claimed_id + when :uri + begin + parsed = URI.parse(endpoint.claimed_id) + rescue URI::InvalidURIError + endpoint.claimed_id + else + parsed.fragment = nil + parsed.to_s + end + else + raise StandardError, 'Not reached' + end + + if defragged_claimed_id != endpoint.claimed_id + raise ProtocolError, ("Claimed ID does not match (different "\ + "subjects!), Expected "\ + "#{defragged_claimed_id}, got "\ + "#{endpoint.claimed_id}") + end + + if to_match.get_local_id != endpoint.get_local_id + raise ProtocolError, ("local_id mismatch. Expected "\ + "#{to_match.get_local_id}, got "\ + "#{endpoint.get_local_id}") + end + + # If the server URL is nil, this must be an OpenID 1 + # response, because op_endpoint is a required parameter in + # OpenID 2. In that case, we don't actually care what the + # discovered server_url is, because signature checking or + # check_auth should take care of that check for us. + if to_match.server_url.nil? + if to_match.preferred_namespace != OPENID1_NS + raise StandardError, + "The code calling this must ensure that OpenID 2 "\ + "responses have a non-none `openid.op_endpoint' and "\ + "that it is set as the `server_url' attribute of the "\ + "`to_match' endpoint." + end + elsif to_match.server_url != endpoint.server_url + raise ProtocolError, ("OP Endpoint mismatch. Expected"\ + "#{to_match.server_url}, got "\ + "#{endpoint.server_url}") + end + end + + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/responses.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/responses.rb new file mode 100644 index 000000000..91262398e --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/responses.rb @@ -0,0 +1,148 @@ +module OpenID + class Consumer + # Code returned when either the of the + # OpenID::OpenIDConsumer.begin_auth or OpenID::OpenIDConsumer.complete_auth + # methods return successfully. + SUCCESS = :success + + # Code OpenID::OpenIDConsumer.complete_auth + # returns when the value it received indicated an invalid login. + FAILURE = :failure + + # Code returned by OpenIDConsumer.complete_auth when the user + # cancels the operation from the server. + CANCEL = :cancel + + # Code returned by OpenID::OpenIDConsumer.complete_auth when the + # OpenIDConsumer instance is in immediate mode and ther server sends back a + # URL for the user to login with. + SETUP_NEEDED = :setup_needed + + + module Response + attr_reader :endpoint + + def status + self.class::STATUS + end + + # The identity URL that has been authenticated; the Claimed Identifier. + # See also display_identifier. + def identity_url + @endpoint ? @endpoint.claimed_id : nil + end + + # The display identifier is related to the Claimed Identifier, but the + # two are not always identical. The display identifier is something the + # user should recognize as what they entered, whereas the response's + # claimed identifier (in the identity_url attribute) may have extra + # information for better persistence. + # + # URLs will be stripped of their fragments for display. XRIs will + # display the human-readable identifier (i-name) instead of the + # persistent identifier (i-number). + # + # Use the display identifier in your user interface. Use identity_url + # for querying your database or authorization server, or other + # identifier equality comparisons. + def display_identifier + @endpoint ? @endpoint.display_identifier : nil + end + end + + # A successful acknowledgement from the OpenID server that the + # supplied URL is, indeed controlled by the requesting agent. + class SuccessResponse + include Response + + STATUS = SUCCESS + + attr_reader :message, :signed_fields + + def initialize(endpoint, message, signed_fields) + # Don't use :endpoint=, because endpoint should never be nil + # for a successfull transaction. + @endpoint = endpoint + @identity_url = endpoint.claimed_id + @message = message + @signed_fields = signed_fields + end + + # Was this authentication response an OpenID 1 authentication + # response? + def is_openid1 + @message.is_openid1 + end + + # Return whether a particular key is signed, regardless of its + # namespace alias + def signed?(ns_uri, ns_key) + @signed_fields.member?(@message.get_key(ns_uri, ns_key)) + end + + # Return the specified signed field if available, otherwise + # return default + def get_signed(ns_uri, ns_key, default=nil) + if singed?(ns_uri, ns_key) + return @message.get_arg(ns_uri, ns_key, default) + else + return default + end + end + + # Get signed arguments from the response message. Return a dict + # of all arguments in the specified namespace. If any of the + # arguments are not signed, return nil. + def get_signed_ns(ns_uri) + msg_args = @message.get_args(ns_uri) + msg_args.each_key do |key| + if !signed?(ns_uri, key) + return nil + end + end + return msg_args + end + + # Return response arguments in the specified namespace. + # If require_signed is true and the arguments are not signed, + # return nil. + def extension_response(namespace_uri, require_signed) + if require_signed + get_signed_ns(namespace_uri) + else + @message.get_args(namespace_uri) + end + end + end + + class FailureResponse + include Response + STATUS = FAILURE + + attr_reader :message, :contact, :reference + def initialize(endpoint, message, contact=nil, reference=nil) + @endpoint = endpoint + @message = message + @contact = contact + @reference = reference + end + end + + class CancelResponse + include Response + STATUS = CANCEL + def initialize(endpoint) + @endpoint = endpoint + end + end + + class SetupNeededResponse + include Response + STATUS = SETUP_NEEDED + def initialize(endpoint, setup_url) + @endpoint = endpoint + @setup_url = setup_url + end + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/cryptutil.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/cryptutil.rb new file mode 100644 index 000000000..d8ffead9b --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/cryptutil.rb @@ -0,0 +1,97 @@ +require "openid/util" +require "digest/sha1" +require "digest/sha2" +begin + require "digest/hmac" +rescue LoadError + require "hmac/sha1" + require "hmac/sha2" +end + +module OpenID + # This module contains everything needed to perform low-level + # cryptograph and data manipulation tasks. + module CryptUtil + + # Generate a random number, doing a little extra work to make it + # more likely that it's suitable for cryptography. If your system + # doesn't have /dev/urandom then this number is not + # cryptographically safe. See + # <http://www.cosine.org/2007/08/07/security-ruby-kernel-rand/> + # for more information. max is the largest possible value of such + # a random number, where the result will be less than max. + def CryptUtil.rand(max) + Kernel.srand() + return Kernel.rand(max) + end + + def CryptUtil.sha1(text) + return Digest::SHA1.digest(text) + end + + def CryptUtil.hmac_sha1(key, text) + if Digest.const_defined? :HMAC + Digest::HMAC.new(key,Digest::SHA1).update(text).digest + else + return HMAC::SHA1.digest(key, text) + end + end + + def CryptUtil.sha256(text) + return Digest::SHA256.digest(text) + end + + def CryptUtil.hmac_sha256(key, text) + if Digest.const_defined? :HMAC + Digest::HMAC.new(key,Digest::SHA256).update(text).digest + else + return HMAC::SHA256.digest(key, text) + end + end + + # Generate a random string of the given length, composed of the + # specified characters. If chars is nil, generate a string + # composed of characters in the range 0..255. + def CryptUtil.random_string(length, chars=nil) + s = "" + + unless chars.nil? + length.times { s << chars[rand(chars.length)] } + else + length.times { s << rand(256).chr } + end + return s + end + + # Convert a number to its binary representation; return a string + # of bytes. + def CryptUtil.num_to_binary(n) + bits = n.to_s(2) + prepend = (8 - bits.length % 8) + bits = ('0' * prepend) + bits + return [bits].pack('B*') + end + + # Convert a string of bytes into a number. + def CryptUtil.binary_to_num(s) + # taken from openid-ruby 0.0.1 + s = "\000" * (4 - (s.length % 4)) + s + num = 0 + s.unpack('N*').each do |x| + num <<= 32 + num |= x + end + return num + end + + # Encode a number as a base64-encoded byte string. + def CryptUtil.num_to_base64(l) + return OpenID::Util.to_base64(num_to_binary(l)) + end + + # Decode a base64 byte string to a number. + def CryptUtil.base64_to_num(s) + return binary_to_num(OpenID::Util.from_base64(s)) + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/dh.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/dh.rb new file mode 100644 index 000000000..cbe531147 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/dh.rb @@ -0,0 +1,89 @@ +require "openid/util" +require "openid/cryptutil" + +module OpenID + + # Encapsulates a Diffie-Hellman key exchange. This class is used + # internally by both the consumer and server objects. + # + # Read more about Diffie-Hellman on wikipedia: + # http://en.wikipedia.org/wiki/Diffie-Hellman + + class DiffieHellman + + # From the OpenID specification + @@default_mod = 155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443 + @@default_gen = 2 + + attr_reader :modulus, :generator, :public + + # A new DiffieHellman object, using the modulus and generator from + # the OpenID specification + def DiffieHellman.from_defaults + DiffieHellman.new(@@default_mod, @@default_gen) + end + + def initialize(modulus=nil, generator=nil, priv=nil) + @modulus = modulus.nil? ? @@default_mod : modulus + @generator = generator.nil? ? @@default_gen : generator + set_private(priv.nil? ? OpenID::CryptUtil.rand(@modulus-2) + 1 : priv) + end + + def get_shared_secret(composite) + DiffieHellman.powermod(composite, @private, @modulus) + end + + def xor_secret(algorithm, composite, secret) + dh_shared = get_shared_secret(composite) + packed_dh_shared = OpenID::CryptUtil.num_to_binary(dh_shared) + hashed_dh_shared = algorithm.call(packed_dh_shared) + return DiffieHellman.strxor(secret, hashed_dh_shared) + end + + def using_default_values? + @generator == @@default_gen && @modulus == @@default_mod + end + + private + def set_private(priv) + @private = priv + @public = DiffieHellman.powermod(@generator, @private, @modulus) + end + + def DiffieHellman.strxor(s, t) + if s.length != t.length + raise ArgumentError, "strxor: lengths don't match. " + + "Inputs were #{s.inspect} and #{t.inspect}" + end + + if String.method_defined? :bytes + s.bytes.zip(t.bytes).map{|sb,tb| sb^tb}.pack('C*') + else + indices = 0...(s.length) + chrs = indices.collect {|i| (s[i]^t[i]).chr} + chrs.join("") + end + end + + # This code is taken from this post: + # <http://blade.nagaokaut.ac.jp/cgi-bin/scat.\rb/ruby/ruby-talk/19098> + # by Eric Lee Green. + def DiffieHellman.powermod(x, n, q) + counter=0 + n_p=n + y_p=1 + z_p=x + while n_p != 0 + if n_p[0]==1 + y_p=(y_p*z_p) % q + end + n_p = n_p >> 1 + z_p = (z_p * z_p) % q + counter += 1 + end + return y_p + end + + end + +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/extension.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/extension.rb new file mode 100644 index 000000000..f0f02bb5c --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/extension.rb @@ -0,0 +1,39 @@ +require 'openid/message' + +module OpenID + # An interface for OpenID extensions. + class Extension < Object + + def initialize + @ns_uri = nil + @ns_alias = nil + end + + # Get the string arguments that should be added to an OpenID + # message for this extension. + def get_extension_args + raise NotImplementedError + end + + # Add the arguments from this extension to the provided + # message, or create a new message containing only those + # arguments. Returns the message with added extension args. + def to_message(message = nil) + if message.nil? +# warnings.warn('Passing None to Extension.toMessage is deprecated. ' +# 'Creating a message assuming you want OpenID 2.', +# DeprecationWarning, stacklevel=2) + Message.new(OPENID2_NS) + end + message = Message.new if message.nil? + + implicit = message.is_openid1() + + message.namespaces.add_alias(@ns_uri, @ns_alias, implicit) + # XXX python ignores keyerror if m.ns.getAlias(uri) == alias + + message.update_args(@ns_uri, get_extension_args) + return message + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/extensions/ax.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/extensions/ax.rb new file mode 100644 index 000000000..55eda8e7c --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/extensions/ax.rb @@ -0,0 +1,516 @@ +# Implements the OpenID attribute exchange specification, version 1.0 + +require 'openid/extension' +require 'openid/trustroot' +require 'openid/message' + +module OpenID + module AX + + UNLIMITED_VALUES = "unlimited" + MINIMUM_SUPPORTED_ALIAS_LENGTH = 32 + + # check alias for invalid characters, raise AXError if found + def self.check_alias(name) + if name.match(/(,|\.)/) + raise Error, ("Alias #{name.inspect} must not contain a "\ + "comma or period.") + end + end + + # Raised when data does not comply with AX 1.0 specification + class Error < ArgumentError + end + + # Abstract class containing common code for attribute exchange messages + class AXMessage < Extension + attr_accessor :ns_alias, :mode, :ns_uri + + NS_URI = 'http://openid.net/srv/ax/1.0' + def initialize + @ns_alias = 'ax' + @ns_uri = NS_URI + @mode = nil + end + + protected + + # Raise an exception if the mode in the attribute exchange + # arguments does not match what is expected for this class. + def check_mode(ax_args) + actual_mode = ax_args['mode'] + if actual_mode != @mode + raise Error, "Expected mode #{mode.inspect}, got #{actual_mode.inspect}" + end + end + + def new_args + {'mode' => @mode} + end + end + + # Represents a single attribute in an attribute exchange + # request. This should be added to an Request object in order to + # request the attribute. + # + # @ivar required: Whether the attribute will be marked as required + # when presented to the subject of the attribute exchange + # request. + # @type required: bool + # + # @ivar count: How many values of this type to request from the + # subject. Defaults to one. + # @type count: int + # + # @ivar type_uri: The identifier that determines what the attribute + # represents and how it is serialized. For example, one type URI + # representing dates could represent a Unix timestamp in base 10 + # and another could represent a human-readable string. + # @type type_uri: str + # + # @ivar ns_alias: The name that should be given to this alias in the + # request. If it is not supplied, a generic name will be + # assigned. For example, if you want to call a Unix timestamp + # value 'tstamp', set its alias to that value. If two attributes + # in the same message request to use the same alias, the request + # will fail to be generated. + # @type alias: str or NoneType + class AttrInfo < Object + attr_reader :type_uri, :count, :ns_alias + attr_accessor :required + def initialize(type_uri, ns_alias=nil, required=false, count=1) + @type_uri = type_uri + @count = count + @required = required + @ns_alias = ns_alias + end + + def wants_unlimited_values? + @count == UNLIMITED_VALUES + end + end + + # Given a namespace mapping and a string containing a + # comma-separated list of namespace aliases, return a list of type + # URIs that correspond to those aliases. + # namespace_map: OpenID::NamespaceMap + def self.to_type_uris(namespace_map, alias_list_s) + return [] if alias_list_s.nil? + alias_list_s.split(',').inject([]) {|uris, name| + type_uri = namespace_map.get_namespace_uri(name) + raise IndexError, "No type defined for attribute name #{name.inspect}" if type_uri.nil? + uris << type_uri + } + end + + + # An attribute exchange 'fetch_request' message. This message is + # sent by a relying party when it wishes to obtain attributes about + # the subject of an OpenID authentication request. + class FetchRequest < AXMessage + attr_reader :requested_attributes + attr_accessor :update_url + + def initialize(update_url = nil) + super() + @mode = 'fetch_request' + @requested_attributes = {} + @update_url = update_url + end + + # Add an attribute to this attribute exchange request. + # attribute: AttrInfo, the attribute being requested + # Raises IndexError if the requested attribute is already present + # in this request. + def add(attribute) + if @requested_attributes[attribute.type_uri] + raise IndexError, "The attribute #{attribute.type_uri} has already been requested" + end + @requested_attributes[attribute.type_uri] = attribute + end + + # Get the serialized form of this attribute fetch request. + # returns a hash of the arguments + def get_extension_args + aliases = NamespaceMap.new + required = [] + if_available = [] + ax_args = new_args + @requested_attributes.each{|type_uri, attribute| + if attribute.ns_alias + name = aliases.add_alias(type_uri, attribute.ns_alias) + else + name = aliases.add(type_uri) + end + if attribute.required + required << name + else + if_available << name + end + if attribute.count != 1 + ax_args["count.#{name}"] = attribute.count.to_s + end + ax_args["type.#{name}"] = type_uri + } + + unless required.empty? + ax_args['required'] = required.join(',') + end + unless if_available.empty? + ax_args['if_available'] = if_available.join(',') + end + return ax_args + end + + # Get the type URIs for all attributes that have been marked + # as required. + def get_required_attrs + @requested_attributes.inject([]) {|required, (type_uri, attribute)| + if attribute.required + required << type_uri + else + required + end + } + end + + # Extract a FetchRequest from an OpenID message + # message: OpenID::Message + # return a FetchRequest or nil if AX arguments are not present + def self.from_openid_request(oidreq) + message = oidreq.message + ax_args = message.get_args(NS_URI) + return nil if ax_args == {} + req = new + req.parse_extension_args(ax_args) + + if req.update_url + realm = message.get_arg(OPENID_NS, 'realm', + message.get_arg(OPENID_NS, 'return_to')) + if realm.nil? or realm.empty? + raise Error, "Cannot validate update_url #{req.update_url.inspect} against absent realm" + end + tr = TrustRoot::TrustRoot.parse(realm) + unless tr.validate_url(req.update_url) + raise Error, "Update URL #{req.update_url.inspect} failed validation against realm #{realm.inspect}" + end + end + + return req + end + + def parse_extension_args(ax_args) + check_mode(ax_args) + + aliases = NamespaceMap.new + + ax_args.each{|k,v| + if k.index('type.') == 0 + name = k[5..-1] + type_uri = v + aliases.add_alias(type_uri, name) + + count_key = 'count.'+name + count_s = ax_args[count_key] + count = 1 + if count_s + if count_s == UNLIMITED_VALUES + count = count_s + else + count = count_s.to_i + if count <= 0 + raise Error, "Invalid value for count #{count_key.inspect}: #{count_s.inspect}" + end + end + end + add(AttrInfo.new(type_uri, name, false, count)) + end + } + + required = AX.to_type_uris(aliases, ax_args['required']) + required.each{|type_uri| + @requested_attributes[type_uri].required = true + } + if_available = AX.to_type_uris(aliases, ax_args['if_available']) + all_type_uris = required + if_available + + aliases.namespace_uris.each{|type_uri| + unless all_type_uris.member? type_uri + raise Error, "Type URI #{type_uri.inspect} was in the request but not present in 'required' or 'if_available'" + end + } + @update_url = ax_args['update_url'] + end + + # return the list of AttrInfo objects contained in the FetchRequest + def attributes + @requested_attributes.values + end + + # return the list of requested attribute type URIs + def requested_types + @requested_attributes.keys + end + + def member?(type_uri) + ! @requested_attributes[type_uri].nil? + end + + end + + # Abstract class that implements a message that has attribute + # keys and values. It contains the common code between + # fetch_response and store_request. + class KeyValueMessage < AXMessage + attr_reader :data + def initialize + super() + @mode = nil + @data = {} + @data.default = [] + end + + # Add a single value for the given attribute type to the + # message. If there are already values specified for this type, + # this value will be sent in addition to the values already + # specified. + def add_value(type_uri, value) + @data[type_uri] = @data[type_uri] << value + end + + # Set the values for the given attribute type. This replaces + # any values that have already been set for this attribute. + def set_values(type_uri, values) + @data[type_uri] = values + end + + # Get the extension arguments for the key/value pairs + # contained in this message. + def _get_extension_kv_args(aliases = nil) + aliases = NamespaceMap.new if aliases.nil? + + ax_args = new_args + + @data.each{|type_uri, values| + name = aliases.add(type_uri) + ax_args['type.'+name] = type_uri + ax_args['count.'+name] = values.size.to_s + + values.each_with_index{|value, i| + key = "value.#{name}.#{i+1}" + ax_args[key] = value + } + } + return ax_args + end + + # Parse attribute exchange key/value arguments into this object. + + def parse_extension_args(ax_args) + check_mode(ax_args) + aliases = NamespaceMap.new + + ax_args.each{|k, v| + if k.index('type.') == 0 + type_uri = v + name = k[5..-1] + + AX.check_alias(name) + aliases.add_alias(type_uri,name) + end + } + + aliases.each{|type_uri, name| + count_s = ax_args['count.'+name] + count = count_s.to_i + if count_s.nil? + value = ax_args['value.'+name] + if value.nil? + raise IndexError, "Missing #{'value.'+name} in FetchResponse" + elsif value.empty? + values = [] + else + values = [value] + end + elsif count_s.to_i == 0 + values = [] + else + values = (1..count).inject([]){|l,i| + key = "value.#{name}.#{i}" + v = ax_args[key] + raise IndexError, "Missing #{key} in FetchResponse" if v.nil? + l << v + } + end + @data[type_uri] = values + } + end + + # Get a single value for an attribute. If no value was sent + # for this attribute, use the supplied default. If there is more + # than one value for this attribute, this method will fail. + def get_single(type_uri, default = nil) + values = @data[type_uri] + return default if values.empty? + if values.size != 1 + raise Error, "More than one value present for #{type_uri.inspect}" + else + return values[0] + end + end + + # retrieve the list of values for this attribute + def get(type_uri) + @data[type_uri] + end + + # retrieve the list of values for this attribute + def [](type_uri) + @data[type_uri] + end + + # get the number of responses for this attribute + def count(type_uri) + @data[type_uri].size + end + + end + + # A fetch_response attribute exchange message + class FetchResponse < KeyValueMessage + attr_reader :update_url + + def initialize(update_url = nil) + super() + @mode = 'fetch_response' + @update_url = update_url + end + + # Serialize this object into arguments in the attribute + # exchange namespace + # Takes an optional FetchRequest. If specified, the response will be + # validated against this request, and empty responses for requested + # fields with no data will be sent. + def get_extension_args(request = nil) + aliases = NamespaceMap.new + zero_value_types = [] + + if request + # Validate the data in the context of the request (the + # same attributes should be present in each, and the + # counts in the response must be no more than the counts + # in the request) + @data.keys.each{|type_uri| + unless request.member? type_uri + raise IndexError, "Response attribute not present in request: #{type_uri.inspect}" + end + } + + request.attributes.each{|attr_info| + # Copy the aliases from the request so that reading + # the response in light of the request is easier + if attr_info.ns_alias.nil? + aliases.add(attr_info.type_uri) + else + aliases.add_alias(attr_info.type_uri, attr_info.ns_alias) + end + values = @data[attr_info.type_uri] + if values.empty? # @data defaults to [] + zero_value_types << attr_info + end + if attr_info.count != UNLIMITED_VALUES and attr_info.count < values.size + raise Error, "More than the number of requested values were specified for #{attr_info.type_uri.inspect}" + end + } + end + + kv_args = _get_extension_kv_args(aliases) + + # Add the KV args into the response with the args that are + # unique to the fetch_response + ax_args = new_args + + zero_value_types.each{|attr_info| + name = aliases.get_alias(attr_info.type_uri) + kv_args['type.' + name] = attr_info.type_uri + kv_args['count.' + name] = '0' + } + update_url = (request and request.update_url or @update_url) + ax_args['update_url'] = update_url unless update_url.nil? + ax_args.update(kv_args) + return ax_args + end + + def parse_extension_args(ax_args) + super + @update_url = ax_args['update_url'] + end + + # Construct a FetchResponse object from an OpenID library + # SuccessResponse object. + def self.from_success_response(success_response, signed=true) + obj = self.new + if signed + ax_args = success_response.get_signed_ns(obj.ns_uri) + else + ax_args = success_response.message.get_args(obj.ns_uri) + end + + begin + obj.parse_extension_args(ax_args) + return obj + rescue Error => e + return nil + end + end + end + + # A store request attribute exchange message representation + class StoreRequest < KeyValueMessage + def initialize + super + @mode = 'store_request' + end + + def get_extension_args(aliases=nil) + ax_args = new_args + kv_args = _get_extension_kv_args(aliases) + ax_args.update(kv_args) + return ax_args + end + end + + # An indication that the store request was processed along with + # this OpenID transaction. + class StoreResponse < AXMessage + SUCCESS_MODE = 'store_response_success' + FAILURE_MODE = 'store_response_failure' + attr_reader :error_message + + def initialize(succeeded = true, error_message = nil) + super() + if succeeded and error_message + raise Error, "Error message included in a success response" + end + if succeeded + @mode = SUCCESS_MODE + else + @mode = FAILURE_MODE + end + @error_message = error_message + end + + def succeeded? + @mode == SUCCESS_MODE + end + + def get_extension_args + ax_args = new_args + if !succeeded? and error_message + ax_args['error'] = @error_message + end + return ax_args + end + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/extensions/pape.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/extensions/pape.rb new file mode 100644 index 000000000..0a7413c1b --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/extensions/pape.rb @@ -0,0 +1,179 @@ +# An implementation of the OpenID Provider Authentication Policy +# Extension 1.0 +# see: http://openid.net/specs/ + +require 'openid/extension' + +module OpenID + + module PAPE + NS_URI = "http://specs.openid.net/extensions/pape/1.0" + AUTH_MULTI_FACTOR_PHYSICAL = + 'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical' + AUTH_MULTI_FACTOR = + 'http://schemas.openid.net/pape/policies/2007/06/multi-factor' + AUTH_PHISHING_RESISTANT = + 'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant' + TIME_VALIDATOR = /\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ/ + # A Provider Authentication Policy request, sent from a relying + # party to a provider + class Request < Extension + attr_accessor :preferred_auth_policies, :max_auth_age, :ns_alias, :ns_uri + def initialize(preferred_auth_policies=[], max_auth_age=nil) + @ns_alias = 'pape' + @ns_uri = NS_URI + @preferred_auth_policies = preferred_auth_policies + @max_auth_age = max_auth_age + end + + # Add an acceptable authentication policy URI to this request + # This method is intended to be used by the relying party to add + # acceptable authentication types to the request. + def add_policy_uri(policy_uri) + unless @preferred_auth_policies.member? policy_uri + @preferred_auth_policies << policy_uri + end + end + + def get_extension_args + ns_args = { + 'preferred_auth_policies' => @preferred_auth_policies.join(' ') + } + ns_args['max_auth_age'] = @max_auth_age.to_s if @max_auth_age + return ns_args + end + + # Instantiate a Request object from the arguments in a + # checkid_* OpenID message + # return nil if the extension was not requested. + def self.from_openid_request(oid_req) + pape_req = new + args = oid_req.message.get_args(NS_URI) + if args == {} + return nil + end + pape_req.parse_extension_args(args) + return pape_req + end + + # Set the state of this request to be that expressed in these + # PAPE arguments + def parse_extension_args(args) + @preferred_auth_policies = [] + policies_str = args['preferred_auth_policies'] + if policies_str + policies_str.split(' ').each{|uri| + add_policy_uri(uri) + } + end + + max_auth_age_str = args['max_auth_age'] + if max_auth_age_str + @max_auth_age = max_auth_age_str.to_i + else + @max_auth_age = nil + end + end + + # Given a list of authentication policy URIs that a provider + # supports, this method returns the subset of those types + # that are preferred by the relying party. + def preferred_types(supported_types) + @preferred_auth_policies.select{|uri| supported_types.member? uri} + end + end + + # A Provider Authentication Policy response, sent from a provider + # to a relying party + class Response < Extension + attr_accessor :ns_alias, :auth_policies, :auth_time, :nist_auth_level + def initialize(auth_policies=[], auth_time=nil, nist_auth_level=nil) + @ns_alias = 'pape' + @ns_uri = NS_URI + @auth_policies = auth_policies + @auth_time = auth_time + @nist_auth_level = nist_auth_level + end + + # Add a policy URI to the response + # see http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#auth_policies + def add_policy_uri(policy_uri) + @auth_policies << policy_uri unless @auth_policies.member?(policy_uri) + end + + # Create a Response object from an OpenID::Consumer::SuccessResponse + def self.from_success_response(success_response) + args = success_response.get_signed_ns(NS_URI) + return nil if args.nil? + pape_resp = new + pape_resp.parse_extension_args(args) + return pape_resp + end + + # parse the provider authentication policy arguments into the + # internal state of this object + # if strict is specified, raise an exception when bad data is + # encountered + def parse_extension_args(args, strict=false) + policies_str = args['auth_policies'] + if policies_str and policies_str != 'none' + @auth_policies = policies_str.split(' ') + end + + nist_level_str = args['nist_auth_level'] + if nist_level_str + # special handling of zero to handle to_i behavior + if nist_level_str.strip == '0' + nist_level = 0 + else + nist_level = nist_level_str.to_i + # if it's zero here we have a bad value + if nist_level == 0 + nist_level = nil + end + end + if nist_level and nist_level >= 0 and nist_level < 5 + @nist_auth_level = nist_level + elsif strict + raise ArgumentError, "nist_auth_level must be an integer 0 through 4, not #{nist_level_str.inspect}" + end + end + + auth_time_str = args['auth_time'] + if auth_time_str + # validate time string + if auth_time_str =~ TIME_VALIDATOR + @auth_time = auth_time_str + elsif strict + raise ArgumentError, "auth_time must be in RFC3339 format" + end + end + end + + def get_extension_args + ns_args = {} + if @auth_policies.empty? + ns_args['auth_policies'] = 'none' + else + ns_args['auth_policies'] = @auth_policies.join(' ') + end + if @nist_auth_level + unless (0..4).member? @nist_auth_level + raise ArgumentError, "nist_auth_level must be an integer 0 through 4, not #{@nist_auth_level.inspect}" + end + ns_args['nist_auth_level'] = @nist_auth_level.to_s + end + + if @auth_time + unless @auth_time =~ TIME_VALIDATOR + raise ArgumentError, "auth_time must be in RFC3339 format" + end + ns_args['auth_time'] = @auth_time + end + return ns_args + end + + end + end + +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/extensions/sreg.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/extensions/sreg.rb new file mode 100644 index 000000000..8dc780eb0 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/extensions/sreg.rb @@ -0,0 +1,277 @@ +require 'openid/extension' +require 'openid/util' +require 'openid/message' + +module OpenID + module SReg + DATA_FIELDS = { + 'fullname'=>'Full Name', + 'nickname'=>'Nickname', + 'dob'=>'Date of Birth', + 'email'=>'E-mail Address', + 'gender'=>'Gender', + 'postcode'=>'Postal Code', + 'country'=>'Country', + 'language'=>'Language', + 'timezone'=>'Time Zone', + } + + NS_URI_1_0 = 'http://openid.net/sreg/1.0' + NS_URI_1_1 = 'http://openid.net/extensions/sreg/1.1' + NS_URI = NS_URI_1_1 + + begin + Message.register_namespace_alias(NS_URI_1_1, 'sreg') + rescue NamespaceAliasRegistrationError => e + Util.log(e) + end + + # raise ArgumentError if fieldname is not in the defined sreg fields + def OpenID.check_sreg_field_name(fieldname) + unless DATA_FIELDS.member? fieldname + raise ArgumentError, "#{fieldname} is not a defined simple registration field" + end + end + + # Does the given endpoint advertise support for simple registration? + def OpenID.supports_sreg?(endpoint) + endpoint.uses_extension(NS_URI_1_1) || endpoint.uses_extension(NS_URI_1_0) + end + + # Extract the simple registration namespace URI from the given + # OpenID message. Handles OpenID 1 and 2, as well as both sreg + # namespace URIs found in the wild, as well as missing namespace + # definitions (for OpenID 1) + def OpenID.get_sreg_ns(message) + [NS_URI_1_1, NS_URI_1_0].each{|ns| + if message.namespaces.get_alias(ns) + return ns + end + } + # try to add an alias, since we didn't find one + ns = NS_URI_1_1 + begin + message.namespaces.add_alias(ns, 'sreg') + rescue IndexError + raise NamespaceError + end + return ns + end + + # The simple registration namespace was not found and could not + # be created using the expected name (there's another extension + # using the name 'sreg') + # + # This is not <em>illegal</em>, for OpenID 2, although it probably + # indicates a problem, since it's not expected that other extensions + # will re-use the alias that is in use for OpenID 1. + # + # If this is an OpenID 1 request, then there is no recourse. This + # should not happen unless some code has modified the namespaces for + # the message that is being processed. + class NamespaceError < ArgumentError + end + + # An object to hold the state of a simple registration request. + class Request < Extension + attr_reader :optional, :required, :ns_uri + attr_accessor :policy_url + def initialize(required = nil, optional = nil, policy_url = nil, ns_uri = NS_URI) + super() + + @policy_url = policy_url + @ns_uri = ns_uri + @ns_alias = 'sreg' + @required = [] + @optional = [] + + if required + request_fields(required, true, true) + end + if optional + request_fields(optional, false, true) + end + end + + # Create a simple registration request that contains the + # fields that were requested in the OpenID request with the + # given arguments + # Takes an OpenID::CheckIDRequest, returns an OpenID::Sreg::Request + # return nil if the extension was not requested. + def self.from_openid_request(request) + # Since we're going to mess with namespace URI mapping, don't + # mutate the object that was passed in. + message = request.message.copy + ns_uri = OpenID::get_sreg_ns(message) + args = message.get_args(ns_uri) + return nil if args == {} + req = new(nil,nil,nil,ns_uri) + req.parse_extension_args(args) + return req + end + + # Parse the unqualified simple registration request + # parameters and add them to this object. + # + # This method is essentially the inverse of + # getExtensionArgs. This method restores the serialized simple + # registration request fields. + # + # If you are extracting arguments from a standard OpenID + # checkid_* request, you probably want to use fromOpenIDRequest, + # which will extract the sreg namespace and arguments from the + # OpenID request. This method is intended for cases where the + # OpenID server needs more control over how the arguments are + # parsed than that method provides. + def parse_extension_args(args, strict = false) + required_items = args['required'] + unless required_items.nil? or required_items.empty? + required_items.split(',').each{|field_name| + begin + request_field(field_name, true, strict) + rescue ArgumentError + raise if strict + end + } + end + + optional_items = args['optional'] + unless optional_items.nil? or optional_items.empty? + optional_items.split(',').each{|field_name| + begin + request_field(field_name, false, strict) + rescue ArgumentError + raise if strict + end + } + end + @policy_url = args['policy_url'] + end + + # A list of all of the simple registration fields that were + # requested, whether they were required or optional. + def all_requested_fields + @required + @optional + end + + # Have any simple registration fields been requested? + def were_fields_requested? + !all_requested_fields.empty? + end + + # Request the specified field from the OpenID user + # field_name: the unqualified simple registration field name + # required: whether the given field should be presented + # to the user as being a required to successfully complete + # the request + # strict: whether to raise an exception when a field is + # added to a request more than once + # Raises ArgumentError if the field_name is not a simple registration + # field, or if strict is set and a field is added more than once + def request_field(field_name, required=false, strict=false) + OpenID::check_sreg_field_name(field_name) + + if strict + if (@required + @optional).member? field_name + raise ArgumentError, 'That field has already been requested' + end + else + return if @required.member? field_name + if @optional.member? field_name + if required + @optional.delete field_name + else + return + end + end + end + if required + @required << field_name + else + @optional << field_name + end + end + + # Add the given list of fields to the request. + def request_fields(field_names, required = false, strict = false) + raise ArgumentError unless field_names.respond_to?(:each) and + field_names[0].is_a?(String) + field_names.each{|fn|request_field(fn, required, strict)} + end + + # Get a hash of unqualified simple registration arguments + # representing this request. + # This method is essentially the inverse of parse_extension_args. + # This method serializes the simple registration request fields. + def get_extension_args + args = {} + args['required'] = @required.join(',') unless @required.empty? + args['optional'] = @optional.join(',') unless @optional.empty? + args['policy_url'] = @policy_url unless @policy_url.nil? + return args + end + + def member?(field_name) + all_requested_fields.member?(field_name) + end + + end + + # Represents the data returned in a simple registration response + # inside of an OpenID id_res response. This object will be + # created by the OpenID server, added to the id_res response + # object, and then extracted from the id_res message by the Consumer. + class Response < Extension + attr_reader :ns_uri, :data + + def initialize(data = {}, ns_uri=NS_URI) + @ns_alias = 'sreg' + @data = data + @ns_uri = ns_uri + end + + # Take a Request and a hash of simple registration + # values and create a Response object containing that data. + def self.extract_response(request, data) + arf = request.all_requested_fields + resp_data = data.reject{|k,v| !arf.member?(k) || v.nil? } + new(resp_data, request.ns_uri) + end + + # Create an Response object from an + # OpenID::Consumer::SuccessResponse from consumer.complete + # If you set the signed_only parameter to false, unsigned data from + # the id_res message from the server will be processed. + def self.from_success_response(success_response, signed_only = true) + ns_uri = OpenID::get_sreg_ns(success_response.message) + if signed_only + args = success_response.get_signed_ns(ns_uri) + return nil if args.nil? # No signed args, so fail + else + args = success_response.message.get_args(ns_uri) + end + args.reject!{|k,v| !DATA_FIELDS.member?(k) } + new(args, ns_uri) + end + + # Get the fields to put in the simple registration namespace + # when adding them to an id_res message. + def get_extension_args + return @data + end + + # Read-only hashlike interface. + # Raises an exception if the field name is bad + def [](field_name) + OpenID::check_sreg_field_name(field_name) + data[field_name] + end + + def empty? + @data.empty? + end + # XXX is there more to a hashlike interface I should add? + end + end +end + diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/extras.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/extras.rb new file mode 100644 index 000000000..0d9560abc --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/extras.rb @@ -0,0 +1,11 @@ +class String + def starts_with?(other) + head = self[0, other.length] + head == other + end + + def ends_with?(other) + tail = self[-1 * other.length, other.length] + tail == other + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/fetchers.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/fetchers.rb new file mode 100644 index 000000000..22c87ac33 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/fetchers.rb @@ -0,0 +1,238 @@ +require 'net/http' +require 'openid' +require 'openid/util' + +begin + require 'net/https' +rescue LoadError + OpenID::Util.log('WARNING: no SSL support found. Will not be able ' + + 'to fetch HTTPS URLs!') + require 'net/http' +end + +MAX_RESPONSE_KB = 1024 + +module Net + class HTTP + def post_connection_check(hostname) + check_common_name = true + cert = @socket.io.peer_cert + cert.extensions.each { |ext| + next if ext.oid != "subjectAltName" + ext.value.split(/,\s+/).each{ |general_name| + if /\ADNS:(.*)/ =~ general_name + check_common_name = false + reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+") + return true if /\A#{reg}\z/i =~ hostname + elsif /\AIP Address:(.*)/ =~ general_name + check_common_name = false + return true if $1 == hostname + end + } + } + if check_common_name + cert.subject.to_a.each{ |oid, value| + if oid == "CN" + reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+") + return true if /\A#{reg}\z/i =~ hostname + end + } + end + raise OpenSSL::SSL::SSLError, "hostname does not match" + end + end +end + +module OpenID + # Our HTTPResponse class extends Net::HTTPResponse with an additional + # method, final_url. + class HTTPResponse + attr_accessor :final_url + + attr_accessor :_response + + def self._from_net_response(response, final_url, headers=nil) + me = self.new + me._response = response + me.final_url = final_url + return me + end + + def method_missing(method, *args) + @_response.send(method, *args) + end + + def body=(s) + @_response.instance_variable_set('@body', s) + # XXX Hack to work around ruby's HTTP library behavior. @body + # is only returned if it has been read from the response + # object's socket, but since we're not using a socket in this + # case, we need to set the @read flag to true to avoid a bug in + # Net::HTTPResponse.stream_check when @socket is nil. + @_response.instance_variable_set('@read', true) + end + end + + class FetchingError < OpenIDError + end + + class HTTPRedirectLimitReached < FetchingError + end + + class SSLFetchingError < FetchingError + end + + @fetcher = nil + + def self.fetch(url, body=nil, headers=nil, + redirect_limit=StandardFetcher::REDIRECT_LIMIT) + return fetcher.fetch(url, body, headers, redirect_limit) + end + + def self.fetcher + if @fetcher.nil? + @fetcher = StandardFetcher.new + end + + return @fetcher + end + + def self.fetcher=(fetcher) + @fetcher = fetcher + end + + # Set the default fetcher to use the HTTP proxy defined in the environment + # variable 'http_proxy'. + def self.fetcher_use_env_http_proxy + proxy_string = ENV['http_proxy'] + return unless proxy_string + + proxy_uri = URI.parse(proxy_string) + @fetcher = StandardFetcher.new(proxy_uri.host, proxy_uri.port, + proxy_uri.user, proxy_uri.password) + end + + class StandardFetcher + + USER_AGENT = "ruby-openid/#{OpenID::VERSION} (#{RUBY_PLATFORM})" + + REDIRECT_LIMIT = 5 + TIMEOUT = 60 + + attr_accessor :ca_file + attr_accessor :timeout + + # I can fetch through a HTTP proxy; arguments are as for Net::HTTP::Proxy. + def initialize(proxy_addr=nil, proxy_port=nil, + proxy_user=nil, proxy_pass=nil) + @ca_file = nil + @proxy = Net::HTTP::Proxy(proxy_addr, proxy_port, proxy_user, proxy_pass) + @timeout = TIMEOUT + end + + def supports_ssl?(conn) + return conn.respond_to?(:use_ssl=) + end + + def make_http(uri) + http = @proxy.new(uri.host, uri.port) + http.read_timeout = @timeout + http.open_timeout = @timeout + return http + end + + def set_verified(conn, verify) + if verify + conn.verify_mode = OpenSSL::SSL::VERIFY_PEER + else + conn.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + end + + def make_connection(uri) + conn = make_http(uri) + + if !conn.is_a?(Net::HTTP) + raise RuntimeError, sprintf("Expected Net::HTTP object from make_http; got %s", + conn.class) + end + + if uri.scheme == 'https' + if supports_ssl?(conn) + + conn.use_ssl = true + + if @ca_file + set_verified(conn, true) + conn.ca_file = @ca_file + else + Util.log("WARNING: making https request to #{uri} without verifying " + + "server certificate; no CA path was specified.") + set_verified(conn, false) + end + else + raise RuntimeError, "SSL support not found; cannot fetch #{uri}" + end + end + + return conn + end + + def fetch(url, body=nil, headers=nil, redirect_limit=REDIRECT_LIMIT) + unparsed_url = url.dup + url = URI::parse(url) + if url.nil? + raise FetchingError, "Invalid URL: #{unparsed_url}" + end + + headers ||= {} + headers['User-agent'] ||= USER_AGENT + + begin + conn = make_connection(url) + response = nil + + response = conn.start { + # Check the certificate against the URL's hostname + if supports_ssl?(conn) and conn.use_ssl? + conn.post_connection_check(url.host) + end + + if body.nil? + conn.request_get(url.request_uri, headers) + else + headers["Content-type"] ||= "application/x-www-form-urlencoded" + conn.request_post(url.request_uri, body, headers) + end + } + rescue RuntimeError => why + raise why + rescue OpenSSL::SSL::SSLError => why + raise SSLFetchingError, "Error connecting to SSL URL #{url}: #{why}" + rescue FetchingError => why + raise why + rescue Exception => why + # Things we've caught here include a Timeout::Error, which descends + # from SignalException. + raise FetchingError, "Error fetching #{url}: #{why}" + end + + case response + when Net::HTTPRedirection + if redirect_limit <= 0 + raise HTTPRedirectLimitReached.new( + "Too many redirects, not fetching #{response['location']}") + end + begin + return fetch(response['location'], body, headers, redirect_limit - 1) + rescue HTTPRedirectLimitReached => e + raise e + rescue FetchingError => why + raise FetchingError, "Error encountered in redirect from #{url}: #{why}" + end + else + return HTTPResponse._from_net_response(response, unparsed_url) + end + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/kvform.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/kvform.rb new file mode 100644 index 000000000..c534d203e --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/kvform.rb @@ -0,0 +1,136 @@ + +module OpenID + + class KVFormError < Exception + end + + module Util + + def Util.seq_to_kv(seq, strict=false) + # Represent a sequence of pairs of strings as newline-terminated + # key:value pairs. The pairs are generated in the order given. + # + # @param seq: The pairs + # + # returns a string representation of the sequence + err = lambda { |msg| + msg = "seq_to_kv warning: #{msg}: #{seq.inspect}" + if strict + raise KVFormError, msg + else + Util.log(msg) + end + } + + lines = [] + seq.each { |k, v| + if !k.is_a?(String) + err.call("Converting key to string: #{k.inspect}") + k = k.to_s + end + + if !k.index("\n").nil? + raise KVFormError, "Invalid input for seq_to_kv: key contains newline: #{k.inspect}" + end + + if !k.index(":").nil? + raise KVFormError, "Invalid input for seq_to_kv: key contains colon: #{k.inspect}" + end + + if k.strip() != k + err.call("Key has whitespace at beginning or end: #{k.inspect}") + end + + if !v.is_a?(String) + err.call("Converting value to string: #{v.inspect}") + v = v.to_s + end + + if !v.index("\n").nil? + raise KVFormError, "Invalid input for seq_to_kv: value contains newline: #{v.inspect}" + end + + if v.strip() != v + err.call("Value has whitespace at beginning or end: #{v.inspect}") + end + + lines << k + ":" + v + "\n" + } + + return lines.join("") + end + + def Util.kv_to_seq(data, strict=false) + # After one parse, seq_to_kv and kv_to_seq are inverses, with no + # warnings: + # + # seq = kv_to_seq(s) + # seq_to_kv(kv_to_seq(seq)) == seq + err = lambda { |msg| + msg = "kv_to_seq warning: #{msg}: #{data.inspect}" + if strict + raise KVFormError, msg + else + Util.log(msg) + end + } + + lines = data.split("\n") + if data.length == 0 + return [] + end + + if data[-1].chr != "\n" + err.call("Does not end in a newline") + # We don't expect the last element of lines to be an empty + # string because split() doesn't behave that way. + end + + pairs = [] + line_num = 0 + lines.each { |line| + line_num += 1 + + # Ignore blank lines + if line.strip() == "" + next + end + + pair = line.split(':', 2) + if pair.length == 2 + k, v = pair + k_s = k.strip() + if k_s != k + msg = "In line #{line_num}, ignoring leading or trailing whitespace in key #{k.inspect}" + err.call(msg) + end + + if k_s.length == 0 + err.call("In line #{line_num}, got empty key") + end + + v_s = v.strip() + if v_s != v + msg = "In line #{line_num}, ignoring leading or trailing whitespace in value #{v.inspect}" + err.call(msg) + end + + pairs << [k_s, v_s] + else + err.call("Line #{line_num} does not contain a colon") + end + } + + return pairs + end + + def Util.dict_to_kv(d) + return seq_to_kv(d.entries.sort) + end + + def Util.kv_to_dict(s) + seq = kv_to_seq(s) + return Hash[*seq.flatten] + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/kvpost.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/kvpost.rb new file mode 100644 index 000000000..1495afe74 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/kvpost.rb @@ -0,0 +1,58 @@ +require "openid/message" +require "openid/fetchers" + +module OpenID + # Exception that is raised when the server returns a 400 response + # code to a direct request. + class ServerError < OpenIDError + attr_reader :error_text, :error_code, :message + + def initialize(error_text, error_code, message) + super(error_text) + @error_text = error_text + @error_code = error_code + @message = message + end + + def self.from_message(msg) + error_text = msg.get_arg(OPENID_NS, 'error', + '<no error message supplied>') + error_code = msg.get_arg(OPENID_NS, 'error_code') + return self.new(error_text, error_code, msg) + end + end + + class KVPostNetworkError < OpenIDError + end + class HTTPStatusError < OpenIDError + end + + class Message + def self.from_http_response(response, server_url) + msg = self.from_kvform(response.body) + case response.code.to_i + when 200 + return msg + when 206 + return msg + when 400 + raise ServerError.from_message(msg) + else + error_message = "bad status code from server #{server_url}: "\ + "#{response.code}" + raise HTTPStatusError.new(error_message) + end + end + end + + # Send the message to the server via HTTP POST and receive and parse + # a response in KV Form + def self.make_kv_post(request_message, server_url) + begin + http_response = self.fetch(server_url, request_message.to_url_encoded) + rescue Exception + raise KVPostNetworkError.new("Unable to contact OpenID server: #{$!.to_s}") + end + return Message.from_http_response(http_response, server_url) + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/message.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/message.rb new file mode 100644 index 000000000..8700378ba --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/message.rb @@ -0,0 +1,553 @@ +require 'openid/util' +require 'openid/kvform' + +module OpenID + + IDENTIFIER_SELECT = 'http://specs.openid.net/auth/2.0/identifier_select' + + # URI for Simple Registration extension, the only commonly deployed + # OpenID 1.x extension, and so a special case. + SREG_URI = 'http://openid.net/sreg/1.0' + + # The OpenID 1.x namespace URIs + OPENID1_NS = 'http://openid.net/signon/1.0' + OPENID11_NS = 'http://openid.net/signon/1.1' + OPENID1_NAMESPACES = [OPENID1_NS, OPENID11_NS] + + # The OpenID 2.0 namespace URI + OPENID2_NS = 'http://specs.openid.net/auth/2.0' + + # The namespace consisting of pairs with keys that are prefixed with + # "openid." but not in another namespace. + NULL_NAMESPACE = :null_namespace + + # The null namespace, when it is an allowed OpenID namespace + OPENID_NS = :openid_namespace + + # The top-level namespace, excluding all pairs with keys that start + # with "openid." + BARE_NS = :bare_namespace + + # Limit, in bytes, of identity provider and return_to URLs, + # including response payload. See OpenID 1.1 specification, + # Appendix D. + OPENID1_URL_LIMIT = 2047 + + # All OpenID protocol fields. Used to check namespace aliases. + OPENID_PROTOCOL_FIELDS = [ + 'ns', 'mode', 'error', 'return_to', + 'contact', 'reference', 'signed', + 'assoc_type', 'session_type', + 'dh_modulus', 'dh_gen', + 'dh_consumer_public', 'claimed_id', + 'identity', 'realm', 'invalidate_handle', + 'op_endpoint', 'response_nonce', 'sig', + 'assoc_handle', 'trust_root', 'openid', + ] + + # Sentinel used for Message implementation to indicate that getArg + # should raise an exception instead of returning a default. + NO_DEFAULT = :no_default + + # Raised if the generic OpenID namespace is accessed when there + # is no OpenID namespace set for this message. + class UndefinedOpenIDNamespace < Exception; end + + # Raised when an alias or namespace URI has already been registered. + class NamespaceAliasRegistrationError < Exception; end + + # Raised if openid.ns is not a recognized value. + # See Message class variable @@allowed_openid_namespaces + class InvalidOpenIDNamespace < Exception; end + + class Message + attr_reader :namespaces + + # Raised when key lookup fails + class KeyNotFound < IndexError ; end + + # Namespace / alias registration map. See + # register_namespace_alias. + @@registered_aliases = {} + + # Registers a (namespace URI, alias) mapping in a global namespace + # alias map. Raises NamespaceAliasRegistrationError if either the + # namespace URI or alias has already been registered with a + # different value. This function is required if you want to use a + # namespace with an OpenID 1 message. + def Message.register_namespace_alias(namespace_uri, alias_) + if @@registered_aliases[alias_] == namespace_uri + return + end + + if @@registered_aliases.values.include?(namespace_uri) + raise NamespaceAliasRegistrationError, + 'Namespace uri #{namespace_uri} already registered' + end + + if @@registered_aliases.member?(alias_) + raise NamespaceAliasRegistrationError, + 'Alias #{alias_} already registered' + end + + @@registered_aliases[alias_] = namespace_uri + end + + @@allowed_openid_namespaces = [OPENID1_NS, OPENID2_NS, OPENID11_NS] + + # Raises InvalidNamespaceError if you try to instantiate a Message + # with a namespace not in the above allowed list + def initialize(openid_namespace=nil) + @args = {} + @namespaces = NamespaceMap.new + if openid_namespace + implicit = OPENID1_NAMESPACES.member? openid_namespace + self.set_openid_namespace(openid_namespace, implicit) + else + @openid_ns_uri = nil + end + end + + # Construct a Message containing a set of POST arguments. + # Raises InvalidNamespaceError if you try to instantiate a Message + # with a namespace not in the above allowed list + def Message.from_post_args(args) + m = Message.new + openid_args = {} + args.each do |key,value| + if value.is_a?(Array) + raise ArgumentError, "Query dict must have one value for each key, " + + "not lists of values. Query is #{args.inspect}" + end + + prefix, rest = key.split('.', 2) + + if prefix != 'openid' or rest.nil? + m.set_arg(BARE_NS, key, value) + else + openid_args[rest] = value + end + end + + m._from_openid_args(openid_args) + return m + end + + # Construct a Message from a parsed KVForm message. + # Raises InvalidNamespaceError if you try to instantiate a Message + # with a namespace not in the above allowed list + def Message.from_openid_args(openid_args) + m = Message.new + m._from_openid_args(openid_args) + return m + end + + # Raises InvalidNamespaceError if you try to instantiate a Message + # with a namespace not in the above allowed list + def _from_openid_args(openid_args) + ns_args = [] + + # resolve namespaces + openid_args.each { |rest, value| + ns_alias, ns_key = rest.split('.', 2) + if ns_key.nil? + ns_alias = NULL_NAMESPACE + ns_key = rest + end + + if ns_alias == 'ns' + @namespaces.add_alias(value, ns_key) + elsif ns_alias == NULL_NAMESPACE and ns_key == 'ns' + set_openid_namespace(value, false) + else + ns_args << [ns_alias, ns_key, value] + end + } + + # implicitly set an OpenID 1 namespace + unless get_openid_namespace + set_openid_namespace(OPENID1_NS, true) + end + + # put the pairs into the appropriate namespaces + ns_args.each { |ns_alias, ns_key, value| + ns_uri = @namespaces.get_namespace_uri(ns_alias) + unless ns_uri + ns_uri = _get_default_namespace(ns_alias) + unless ns_uri + ns_uri = get_openid_namespace + ns_key = "#{ns_alias}.#{ns_key}" + else + @namespaces.add_alias(ns_uri, ns_alias, true) + end + end + self.set_arg(ns_uri, ns_key, value) + } + end + + def _get_default_namespace(mystery_alias) + # only try to map an alias to a default if it's an + # OpenID 1.x namespace + if is_openid1 + @@registered_aliases[mystery_alias] + end + end + + def set_openid_namespace(openid_ns_uri, implicit) + if !@@allowed_openid_namespaces.include?(openid_ns_uri) + raise InvalidOpenIDNamespace, "Invalid null namespace: #{openid_ns_uri}" + end + @namespaces.add_alias(openid_ns_uri, NULL_NAMESPACE, implicit) + @openid_ns_uri = openid_ns_uri + end + + def get_openid_namespace + return @openid_ns_uri + end + + def is_openid1 + return OPENID1_NAMESPACES.member?(@openid_ns_uri) + end + + def is_openid2 + return @openid_ns_uri == OPENID2_NS + end + + # Create a message from a KVForm string + def Message.from_kvform(kvform_string) + return Message.from_openid_args(Util.kv_to_dict(kvform_string)) + end + + def copy + return Marshal.load(Marshal.dump(self)) + end + + # Return all arguments with "openid." in from of namespaced arguments. + def to_post_args + args = {} + + # add namespace defs to the output + @namespaces.each { |ns_uri, ns_alias| + if @namespaces.implicit?(ns_uri) + next + end + if ns_alias == NULL_NAMESPACE + ns_key = 'openid.ns' + else + ns_key = 'openid.ns.' + ns_alias + end + args[ns_key] = ns_uri + } + + @args.each { |k, value| + ns_uri, ns_key = k + key = get_key(ns_uri, ns_key) + args[key] = value + } + + return args + end + + # Return all namespaced arguments, failing if any non-namespaced arguments + # exist. + def to_args + post_args = self.to_post_args + kvargs = {} + post_args.each { |k,v| + if !k.starts_with?('openid.') + raise ArgumentError, "This message can only be encoded as a POST, because it contains arguments that are not prefixed with 'openid.'" + else + kvargs[k[7..-1]] = v + end + } + return kvargs + end + + # Generate HTML form markup that contains the values in this + # message, to be HTTP POSTed as x-www-form-urlencoded UTF-8. + def to_form_markup(action_url, form_tag_attrs=nil, submit_text='Continue') + form_tag_attr_map = {} + + if form_tag_attrs + form_tag_attrs.each { |name, attr| + form_tag_attr_map[name] = attr + } + end + + form_tag_attr_map['action'] = action_url + form_tag_attr_map['method'] = 'post' + form_tag_attr_map['accept-charset'] = 'UTF-8' + form_tag_attr_map['enctype'] = 'application/x-www-form-urlencoded' + + markup = "<form " + + form_tag_attr_map.each { |k, v| + markup += " #{k}=\"#{v}\"" + } + + markup += ">\n" + + to_post_args.each { |k,v| + markup += "<input type='hidden' name='#{k}' value='#{v}' />\n" + } + markup += "<input type='submit' value='#{submit_text}' />\n" + markup += "\n</form>" + return markup + end + + # Generate a GET URL with the paramters in this message attacked as + # query parameters. + def to_url(base_url) + return Util.append_args(base_url, self.to_post_args) + end + + # Generate a KVForm string that contains the parameters in this message. + # This will fail is the message contains arguments outside of the + # "openid." prefix. + def to_kvform + return Util.dict_to_kv(to_args) + end + + # Generate an x-www-urlencoded string. + def to_url_encoded + args = to_post_args.map.sort + return Util.urlencode(args) + end + + # Convert an input value into the internally used values of this obejct. + def _fix_ns(namespace) + if namespace == OPENID_NS + unless @openid_ns_uri + raise UndefinedOpenIDNamespace, 'OpenID namespace not set' + else + namespace = @openid_ns_uri + end + end + + if namespace == BARE_NS + return namespace + end + + if !namespace.is_a?(String) + raise ArgumentError, ("Namespace must be BARE_NS, OPENID_NS or "\ + "a string. Got #{namespace.inspect}") + end + + if namespace.index(':').nil? + msg = ("OpenID 2.0 namespace identifiers SHOULD be URIs. "\ + "Got #{namespace.inspect}") + Util.log(msg) + + if namespace == 'sreg' + msg = "Using #{SREG_URI} instead of \"sreg\" as namespace" + Util.log(msg) + return SREG_URI + end + end + + return namespace + end + + def has_key?(namespace, ns_key) + namespace = _fix_ns(namespace) + return @args.member?([namespace, ns_key]) + end + + # Get the key for a particular namespaced argument + def get_key(namespace, ns_key) + namespace = _fix_ns(namespace) + return ns_key if namespace == BARE_NS + + ns_alias = @namespaces.get_alias(namespace) + + # no alias is defined, so no key can exist + return nil if ns_alias.nil? + + if ns_alias == NULL_NAMESPACE + tail = ns_key + else + tail = "#{ns_alias}.#{ns_key}" + end + + return 'openid.' + tail + end + + # Get a value for a namespaced key. + def get_arg(namespace, key, default=nil) + namespace = _fix_ns(namespace) + @args.fetch([namespace, key]) { + if default == NO_DEFAULT + raise KeyNotFound, "<#{namespace}>#{key} not in this message" + else + default + end + } + end + + # Get the arguments that are defined for this namespace URI. + def get_args(namespace) + namespace = _fix_ns(namespace) + args = {} + @args.each { |k,v| + pair_ns, ns_key = k + args[ns_key] = v if pair_ns == namespace + } + return args + end + + # Set multiple key/value pairs in one call. + def update_args(namespace, updates) + namespace = _fix_ns(namespace) + updates.each {|k,v| set_arg(namespace, k, v)} + end + + # Set a single argument in this namespace + def set_arg(namespace, key, value) + namespace = _fix_ns(namespace) + @args[[namespace, key].freeze] = value + if namespace != BARE_NS + @namespaces.add(namespace) + end + end + + # Remove a single argument from this namespace. + def del_arg(namespace, key) + namespace = _fix_ns(namespace) + _key = [namespace, key] + @args.delete(_key) + end + + def ==(other) + other.is_a?(self.class) && @args == other.instance_eval { @args } + end + + def get_aliased_arg(aliased_key, default=nil) + if aliased_key == 'ns' + return get_openid_namespace() + end + + ns_alias, key = aliased_key.split('.', 2) + if ns_alias == 'ns' + uri = @namespaces.get_namespace_uri(key) + if uri.nil? and default == NO_DEFAULT + raise KeyNotFound, "Namespace #{key} not defined when looking "\ + "for #{aliased_key}" + else + return (uri.nil? ? default : uri) + end + end + + if key.nil? + key = aliased_key + ns = nil + else + ns = @namespaces.get_namespace_uri(ns_alias) + end + + if ns.nil? + key = aliased_key + ns = get_openid_namespace + end + + return get_arg(ns, key, default) + end + end + + + # Maintains a bidirectional map between namespace URIs and aliases. + class NamespaceMap + + def initialize + @alias_to_namespace = {} + @namespace_to_alias = {} + @implicit_namespaces = [] + end + + def get_alias(namespace_uri) + @namespace_to_alias[namespace_uri] + end + + def get_namespace_uri(namespace_alias) + @alias_to_namespace[namespace_alias] + end + + # Add an alias from this namespace URI to the alias. + def add_alias(namespace_uri, desired_alias, implicit=false) + # Check that desired_alias is not an openid protocol field as + # per the spec. + Util.assert(!OPENID_PROTOCOL_FIELDS.include?(desired_alias), + "#{desired_alias} is not an allowed namespace alias") + + # check that there is not a namespace already defined for the + # desired alias + current_namespace_uri = @alias_to_namespace.fetch(desired_alias, nil) + if current_namespace_uri and current_namespace_uri != namespace_uri + raise IndexError, "Cannot map #{namespace_uri} to alias #{desired_alias}. #{current_namespace_uri} is already mapped to alias #{desired_alias}" + end + + # Check that desired_alias does not contain a period as per the + # spec. + if desired_alias.is_a?(String) + Util.assert(desired_alias.index('.').nil?, + "#{desired_alias} must not contain a dot") + end + + # check that there is not already a (different) alias for this + # namespace URI. + _alias = @namespace_to_alias[namespace_uri] + if _alias and _alias != desired_alias + raise IndexError, "Cannot map #{namespace_uri} to alias #{desired_alias}. It is already mapped to alias #{_alias}" + end + + @alias_to_namespace[desired_alias] = namespace_uri + @namespace_to_alias[namespace_uri] = desired_alias + @implicit_namespaces << namespace_uri if implicit + return desired_alias + end + + # Add this namespace URI to the mapping, without caring what alias + # it ends up with. + def add(namespace_uri) + # see if this namepace is already mapped to an alias + _alias = @namespace_to_alias[namespace_uri] + return _alias if _alias + + # Fall back to generating a numberical alias + i = 0 + while true + _alias = 'ext' + i.to_s + begin + add_alias(namespace_uri, _alias) + rescue IndexError + i += 1 + else + return _alias + end + end + + raise StandardError, 'Unreachable' + end + + def member?(namespace_uri) + @namespace_to_alias.has_key?(namespace_uri) + end + + def each + @namespace_to_alias.each {|k,v| yield k,v} + end + + def namespace_uris + # Return an iterator over the namespace URIs + return @namespace_to_alias.keys() + end + + def implicit?(namespace_uri) + return @implicit_namespaces.member?(namespace_uri) + end + + def aliases + # Return an iterator over the aliases + return @alias_to_namespace.keys() + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/protocolerror.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/protocolerror.rb new file mode 100644 index 000000000..2aad0e4a2 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/protocolerror.rb @@ -0,0 +1,8 @@ +require 'openid/util' + +module OpenID + + # An error in the OpenID protocol + class ProtocolError < OpenIDError + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/server.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/server.rb new file mode 100644 index 000000000..897b8bdb3 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/server.rb @@ -0,0 +1,1544 @@ + +require 'openid/cryptutil' +require 'openid/util' +require 'openid/dh' +require 'openid/store/nonce' +require 'openid/trustroot' +require 'openid/association' +require 'openid/message' + +require 'time' + +module OpenID + + module Server + + HTTP_OK = 200 + HTTP_REDIRECT = 302 + HTTP_ERROR = 400 + + BROWSER_REQUEST_MODES = ['checkid_setup', 'checkid_immediate'] + + ENCODE_KVFORM = ['kvform'].freeze + ENCODE_URL = ['URL/redirect'].freeze + ENCODE_HTML_FORM = ['HTML form'].freeze + + UNUSED = nil + + class OpenIDRequest + attr_accessor :message, :mode + + # I represent an incoming OpenID request. + # + # Attributes: + # mode:: The "openid.mode" of this request + def initialize + @mode = nil + @message = nil + end + + def namespace + if @message.nil? + raise RuntimeError, "Request has no message" + else + return @message.get_openid_namespace + end + end + end + + # A request to verify the validity of a previous response. + # + # See OpenID Specs, Verifying Directly with the OpenID Provider + # <http://openid.net/specs/openid-authentication-2_0-12.html#verifying_signatures> + class CheckAuthRequest < OpenIDRequest + + # The association handle the response was signed with. + attr_accessor :assoc_handle + + # The message with the signature which wants checking. + attr_accessor :signed + + # An association handle the client is asking about the validity + # of. May be nil. + attr_accessor :invalidate_handle + + attr_accessor :sig + + # Construct me. + # + # These parameters are assigned directly as class attributes. + # + # Parameters: + # assoc_handle:: the association handle for this request + # signed:: The signed message + # invalidate_handle:: An association handle that the relying + # party is checking to see if it is invalid + def initialize(assoc_handle, signed, invalidate_handle=nil) + super() + + @mode = "check_authentication" + @required_fields = ["identity", "return_to", "response_nonce"].freeze + + @sig = nil + @assoc_handle = assoc_handle + @signed = signed + @invalidate_handle = invalidate_handle + end + + # Construct me from an OpenID::Message. + def self.from_message(message, op_endpoint=UNUSED) + assoc_handle = message.get_arg(OPENID_NS, 'assoc_handle') + invalidate_handle = message.get_arg(OPENID_NS, 'invalidate_handle') + + signed = message.copy() + # openid.mode is currently check_authentication because + # that's the mode of this request. But the signature + # was made on something with a different openid.mode. + # http://article.gmane.org/gmane.comp.web.openid.general/537 + if signed.has_key?(OPENID_NS, "mode") + signed.set_arg(OPENID_NS, "mode", "id_res") + end + + obj = self.new(assoc_handle, signed, invalidate_handle) + obj.message = message + obj.sig = message.get_arg(OPENID_NS, 'sig') + + if !obj.assoc_handle or + !obj.sig + msg = sprintf("%s request missing required parameter from message %s", + obj.mode, message) + raise ProtocolError.new(message, msg) + end + + return obj + end + + # Respond to this request. + # + # Given a Signatory, I can check the validity of the signature + # and the invalidate_handle. I return a response with an + # is_valid (and, if appropriate invalidate_handle) field. + def answer(signatory) + is_valid = signatory.verify(@assoc_handle, @signed) + # Now invalidate that assoc_handle so it this checkAuth + # message cannot be replayed. + signatory.invalidate(@assoc_handle, dumb=true) + response = OpenIDResponse.new(self) + valid_str = is_valid ? "true" : "false" + response.fields.set_arg(OPENID_NS, 'is_valid', valid_str) + + if @invalidate_handle + assoc = signatory.get_association(@invalidate_handle, false) + if !assoc + response.fields.set_arg( + OPENID_NS, 'invalidate_handle', @invalidate_handle) + end + end + + return response + end + + def to_s + ih = nil + + if @invalidate_handle + ih = sprintf(" invalidate? %s", @invalidate_handle) + else + ih = "" + end + + s = sprintf("<%s handle: %s sig: %s: signed: %s%s>", + self.class, @assoc_handle, + @sig, @signed, ih) + return s + end + end + + class BaseServerSession + attr_reader :session_type + + def initialize(session_type, allowed_assoc_types) + @session_type = session_type + @allowed_assoc_types = allowed_assoc_types.dup.freeze + end + + def allowed_assoc_type?(typ) + @allowed_assoc_types.member?(typ) + end + end + + # An object that knows how to handle association requests with + # no session type. + # + # See OpenID Specs, Section 8: Establishing Associations + # <http://openid.net/specs/openid-authentication-2_0-12.html#associations> + class PlainTextServerSession < BaseServerSession + # The session_type for this association session. There is no + # type defined for plain-text in the OpenID specification, so we + # use 'no-encryption'. + attr_reader :session_type + + def initialize + super('no-encryption', ['HMAC-SHA1', 'HMAC-SHA256']) + end + + def self.from_message(unused_request) + return self.new + end + + def answer(secret) + return {'mac_key' => Util.to_base64(secret)} + end + end + + # An object that knows how to handle association requests with the + # Diffie-Hellman session type. + # + # See OpenID Specs, Section 8: Establishing Associations + # <http://openid.net/specs/openid-authentication-2_0-12.html#associations> + class DiffieHellmanSHA1ServerSession < BaseServerSession + + # The Diffie-Hellman algorithm values for this request + attr_accessor :dh + + # The public key sent by the consumer in the associate request + attr_accessor :consumer_pubkey + + # The session_type for this association session. + attr_reader :session_type + + def initialize(dh, consumer_pubkey) + super('DH-SHA1', ['HMAC-SHA1']) + + @hash_func = CryptUtil.method('sha1') + @dh = dh + @consumer_pubkey = consumer_pubkey + end + + # Construct me from OpenID Message + # + # Raises ProtocolError when parameters required to establish the + # session are missing. + def self.from_message(message) + dh_modulus = message.get_arg(OPENID_NS, 'dh_modulus') + dh_gen = message.get_arg(OPENID_NS, 'dh_gen') + if ((!dh_modulus and dh_gen) or + (!dh_gen and dh_modulus)) + + if !dh_modulus + missing = 'modulus' + else + missing = 'generator' + end + + raise ProtocolError.new(message, + sprintf('If non-default modulus or generator is ' + + 'supplied, both must be supplied. Missing %s', + missing)) + end + + if dh_modulus or dh_gen + dh_modulus = CryptUtil.base64_to_num(dh_modulus) + dh_gen = CryptUtil.base64_to_num(dh_gen) + dh = DiffieHellman.new(dh_modulus, dh_gen) + else + dh = DiffieHellman.from_defaults() + end + + consumer_pubkey = message.get_arg(OPENID_NS, 'dh_consumer_public') + if !consumer_pubkey + raise ProtocolError.new(message, + sprintf("Public key for DH-SHA1 session " + + "not found in message %s", message)) + end + + consumer_pubkey = CryptUtil.base64_to_num(consumer_pubkey) + + return self.new(dh, consumer_pubkey) + end + + def answer(secret) + mac_key = @dh.xor_secret(@hash_func, + @consumer_pubkey, + secret) + return { + 'dh_server_public' => CryptUtil.num_to_base64(@dh.public), + 'enc_mac_key' => Util.to_base64(mac_key), + } + end + end + + class DiffieHellmanSHA256ServerSession < DiffieHellmanSHA1ServerSession + def initialize(*args) + super(*args) + @session_type = 'DH-SHA256' + @hash_func = CryptUtil.method('sha256') + @allowed_assoc_types = ['HMAC-SHA256'].freeze + end + end + + # A request to establish an association. + # + # See OpenID Specs, Section 8: Establishing Associations + # <http://openid.net/specs/openid-authentication-2_0-12.html#associations> + class AssociateRequest < OpenIDRequest + # An object that knows how to handle association requests of a + # certain type. + attr_accessor :session + + # The type of association. Supported values include HMAC-SHA256 + # and HMAC-SHA1 + attr_accessor :assoc_type + + @@session_classes = { + 'no-encryption' => PlainTextServerSession, + 'DH-SHA1' => DiffieHellmanSHA1ServerSession, + 'DH-SHA256' => DiffieHellmanSHA256ServerSession, + } + + # Construct me. + # + # The session is assigned directly as a class attribute. See my + # class documentation for its description. + def initialize(session, assoc_type) + super() + @session = session + @assoc_type = assoc_type + + @mode = "associate" + end + + # Construct me from an OpenID Message. + def self.from_message(message, op_endpoint=UNUSED) + if message.is_openid1() + session_type = message.get_arg(OPENID_NS, 'session_type') + if session_type == 'no-encryption' + Util.log('Received OpenID 1 request with a no-encryption ' + + 'association session type. Continuing anyway.') + elsif !session_type + session_type = 'no-encryption' + end + else + session_type = message.get_arg(OPENID2_NS, 'session_type') + if !session_type + raise ProtocolError.new(message, + text="session_type missing from request") + end + end + + session_class = @@session_classes[session_type] + + if !session_class + raise ProtocolError.new(message, + sprintf("Unknown session type %s", session_type)) + end + + begin + session = session_class.from_message(message) + rescue ArgumentError => why + # XXX + raise ProtocolError.new(message, + sprintf('Error parsing %s session: %s', + session_type, why)) + end + + assoc_type = message.get_arg(OPENID_NS, 'assoc_type', 'HMAC-SHA1') + if !session.allowed_assoc_type?(assoc_type) + msg = sprintf('Session type %s does not support association type %s', + session_type, assoc_type) + raise ProtocolError.new(message, msg) + end + + obj = self.new(session, assoc_type) + obj.message = message + return obj + end + + # Respond to this request with an association. + # + # assoc:: The association to send back. + # + # Returns a response with the association information, encrypted + # to the consumer's public key if appropriate. + def answer(assoc) + response = OpenIDResponse.new(self) + response.fields.update_args(OPENID_NS, { + 'expires_in' => sprintf('%d', assoc.expires_in()), + 'assoc_type' => @assoc_type, + 'assoc_handle' => assoc.handle, + }) + response.fields.update_args(OPENID_NS, + @session.answer(assoc.secret)) + unless (@session.session_type == 'no-encryption' and + @message.is_openid1) + response.fields.set_arg( + OPENID_NS, 'session_type', @session.session_type) + end + + return response + end + + # Respond to this request indicating that the association type + # or association session type is not supported. + def answer_unsupported(message, preferred_association_type=nil, + preferred_session_type=nil) + if @message.is_openid1() + raise ProtocolError.new(@message) + end + + response = OpenIDResponse.new(self) + response.fields.set_arg(OPENID_NS, 'error_code', 'unsupported-type') + response.fields.set_arg(OPENID_NS, 'error', message) + + if preferred_association_type + response.fields.set_arg( + OPENID_NS, 'assoc_type', preferred_association_type) + end + + if preferred_session_type + response.fields.set_arg( + OPENID_NS, 'session_type', preferred_session_type) + end + + return response + end + end + + # A request to confirm the identity of a user. + # + # This class handles requests for openid modes + # +checkid_immediate+ and +checkid_setup+ . + class CheckIDRequest < OpenIDRequest + + # Provided in smart mode requests, a handle for a previously + # established association. nil for dumb mode requests. + attr_accessor :assoc_handle + + # Is this an immediate-mode request? + attr_accessor :immediate + + # The URL to send the user agent back to to reply to this + # request. + attr_accessor :return_to + + # The OP-local identifier being checked. + attr_accessor :identity + + # The claimed identifier. Not present in OpenID 1.x + # messages. + attr_accessor :claimed_id + + # This URL identifies the party making the request, and the user + # will use that to make her decision about what answer she + # trusts them to have. Referred to as "realm" in OpenID 2.0. + attr_accessor :trust_root + + # mode:: +checkid_immediate+ or +checkid_setup+ + attr_accessor :mode + + attr_accessor :op_endpoint + + # These parameters are assigned directly as attributes, + # see the #CheckIDRequest class documentation for their + # descriptions. + # + # Raises #MalformedReturnURL when the +return_to+ URL is not + # a URL. + def initialize(identity, return_to, op_endpoint, trust_root=nil, + immediate=false, assoc_handle=nil, claimed_id=nil) + @assoc_handle = assoc_handle + @identity = identity + @claimed_id = (claimed_id or identity) + @return_to = return_to + @trust_root = (trust_root or return_to) + @op_endpoint = op_endpoint + @message = nil + + if immediate + @immediate = true + @mode = "checkid_immediate" + else + @immediate = false + @mode = "checkid_setup" + end + + if @return_to and + !TrustRoot::TrustRoot.parse(@return_to) + raise MalformedReturnURL.new(nil, @return_to) + end + + if !trust_root_valid() + raise UntrustedReturnURL.new(nil, @return_to, @trust_root) + end + end + + # Construct me from an OpenID message. + # + # message:: An OpenID checkid_* request Message + # + # op_endpoint:: The endpoint URL of the server that this + # message was sent to. + # + # Raises: + # ProtocolError:: When not all required parameters are present + # in the message. + # + # MalformedReturnURL:: When the +return_to+ URL is not a URL. + # + # UntrustedReturnURL:: When the +return_to+ URL is + # outside the +trust_root+. + def self.from_message(message, op_endpoint) + obj = self.allocate + obj.message = message + obj.op_endpoint = op_endpoint + mode = message.get_arg(OPENID_NS, 'mode') + if mode == "checkid_immediate" + obj.immediate = true + obj.mode = "checkid_immediate" + else + obj.immediate = false + obj.mode = "checkid_setup" + end + + obj.return_to = message.get_arg(OPENID_NS, 'return_to') + if message.is_openid1 and !obj.return_to + msg = sprintf("Missing required field 'return_to' from %s", + message) + raise ProtocolError.new(message, msg) + end + + obj.identity = message.get_arg(OPENID_NS, 'identity') + obj.claimed_id = message.get_arg(OPENID_NS, 'claimed_id') + if message.is_openid1() + if !obj.identity + s = "OpenID 1 message did not contain openid.identity" + raise ProtocolError.new(message, s) + end + else + if obj.identity and not obj.claimed_id + s = ("OpenID 2.0 message contained openid.identity but not " + + "claimed_id") + raise ProtocolError.new(message, s) + elsif obj.claimed_id and not obj.identity + s = ("OpenID 2.0 message contained openid.claimed_id but not " + + "identity") + raise ProtocolError.new(message, s) + end + end + + # There's a case for making self.trust_root be a TrustRoot + # here. But if TrustRoot isn't currently part of the "public" + # API, I'm not sure it's worth doing. + if message.is_openid1 + trust_root_param = 'trust_root' + else + trust_root_param = 'realm' + end + trust_root = message.get_arg(OPENID_NS, trust_root_param) + trust_root = obj.return_to if (trust_root.nil? || trust_root.empty?) + obj.trust_root = trust_root + + if !message.is_openid1 and !obj.return_to and !obj.trust_root + raise ProtocolError.new(message, "openid.realm required when " + + "openid.return_to absent") + end + + obj.assoc_handle = message.get_arg(OPENID_NS, 'assoc_handle') + + # Using TrustRoot.parse here is a bit misleading, as we're not + # parsing return_to as a trust root at all. However, valid + # URLs are valid trust roots, so we can use this to get an + # idea if it is a valid URL. Not all trust roots are valid + # return_to URLs, however (particularly ones with wildcards), + # so this is still a little sketchy. + if obj.return_to and \ + !TrustRoot::TrustRoot.parse(obj.return_to) + raise MalformedReturnURL.new(message, obj.return_to) + end + + # I first thought that checking to see if the return_to is + # within the trust_root is premature here, a + # logic-not-decoding thing. But it was argued that this is + # really part of data validation. A request with an invalid + # trust_root/return_to is broken regardless of application, + # right? + if !obj.trust_root_valid() + raise UntrustedReturnURL.new(message, obj.return_to, obj.trust_root) + end + + return obj + end + + # Is the identifier to be selected by the IDP? + def id_select + # So IDPs don't have to import the constant + return @identity == IDENTIFIER_SELECT + end + + # Is my return_to under my trust_root? + def trust_root_valid + if !@trust_root + return true + end + + tr = TrustRoot::TrustRoot.parse(@trust_root) + if !tr + raise MalformedTrustRoot.new(@message, @trust_root) + end + + if @return_to + return tr.validate_url(@return_to) + else + return true + end + end + + # Does the relying party publish the return_to URL for this + # response under the realm? It is up to the provider to set a + # policy for what kinds of realms should be allowed. This + # return_to URL verification reduces vulnerability to + # data-theft attacks based on open proxies, + # corss-site-scripting, or open redirectors. + # + # This check should only be performed after making sure that + # the return_to URL matches the realm. + # + # Raises DiscoveryFailure if the realm + # URL does not support Yadis discovery (and so does not + # support the verification process). + # + # Returns true if the realm publishes a document with the + # return_to URL listed + def return_to_verified + return TrustRoot.verify_return_to(@trust_root, @return_to) + end + + # Respond to this request. + # + # allow:: Allow this user to claim this identity, and allow the + # consumer to have this information? + # + # server_url:: DEPRECATED. Passing op_endpoint to the + # #Server constructor makes this optional. + # + # When an OpenID 1.x immediate mode request does + # not succeed, it gets back a URL where the request + # may be carried out in a not-so-immediate fashion. + # Pass my URL in here (the fully qualified address + # of this server's endpoint, i.e. + # <tt>http://example.com/server</tt>), and I will + # use it as a base for the URL for a new request. + # + # Optional for requests where + # #CheckIDRequest.immediate is false or +allow+ is + # true. + # + # identity:: The OP-local identifier to answer with. Only for use + # when the relying party requested identifier selection. + # + # claimed_id:: The claimed identifier to answer with, + # for use with identifier selection in the case where the + # claimed identifier and the OP-local identifier differ, + # i.e. when the claimed_id uses delegation. + # + # If +identity+ is provided but this is not, + # +claimed_id+ will default to the value of +identity+. + # When answering requests that did not ask for identifier + # selection, the response +claimed_id+ will default to + # that of the request. + # + # This parameter is new in OpenID 2.0. + # + # Returns an OpenIDResponse object containing a OpenID id_res message. + # + # Raises NoReturnToError if the return_to is missing. + # + # Version 2.0 deprecates +server_url+ and adds +claimed_id+. + def answer(allow, server_url=nil, identity=nil, claimed_id=nil) + if !@return_to + raise NoReturnToError + end + + if !server_url + if @message.is_openid2 and !@op_endpoint + # In other words, that warning I raised in + # Server.__init__? You should pay attention to it now. + raise RuntimeError, ("#{self} should be constructed with "\ + "op_endpoint to respond to OpenID 2.0 "\ + "messages.") + end + + server_url = @op_endpoint + end + + if allow + mode = 'id_res' + elsif @message.is_openid1 + if @immediate + mode = 'id_res' + else + mode = 'cancel' + end + else + if @immediate + mode = 'setup_needed' + else + mode = 'cancel' + end + end + + response = OpenIDResponse.new(self) + + if claimed_id and @message.is_openid1 + raise VersionError, ("claimed_id is new in OpenID 2.0 and not "\ + "available for #{@message.get_openid_namespace}") + end + + if identity and !claimed_id + claimed_id = identity + end + + if allow + if @identity == IDENTIFIER_SELECT + if !identity + raise ArgumentError, ("This request uses IdP-driven "\ + "identifier selection.You must supply "\ + "an identifier in the response.") + end + + response_identity = identity + response_claimed_id = claimed_id + + elsif @identity + if identity and (@identity != identity) + raise ArgumentError, ("Request was for identity #{@identity}, "\ + "cannot reply with identity #{identity}") + end + + response_identity = @identity + response_claimed_id = @claimed_id + else + if identity + raise ArgumentError, ("This request specified no identity "\ + "and you supplied #{identity}") + end + response_identity = nil + end + + if @message.is_openid1 and !response_identity + raise ArgumentError, ("Request was an OpenID 1 request, so "\ + "response must include an identifier.") + end + + response.fields.update_args(OPENID_NS, { + 'mode' => mode, + 'op_endpoint' => server_url, + 'return_to' => @return_to, + 'response_nonce' => Nonce.mk_nonce(), + }) + + if response_identity + response.fields.set_arg(OPENID_NS, 'identity', response_identity) + if @message.is_openid2 + response.fields.set_arg(OPENID_NS, + 'claimed_id', response_claimed_id) + end + end + else + response.fields.set_arg(OPENID_NS, 'mode', mode) + if @immediate + if @message.is_openid1 and !server_url + raise ArgumentError, ("setup_url is required for allow=false "\ + "in OpenID 1.x immediate mode.") + end + + # Make a new request just like me, but with + # immediate=false. + setup_request = self.class.new(@identity, @return_to, + @op_endpoint, @trust_root, false, + @assoc_handle, @claimed_id) + setup_request.message = Message.new(@message.get_openid_namespace) + setup_url = setup_request.encode_to_url(server_url) + response.fields.set_arg(OPENID_NS, 'user_setup_url', setup_url) + end + end + + return response + end + + def encode_to_url(server_url) + # Encode this request as a URL to GET. + # + # server_url:: The URL of the OpenID server to make this + # request of. + if !@return_to + raise NoReturnToError + end + + # Imported from the alternate reality where these classes are + # used in both the client and server code, so Requests are + # Encodable too. That's right, code imported from alternate + # realities all for the love of you, id_res/user_setup_url. + q = {'mode' => @mode, + 'identity' => @identity, + 'claimed_id' => @claimed_id, + 'return_to' => @return_to} + + if @trust_root + if @message.is_openid1 + q['trust_root'] = @trust_root + else + q['realm'] = @trust_root + end + end + + if @assoc_handle + q['assoc_handle'] = @assoc_handle + end + + response = Message.new(@message.get_openid_namespace) + response.update_args(@message.get_openid_namespace, q) + return response.to_url(server_url) + end + + def cancel_url + # Get the URL to cancel this request. + # + # Useful for creating a "Cancel" button on a web form so that + # operation can be carried out directly without another trip + # through the server. + # + # (Except you may want to make another trip through the + # server so that it knows that the user did make a decision.) + # + # Returns a URL as a string. + if !@return_to + raise NoReturnToError + end + + if @immediate + raise ArgumentError.new("Cancel is not an appropriate response to " + + "immediate mode requests.") + end + + response = Message.new(@message.get_openid_namespace) + response.set_arg(OPENID_NS, 'mode', 'cancel') + return response.to_url(@return_to) + end + + def to_s + return sprintf('<%s id:%s im:%s tr:%s ah:%s>', self.class, + @identity, + @immediate, + @trust_root, + @assoc_handle) + end + end + + # I am a response to an OpenID request. + # + # Attributes: + # signed:: A list of the names of the fields which should be signed. + # + # Implementer's note: In a more symmetric client/server + # implementation, there would be more types of #OpenIDResponse + # object and they would have validated attributes according to + # the type of response. But as it is, Response objects in a + # server are basically write-only, their only job is to go out + # over the wire, so this is just a loose wrapper around + # #OpenIDResponse.fields. + class OpenIDResponse + # The #OpenIDRequest I respond to. + attr_accessor :request + + # An #OpenID::Message with the data to be returned. + # Keys are parameter names with no + # leading openid. e.g. identity and mac_key + # never openid.identity. + attr_accessor :fields + + def initialize(request) + # Make a response to an OpenIDRequest. + @request = request + @fields = Message.new(request.namespace) + end + + def to_s + return sprintf("%s for %s: %s", + self.class, + @request.class, + @fields) + end + + # form_tag_attrs is a hash of attributes to be added to the form + # tag. 'accept-charset' and 'enctype' have defaults that can be + # overridden. If a value is supplied for 'action' or 'method', + # it will be replaced. + # Returns the form markup for this response. + def to_form_markup(form_tag_attrs=nil) + return @fields.to_form_markup(@request.return_to, form_tag_attrs) + end + + # Wraps the form tag from to_form_markup in a complete HTML document + # that uses javascript to autosubmit the form. + def to_html(form_tag_attrs=nil) + return Util.auto_submit_html(to_form_markup(form_tag_attrs)) + end + + def render_as_form + # Returns true if this response's encoding is + # ENCODE_HTML_FORM. Convenience method for server authors. + return self.which_encoding == ENCODE_HTML_FORM + end + + def needs_signing + # Does this response require signing? + return @fields.get_arg(OPENID_NS, 'mode') == 'id_res' + end + + # implements IEncodable + + def which_encoding + # How should I be encoded? + # returns one of ENCODE_URL or ENCODE_KVFORM. + if BROWSER_REQUEST_MODES.member?(@request.mode) + if @fields.is_openid2 and + encode_to_url.length > OPENID1_URL_LIMIT + return ENCODE_HTML_FORM + else + return ENCODE_URL + end + else + return ENCODE_KVFORM + end + end + + def encode_to_url + # Encode a response as a URL for the user agent to GET. + # You will generally use this URL with a HTTP redirect. + return @fields.to_url(@request.return_to) + end + + def add_extension(extension_response) + # Add an extension response to this response message. + # + # extension_response:: An object that implements the + # #OpenID::Extension interface for adding arguments to an OpenID + # message. + extension_response.to_message(@fields) + end + + def encode_to_kvform + # Encode a response in key-value colon/newline format. + # + # This is a machine-readable format used to respond to + # messages which came directly from the consumer and not + # through the user agent. + # + # see: OpenID Specs, + # <a href="http://openid.net/specs.bml#keyvalue">Key-Value Colon/Newline format</a> + return @fields.to_kvform + end + + def copy + return Marshal.load(Marshal.dump(self)) + end + end + + # I am a response to an OpenID request in terms a web server + # understands. + # + # I generally come from an #Encoder, either directly or from + # #Server.encodeResponse. + class WebResponse + + # The HTTP code of this response as an integer. + attr_accessor :code + + # #Hash of headers to include in this response. + attr_accessor :headers + + # The body of this response. + attr_accessor :body + + def initialize(code=HTTP_OK, headers=nil, body="") + # Construct me. + # + # These parameters are assigned directly as class attributes, + # see my class documentation for their + # descriptions. + @code = code + if headers + @headers = headers + else + @headers = {} + end + @body = body + end + end + + # I sign things. + # + # I also check signatures. + # + # All my state is encapsulated in a store, which means I'm not + # generally pickleable but I am easy to reconstruct. + class Signatory + # The number of seconds a secret remains valid. Defaults to 14 days. + attr_accessor :secret_lifetime + + # keys have a bogus server URL in them because the filestore + # really does expect that key to be a URL. This seems a little + # silly for the server store, since I expect there to be only + # one server URL. + @@_normal_key = 'http://localhost/|normal' + @@_dumb_key = 'http://localhost/|dumb' + + def self._normal_key + @@_normal_key + end + + def self._dumb_key + @@_dumb_key + end + + attr_accessor :store + + # Create a new Signatory. store is The back-end where my + # associations are stored. + def initialize(store) + Util.assert(store) + @store = store + @secret_lifetime = 14 * 24 * 60 * 60 + end + + # Verify that the signature for some data is valid. + def verify(assoc_handle, message) + assoc = get_association(assoc_handle, true) + if !assoc + Util.log(sprintf("failed to get assoc with handle %s to verify " + + "message %s", assoc_handle, message)) + return false + end + + begin + valid = assoc.check_message_signature(message) + rescue StandardError => ex + Util.log(sprintf("Error in verifying %s with %s: %s", + message, assoc, ex)) + return false + end + + return valid + end + + # Sign a response. + # + # I take an OpenIDResponse, create a signature for everything in + # its signed list, and return a new copy of the response object + # with that signature included. + def sign(response) + signed_response = response.copy + assoc_handle = response.request.assoc_handle + if assoc_handle + # normal mode disabling expiration check because even if the + # association is expired, we still need to know some + # properties of the association so that we may preserve + # those properties when creating the fallback association. + assoc = get_association(assoc_handle, false, false) + + if !assoc or assoc.expires_in <= 0 + # fall back to dumb mode + signed_response.fields.set_arg( + OPENID_NS, 'invalidate_handle', assoc_handle) + assoc_type = assoc ? assoc.assoc_type : 'HMAC-SHA1' + if assoc and assoc.expires_in <= 0 + # now do the clean-up that the disabled checkExpiration + # code didn't get to do. + invalidate(assoc_handle, false) + end + assoc = create_association(true, assoc_type) + end + else + # dumb mode. + assoc = create_association(true) + end + + begin + signed_response.fields = assoc.sign_message(signed_response.fields) + rescue KVFormError => err + raise EncodingError, err + end + return signed_response + end + + # Make a new association. + def create_association(dumb=true, assoc_type='HMAC-SHA1') + secret = CryptUtil.random_string(OpenID.get_secret_size(assoc_type)) + uniq = Util.to_base64(CryptUtil.random_string(4)) + handle = sprintf('{%s}{%x}{%s}', assoc_type, Time.now.to_i, uniq) + + assoc = Association.from_expires_in( + secret_lifetime, handle, secret, assoc_type) + + if dumb + key = @@_dumb_key + else + key = @@_normal_key + end + + @store.store_association(key, assoc) + return assoc + end + + # Get the association with the specified handle. + def get_association(assoc_handle, dumb, checkExpiration=true) + # Hmm. We've created an interface that deals almost entirely + # with assoc_handles. The only place outside the Signatory + # that uses this (and thus the only place that ever sees + # Association objects) is when creating a response to an + # association request, as it must have the association's + # secret. + + if !assoc_handle + raise ArgumentError.new("assoc_handle must not be None") + end + + if dumb + key = @@_dumb_key + else + key = @@_normal_key + end + + assoc = @store.get_association(key, assoc_handle) + if assoc and assoc.expires_in <= 0 + Util.log(sprintf("requested %sdumb key %s is expired (by %s seconds)", + (!dumb) ? 'not-' : '', + assoc_handle, assoc.expires_in)) + if checkExpiration + @store.remove_association(key, assoc_handle) + assoc = nil + end + end + + return assoc + end + + # Invalidates the association with the given handle. + def invalidate(assoc_handle, dumb) + if dumb + key = @@_dumb_key + else + key = @@_normal_key + end + + @store.remove_association(key, assoc_handle) + end + end + + # I encode responses in to WebResponses. + # + # If you don't like WebResponses, you can do + # your own handling of OpenIDResponses with + # OpenIDResponse.whichEncoding, + # OpenIDResponse.encodeToURL, and + # OpenIDResponse.encodeToKVForm. + class Encoder + @@responseFactory = WebResponse + + # Encode a response to a WebResponse. + # + # Raises EncodingError when I can't figure out how to encode + # this message. + def encode(response) + encode_as = response.which_encoding() + if encode_as == ENCODE_KVFORM + wr = @@responseFactory.new(HTTP_OK, nil, + response.encode_to_kvform()) + if response.is_a?(Exception) + wr.code = HTTP_ERROR + end + elsif encode_as == ENCODE_URL + location = response.encode_to_url() + wr = @@responseFactory.new(HTTP_REDIRECT, + {'location' => location}) + elsif encode_as == ENCODE_HTML_FORM + wr = @@responseFactory.new(HTTP_OK, nil, + response.to_form_markup()) + else + # Can't encode this to a protocol message. You should + # probably render it to HTML and show it to the user. + raise EncodingError.new(response) + end + + return wr + end + end + + # I encode responses in to WebResponses, signing + # them when required. + class SigningEncoder < Encoder + + attr_accessor :signatory + + # Create a SigningEncoder given a Signatory + def initialize(signatory) + @signatory = signatory + end + + # Encode a response to a WebResponse, signing it first if + # appropriate. + # + # Raises EncodingError when I can't figure out how to encode this + # message. + # + # Raises AlreadySigned when this response is already signed. + def encode(response) + # the is_a? is a bit of a kludge... it means there isn't + # really an adapter to make the interfaces quite match. + if !response.is_a?(Exception) and response.needs_signing() + if !@signatory + raise ArgumentError.new( + sprintf("Must have a store to sign this request: %s", + response), response) + end + + if response.fields.has_key?(OPENID_NS, 'sig') + raise AlreadySigned.new(response) + end + + response = @signatory.sign(response) + end + + return super(response) + end + end + + # I decode an incoming web request in to a OpenIDRequest. + class Decoder + + @@handlers = { + 'checkid_setup' => CheckIDRequest.method('from_message'), + 'checkid_immediate' => CheckIDRequest.method('from_message'), + 'check_authentication' => CheckAuthRequest.method('from_message'), + 'associate' => AssociateRequest.method('from_message'), + } + + attr_accessor :server + + # Construct a Decoder. The server is necessary because some + # replies reference their server. + def initialize(server) + @server = server + end + + # I transform query parameters into an OpenIDRequest. + # + # If the query does not seem to be an OpenID request at all, I + # return nil. + # + # Raises ProtocolError when the query does not seem to be a valid + # OpenID request. + def decode(query) + if query.nil? or query.length == 0 + return nil + end + + begin + message = Message.from_post_args(query) + rescue InvalidOpenIDNamespace => e + query = query.dup + query['openid.ns'] = OPENID2_NS + message = Message.from_post_args(query) + raise ProtocolError.new(message, e.to_s) + end + + mode = message.get_arg(OPENID_NS, 'mode') + if !mode + msg = sprintf("No mode value in message %s", message) + raise ProtocolError.new(message, msg) + end + + handler = @@handlers.fetch(mode, self.method('default_decoder')) + return handler.call(message, @server.op_endpoint) + end + + # Called to decode queries when no handler for that mode is + # found. + # + # This implementation always raises ProtocolError. + def default_decoder(message, server) + mode = message.get_arg(OPENID_NS, 'mode') + msg = sprintf("Unrecognized OpenID mode %s", mode) + raise ProtocolError.new(message, msg) + end + end + + # I handle requests for an OpenID server. + # + # Some types of requests (those which are not checkid requests) + # may be handed to my handleRequest method, and I will take care + # of it and return a response. + # + # For your convenience, I also provide an interface to + # Decoder.decode and SigningEncoder.encode through my methods + # decodeRequest and encodeResponse. + # + # All my state is encapsulated in an store, which means I'm not + # generally pickleable but I am easy to reconstruct. + class Server + @@signatoryClass = Signatory + @@encoderClass = SigningEncoder + @@decoderClass = Decoder + + # The back-end where my associations and nonces are stored. + attr_accessor :store + + # I'm using this for associate requests and to sign things. + attr_accessor :signatory + + # I'm using this to encode things. + attr_accessor :encoder + + # I'm using this to decode things. + attr_accessor :decoder + + # I use this instance of OpenID::AssociationNegotiator to + # determine which kinds of associations I can make and how. + attr_accessor :negotiator + + # My URL. + attr_accessor :op_endpoint + + # op_endpoint is new in library version 2.0. + def initialize(store, op_endpoint) + @store = store + @signatory = @@signatoryClass.new(@store) + @encoder = @@encoderClass.new(@signatory) + @decoder = @@decoderClass.new(self) + @negotiator = DefaultNegotiator.copy() + @op_endpoint = op_endpoint + end + + # Handle a request. + # + # Give me a request, I will give you a response. Unless it's a + # type of request I cannot handle myself, in which case I will + # raise RuntimeError. In that case, you can handle it yourself, + # or add a method to me for handling that request type. + def handle_request(request) + begin + handler = self.method('openid_' + request.mode) + rescue NameError + raise RuntimeError.new( + sprintf("%s has no handler for a request of mode %s.", + self, request.mode)) + end + + return handler.call(request) + end + + # Handle and respond to check_authentication requests. + def openid_check_authentication(request) + return request.answer(@signatory) + end + + # Handle and respond to associate requests. + def openid_associate(request) + assoc_type = request.assoc_type + session_type = request.session.session_type + if @negotiator.allowed?(assoc_type, session_type) + assoc = @signatory.create_association(false, + assoc_type) + return request.answer(assoc) + else + message = sprintf('Association type %s is not supported with ' + + 'session type %s', assoc_type, session_type) + preferred_assoc_type, preferred_session_type = @negotiator.get_allowed_type() + return request.answer_unsupported(message, + preferred_assoc_type, + preferred_session_type) + end + end + + # Transform query parameters into an OpenIDRequest. + # query should contain the query parameters as a Hash with + # each key mapping to one value. + # + # If the query does not seem to be an OpenID request at all, I + # return nil. + def decode_request(query) + return @decoder.decode(query) + end + + # Encode a response to a WebResponse, signing it first if + # appropriate. + # + # Raises EncodingError when I can't figure out how to encode this + # message. + # + # Raises AlreadySigned When this response is already signed. + def encode_response(response) + return @encoder.encode(response) + end + end + + # A message did not conform to the OpenID protocol. + class ProtocolError < Exception + # The query that is failing to be a valid OpenID request. + attr_accessor :openid_message + attr_accessor :reference + attr_accessor :contact + + # text:: A message about the encountered error. + def initialize(message, text=nil, reference=nil, contact=nil) + @openid_message = message + @reference = reference + @contact = contact + Util.assert(!message.is_a?(String)) + super(text) + end + + # Get the return_to argument from the request, if any. + def get_return_to + if @openid_message.nil? + return nil + else + return @openid_message.get_arg(OPENID_NS, 'return_to') + end + end + + # Did this request have a return_to parameter? + def has_return_to + return !get_return_to.nil? + end + + # Generate a Message object for sending to the relying party, + # after encoding. + def to_message + namespace = @openid_message.get_openid_namespace() + reply = Message.new(namespace) + reply.set_arg(OPENID_NS, 'mode', 'error') + reply.set_arg(OPENID_NS, 'error', self.to_s) + + if @contact + reply.set_arg(OPENID_NS, 'contact', @contact.to_s) + end + + if @reference + reply.set_arg(OPENID_NS, 'reference', @reference.to_s) + end + + return reply + end + + # implements IEncodable + + def encode_to_url + return to_message().to_url(get_return_to()) + end + + def encode_to_kvform + return to_message().to_kvform() + end + + def to_form_markup + return to_message().to_form_markup(get_return_to()) + end + + def to_html + return Util.auto_submit_html(to_form_markup) + end + + # How should I be encoded? + # + # Returns one of ENCODE_URL, ENCODE_KVFORM, or None. If None, + # I cannot be encoded as a protocol message and should be + # displayed to the user. + def which_encoding + if has_return_to() + if @openid_message.is_openid2 and + encode_to_url().length > OPENID1_URL_LIMIT + return ENCODE_HTML_FORM + else + return ENCODE_URL + end + end + + if @openid_message.nil? + return nil + end + + mode = @openid_message.get_arg(OPENID_NS, 'mode') + if mode + if !BROWSER_REQUEST_MODES.member?(mode) + return ENCODE_KVFORM + end + end + + # If your request was so broken that you didn't manage to + # include an openid.mode, I'm not going to worry too much + # about returning you something you can't parse. + return nil + end + end + + # Raised when an operation was attempted that is not compatible + # with the protocol version being used. + class VersionError < Exception + end + + # Raised when a response to a request cannot be generated + # because the request contains no return_to URL. + class NoReturnToError < Exception + end + + # Could not encode this as a protocol message. + # + # You should probably render it and show it to the user. + class EncodingError < Exception + # The response that failed to encode. + attr_reader :response + + def initialize(response) + super(response) + @response = response + end + end + + # This response is already signed. + class AlreadySigned < EncodingError + end + + # A return_to is outside the trust_root. + class UntrustedReturnURL < ProtocolError + attr_reader :return_to, :trust_root + + def initialize(message, return_to, trust_root) + super(message) + @return_to = return_to + @trust_root = trust_root + end + + def to_s + return sprintf("return_to %s not under trust_root %s", + @return_to, + @trust_root) + end + end + + # The return_to URL doesn't look like a valid URL. + class MalformedReturnURL < ProtocolError + attr_reader :return_to + + def initialize(openid_message, return_to) + @return_to = return_to + super(openid_message) + end + end + + # The trust root is not well-formed. + class MalformedTrustRoot < ProtocolError + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/store/filesystem.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/store/filesystem.rb new file mode 100644 index 000000000..e2993eea4 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/store/filesystem.rb @@ -0,0 +1,271 @@ +require 'fileutils' +require 'pathname' +require 'tempfile' + +require 'openid/util' +require 'openid/store/interface' +require 'openid/association' + +module OpenID + module Store + class Filesystem < Interface + @@FILENAME_ALLOWED = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-".split("") + + # Create a Filesystem store instance, putting all data in +directory+. + def initialize(directory) + p_dir = Pathname.new(directory) + @nonce_dir = p_dir.join('nonces') + @association_dir = p_dir.join('associations') + @temp_dir = p_dir.join('temp') + + self.ensure_dir(@nonce_dir) + self.ensure_dir(@association_dir) + self.ensure_dir(@temp_dir) + end + + # Create a unique filename for a given server url and handle. The + # filename that is returned will contain the domain name from the + # server URL for ease of human inspection of the data dir. + def get_association_filename(server_url, handle) + unless server_url.index('://') + raise ArgumentError, "Bad server URL: #{server_url}" + end + + proto, rest = server_url.split('://', 2) + domain = filename_escape(rest.split('/',2)[0]) + url_hash = safe64(server_url) + if handle + handle_hash = safe64(handle) + else + handle_hash = '' + end + filename = [proto,domain,url_hash,handle_hash].join('-') + @association_dir.join(filename) + end + + # Store an association in the assoc directory + def store_association(server_url, association) + assoc_s = association.serialize + filename = get_association_filename(server_url, association.handle) + f, tmp = mktemp + + begin + begin + f.write(assoc_s) + f.fsync + ensure + f.close + end + + begin + File.rename(tmp, filename) + rescue Errno::EEXIST + + begin + File.unlink(filename) + rescue Errno::ENOENT + # do nothing + end + + File.rename(tmp, filename) + end + + rescue + self.remove_if_present(tmp) + raise + end + end + + # Retrieve an association + def get_association(server_url, handle=nil) + # the filename with empty handle is the prefix for the associations + # for a given server url + filename = get_association_filename(server_url, handle) + if handle + return _get_association(filename) + end + assoc_filenames = Dir.glob(filename.to_s + '*') + + assocs = assoc_filenames.collect do |f| + _get_association(f) + end + + assocs = assocs.find_all { |a| not a.nil? } + assocs = assocs.sort_by { |a| a.issued } + + return nil if assocs.empty? + return assocs[-1] + end + + def _get_association(filename) + begin + assoc_file = File.open(filename, "r") + rescue Errno::ENOENT + return nil + else + begin + assoc_s = assoc_file.read + ensure + assoc_file.close + end + + begin + association = Association.deserialize(assoc_s) + rescue + self.remove_if_present(filename) + return nil + end + + # clean up expired associations + if association.expires_in == 0 + self.remove_if_present(filename) + return nil + else + return association + end + end + end + + # Remove an association if it exists, otherwise do nothing. + def remove_association(server_url, handle) + assoc = get_association(server_url, handle) + + if assoc.nil? + return false + else + filename = get_association_filename(server_url, handle) + return self.remove_if_present(filename) + end + end + + # Return whether the nonce is valid + def use_nonce(server_url, timestamp, salt) + return false if (timestamp - Time.now.to_i).abs > Nonce.skew + + if server_url and !server_url.empty? + proto, rest = server_url.split('://',2) + else + proto, rest = '','' + end + raise "Bad server URL" unless proto && rest + + domain = filename_escape(rest.split('/',2)[0]) + url_hash = safe64(server_url) + salt_hash = safe64(salt) + + nonce_fn = '%08x-%s-%s-%s-%s'%[timestamp, proto, domain, url_hash, salt_hash] + + filename = @nonce_dir.join(nonce_fn) + + begin + fd = File.new(filename, File::CREAT | File::EXCL | File::WRONLY, 0200) + fd.close + return true + rescue Errno::EEXIST + return false + end + end + + # Remove expired entries from the database. This is potentially expensive, + # so only run when it is acceptable to take time. + def cleanup + cleanup_associations + cleanup_nonces + end + + def cleanup_associations + association_filenames = Dir[@association_dir.join("*").to_s] + count = 0 + association_filenames.each do |af| + begin + f = File.open(af, 'r') + rescue Errno::ENOENT + next + else + begin + assoc_s = f.read + ensure + f.close + end + begin + association = OpenID::Association.deserialize(assoc_s) + rescue StandardError + self.remove_if_present(af) + next + else + if association.expires_in == 0 + self.remove_if_present(af) + count += 1 + end + end + end + end + return count + end + + def cleanup_nonces + nonces = Dir[@nonce_dir.join("*").to_s] + now = Time.now.to_i + + count = 0 + nonces.each do |filename| + nonce = filename.split('/')[-1] + timestamp = nonce.split('-', 2)[0].to_i(16) + nonce_age = (timestamp - now).abs + if nonce_age > Nonce.skew + self.remove_if_present(filename) + count += 1 + end + end + return count + end + + protected + + # Create a temporary file and return the File object and filename. + def mktemp + f = Tempfile.new('tmp', @temp_dir) + [f, f.path] + end + + # create a safe filename from a url + def filename_escape(s) + s = '' if s.nil? + filename_chunks = [] + s.split('').each do |c| + if @@FILENAME_ALLOWED.index(c) + filename_chunks << c + else + filename_chunks << sprintf("_%02X", c[0]) + end + end + filename_chunks.join("") + end + + def safe64(s) + s = OpenID::CryptUtil.sha1(s) + s = OpenID::Util.to_base64(s) + s.gsub!('+', '_') + s.gsub!('/', '.') + s.gsub!('=', '') + return s + end + + # remove file if present in filesystem + def remove_if_present(filename) + begin + File.unlink(filename) + rescue Errno::ENOENT + return false + end + return true + end + + # ensure that a path exists + def ensure_dir(dir_name) + FileUtils::mkdir_p(dir_name) + end + end + end +end + diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/store/interface.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/store/interface.rb new file mode 100644 index 000000000..50819f6f7 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/store/interface.rb @@ -0,0 +1,75 @@ +require 'openid/util' + +module OpenID + + # Stores for Associations and nonces. Used by both the Consumer and + # the Server. If you have a database abstraction layer or other + # state storage in your application or framework already, you can + # implement the store interface. + module Store + # Abstract Store + # Changes in 2.0: + # * removed store_nonce, get_auth_key, is_dumb + # * changed use_nonce to support one-way nonces + # * added cleanup_nonces, cleanup_associations, cleanup + class Interface < Object + + # Put a Association object into storage. + # When implementing a store, don't assume that there are any limitations + # on the character set of the server_url. In particular, expect to see + # unescaped non-url-safe characters in the server_url field. + def store_association(server_url, association) + raise NotImplementedError + end + + # Returns a Association object from storage that matches + # the server_url. Returns nil if no such association is found or if + # the one matching association is expired. (Is allowed to GC expired + # associations when found.) + def get_association(server_url, handle=nil) + raise NotImplementedError + end + + # If there is a matching association, remove it from the store and + # return true, otherwise return false. + def remove_association(server_url, handle) + raise NotImplementedError + end + + # Return true if the nonce has not been used before, and store it + # for a while to make sure someone doesn't try to use the same value + # again. Return false if the nonce has already been used or if the + # timestamp is not current. + # You can use OpenID::Store::Nonce::SKEW for your timestamp window. + # server_url: URL of the server from which the nonce originated + # timestamp: time the nonce was created in seconds since unix epoch + # salt: A random string that makes two nonces issued by a server in + # the same second unique + def use_nonce(server_url, timestamp, salt) + raise NotImplementedError + end + + # Remove expired nonces from the store + # Discards any nonce that is old enough that it wouldn't pass use_nonce + # Not called during normal library operation, this method is for store + # admins to keep their storage from filling up with expired data + def cleanup_nonces + raise NotImplementedError + end + + # Remove expired associations from the store + # Not called during normal library operation, this method is for store + # admins to keep their storage from filling up with expired data + def cleanup_associations + raise NotImplementedError + end + + # Remove expired nonces and associations from the store + # Not called during normal library operation, this method is for store + # admins to keep their storage from filling up with expired data + def cleanup + return cleanup_nonces, cleanup_associations + end + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/store/memory.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/store/memory.rb new file mode 100644 index 000000000..58455b958 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/store/memory.rb @@ -0,0 +1,84 @@ +require 'openid/store/interface' +module OpenID + module Store + # An in-memory implementation of Store. This class is mainly used + # for testing, though it may be useful for long-running single + # process apps. Note that this store is NOT thread-safe. + # + # You should probably be looking at OpenID::Store::Filesystem + class Memory < Interface + + def initialize + @associations = {} + @associations.default = {} + @nonces = {} + end + + def store_association(server_url, assoc) + assocs = @associations[server_url] + @associations[server_url] = assocs.merge({assoc.handle => deepcopy(assoc)}) + end + + def get_association(server_url, handle=nil) + assocs = @associations[server_url] + assoc = nil + if handle + assoc = assocs[handle] + else + assoc = assocs.values.sort{|a,b| a.issued <=> b.issued}[-1] + end + + return assoc + end + + def remove_association(server_url, handle) + assocs = @associations[server_url] + if assocs.delete(handle) + return true + else + return false + end + end + + def use_nonce(server_url, timestamp, salt) + return false if (timestamp - Time.now.to_i).abs > Nonce.skew + nonce = [server_url, timestamp, salt].join('') + return false if @nonces[nonce] + @nonces[nonce] = timestamp + return true + end + + def cleanup_associations + count = 0 + @associations.each{|server_url, assocs| + assocs.each{|handle, assoc| + if assoc.expires_in == 0 + assocs.delete(handle) + count += 1 + end + } + } + return count + end + + def cleanup_nonces + count = 0 + now = Time.now.to_i + @nonces.each{|nonce, timestamp| + if (timestamp - now).abs > Nonce.skew + @nonces.delete(nonce) + count += 1 + end + } + return count + end + + protected + + def deepcopy(o) + Marshal.load(Marshal.dump(o)) + end + + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/store/nonce.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/store/nonce.rb new file mode 100644 index 000000000..08a9e5298 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/store/nonce.rb @@ -0,0 +1,68 @@ +require 'openid/cryptutil' +require 'date' +require 'time' + +module OpenID + module Nonce + DEFAULT_SKEW = 60*60*5 + TIME_FMT = '%Y-%m-%dT%H:%M:%SZ' + TIME_STR_LEN = '0000-00-00T00:00:00Z'.size + @@NONCE_CHRS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + TIME_VALIDATOR = /\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ/ + + @skew = DEFAULT_SKEW + + # The allowed nonce time skew in seconds. Defaults to 5 hours. + # Used for checking nonce validity, and by stores' cleanup methods. + def Nonce.skew + @skew + end + + def Nonce.skew=(new_skew) + @skew = new_skew + end + + # Extract timestamp from a nonce string + def Nonce.split_nonce(nonce_str) + timestamp_str = nonce_str[0...TIME_STR_LEN] + raise ArgumentError if timestamp_str.size < TIME_STR_LEN + raise ArgumentError unless timestamp_str.match(TIME_VALIDATOR) + ts = Time.parse(timestamp_str).to_i + raise ArgumentError if ts < 0 + return ts, nonce_str[TIME_STR_LEN..-1] + end + + # Is the timestamp that is part of the specified nonce string + # within the allowed clock-skew of the current time? + def Nonce.check_timestamp(nonce_str, allowed_skew=nil, now=nil) + allowed_skew = skew if allowed_skew.nil? + begin + stamp, foo = split_nonce(nonce_str) + rescue ArgumentError # bad timestamp + return false + end + now = Time.now.to_i unless now + + # times before this are too old + past = now - allowed_skew + + # times newer than this are too far in the future + future = now + allowed_skew + + return (past <= stamp and stamp <= future) + end + + # generate a nonce with the specified timestamp (defaults to now) + def Nonce.mk_nonce(time = nil) + salt = CryptUtil::random_string(6, @@NONCE_CHRS) + if time.nil? + t = Time.now.getutc + else + t = Time.at(time).getutc + end + time_str = t.strftime(TIME_FMT) + return time_str + salt + end + + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/trustroot.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/trustroot.rb new file mode 100644 index 000000000..16695f05c --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/trustroot.rb @@ -0,0 +1,349 @@ +require 'uri' +require 'openid/urinorm' + +module OpenID + + class RealmVerificationRedirected < Exception + # Attempting to verify this realm resulted in a redirect. + def initialize(relying_party_url, rp_url_after_redirects) + @relying_party_url = relying_party_url + @rp_url_after_redirects = rp_url_after_redirects + end + + def to_s + return "Attempting to verify #{@relying_party_url} resulted in " + + "redirect to #{@rp_url_after_redirects}" + end + end + + module TrustRoot + TOP_LEVEL_DOMAINS = %w' + ac ad ae aero af ag ai al am an ao aq ar arpa as asia at + au aw ax az ba bb bd be bf bg bh bi biz bj bm bn bo br bs bt + bv bw by bz ca cat cc cd cf cg ch ci ck cl cm cn co com coop + cr cu cv cx cy cz de dj dk dm do dz ec edu ee eg er es et eu + fi fj fk fm fo fr ga gb gd ge gf gg gh gi gl gm gn gov gp gq + gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in info int + io iq ir is it je jm jo jobs jp ke kg kh ki km kn kp kr kw + ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mg mh mil + mk ml mm mn mo mobi mp mq mr ms mt mu museum mv mw mx my mz + na name nc ne net nf ng ni nl no np nr nu nz om org pa pe pf + pg ph pk pl pm pn pr pro ps pt pw py qa re ro rs ru rw sa sb + sc sd se sg sh si sj sk sl sm sn so sr st su sv sy sz tc td + tel tf tg th tj tk tl tm tn to tp tr travel tt tv tw tz ua + ug uk us uy uz va vc ve vg vi vn vu wf ws xn--0zwm56d + xn--11b5bs3a9aj6g xn--80akhbyknj4f xn--9t4b11yi5a + xn--deba0ad xn--g6w251d xn--hgbk6aj7f53bba + xn--hlcj6aya9esc7a xn--jxalpdlp xn--kgbechtv xn--zckzah ye + yt yu za zm zw' + + ALLOWED_PROTOCOLS = ['http', 'https'] + + # The URI for relying party discovery, used in realm verification. + # + # XXX: This should probably live somewhere else (like in + # OpenID or OpenID::Yadis somewhere) + RP_RETURN_TO_URL_TYPE = 'http://specs.openid.net/auth/2.0/return_to' + + # If the endpoint is a relying party OpenID return_to endpoint, + # return the endpoint URL. Otherwise, return None. + # + # This function is intended to be used as a filter for the Yadis + # filtering interface. + # + # endpoint: An XRDS BasicServiceEndpoint, as returned by + # performing Yadis dicovery. + # + # returns the endpoint URL or None if the endpoint is not a + # relying party endpoint. + def TrustRoot._extract_return_url(endpoint) + if endpoint.matchTypes([RP_RETURN_TO_URL_TYPE]) + return endpoint.uri + else + return nil + end + end + + # Is the return_to URL under one of the supplied allowed + # return_to URLs? + def TrustRoot.return_to_matches(allowed_return_to_urls, return_to) + allowed_return_to_urls.each { |allowed_return_to| + # A return_to pattern works the same as a realm, except that + # it's not allowed to use a wildcard. We'll model this by + # parsing it as a realm, and not trying to match it if it has + # a wildcard. + + return_realm = TrustRoot.parse(allowed_return_to) + if (# Parses as a trust root + !return_realm.nil? and + + # Does not have a wildcard + !return_realm.wildcard and + + # Matches the return_to that we passed in with it + return_realm.validate_url(return_to) + ) + return true + end + } + + # No URL in the list matched + return false + end + + # Given a relying party discovery URL return a list of return_to + # URLs. + def TrustRoot.get_allowed_return_urls(relying_party_url) + rp_url_after_redirects, return_to_urls = services.get_service_endpoints( + relying_party_url, _extract_return_url) + + if rp_url_after_redirects != relying_party_url + # Verification caused a redirect + raise RealmVerificationRedirected.new( + relying_party_url, rp_url_after_redirects) + end + + return return_to_urls + end + + # Verify that a return_to URL is valid for the given realm. + # + # This function builds a discovery URL, performs Yadis discovery + # on it, makes sure that the URL does not redirect, parses out + # the return_to URLs, and finally checks to see if the current + # return_to URL matches the return_to. + # + # raises DiscoveryFailure when Yadis discovery fails returns + # true if the return_to URL is valid for the realm + def TrustRoot.verify_return_to(realm_str, return_to, _vrfy=nil) + # _vrfy parameter is there to make testing easier + if _vrfy.nil? + _vrfy = self.method('get_allowed_return_urls') + end + + if !(_vrfy.is_a?(Proc) or _vrfy.is_a?(Method)) + raise ArgumentError, "_vrfy must be a Proc or Method" + end + + realm = TrustRoot.parse(realm_str) + if realm.nil? + # The realm does not parse as a URL pattern + return false + end + + begin + allowable_urls = _vrfy.call(realm.build_discovery_url()) + rescue RealmVerificationRedirected => err + Util.log(err.to_s) + return false + end + + if return_to_matches(allowable_urls, return_to) + return true + else + Util.log("Failed to validate return_to #{return_to} for " + + "realm #{realm_str}, was not in #{allowable_urls}") + return false + end + end + + class TrustRoot + + attr_reader :unparsed, :proto, :wildcard, :host, :port, :path + + @@empty_re = Regexp.new('^http[s]*:\/\/\*\/$') + + def TrustRoot._build_path(path, query=nil, frag=nil) + s = path.dup + + frag = nil if frag == '' + query = nil if query == '' + + if query + s << "?" << query + end + + if frag + s << "#" << frag + end + + return s + end + + def TrustRoot._parse_url(url) + begin + url = URINorm.urinorm(url) + rescue URI::InvalidURIError => err + nil + end + + begin + parsed = URI::parse(url) + rescue URI::InvalidURIError + return nil + end + + path = TrustRoot._build_path(parsed.path, + parsed.query, + parsed.fragment) + + return [parsed.scheme || '', parsed.host || '', + parsed.port || '', path || ''] + end + + def TrustRoot.parse(trust_root) + trust_root = trust_root.dup + unparsed = trust_root.dup + + # look for wildcard + wildcard = (not trust_root.index('://*.').nil?) + trust_root.sub!('*.', '') if wildcard + + # handle http://*/ case + if not wildcard and @@empty_re.match(trust_root) + proto = trust_root.split(':')[0] + port = proto == 'http' ? 80 : 443 + return new(unparsed, proto, true, '', port, '/') + end + + parts = TrustRoot._parse_url(trust_root) + return nil if parts.nil? + + proto, host, port, path = parts + + # check for URI fragment + if path and !path.index('#').nil? + return nil + end + + return nil unless ['http', 'https'].member?(proto) + return new(unparsed, proto, wildcard, host, port, path) + end + + def TrustRoot.check_sanity(trust_root_string) + trust_root = TrustRoot.parse(trust_root_string) + if trust_root.nil? + return false + else + return trust_root.sane? + end + end + + # quick func for validating a url against a trust root. See the + # TrustRoot class if you need more control. + def self.check_url(trust_root, url) + tr = self.parse(trust_root) + return (!tr.nil? and tr.validate_url(url)) + end + + # Return a discovery URL for this realm. + # + # This function does not check to make sure that the realm is + # valid. Its behaviour on invalid inputs is undefined. + # + # return_to:: The relying party return URL of the OpenID + # authentication request + # + # Returns the URL upon which relying party discovery should be + # run in order to verify the return_to URL + def build_discovery_url + if self.wildcard + # Use "www." in place of the star + www_domain = 'www.' + @host + port = (!@port.nil? and ![80, 443].member?(@port)) ? (":" + @port.to_s) : '' + return "#{@proto}://#{www_domain}#{port}#{@path}" + else + return @unparsed + end + end + + def initialize(unparsed, proto, wildcard, host, port, path) + @unparsed = unparsed + @proto = proto + @wildcard = wildcard + @host = host + @port = port + @path = path + end + + def sane? + return true if @host == 'localhost' + + host_parts = @host.split('.') + + # a note: ruby string split does not put an empty string at + # the end of the list if the split element is last. for + # example, 'foo.com.'.split('.') => ['foo','com']. Mentioned + # because the python code differs here. + + return false if host_parts.length == 0 + + # no adjacent dots + return false if host_parts.member?('') + + # last part must be a tld + tld = host_parts[-1] + return false unless TOP_LEVEL_DOMAINS.member?(tld) + + return false if host_parts.length == 1 + + if @wildcard + if tld.length == 2 and host_parts[-2].length <= 3 + # It's a 2-letter tld with a short second to last segment + # so there needs to be more than two segments specified + # (e.g. *.co.uk is insane) + return host_parts.length > 2 + end + end + + return true + end + + def validate_url(url) + parts = TrustRoot._parse_url(url) + return false if parts.nil? + + proto, host, port, path = parts + + return false unless proto == @proto + return false unless port == @port + return false unless host.index('*').nil? + + if !@wildcard + if host != @host + return false + end + elsif ((@host != '') and + (!host.ends_with?('.' + @host)) and + (host != @host)) + return false + end + + if path != @path + path_len = @path.length + trust_prefix = @path[0...path_len] + url_prefix = path[0...path_len] + + # must be equal up to the length of the path, at least + if trust_prefix != url_prefix + return false + end + + # These characters must be on the boundary between the end + # of the trust root's path and the start of the URL's path. + if !@path.index('?').nil? + allowed = '&' + else + allowed = '?/' + end + + return (!allowed.index(@path[-1]).nil? or + !allowed.index(path[path_len]).nil?) + end + + return true + end + end + end +end + diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/urinorm.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/urinorm.rb new file mode 100644 index 000000000..f30893eaa --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/urinorm.rb @@ -0,0 +1,75 @@ +require 'uri' + +require "openid/extras" + +module OpenID + + module URINorm + public + def URINorm.urinorm(uri) + uri = URI.parse(uri) + + raise URI::InvalidURIError.new('no scheme') unless uri.scheme + uri.scheme = uri.scheme.downcase + unless ['http','https'].member?(uri.scheme) + raise URI::InvalidURIError.new('Not an HTTP or HTTPS URI') + end + + raise URI::InvalidURIError.new('no host') unless uri.host + uri.host = uri.host.downcase + + uri.path = remove_dot_segments(uri.path) + uri.path = '/' if uri.path.length == 0 + + uri = uri.normalize.to_s + uri = uri.gsub(PERCENT_ESCAPE_RE) { + sub = $&[1..2].to_i(16).chr + reserved(sub) ? $&.upcase : sub + } + + return uri + end + + private + RESERVED_RE = /[A-Za-z0-9._~-]/ + PERCENT_ESCAPE_RE = /%[0-9a-zA-Z]{2}/ + + def URINorm.reserved(chr) + not RESERVED_RE =~ chr + end + + def URINorm.remove_dot_segments(path) + result_segments = [] + + while path.length > 0 + if path.starts_with?('../') + path = path[3..-1] + elsif path.starts_with?('./') + path = path[2..-1] + elsif path.starts_with?('/./') + path = path[2..-1] + elsif path == '/.' + path = '/' + elsif path.starts_with?('/../') + path = path[3..-1] + result_segments.pop if result_segments.length > 0 + elsif path == '/..' + path = '/' + result_segments.pop if result_segments.length > 0 + elsif path == '..' or path == '.' + path = '' + else + i = 0 + i = 1 if path[0].chr == '/' + i = path.index('/', i) + i = path.length if i.nil? + result_segments << path[0...i] + path = path[i..-1] + end + end + + return result_segments.join('') + end + end + +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/util.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/util.rb new file mode 100644 index 000000000..c5a6716b2 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/util.rb @@ -0,0 +1,110 @@ +require "cgi" +require "uri" +require "logger" + +require "openid/extras" + +# See OpenID::Consumer or OpenID::Server modules, as well as the store classes +module OpenID + class AssertionError < Exception + end + + # Exceptions that are raised by the library are subclasses of this + # exception type, so if you want to catch all exceptions raised by + # the library, you can catch OpenIDError + class OpenIDError < StandardError + end + + module Util + + BASE64_CHARS = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' \ + 'abcdefghijklmnopqrstuvwxyz0123456789+/') + BASE64_RE = Regexp.compile(" + \\A + ([#{BASE64_CHARS}]{4})* + ([#{BASE64_CHARS}]{2}==| + [#{BASE64_CHARS}]{3}=)? + \\Z", Regexp::EXTENDED) + + def Util.assert(value, message=nil) + if not value + raise AssertionError, message or value + end + end + + def Util.to_base64(s) + [s].pack('m').gsub("\n", "") + end + + def Util.from_base64(s) + without_newlines = s.gsub(/[\r\n]+/, '') + if !BASE64_RE.match(without_newlines) + raise ArgumentError, "Malformed input: #{s.inspect}" + end + without_newlines.unpack('m').first + end + + def Util.urlencode(args) + a = [] + args.each do |key, val| + val = '' unless val + a << (CGI::escape(key) + "=" + CGI::escape(val)) + end + a.join("&") + end + + def Util.parse_query(qs) + query = {} + CGI::parse(qs).each {|k,v| query[k] = v[0]} + return query + end + + def Util.append_args(url, args) + url = url.dup + return url if args.length == 0 + + if args.respond_to?('each_pair') + args = args.sort + end + + url << (url.include?("?") ? "&" : "?") + url << Util.urlencode(args) + end + + @@logger = Logger.new(STDERR) + @@logger.progname = "OpenID" + + def Util.logger=(logger) + @@logger = logger + end + + def Util.logger + @@logger + end + + # change the message below to do whatever you like for logging + def Util.log(message) + logger.info(message) + end + + def Util.auto_submit_html(form, title='OpenID transaction in progress') + return " +<html> +<head> + <title>#{title}</title> +</head> +<body onload='document.forms[0].submit();'> +#{form} +<script> +var elements = document.forms[0].elements; +for (var i = 0; i < elements.length; i++) { + elements[i].style.display = \"none\"; +} +</script> +</body> +</html> +" + end + end + +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/accept.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/accept.rb new file mode 100644 index 000000000..a1657482f --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/accept.rb @@ -0,0 +1,148 @@ +module OpenID + + module Yadis + + # Generate an accept header value + # + # [str or (str, float)] -> str + 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 + + 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 + + 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 + + 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 + + end + +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/constants.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/constants.rb new file mode 100644 index 000000000..99b58b138 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/constants.rb @@ -0,0 +1,21 @@ + +require 'openid/yadis/accept' + +module OpenID + + module Yadis + + YADIS_HEADER_NAME = 'X-XRDS-Location' + YADIS_CONTENT_TYPE = 'application/xrds+xml' + + # A value suitable for using as an accept header when performing + # YADIS discovery, unless the application has special requirements + YADIS_ACCEPT_HEADER = generate_accept_header( + ['text/html', 0.3], + ['application/xhtml+xml', 0.5], + [YADIS_CONTENT_TYPE, 1.0] + ) + + end + +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/discovery.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/discovery.rb new file mode 100644 index 000000000..55d6f09b1 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/discovery.rb @@ -0,0 +1,153 @@ + +require 'openid/util' +require 'openid/fetchers' +require 'openid/yadis/constants' +require 'openid/yadis/parsehtml' + +module OpenID + + # Raised when a error occurs in the discovery process + class DiscoveryFailure < OpenIDError + attr_accessor :identity_url, :http_response + + def initialize(message, http_response) + super(message) + @identity_url = nil + @http_response = http_response + end + end + + module Yadis + + # Contains the result of performing Yadis discovery on a URI + class DiscoveryResult + + # The result of following redirects from the request_uri + attr_accessor :normalize_uri + + # The URI from which the response text was returned (set to + # nil if there was no XRDS document found) + attr_accessor :xrds_uri + + # The content-type returned with the response_text + attr_accessor :content_type + + # The document returned from the xrds_uri + attr_accessor :response_text + + attr_accessor :request_uri, :normalized_uri + + def initialize(request_uri) + # Initialize the state of the object + # + # sets all attributes to None except the request_uri + @request_uri = request_uri + @normalized_uri = nil + @xrds_uri = nil + @content_type = nil + @response_text = nil + end + + # Was the Yadis protocol's indirection used? + def used_yadis_location? + return @normalized_uri != @xrds_uri + end + + # Is the response text supposed to be an XRDS document? + def is_xrds + return (used_yadis_location?() or + @content_type == YADIS_CONTENT_TYPE) + end + 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. + 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 + + # 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] + 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 + + end + +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/filters.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/filters.rb new file mode 100644 index 000000000..90f350ea3 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/filters.rb @@ -0,0 +1,205 @@ +# This file contains functions and classes used for extracting +# endpoint information out of a Yadis XRD file using the REXML +# XML parser. + +# +module OpenID + module Yadis + class BasicServiceEndpoint + attr_reader :type_uris, :yadis_url, :uri, :service_element + + # Generic endpoint object that contains parsed service + # information, as well as a reference to the service element + # from which it was generated. If there is more than one + # xrd:Type or xrd:URI in the xrd:Service, this object represents + # just one of those pairs. + # + # This object can be used as a filter, because it implements + # fromBasicServiceEndpoint. + # + # The simplest kind of filter you can write implements + # fromBasicServiceEndpoint, which takes one of these objects. + def initialize(yadis_url, type_uris, uri, service_element) + @type_uris = type_uris + @yadis_url = yadis_url + @uri = uri + @service_element = service_element + end + + # Query this endpoint to see if it has any of the given type + # URIs. This is useful for implementing other endpoint classes + # that e.g. need to check for the presence of multiple + # versions of a single protocol. + def match_types(type_uris) + return @type_uris & type_uris + end + + # Trivial transform from a basic endpoint to itself. This + # method exists to allow BasicServiceEndpoint to be used as a + # filter. + # + # If you are subclassing this object, re-implement this function. + def self.from_basic_service_endpoint(endpoint) + return endpoint + end + + # A hack to make both this class and its instances respond to + # this message since Ruby doesn't support static methods. + def from_basic_service_endpoint(endpoint) + return self.class.from_basic_service_endpoint(endpoint) + end + + end + + # Take a list of basic filters and makes a filter that + # transforms the basic filter into a top-level filter. This is + # mostly useful for the implementation of make_filter, which + # should only be needed for special cases or internal use by + # this library. + # + # This object is useful for creating simple filters for services + # that use one URI and are specified by one Type (we expect most + # Types will fit this paradigm). + # + # Creates a BasicServiceEndpoint object and apply the filter + # functions to it until one of them returns a value. + class TransformFilterMaker + attr_reader :filter_procs + + # Initialize the filter maker's state + # + # filter_functions are the endpoint transformer + # Procs to apply to the basic endpoint. These are called in + # turn until one of them does not return nil, and the result + # of that transformer is returned. + def initialize(filter_procs) + @filter_procs = filter_procs + end + + # Returns an array of endpoint objects produced by the + # filter procs. + def get_service_endpoints(yadis_url, service_element) + endpoints = [] + + # Do an expansion of the service element by xrd:Type and + # xrd:URI + Yadis::expand_service(service_element).each { |type_uris, uri, _| + # Create a basic endpoint object to represent this + # yadis_url, Service, Type, URI combination + endpoint = BasicServiceEndpoint.new( + yadis_url, type_uris, uri, service_element) + + e = apply_filters(endpoint) + if !e.nil? + endpoints << e + end + } + return endpoints + end + + def apply_filters(endpoint) + # Apply filter procs to an endpoint until one of them returns + # non-nil. + @filter_procs.each { |filter_proc| + e = filter_proc.call(endpoint) + if !e.nil? + # Once one of the filters has returned an endpoint, do not + # apply any more. + return e + end + } + + return nil + end + end + + class CompoundFilter + attr_reader :subfilters + + # Create a new filter that applies a set of filters to an + # endpoint and collects their results. + def initialize(subfilters) + @subfilters = subfilters + end + + # Generate all endpoint objects for all of the subfilters of + # this filter and return their concatenation. + def get_service_endpoints(yadis_url, service_element) + endpoints = [] + @subfilters.each { |subfilter| + endpoints += subfilter.get_service_endpoints(yadis_url, service_element) + } + return endpoints + end + end + + # Exception raised when something is not able to be turned into a + # filter + @@filter_type_error = TypeError.new( + 'Expected a filter, an endpoint, a callable or a list of any of these.') + + # Convert a filter-convertable thing into a filter + # + # parts should be a filter, an endpoint, a callable, or a list of + # any of these. + 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 + + # 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 + 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 + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/htmltokenizer.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/htmltokenizer.rb new file mode 100644 index 000000000..b20810975 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/htmltokenizer.rb @@ -0,0 +1,305 @@ +# = HTMLTokenizer +# +# Author:: Ben Giddings (mailto:bg-rubyforge@infofiend.com) +# Copyright:: Copyright (c) 2004 Ben Giddings +# License:: Distributes under the same terms as Ruby +# +# +# This is a partial port of the functionality behind Perl's TokeParser +# Provided a page it progressively returns tokens from that page +# +# $Id: htmltokenizer.rb,v 1.7 2005/06/07 21:05:53 merc Exp $ + +# +# A class to tokenize HTML. +# +# Example: +# +# page = "<HTML> +# <HEAD> +# <TITLE>This is the title</TITLE> +# </HEAD> +# <!-- Here comes the <a href=\"missing.link\">blah</a> +# comment body +# --> +# <BODY> +# <H1>This is the header</H1> +# <P> +# This is the paragraph, it contains +# <a href=\"link.html\">links</a>, +# <img src=\"blah.gif\" optional alt='images +# are +# really cool'>. Ok, here is some more text and +# <A href=\"http://another.link.com/\" target=\"_blank\">another link</A>. +# </P> +# </body> +# </HTML> +# " +# toke = HTMLTokenizer.new(page) +# +# assert("<h1>" == toke.getTag("h1", "h2", "h3").to_s.downcase) +# assert(HTMLTag.new("<a href=\"link.html\">") == toke.getTag("IMG", "A")) +# assert("links" == toke.getTrimmedText) +# assert(toke.getTag("IMG", "A").attr_hash['optional']) +# assert("_blank" == toke.getTag("IMG", "A").attr_hash['target']) +# +class HTMLTokenizer + @@version = 1.0 + + # Get version of HTMLTokenizer lib + def self.version + @@version + end + + attr_reader :page + + # Create a new tokenizer, based on the content, used as a string. + def initialize(content) + @page = content.to_s + @cur_pos = 0 + end + + # Reset the parser, setting the current position back at the stop + def reset + @cur_pos = 0 + end + + # Look at the next token, but don't actually grab it + def peekNextToken + if @cur_pos == @page.length then return nil end + + if ?< == @page[@cur_pos] + # Next token is a tag of some kind + if '!--' == @page[(@cur_pos + 1), 3] + # Token is a comment + tag_end = @page.index('-->', (@cur_pos + 1)) + if tag_end.nil? + raise HTMLTokenizerError, "No end found to started comment:\n#{@page[@cur_pos,80]}" + end + # p @page[@cur_pos .. (tag_end+2)] + HTMLComment.new(@page[@cur_pos .. (tag_end + 2)]) + else + # Token is a html tag + tag_end = @page.index('>', (@cur_pos + 1)) + if tag_end.nil? + raise HTMLTokenizerError, "No end found to started tag:\n#{@page[@cur_pos,80]}" + end + # p @page[@cur_pos .. tag_end] + HTMLTag.new(@page[@cur_pos .. tag_end]) + end + else + # Next token is text + text_end = @page.index('<', @cur_pos) + text_end = text_end.nil? ? -1 : (text_end - 1) + # p @page[@cur_pos .. text_end] + HTMLText.new(@page[@cur_pos .. text_end]) + end + end + + # Get the next token, returns an instance of + # * HTMLText + # * HTMLToken + # * HTMLTag + def getNextToken + token = peekNextToken + if token + # @page = @page[token.raw.length .. -1] + # @page.slice!(0, token.raw.length) + @cur_pos += token.raw.length + end + #p token + #print token.raw + return token + end + + # Get a tag from the specified set of desired tags. + # For example: + # <tt>foo = toke.getTag("h1", "h2", "h3")</tt> + # Will return the next header tag encountered. + def getTag(*sought_tags) + sought_tags.collect! {|elm| elm.downcase} + + while (tag = getNextToken) + if tag.kind_of?(HTMLTag) and + (0 == sought_tags.length or sought_tags.include?(tag.tag_name)) + break + end + end + tag + end + + # Get all the text between the current position and the next tag + # (if specified) or a specific later tag + def getText(until_tag = nil) + if until_tag.nil? + if ?< == @page[@cur_pos] + # Next token is a tag, not text + "" + else + # Next token is text + getNextToken.text + end + else + ret_str = "" + + while (tag = peekNextToken) + if tag.kind_of?(HTMLTag) and tag.tag_name == until_tag + break + end + + if ("" != tag.text) + ret_str << (tag.text + " ") + end + getNextToken + end + + ret_str + end + end + + # Like getText, but squeeze all whitespace, getting rid of + # leading and trailing whitespace, and squeezing multiple + # spaces into a single space. + def getTrimmedText(until_tag = nil) + getText(until_tag).strip.gsub(/\s+/m, " ") + end + +end + +class HTMLTokenizerError < Exception +end + +# The parent class for all three types of HTML tokens +class HTMLToken + attr_accessor :raw + + # Initialize the token based on the raw text + def initialize(text) + @raw = text + end + + # By default, return exactly the string used to create the text + def to_s + raw + end + + # By default tokens have no text representation + def text + "" + end + + def trimmed_text + text.strip.gsub(/\s+/m, " ") + end + + # Compare to another based on the raw source + def ==(other) + raw == other.to_s + end +end + +# Class representing text that isn't inside a tag +class HTMLText < HTMLToken + def text + raw + end +end + +# Class representing an HTML comment +class HTMLComment < HTMLToken + attr_accessor :contents + def initialize(text) + super(text) + temp_arr = text.scan(/^<!--\s*(.*?)\s*-->$/m) + if temp_arr[0].nil? + raise HTMLTokenizerError, "Text passed to HTMLComment.initialize is not a comment" + end + + @contents = temp_arr[0][0] + end +end + +# Class representing an HTML tag +class HTMLTag < HTMLToken + attr_reader :end_tag, :tag_name + def initialize(text) + super(text) + if ?< != text[0] or ?> != text[-1] + raise HTMLTokenizerError, "Text passed to HTMLComment.initialize is not a comment" + end + + @attr_hash = Hash.new + @raw = text + + tag_name = text.scan(/[\w:-]+/)[0] + if tag_name.nil? + raise HTMLTokenizerError, "Error, tag is nil: #{tag_name}" + end + + if ?/ == text[1] + # It's an end tag + @end_tag = true + @tag_name = '/' + tag_name.downcase + else + @end_tag = false + @tag_name = tag_name.downcase + end + + @hashed = false + end + + # Retrieve a hash of all the tag's attributes. + # Lazily done, so that if you don't look at a tag's attributes + # things go quicker + def attr_hash + # Lazy initialize == don't build the hash until it's needed + if !@hashed + if !@end_tag + # Get the attributes + attr_arr = @raw.scan(/<[\w:-]+\s+(.*?)\/?>/m)[0] + if attr_arr.kind_of?(Array) + # Attributes found, parse them + attrs = attr_arr[0] + attr_arr = attrs.scan(/\s*([\w:-]+)(?:\s*=\s*("[^"]*"|'[^']*'|([^"'>][^\s>]*)))?/m) + # clean up the array by: + # * setting all nil elements to true + # * removing enclosing quotes + attr_arr.each { + |item| + val = if item[1].nil? + item[0] + elsif '"'[0] == item[1][0] or '\''[0] == item[1][0] + item[1][1 .. -2] + else + item[1] + end + @attr_hash[item[0].downcase] = val + } + end + end + @hashed = true + end + + #p self + + @attr_hash + end + + # Get the 'alt' text for a tag, if it exists, or an empty string otherwise + def text + if !end_tag + case tag_name + when 'img' + if !attr_hash['alt'].nil? + return attr_hash['alt'] + end + when 'applet' + if !attr_hash['alt'].nil? + return attr_hash['alt'] + end + end + end + return '' + end +end + diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/parsehtml.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/parsehtml.rb new file mode 100644 index 000000000..877c714bc --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/parsehtml.rb @@ -0,0 +1,45 @@ +require "openid/yadis/htmltokenizer" +require 'cgi' + +module OpenID + module Yadis + 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 + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/services.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/services.rb new file mode 100644 index 000000000..e3b3e0f69 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/services.rb @@ -0,0 +1,42 @@ + +require 'openid/yadis/filters' +require 'openid/yadis/discovery' +require 'openid/yadis/xrds' + +module OpenID + module Yadis + 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 + + 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 + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/xrds.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/xrds.rb new file mode 100644 index 000000000..0ebf34ab4 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/xrds.rb @@ -0,0 +1,155 @@ +require 'rexml/document' +require 'rexml/element' +require 'rexml/xpath' + +require 'openid/yadis/xri' + +module OpenID + module Yadis + + XRD_NS_2_0 = 'xri://$xrd*($v*2.0)' + XRDS_NS = 'xri://$xrds' + + XRDS_NAMESPACES = { + 'xrds' => XRDS_NS, + 'xrd' => XRD_NS_2_0, + } + + class XRDSError < StandardError; end + + # Raised when there's an assertion in the XRDS that it does not + # have the authority to make. + class XRDSFraud < XRDSError + end + + 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 + + class XRDSError < StandardError + end + + 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 + + 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 + + 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 + + # aka iterServices in Python + def Yadis::each_service(xrds_tree, &block) + xrd = get_yadis_xrd(xrds_tree) + xrd.each_element('Service', &block) + end + + def Yadis::services(xrds_tree) + s = [] + each_service(xrds_tree) { |service| + s << service + } + return s + end + + 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 + + # Sort a list of elements that have priority attributes. + def Yadis::prio_sort(elements) + elements.sort { |a,b| + a.attribute('priority').to_s.to_i <=> b.attribute('priority').to_s.to_i + } + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/xri.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/xri.rb new file mode 100644 index 000000000..89dd99afc --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/xri.rb @@ -0,0 +1,90 @@ +require 'openid/yadis/xrds' +require 'openid/fetchers' + +module OpenID + module Yadis + module XRI + + # The '(' is for cross-reference authorities, and hopefully has a + # matching ')' somewhere. + XRI_AUTHORITIES = ["!", "=", "@", "+", "$", "("] + + def self.identifier_scheme(identifier) + if (!identifier.nil? and + identifier.length > 0 and + (identifier.match('^xri://') or + XRI_AUTHORITIES.member?(identifier[0].chr))) + return :xri + else + return :uri + end + end + + # Transform an XRI reference to an IRI reference. Note this is + # not not idempotent, so do not apply this to an identifier more + # than once. XRI Syntax section 2.3.1 + def self.to_iri_normal(xri) + iri = xri.dup + iri.insert(0, 'xri://') if not iri.match('^xri://') + return escape_for_iri(iri) + end + + # Note this is not not idempotent, so do not apply this more than + # once. XRI Syntax section 2.3.2 + def self.escape_for_iri(xri) + esc = xri.dup + # encode all % + esc.gsub!(/%/, '%25') + esc.gsub!(/\((.*?)\)/) { |xref_match| + xref_match.gsub(/[\/\?\#]/) { |char_match| + CGI::escape(char_match) + } + } + return esc + end + + # Transform an XRI reference to a URI reference. Note this is not + # not idempotent, so do not apply this to an identifier more than + # once. XRI Syntax section 2.3.1 + def self.to_uri_normal(xri) + return iri_to_uri(to_iri_normal(xri)) + end + + # RFC 3987 section 3.1 + def self.iri_to_uri(iri) + uri = iri.dup + # for char in ucschar or iprivate + # convert each char to %HH%HH%HH (as many %HH as octets) + return uri + end + + def self.provider_is_authoritative(provider_id, canonical_id) + lastbang = canonical_id.rindex('!') + return false unless lastbang + parent = canonical_id[0...lastbang] + return parent == provider_id + end + + def self.root_authority(xri) + xri = xri[6..-1] if xri.index('xri://') == 0 + authority = xri.split('/', 2)[0] + if authority[0].chr == '(' + root = authority[0...authority.index(')')+1] + elsif XRI_AUTHORITIES.member?(authority[0].chr) + root = authority[0].chr + else + root = authority.split(/[!*]/)[0] + end + + self.make_xri(root) + end + + def self.make_xri(xri) + if xri.index('xri://') != 0 + xri = 'xri://' + xri + end + return xri + end + end + end +end diff --git a/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/xrires.rb b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/xrires.rb new file mode 100644 index 000000000..394391198 --- /dev/null +++ b/vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/xrires.rb @@ -0,0 +1,106 @@ +require "cgi" +require "openid/yadis/xri" +require "openid/yadis/xrds" +require "openid/fetchers" + +module OpenID + + module Yadis + + module XRI + + class XRIHTTPError < StandardError; end + + class ProxyResolver + + DEFAULT_PROXY = 'http://proxy.xri.net/' + + def initialize(proxy_url=nil) + if proxy_url + @proxy_url = proxy_url + else + @proxy_url = DEFAULT_PROXY + end + + @proxy_url += '/' unless @proxy_url.match('/$') + end + + def query_url(xri, service_type=nil) + # URI normal form has a leading xri://, but we need to strip + # that off again for the QXRI. This is under discussion for + # XRI Resolution WD 11. + qxri = XRI.to_uri_normal(xri)[6..-1] + hxri = @proxy_url + qxri + args = {'_xrd_r' => 'application/xrds+xml'} + if service_type + args['_xrd_t'] = service_type + else + # don't perform service endpoint selection + args['_xrd_r'] += ';sep=false' + end + + return XRI.append_args(hxri, args) + end + + def query(xri, service_types) + # these can be query args or http headers, needn't be both. + # headers = {'Accept' => 'application/xrds+xml;sep=true'} + canonicalID = nil + + services = service_types.collect { |service_type| + url = self.query_url(xri, service_type) + begin + response = OpenID.fetch(url) + rescue + raise XRIHTTPError, ["Could not fetch #{xri}", $!] + end + raise XRIHTTPError, "Could not fetch #{xri}" if response.nil? + + xrds = Yadis::parseXRDS(response.body) + canonicalID = Yadis::get_canonical_id(xri, xrds) + + Yadis::services(xrds) unless xrds.nil? + } + # TODO: + # * If we do get hits for multiple service_types, we're almost + # certainly going to have duplicated service entries and + # broken priority ordering. + services = services.inject([]) { |flatter, some_services| + flatter += some_services unless some_services.nil? + } + + return canonicalID, services + end + end + + def self.urlencode(args) + a = [] + args.each do |key, val| + a << (CGI::escape(key) + "=" + CGI::escape(val)) + end + a.join("&") + end + + def self.append_args(url, args) + return url if args.length == 0 + + # rstrip question marks + rstripped = url.dup + while rstripped[-1].chr == '?' + rstripped = rstripped[0...rstripped.length-1] + end + + if rstripped.index('?') + sep = '&' + else + sep = '?' + end + + return url + sep + XRI.urlencode(args) + end + + end + + end + +end |