diff options
Diffstat (limited to 'actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth')
8 files changed, 830 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/abstract/handler.rb b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/abstract/handler.rb new file mode 100644 index 0000000000..b213eac6f4 --- /dev/null +++ b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/abstract/handler.rb @@ -0,0 +1,28 @@ +module Rack + module Auth + # Rack::Auth::AbstractHandler implements common authentication functionality. + # + # +realm+ should be set for all handlers. + + class AbstractHandler + + attr_accessor :realm + + def initialize(app, &authenticator) + @app, @authenticator = app, authenticator + end + + + private + + def unauthorized(www_authenticate = challenge) + return [ 401, { 'WWW-Authenticate' => www_authenticate.to_s }, [] ] + end + + def bad_request + [ 400, {}, [] ] + end + + end + end +end diff --git a/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/abstract/request.rb b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/abstract/request.rb new file mode 100644 index 0000000000..1d9ccec685 --- /dev/null +++ b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/abstract/request.rb @@ -0,0 +1,37 @@ +module Rack + module Auth + class AbstractRequest + + def initialize(env) + @env = env + end + + def provided? + !authorization_key.nil? + end + + def parts + @parts ||= @env[authorization_key].split(' ', 2) + end + + def scheme + @scheme ||= parts.first.downcase.to_sym + end + + def params + @params ||= parts.last + end + + + private + + AUTHORIZATION_KEYS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION'] + + def authorization_key + @authorization_key ||= AUTHORIZATION_KEYS.detect { |key| @env.has_key?(key) } + end + + end + + end +end diff --git a/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/basic.rb b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/basic.rb new file mode 100644 index 0000000000..9557224648 --- /dev/null +++ b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/basic.rb @@ -0,0 +1,58 @@ +require 'rack/auth/abstract/handler' +require 'rack/auth/abstract/request' + +module Rack + module Auth + # Rack::Auth::Basic implements HTTP Basic Authentication, as per RFC 2617. + # + # Initialize with the Rack application that you want protecting, + # and a block that checks if a username and password pair are valid. + # + # See also: <tt>example/protectedlobster.rb</tt> + + class Basic < AbstractHandler + + def call(env) + auth = Basic::Request.new(env) + + return unauthorized unless auth.provided? + + return bad_request unless auth.basic? + + if valid?(auth) + env['REMOTE_USER'] = auth.username + + return @app.call(env) + end + + unauthorized + end + + + private + + def challenge + 'Basic realm="%s"' % realm + end + + def valid?(auth) + @authenticator.call(*auth.credentials) + end + + class Request < Auth::AbstractRequest + def basic? + :basic == scheme + end + + def credentials + @credentials ||= params.unpack("m*").first.split(/:/, 2) + end + + def username + credentials.first + end + end + + end + end +end diff --git a/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/digest/md5.rb b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/digest/md5.rb new file mode 100644 index 0000000000..6d2bd29c2e --- /dev/null +++ b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/digest/md5.rb @@ -0,0 +1,124 @@ +require 'rack/auth/abstract/handler' +require 'rack/auth/digest/request' +require 'rack/auth/digest/params' +require 'rack/auth/digest/nonce' +require 'digest/md5' + +module Rack + module Auth + module Digest + # Rack::Auth::Digest::MD5 implements the MD5 algorithm version of + # HTTP Digest Authentication, as per RFC 2617. + # + # Initialize with the [Rack] application that you want protecting, + # and a block that looks up a plaintext password for a given username. + # + # +opaque+ needs to be set to a constant base64/hexadecimal string. + # + class MD5 < AbstractHandler + + attr_accessor :opaque + + attr_writer :passwords_hashed + + def initialize(app) + super + @passwords_hashed = nil + end + + def passwords_hashed? + !!@passwords_hashed + end + + def call(env) + auth = Request.new(env) + + unless auth.provided? + return unauthorized + end + + if !auth.digest? || !auth.correct_uri? || !valid_qop?(auth) + return bad_request + end + + if valid?(auth) + if auth.nonce.stale? + return unauthorized(challenge(:stale => true)) + else + env['REMOTE_USER'] = auth.username + + return @app.call(env) + end + end + + unauthorized + end + + + private + + QOP = 'auth'.freeze + + def params(hash = {}) + Params.new do |params| + params['realm'] = realm + params['nonce'] = Nonce.new.to_s + params['opaque'] = H(opaque) + params['qop'] = QOP + + hash.each { |k, v| params[k] = v } + end + end + + def challenge(hash = {}) + "Digest #{params(hash)}" + end + + def valid?(auth) + valid_opaque?(auth) && valid_nonce?(auth) && valid_digest?(auth) + end + + def valid_qop?(auth) + QOP == auth.qop + end + + def valid_opaque?(auth) + H(opaque) == auth.opaque + end + + def valid_nonce?(auth) + auth.nonce.valid? + end + + def valid_digest?(auth) + digest(auth, @authenticator.call(auth.username)) == auth.response + end + + def md5(data) + ::Digest::MD5.hexdigest(data) + end + + alias :H :md5 + + def KD(secret, data) + H([secret, data] * ':') + end + + def A1(auth, password) + [ auth.username, auth.realm, password ] * ':' + end + + def A2(auth) + [ auth.method, auth.uri ] * ':' + end + + def digest(auth, password) + password_hash = passwords_hashed? ? password : H(A1(auth, password)) + + KD(password_hash, [ auth.nonce, auth.nc, auth.cnonce, QOP, H(A2(auth)) ] * ':') + end + + end + end + end +end diff --git a/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/digest/nonce.rb b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/digest/nonce.rb new file mode 100644 index 0000000000..dbe109f29a --- /dev/null +++ b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/digest/nonce.rb @@ -0,0 +1,51 @@ +require 'digest/md5' + +module Rack + module Auth + module Digest + # Rack::Auth::Digest::Nonce is the default nonce generator for the + # Rack::Auth::Digest::MD5 authentication handler. + # + # +private_key+ needs to set to a constant string. + # + # +time_limit+ can be optionally set to an integer (number of seconds), + # to limit the validity of the generated nonces. + + class Nonce + + class << self + attr_accessor :private_key, :time_limit + end + + def self.parse(string) + new(*string.unpack("m*").first.split(' ', 2)) + end + + def initialize(timestamp = Time.now, given_digest = nil) + @timestamp, @given_digest = timestamp.to_i, given_digest + end + + def to_s + [([ @timestamp, digest ] * ' ')].pack("m*").strip + end + + def digest + ::Digest::MD5.hexdigest([ @timestamp, self.class.private_key ] * ':') + end + + def valid? + digest == @given_digest + end + + def stale? + !self.class.time_limit.nil? && (@timestamp - Time.now.to_i) < self.class.time_limit + end + + def fresh? + !stale? + end + + end + end + end +end diff --git a/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/digest/params.rb b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/digest/params.rb new file mode 100644 index 0000000000..730e2efdc8 --- /dev/null +++ b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/digest/params.rb @@ -0,0 +1,55 @@ +module Rack + module Auth + module Digest + class Params < Hash + + def self.parse(str) + split_header_value(str).inject(new) do |header, param| + k, v = param.split('=', 2) + header[k] = dequote(v) + header + end + end + + def self.dequote(str) # From WEBrick::HTTPUtils + ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup + ret.gsub!(/\\(.)/, "\\1") + ret + end + + def self.split_header_value(str) + str.scan( /(\w+\=(?:"[^\"]+"|[^,]+))/n ).collect{ |v| v[0] } + end + + def initialize + super + + yield self if block_given? + end + + def [](k) + super k.to_s + end + + def []=(k, v) + super k.to_s, v.to_s + end + + UNQUOTED = ['qop', 'nc', 'stale'] + + def to_s + inject([]) do |parts, (k, v)| + parts << "#{k}=" + (UNQUOTED.include?(k) ? v.to_s : quote(v)) + parts + end.join(', ') + end + + def quote(str) # From WEBrick::HTTPUtils + '"' << str.gsub(/[\\\"]/o, "\\\1") << '"' + end + + end + end + end +end + diff --git a/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/digest/request.rb b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/digest/request.rb new file mode 100644 index 0000000000..a0227543fb --- /dev/null +++ b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/digest/request.rb @@ -0,0 +1,40 @@ +require 'rack/auth/abstract/request' +require 'rack/auth/digest/params' +require 'rack/auth/digest/nonce' + +module Rack + module Auth + module Digest + class Request < Auth::AbstractRequest + + def method + @env['REQUEST_METHOD'] + end + + def digest? + :digest == scheme + end + + def correct_uri? + @env['PATH_INFO'] == uri + end + + def nonce + @nonce ||= Nonce.parse(params['nonce']) + end + + def params + @params ||= Params.parse(parts.last) + end + + def method_missing(sym) + if params.has_key? key = sym.to_s + return params[key] + end + super + end + + end + end + end +end diff --git a/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/openid.rb b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/openid.rb new file mode 100644 index 0000000000..2bd064ea4a --- /dev/null +++ b/actionpack/lib/action_controller/vendor/rack-0.4.0/rack/auth/openid.rb @@ -0,0 +1,437 @@ +# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net + +gem 'ruby-openid', '~> 2' if defined? Gem +require 'rack/auth/abstract/handler' #rack +require 'uri' #std +require 'pp' #std +require 'openid' #gem +require 'openid/extension' #gem +require 'openid/store/memory' #gem + +module Rack + module Auth + # Rack::Auth::OpenID provides a simple method for permitting + # openid based logins. It requires the ruby-openid library from + # janrain to operate, as well as a rack method of session management. + # + # The ruby-openid home page is at http://openidenabled.com/ruby-openid/. + # + # The OpenID specifications can be found at + # http://openid.net/specs/openid-authentication-1_1.html + # and + # http://openid.net/specs/openid-authentication-2_0.html. Documentation + # for published OpenID extensions and related topics can be found at + # http://openid.net/developers/specs/. + # + # It is recommended to read through the OpenID spec, as well as + # ruby-openid's documentation, to understand what exactly goes on. However + # a setup as simple as the presented examples is enough to provide + # functionality. + # + # This library strongly intends to utilize the OpenID 2.0 features of the + # ruby-openid library, while maintaining OpenID 1.0 compatiblity. + # + # All responses from this rack application will be 303 redirects unless an + # error occurs, with the exception of an authentication request requiring + # an HTML form submission. + # + # NOTE: Extensions are not currently supported by this implimentation of + # the OpenID rack application due to the complexity of the current + # ruby-openid extension handling. + # + # NOTE: Due to the amount of data that this library stores in the + # session, Rack::Session::Cookie may fault. + class OpenID < AbstractHandler + class NoSession < RuntimeError; end + # Required for ruby-openid + OIDStore = ::OpenID::Store::Memory.new + HTML = '<html><head><title>%s</title></head><body>%s</body></html>' + + # A Hash of options is taken as it's single initializing + # argument. For example: + # + # simple_oid = OpenID.new('http://mysite.com/') + # + # return_oid = OpenID.new('http://mysite.com/', { + # :return_to => 'http://mysite.com/openid' + # }) + # + # page_oid = OpenID.new('http://mysite.com/', + # :login_good => 'http://mysite.com/auth_good' + # ) + # + # complex_oid = OpenID.new('http://mysite.com/', + # :return_to => 'http://mysite.com/openid', + # :login_good => 'http://mysite.com/user/preferences', + # :auth_fail => [500, {'Content-Type'=>'text/plain'}, + # 'Unable to negotiate with foreign server.'], + # :immediate => true, + # :extensions => { + # ::OpenID::SReg => [['email'],['nickname']] + # } + # ) + # + # = Arguments + # + # The first argument is the realm, identifying the site they are trusting + # with their identity. This is required. + # + # NOTE: In OpenID 1.x, the realm or trust_root is optional and the + # return_to url is required. As this library strives tward ruby-openid + # 2.0, and OpenID 2.0 compatibiliy, the realm is required and return_to + # is optional. However, this implimentation is still backwards compatible + # with OpenID 1.0 servers. + # + # The optional second argument is a hash of options. + # + # == Options + # + # <tt>:return_to</tt> defines the url to return to after the client + # authenticates with the openid service provider. This url should point + # to where Rack::Auth::OpenID is mounted. If <tt>:return_to</tt> is not + # provided, :return_to will be the current url including all query + # parameters. + # + # <tt>:session_key</tt> defines the key to the session hash in the env. + # It defaults to 'rack.session'. + # + # <tt>:openid_param</tt> defines at what key in the request parameters to + # find the identifier to resolve. As per the 2.0 spec, the default is + # 'openid_identifier'. + # + # <tt>:immediate</tt> as true will make immediate type of requests the + # default. See OpenID specification documentation. + # + # === URL options + # + # <tt>:login_good</tt> is the url to go to after the authentication + # process has completed. + # + # <tt>:login_fail</tt> is the url to go to after the authentication + # process has failed. + # + # <tt>:login_quit</tt> is the url to go to after the authentication + # process + # has been cancelled. + # + # === Response options + # + # <tt>:no_session</tt> should be a rack response to be returned if no or + # an incompatible session is found. + # + # <tt>:auth_fail</tt> should be a rack response to be returned if an + # OpenID::DiscoveryFailure occurs. This is typically due to being unable + # to access the identity url or identity server. + # + # <tt>:error</tt> should be a rack response to return if any other + # generic error would occur and <tt>options[:catch_errors]</tt> is true. + # + # === Extensions + # + # <tt>:extensions</tt> should be a hash of openid extension + # implementations. The key should be the extension main module, the value + # should be an array of arguments for extension::Request.new + # + # The hash is iterated over and passed to #add_extension for processing. + # Please see #add_extension for further documentation. + def initialize(realm, options={}) + @realm = realm + realm = URI(realm) + if realm.path.empty? + raise ArgumentError, "Invalid realm path: '#{realm.path}'" + elsif not realm.absolute? + raise ArgumentError, "Realm '#{@realm}' not absolute" + end + + [:return_to, :login_good, :login_fail, :login_quit].each do |key| + if options.key? key and luri = URI(options[key]) + if !luri.absolute? + raise ArgumentError, ":#{key} is not an absolute uri: '#{luri}'" + end + end + end + + if options[:return_to] and ruri = URI(options[:return_to]) + if ruri.path.empty? + raise ArgumentError, "Invalid return_to path: '#{ruri.path}'" + elsif realm.path != ruri.path[0, realm.path.size] + raise ArgumentError, 'return_to not within realm.' \ + end + end + + # TODO: extension support + if extensions = options.delete(:extensions) + extensions.each do |ext, args| + add_extension ext, *args + end + end + + @options = { + :session_key => 'rack.session', + :openid_param => 'openid_identifier', + #:return_to, :login_good, :login_fail, :login_quit + #:no_session, :auth_fail, :error + :store => OIDStore, + :immediate => false, + :anonymous => false, + :catch_errors => false + }.merge(options) + @extensions = {} + end + + attr_reader :options, :extensions + + # It sets up and uses session data at <tt>:openid</tt> within the + # session. It sets up the ::OpenID::Consumer using the store specified by + # <tt>options[:store]</tt>. + # + # If the parameter specified by <tt>options[:openid_param]</tt> is + # present, processing is passed to #check and the result is returned. + # + # If the parameter 'openid.mode' is set, implying a followup from the + # openid server, processing is passed to #finish and the result is + # returned. + # + # If neither of these conditions are met, a 400 error is returned. + # + # If an error is thrown and <tt>options[:catch_errors]</tt> is false, the + # exception will be reraised. Otherwise a 500 error is returned. + def call(env) + env['rack.auth.openid'] = self + session = env[@options[:session_key]] + unless session and session.is_a? Hash + raise(NoSession, 'No compatible session') + end + # let us work in our own namespace... + session = (session[:openid] ||= {}) + unless session and session.is_a? Hash + raise(NoSession, 'Incompatible session') + end + + request = Rack::Request.new env + consumer = ::OpenID::Consumer.new session, @options[:store] + + if request.params['openid.mode'] + finish consumer, session, request + elsif request.params[@options[:openid_param]] + check consumer, session, request + else + env['rack.errors'].puts "No valid params provided." + bad_request + end + rescue NoSession + env['rack.errors'].puts($!.message, *$@) + + @options. ### Missing or incompatible session + fetch :no_session, [ 500, + {'Content-Type'=>'text/plain'}, + $!.message ] + rescue + env['rack.errors'].puts($!.message, *$@) + + if not @options[:catch_error] + raise($!) + end + @options. + fetch :error, [ 500, + {'Content-Type'=>'text/plain'}, + 'OpenID has encountered an error.' ] + end + + # As the first part of OpenID consumer action, #check retrieves the data + # required for completion. + # + # * <tt>session[:openid][:openid_param]</tt> is set to the submitted + # identifier to be authenticated. + # * <tt>session[:openid][:site_return]</tt> is set as the request's + # HTTP_REFERER, unless already set. + # * <tt>env['rack.auth.openid.request']</tt> is the openid checkid + # request instance. + def check(consumer, session, req) + session[:openid_param] = req.params[@options[:openid_param]] + oid = consumer.begin(session[:openid_param], @options[:anonymous]) + pp oid if $DEBUG + req.env['rack.auth.openid.request'] = oid + + session[:site_return] ||= req.env['HTTP_REFERER'] + + # SETUP_NEEDED check! + # see OpenID::Consumer::CheckIDRequest docs + query_args = [@realm, *@options.values_at(:return_to, :immediate)] + query_args[1] ||= req.url + query_args[2] = false if session.key? :setup_needed + pp query_args if $DEBUG + + ## Extension support + extensions.each do |ext,args| + oid.add_extension ext::Request.new(*args) + end + + if oid.send_redirect?(*query_args) + redirect = oid.redirect_url(*query_args) + if $DEBUG + pp redirect + pp Rack::Utils.parse_query(URI(redirect).query) + end + [ 303, {'Location'=>redirect}, [] ] + else + # check on 'action' option. + formbody = oid.form_markup(*query_args) + if $DEBUG + pp formbody + end + body = HTML % ['Confirm...', formbody] + [ 200, {'Content-Type'=>'text/html'}, body.to_a ] + end + rescue ::OpenID::DiscoveryFailure => e + # thrown from inside OpenID::Consumer#begin by yadis stuff + req.env['rack.errors'].puts($!.message, *$@) + + @options. ### Foreign server failed + fetch :auth_fail, [ 503, + {'Content-Type'=>'text/plain'}, + 'Foreign server failure.' ] + end + + # This is the final portion of authentication. Unless any errors outside + # of specification occur, a 303 redirect will be returned with Location + # determined by the OpenID response type. If none of the response type + # :login_* urls are set, the redirect will be set to + # <tt>session[:openid][:site_return]</tt>. If + # <tt>session[:openid][:site_return]</tt> is unset, the realm will be + # used. + # + # Any messages from OpenID's response are appended to the 303 response + # body. + # + # Data gathered from extensions are stored in session[:openid] with the + # extension's namespace uri as the key. + # + # * <tt>env['rack.auth.openid.response']</tt> is the openid response. + # + # The four valid possible outcomes are: + # * failure: <tt>options[:login_fail]</tt> or + # <tt>session[:site_return]</tt> or the realm + # * <tt>session[:openid]</tt> is cleared and any messages are send to + # rack.errors + # * <tt>session[:openid]['authenticated']</tt> is <tt>false</tt> + # * success: <tt>options[:login_good]</tt> or + # <tt>session[:site_return]</tt> or the realm + # * <tt>session[:openid]</tt> is cleared + # * <tt>session[:openid]['authenticated']</tt> is <tt>true</tt> + # * <tt>session[:openid]['identity']</tt> is the actual identifier + # * <tt>session[:openid]['identifier']</tt> is the pretty identifier + # * cancel: <tt>options[:login_good]</tt> or + # <tt>session[:site_return]</tt> or the realm + # * <tt>session[:openid]</tt> is cleared + # * <tt>session[:openid]['authenticated']</tt> is <tt>false</tt> + # * setup_needed: resubmits the authentication request. A flag is set for + # non-immediate handling. + # * <tt>session[:openid][:setup_needed]</tt> is set to <tt>true</tt>, + # which will prevent immediate style openid authentication. + def finish(consumer, session, req) + oid = consumer.complete(req.params, req.url) + pp oid if $DEBUG + req.env['rack.auth.openid.response'] = oid + + goto = session.fetch :site_return, @realm + body = [] + + case oid.status + when ::OpenID::Consumer::FAILURE + session.clear + session['authenticated'] = false + req.env['rack.errors'].puts oid.message + + goto = @options[:login_fail] if @option.key? :login_fail + body << "Authentication unsuccessful.\n" + when ::OpenID::Consumer::SUCCESS + session.clear + + ## Extension support + extensions.each do |ext, args| + session[ext::NS_URI] = ext::Response. + from_success_response(oid). + get_extension_args + end + + session['authenticated'] = true + # Value for unique identification and such + session['identity'] = oid.identity_url + # Value for display and UI labels + session['identifier'] = oid.display_identifier + + goto = @options[:login_good] if @options.key? :login_good + body << "Authentication successful.\n" + when ::OpenID::Consumer::CANCEL + session.clear + session['authenticated'] = false + + goto = @options[:login_fail] if @option.key? :login_fail + body << "Authentication cancelled.\n" + when ::OpenID::Consumer::SETUP_NEEDED + session[:setup_needed] = true + unless o_id = session[:openid_param] + raise('Required values missing.') + end + + goto = req.script_name+ + '?'+@options[:openid_param]+ + '='+o_id + body << "Reauthentication required.\n" + end + body << oid.message if oid.message + [ 303, {'Location'=>goto}, body] + end + + # The first argument should be the main extension module. + # The extension module should contain the constants: + # * class Request, with OpenID::Extension as an ancestor + # * class Response, with OpenID::Extension as an ancestor + # * string NS_URI, which defines the namespace of the extension, should + # be an absolute http uri + # + # All trailing arguments will be passed to extension::Request.new in + # #check. + # The openid response will be passed to + # extension::Response#from_success_response, #get_extension_args will be + # called on the result to attain the gathered data. + # + # This method returns the key at which the response data will be found in + # the session, which is the namespace uri by default. + def add_extension ext, *args + if not ext.is_a? Module + raise TypeError, "#{ext.inspect} is not a module" + elsif not (m = %w'Request Response NS_URI' - ext.constants).empty? + raise ArgumentError, "#{ext.inspect} missing #{m*', '}" + end + + consts = [ext::Request, ext::Response] + + if not consts.all?{|c| c.is_a? Class } + raise TypeError, "#{ext.inspect}'s Request or Response is not a class" + elsif not consts.all?{|c| ::OpenID::Extension > c } + raise ArgumentError, "#{ext.inspect}'s Request or Response not a decendant of OpenID::Extension" + end + + if not ext::NS_URI.is_a? String + raise TypeError, "#{ext.inspect}'s NS_URI is not a string" + elsif not uri = URI(ext::NS_URI) + raise ArgumentError, "#{ext.inspect}'s NS_URI is not a valid uri" + elsif not uri.scheme =~ /^https?$/ + raise ArgumentError, "#{ext.inspect}'s NS_URI is not an http uri" + elsif not uri.absolute? + raise ArgumentError, "#{ext.inspect}'s NS_URI is not and absolute uri" + end + @extensions[ext] = args + return ext::NS_URI + end + + # A conveniance method that returns the namespace of all current + # extensions used by this instance. + def extension_namespaces + @extensions.keys.map{|e|e::NS_URI} + end + end + end +end |