summaryrefslogtreecommitdiffstats
path: root/vendor/gems/ruby-openid-2.1.4/lib
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/gems/ruby-openid-2.1.4/lib')
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/hmac/hmac.rb112
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/hmac/sha1.rb11
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/hmac/sha2.rb25
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid.rb20
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/association.rb249
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/consumer.rb395
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/associationmanager.rb340
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/checkid_request.rb186
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/discovery.rb498
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/discovery_manager.rb123
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/html_parse.rb134
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/idres.rb523
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/consumer/responses.rb148
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/cryptutil.rb97
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/dh.rb89
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/extension.rb39
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/extensions/ax.rb516
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/extensions/pape.rb179
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/extensions/sreg.rb277
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/extras.rb11
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/fetchers.rb238
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/kvform.rb136
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/kvpost.rb58
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/message.rb553
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/protocolerror.rb8
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/server.rb1544
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/store/filesystem.rb271
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/store/interface.rb75
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/store/memory.rb84
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/store/nonce.rb68
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/trustroot.rb349
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/urinorm.rb75
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/util.rb110
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/accept.rb148
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/constants.rb21
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/discovery.rb153
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/filters.rb205
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/htmltokenizer.rb305
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/parsehtml.rb45
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/services.rb42
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/xrds.rb155
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/xri.rb90
-rw-r--r--vendor/gems/ruby-openid-2.1.4/lib/openid/yadis/xrires.rb106
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('&amp;','&').gsub('&lt;','<').gsub('&gt;','>').gsub('&quot;','"')
+ 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