diff options
author | Christian Neukirchen <chneukirchen@gmail.com> | 2009-08-12 00:36:25 +0200 |
---|---|---|
committer | Christian Neukirchen <chneukirchen@gmail.com> | 2009-08-12 00:36:25 +0200 |
commit | 503130e322cb9390cba028a0ef58c1e24848ff21 (patch) | |
tree | a33662299014936ac107c656e0097b451bde1929 | |
parent | 7b6046b764eafd332b3b2d9d93b3915c425fae54 (diff) | |
parent | ca6f9f9f9232d5f27b5161135b1bd668c1dc9790 (diff) | |
download | rack-503130e322cb9390cba028a0ef58c1e24848ff21.tar.gz |
Merge commit 'official/master'
-rw-r--r-- | lib/rack/auth/openid.rb | 239 | ||||
-rw-r--r-- | lib/rack/deflater.rb | 2 | ||||
-rw-r--r-- | lib/rack/handler/lsws.rb | 21 | ||||
-rw-r--r-- | lib/rack/handler/mongrel.rb | 5 | ||||
-rw-r--r-- | lib/rack/handler/scgi.rb | 7 | ||||
-rw-r--r-- | lib/rack/handler/webrick.rb | 6 | ||||
-rw-r--r-- | lib/rack/lint.rb | 11 | ||||
-rw-r--r-- | lib/rack/mime.rb | 3 | ||||
-rw-r--r-- | lib/rack/mock.rb | 11 | ||||
-rw-r--r-- | lib/rack/reloader.rb | 9 | ||||
-rw-r--r-- | lib/rack/request.rb | 2 | ||||
-rw-r--r-- | lib/rack/response.rb | 38 | ||||
-rw-r--r-- | lib/rack/rewindable_input.rb | 2 | ||||
-rw-r--r-- | lib/rack/session/abstract/id.rb | 8 | ||||
-rw-r--r-- | lib/rack/session/cookie.rb | 7 | ||||
-rw-r--r-- | lib/rack/utils.rb | 80 | ||||
-rw-r--r-- | test/spec_rack_commonlogger.rb | 12 | ||||
-rw-r--r-- | test/spec_rack_lint.rb | 36 | ||||
-rw-r--r-- | test/spec_rack_request.rb | 6 | ||||
-rw-r--r-- | test/spec_rack_utils.rb | 40 |
20 files changed, 351 insertions, 194 deletions
diff --git a/lib/rack/auth/openid.rb b/lib/rack/auth/openid.rb index c5f6a514..43cbe4f9 100644 --- a/lib/rack/auth/openid.rb +++ b/lib/rack/auth/openid.rb @@ -1,13 +1,14 @@ -# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net +# AUTHOR: Scytrin dai Kinthra <scytrin@gmail.com>; blink#ruby-lang@irc.freenode.net gem 'ruby-openid', '~> 2' if defined? Gem require 'rack/request' require 'rack/utils' require 'rack/auth/abstract/handler' + require 'uri' -require 'openid' #gem -require 'openid/extension' #gem -require 'openid/store/memory' #gem +require 'openid' +require 'openid/extension' +require 'openid/store/memory' module Rack class Request @@ -45,108 +46,111 @@ module Rack # # NOTE: Due to the amount of data that this library stores in the # session, Rack::Session::Cookie may fault. + # + # == Examples + # + # simple_oid = OpenID.new('http://mysite.com/') + # + # return_oid = OpenID.new('http://mysite.com/', { + # :return_to => 'http://mysite.com/openid' + # }) + # + # complex_oid = OpenID.new('http://mysite.com/', + # :immediate => true, + # :extensions => { + # ::OpenID::SReg => [['email'],['nickname']] + # } + # ) + # + # = Advanced + # + # Most of the functionality of this library is encapsulated such that + # expansion and overriding functions isn't difficult nor tricky. + # Alternately, to avoid opening up singleton objects or subclassing, a + # wrapper rack middleware can be composed to act upon Auth::OpenID's + # responses. See #check and #finish for locations of pertinent data. + # + # == Responses + # + # To change the responses that Auth::OpenID returns, override the methods + # #redirect, #bad_request, #unauthorized, #access_denied, and + # #foreign_server_failure. + # + # Additionally #confirm_post_params is used when the URI would exceed + # length limits on a GET request when doing the initial verification + # request. + # + # == Processing + # + # To change methods of processing completed transactions, override the + # methods #success, #setup_needed, #cancel, and #failure. Please ensure + # the returned object is a rack compatible response. + # + # The first argument is an OpenID::Response, the second is a + # Rack::Request of the current request, the last is the hash used in + # ruby-openid handling, which can be found manually at + # env['rack.session'][:openid]. + # + # This is useful if you wanted to expand the processing done, such as + # setting up user accounts. + # + # oid_app = Rack::Auth::OpenID.new realm, :return_to => return_to + # def oid_app.success oid, request, session + # user = Models::User[oid.identity_url] + # user ||= Models::User.create_from_openid oid + # request['rack.session'][:user] = user.id + # redirect MyApp.site_home + # end + # + # site_map['/openid'] = oid_app + # map = Rack::URLMap.new site_map + # ... class OpenID - + # Raised if an incompatible session is being used. class NoSession < RuntimeError; end + # Raised if an extension not matching specifications is provided. class BadExtension < RuntimeError; end - # Required for ruby-openid - ValidStatus = [:success, :setup_needed, :cancel, :failure] + # Possible statuses returned from consumer responses. See definitions + # in the ruby-openid library. + ValidStatus = [ + ::OpenID::Consumer::SUCCESS, + ::OpenID::Consumer::FAILURE, + ::OpenID::Consumer::CANCEL, + ::OpenID::Consumer::SETUP_NEEDED + ] - # = Arguments - # # The first argument is the realm, identifying the site they are trusting # with their identity. This is required, also treated as the trust_root # in OpenID 1.x exchanges. # - # The optional second argument is a hash of options. - # - # == Options + # The lits of acceptable options include :return_to, :session_key, + # :openid_param, :store, :immediate, :extensions. # # <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 which allows flexibility - # with caveats. + # to where Rack::Auth::OpenID is mounted. If unprovided, the url of + # the current request is used. # # <tt>:session_key</tt> defines the key to the session hash in the env. - # It defaults to 'rack.session'. + # The default is '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>:store</tt> defined what OpenID Store to use for persistant - # information. By default a Store::Memory will be used. + # information. By default a Store::Memory is used. # # <tt>:immediate</tt> as true will make initial requests to be of an # immediate type. This is false by default. See OpenID specification # documentation. # # <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. + # implementations. The key should be the extension 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. - # - # == Examples - # - # simple_oid = OpenID.new('http://mysite.com/') - # - # return_oid = OpenID.new('http://mysite.com/', { - # :return_to => 'http://mysite.com/openid' - # }) - # - # complex_oid = OpenID.new('http://mysite.com/', - # :immediate => true, - # :extensions => { - # ::OpenID::SReg => [['email'],['nickname']] - # } - # ) - # - # = Advanced - # - # Most of the functionality of this library is encapsulated such that - # expansion and overriding functions isn't difficult nor tricky. - # Alternately, to avoid opening up singleton objects or subclassing, a - # wrapper rack middleware can be composed to act upon Auth::OpenID's - # responses. See #check and #finish for locations of pertinent data. - # - # == Responses - # - # To change the responses that Auth::OpenID returns, override the methods - # #redirect, #bad_request, #unauthorized, #access_denied, and - # #foreign_server_failure. - # - # Additionally #confirm_post_params is used when the URI would exceed - # length limits on a GET request when doing the initial verification - # request. - # - # == Processing - # - # To change methods of processing completed transactions, override the - # methods #success, #setup_needed, #cancel, and #failure. Please ensure - # the returned object is a rack compatible response. - # - # The first argument is an OpenID::Response, the second is a - # Rack::Request of the current request, the last is the hash used in - # ruby-openid handling, which can be found manually at - # env['rack.session'][:openid]. - # - # This is useful if you wanted to expand the processing done, such as - # setting up user accounts. - # - # oid_app = Rack::Auth::OpenID.new realm, :return_to => return_to - # def oid_app.success oid, request, session - # user = Models::User[oid.identity_url] - # user ||= Models::User.create_from_openid oid - # request['rack.session'][:user] = user.id - # redirect MyApp.site_home - # end - # - # site_map['/openid'] = oid_app - # map = Rack::URLMap.new site_map - # ... def initialize(realm, options={}) realm = URI(realm) @@ -162,7 +166,7 @@ module Rack ruri = URI(ruri) raise ArgumentError, "Invalid return_to: #{ruri}" \ unless ruri.absolute? \ - and ruri.scheme =~ /^https?$/ \ + and ruri.scheme =~ /^https?$/ \ and ruri.fragment.nil? raise ArgumentError, "return_to #{ruri} not within realm #{realm}" \ unless self.within_realm?(ruri) @@ -174,10 +178,10 @@ module Rack @store = options[:store] || ::OpenID::Store::Memory.new @immediate = !!options[:immediate] - @extensions = {} - if extensions = options.delete(:extensions) + @extensions = {} + if extensions = options[:extensions] extensions.each do |ext, args| - add_extension ext, *args + add_extension(ext, *args) end end @@ -199,33 +203,29 @@ module Rack # If the parameter specified by <tt>options[:openid_param]</tt> is # present, processing is passed to #check and the result is returned. # - # If neither of these conditions are met, #unauthorized is called. + # If neither of these conditions are met, #bad_request is called. def call(env) env['rack.auth.openid'] = self env_session = env[@session_key] unless env_session and env_session.is_a?(Hash) - raise NoSession, 'No compatible session' + raise NoSession, 'No compatible session.' end # let us work in our own namespace... session = (env_session[:openid] ||= {}) unless session and session.is_a?(Hash) - raise NoSession, 'Incompatible openid session' + raise NoSession, 'Incompatible openid session.' end request = Rack::Request.new(env) consumer = ::OpenID::Consumer.new(session, @store) if mode = request.GET['openid.mode'] - if session.key?(:openid_param) - finish(consumer, session, request) - else - bad_request - end + finish(consumer, session, request) elsif request.GET[@openid_param] check(consumer, session, request) else - unauthorized + bad_request end end @@ -263,14 +263,13 @@ module Rack immediate = session.key?(:setup_needed) ? false : immediate if oid.send_redirect?(realm, return_to_uri, immediate) - uri = oid.redirect_url(realm, return_to_uri, immediate) - redirect(uri) + redirect(oid.redirect_url(realm, return_to_uri, immediate)) else confirm_post_params(oid, realm, return_to_uri, immediate) end rescue ::OpenID::DiscoveryFailure => e # thrown from inside OpenID::Consumer#begin by yadis stuff - req.env['rack.errors'].puts([e.message, *e.backtrace]*"\n") + req.env['rack.errors'].puts( [e.message, *e.backtrace]*"\n" ) return foreign_server_failure end @@ -290,21 +289,24 @@ module Rack req.env['rack.errors'].puts(oid.message) p oid if $DEBUG - raise unless ValidStatus.include?(oid.status) - __send__(oid.status, oid, req, session) + if ValidStatus.include?(oid.status) + __send__(oid.status, oid, req, session) + else + invalid_status(oid, req, session) + end end # The first argument should be the main extension module. # The extension module should contain the constants: - # * class Request, should have OpenID::Extension as an ancestor - # * class Response, should have OpenID::Extension as an ancestor - # * string NS_URI, which defining the namespace of the extension + # * class Request, should have OpenID::Extension as an ancestor + # * class Response, should have OpenID::Extension as an ancestor + # * string NS_URI, which defining the namespace of the extension # # 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. + # extension::Response#from_success_response, oid#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. @@ -344,28 +346,27 @@ module Rack return false unless uri.host.match(realm_match) return true end + alias_method :include?, :within_realm? protected - ### These methods define some of the boilerplate responses. - # Returns an html form page for posting to an Identity Provider if the # GET request would exceed the upper URI length limit. def confirm_post_params(oid, realm, return_to, immediate) - Rack::Response.new.finish do |r| - r.write '<html><head><title>Confirm...</title></head><body>' - r.write oid.form_markup(realm, return_to, immediate) - r.write '</body></html>' - end + response = Rack::Response.new '<html>'+ + '<head><title>Confirm...</title></head>'+ + '<body>'+oid.form_markup(realm, return_to, immediate)+'</body>'+ + '</html>' + response.finish end # Returns a 303 redirect with the destination of that provided by the # argument. def redirect(uri) - [ 303, {'Content-Length'=>'0', 'Content-Type'=>'text/plain', + [ 303, {'Content-Type'=>'text/plain', 'Content-Length'=>'0', 'Location' => uri}, [] ] end @@ -401,10 +402,6 @@ module Rack private - ### These methods are called after a transaction is completed, depending - # on its outcome. These should all return a rack compatible response. - # You'd want to override these to provide additional functionality. - # Called to complete processing on a successful transaction. # Within the openid session, :openid_identity and :openid_identifier are # set to the user friendly and the standard representation of the @@ -430,7 +427,7 @@ module Rack def setup_needed(oid, request, session) identifier = session[:openid_param] session[:setup_needed] = true - redirect req.script_name + '?' + openid_param + '=' + identifier + redirect(req.script_name + '?' + openid_param + '=' + identifier) end # Called if the user indicates they wish to cancel identification. @@ -448,6 +445,16 @@ module Rack def failure(oid, request, session) unauthorized end + + # To be called if there is no method for handling the OpenID response + # status. + + def invalid_status(oid, request, session) + msg = 'Invalid status returned by the OpenID authorization reponse.' + [ 500, + {'Content-Type'=>'text/plain','Content-Length'=>msg.length.to_s}, + [msg] ] + end end # A class developed out of the request to use OpenID as an authentication @@ -472,8 +479,8 @@ module Rack end def call(env) - to = auth.call(env) ? @app : @oid - to.call env + to = @authenticator.call(env) ? @app : @oid + to.call(env) end end end diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb index 14137a94..ad0f5316 100644 --- a/lib/rack/deflater.rb +++ b/lib/rack/deflater.rb @@ -60,7 +60,7 @@ module Rack @writer = block gzip =::Zlib::GzipWriter.new(self) gzip.mtime = @mtime - @body.each { |part| gzip << part } + @body.each { |part| gzip.write(part) } @body.close if @body.respond_to?(:close) gzip.close @writer = nil diff --git a/lib/rack/handler/lsws.rb b/lib/rack/handler/lsws.rb index 7231336d..b4ddf4bb 100644 --- a/lib/rack/handler/lsws.rb +++ b/lib/rack/handler/lsws.rb @@ -15,14 +15,19 @@ module Rack env = ENV.to_hash env.delete "HTTP_CONTENT_LENGTH" env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" - env.update({"rack.version" => [1,0], - "rack.input" => StringIO.new($stdin.read.to_s), - "rack.errors" => $stderr, - "rack.multithread" => false, - "rack.multiprocess" => true, - "rack.run_once" => false, - "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http" - }) + + rack_input = RewindableInput.new($stdin.read.to_s) + + env.update( + "rack.version" => [1,0], + "rack.input" => rack_input, + "rack.errors" => $stderr, + "rack.multithread" => false, + "rack.multiprocess" => true, + "rack.run_once" => false, + "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http" + ) + env["QUERY_STRING"] ||= "" env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"] env["REQUEST_PATH"] ||= "/" diff --git a/lib/rack/handler/mongrel.rb b/lib/rack/handler/mongrel.rb index 3a5ef32d..7b448261 100644 --- a/lib/rack/handler/mongrel.rb +++ b/lib/rack/handler/mongrel.rb @@ -45,8 +45,11 @@ module Rack env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" + rack_input = request.body || StringIO.new('') + rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding) + env.update({"rack.version" => [1,0], - "rack.input" => request.body || StringIO.new(""), + "rack.input" => rack_input, "rack.errors" => $stderr, "rack.multithread" => true, diff --git a/lib/rack/handler/scgi.rb b/lib/rack/handler/scgi.rb index 6c4932df..bd860a5d 100644 --- a/lib/rack/handler/scgi.rb +++ b/lib/rack/handler/scgi.rb @@ -32,10 +32,13 @@ module Rack env["PATH_INFO"] = env["REQUEST_PATH"] env["QUERY_STRING"] ||= "" env["SCRIPT_NAME"] = "" + + rack_input = StringIO.new(input_body) + rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding) + env.update({"rack.version" => [1,0], - "rack.input" => StringIO.new(input_body), + "rack.input" => rack_input, "rack.errors" => $stderr, - "rack.multithread" => true, "rack.multiprocess" => true, "rack.run_once" => false, diff --git a/lib/rack/handler/webrick.rb b/lib/rack/handler/webrick.rb index 2bdc83a9..5b9ae740 100644 --- a/lib/rack/handler/webrick.rb +++ b/lib/rack/handler/webrick.rb @@ -6,6 +6,7 @@ module Rack module Handler class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet def self.run(app, options={}) + options[:BindAddress] = options.delete(:Host) if options[:Host] server = ::WEBrick::HTTPServer.new(options) server.mount "/", Rack::Handler::WEBrick, app trap(:INT) { server.shutdown } @@ -22,8 +23,11 @@ module Rack env = req.meta_vars env.delete_if { |k, v| v.nil? } + rack_input = StringIO.new(req.body.to_s) + rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding) + env.update({"rack.version" => [1,0], - "rack.input" => StringIO.new(req.body.to_s), + "rack.input" => rack_input, "rack.errors" => $stderr, "rack.multithread" => true, diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb index bf2e9787..796807a0 100644 --- a/lib/rack/lint.rb +++ b/lib/rack/lint.rb @@ -233,8 +233,17 @@ module Rack ## === The Input Stream ## ## The input stream is an IO-like object which contains the raw HTTP - ## POST data. If it is a file then it must be opened in binary mode. + ## POST data. def check_input(input) + ## When applicable, its external encoding must be "ASCII-8BIT" and it + ## must be opened in binary mode, for Ruby 1.9 compatibility. + assert("rack.input #{input} does not have ASCII-8BIT as its external encoding") { + input.external_encoding.name == "ASCII-8BIT" + } if input.respond_to?(:external_encoding) + assert("rack.input #{input} is not opened in binary mode") { + input.binmode? + } if input.respond_to?(:binmode?) + ## The input stream must respond to +gets+, +each+, +read+ and +rewind+. [:gets, :each, :read, :rewind].each { |method| assert("rack.input #{input} does not respond to ##{method}") { diff --git a/lib/rack/mime.rb b/lib/rack/mime.rb index 5a6a73a9..853f16bd 100644 --- a/lib/rack/mime.rb +++ b/lib/rack/mime.rb @@ -14,7 +14,7 @@ module Rack # Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream') def mime_type(ext, fallback='application/octet-stream') - MIME_TYPES.fetch(ext, fallback) + MIME_TYPES.fetch(ext.to_s.downcase, fallback) end module_function :mime_type @@ -126,6 +126,7 @@ module Rack ".ods" => "application/vnd.oasis.opendocument.spreadsheet", ".odt" => "application/vnd.oasis.opendocument.text", ".ogg" => "application/ogg", + ".ogv" => "video/ogg", ".p" => "text/x-pascal", ".pas" => "text/x-pascal", ".pbm" => "image/x-portable-bitmap", diff --git a/lib/rack/mock.rb b/lib/rack/mock.rb index fdefb034..7964c447 100644 --- a/lib/rack/mock.rb +++ b/lib/rack/mock.rb @@ -114,13 +114,18 @@ module Rack end end - opts[:input] ||= "" + empty_str = "" + empty_str.force_encoding("ASCII-8BIT") if empty_str.respond_to? :force_encoding + opts[:input] ||= empty_str if String === opts[:input] - env["rack.input"] = StringIO.new(opts[:input]) + rack_input = StringIO.new(opts[:input]) else - env["rack.input"] = opts[:input] + rack_input = opts[:input] end + rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding) + env['rack.input'] = rack_input + env["CONTENT_LENGTH"] ||= env["rack.input"].length.to_s opts.each { |field, value| diff --git a/lib/rack/reloader.rb b/lib/rack/reloader.rb index aa2f060b..a06de23a 100644 --- a/lib/rack/reloader.rb +++ b/lib/rack/reloader.rb @@ -1,5 +1,6 @@ # Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com -# All files in this distribution are subject to the terms of the Ruby license. +# Rack::Reloader is subject to the terms of an MIT-style license. +# See COPYING or http://www.opensource.org/licenses/mit-license.php. require 'pathname' @@ -70,7 +71,7 @@ module Rack next if file =~ /\.(so|bundle)$/ # cannot reload compiled files found, stat = figure_path(file, paths) - next unless found and stat and mtime = stat.mtime + next unless found && stat && mtime = stat.mtime @cache[file] = found @@ -87,11 +88,13 @@ module Rack found, stat = safe_stat(found) return found, stat if found - paths.each do |possible_path| + paths.find do |possible_path| path = ::File.join(possible_path, file) found, stat = safe_stat(path) return ::File.expand_path(found), stat if found end + + return false, false end def safe_stat(file) diff --git a/lib/rack/request.rb b/lib/rack/request.rb index 4c4cf61a..248ce18d 100644 --- a/lib/rack/request.rb +++ b/lib/rack/request.rb @@ -65,7 +65,7 @@ module Rack def host # Remove port number. - (@env["HTTP_HOST"] || @env["SERVER_NAME"]).gsub(/:\d+\z/, '') + (@env["HTTP_HOST"] || @env["SERVER_NAME"]).to_s.gsub(/:\d+\z/, '') end def script_name=(s); @env["SCRIPT_NAME"] = s.to_s end diff --git a/lib/rack/response.rb b/lib/rack/response.rb index 28b4d830..d1f6a123 100644 --- a/lib/rack/response.rb +++ b/lib/rack/response.rb @@ -54,45 +54,11 @@ module Rack end def set_cookie(key, value) - case value - when Hash - domain = "; domain=" + value[:domain] if value[:domain] - path = "; path=" + value[:path] if value[:path] - # According to RFC 2109, we need dashes here. - # N.B.: cgi.rb uses spaces... - expires = "; expires=" + value[:expires].clone.gmtime. - strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires] - secure = "; secure" if value[:secure] - httponly = "; HttpOnly" if value[:httponly] - value = value[:value] - end - value = [value] unless Array === value - cookie = Utils.escape(key) + "=" + - value.map { |v| Utils.escape v }.join("&") + - "#{domain}#{path}#{expires}#{secure}#{httponly}" - - case self["Set-Cookie"] - when Array - self["Set-Cookie"] << cookie - when String - self["Set-Cookie"] = [self["Set-Cookie"], cookie] - when nil - self["Set-Cookie"] = cookie - end + Utils.set_cookie_header!(header, key, value) end def delete_cookie(key, value={}) - unless Array === self["Set-Cookie"] - self["Set-Cookie"] = [self["Set-Cookie"]].compact - end - - self["Set-Cookie"].reject! { |cookie| - cookie =~ /\A#{Utils.escape(key)}=/ - } - - set_cookie(key, - {:value => '', :path => nil, :domain => nil, - :expires => Time.at(0) }.merge(value)) + Utils.delete_cookie_header!(header, key, value) end def redirect(target, status=302) diff --git a/lib/rack/rewindable_input.rb b/lib/rack/rewindable_input.rb index 9e9b21ff..accd96be 100644 --- a/lib/rack/rewindable_input.rb +++ b/lib/rack/rewindable_input.rb @@ -72,6 +72,8 @@ module Rack # access it because we have the file handle open. @rewindable_io = Tempfile.new('RackRewindableInput') @rewindable_io.chmod(0000) + @rewindable_io.set_encoding(Encoding::BINARY) if @rewindable_io.respond_to?(:set_encoding) + @rewindable_io.binmode if filesystem_has_posix_semantics? @rewindable_io.unlink @unlinked = true diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb index 218144c1..98746705 100644 --- a/lib/rack/session/abstract/id.rb +++ b/lib/rack/session/abstract/id.rb @@ -107,18 +107,16 @@ module Rack if not session_id = set_session(env, session_id, session, options) env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.") - [status, headers, body] elsif options[:defer] and not options[:renew] env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE - [status, headers, body] else cookie = Hash.new cookie[:value] = session_id cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil? - response = Rack::Response.new(body, status, headers) - response.set_cookie(@key, cookie.merge(options)) - response.to_a + Utils.set_cookie_header!(headers, @key, cookie.merge(options)) end + + [status, headers, body] end # All thread safety and session retrival proceedures should occur here. diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb index eace9bd0..240e6c8d 100644 --- a/lib/rack/session/cookie.rb +++ b/lib/rack/session/cookie.rb @@ -70,16 +70,15 @@ module Rack if session_data.size > (4096 - @key.size) env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K. Content dropped.") - [status, headers, body] else options = env["rack.session.options"] cookie = Hash.new cookie[:value] = session_data cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil? - response = Rack::Response.new(body, status, headers) - response.set_cookie(@key, cookie.merge(options)) - response.to_a + Utils.set_cookie_header!(headers, @key, cookie.merge(options)) end + + [status, headers, body] end def generate_hmac(data) diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb index 228488c1..74303eff 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -13,7 +13,7 @@ module Rack # version since it's faster. (Stolen from Camping). def escape(s) s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) { - '%'+$1.unpack('H2'*$1.size).join('%').upcase + '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase }.tr(' ', '+') end module_function :escape @@ -168,6 +168,54 @@ module Rack end module_function :select_best_encoding + def set_cookie_header!(header, key, value) + case value + when Hash + domain = "; domain=" + value[:domain] if value[:domain] + path = "; path=" + value[:path] if value[:path] + # According to RFC 2109, we need dashes here. + # N.B.: cgi.rb uses spaces... + expires = "; expires=" + value[:expires].clone.gmtime. + strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires] + secure = "; secure" if value[:secure] + httponly = "; HttpOnly" if value[:httponly] + value = value[:value] + end + value = [value] unless Array === value + cookie = escape(key) + "=" + + value.map { |v| escape v }.join("&") + + "#{domain}#{path}#{expires}#{secure}#{httponly}" + + case header["Set-Cookie"] + when Array + header["Set-Cookie"] << cookie + when String + header["Set-Cookie"] = [header["Set-Cookie"], cookie] + when nil + header["Set-Cookie"] = cookie + end + + nil + end + module_function :set_cookie_header! + + def delete_cookie_header!(header, key, value = {}) + unless Array === header["Set-Cookie"] + header["Set-Cookie"] = [header["Set-Cookie"]].compact + end + + header["Set-Cookie"].reject! { |cookie| + cookie =~ /\A#{escape(key)}=/ + } + + set_cookie_header!(header, key, + {:value => '', :path => nil, :domain => nil, + :expires => Time.at(0) }.merge(value)) + + nil + end + module_function :delete_cookie_header! + # Return the bytesize of String; uses String#length under Ruby 1.8 and # String#bytesize under 1.9. if ''.respond_to?(:bytesize) @@ -211,6 +259,7 @@ module Rack # header when set. class HeaderHash < Hash def initialize(hash={}) + super() @names = {} hash.each { |k, v| self[k] = v } end @@ -238,8 +287,9 @@ module Rack def delete(k) canonical = k.downcase - super @names.delete(canonical) + result = super @names.delete(canonical) @names.delete_if { |name,| name.downcase == canonical } + result end def include?(k) @@ -259,13 +309,23 @@ module Rack hash = dup hash.merge! other end + + def replace(other) + clear + other.each { |k, v| self[k] = v } + self + end end # Every standard HTTP code mapped to the appropriate message. - # Stolen from Mongrel. + # Generated with: + # curl -s http://www.iana.org/assignments/http-status-codes | \ + # ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and + # puts " #{m[1]} => \x27#{m[2].strip}x27,"' HTTP_STATUS_CODES = { 100 => 'Continue', 101 => 'Switching Protocols', + 102 => 'Processing', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', @@ -273,12 +333,15 @@ module Rack 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', + 207 => 'Multi-Status', + 226 => 'IM Used', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', + 306 => 'Reserved', 307 => 'Temporary Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', @@ -294,16 +357,23 @@ module Rack 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Large', + 414 => 'Request-URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', 417 => 'Expectation Failed', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 426 => 'Upgrade Required', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', - 505 => 'HTTP Version Not Supported' + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 510 => 'Not Extended', } # Responses with HTTP status codes that should not have an entity body diff --git a/test/spec_rack_commonlogger.rb b/test/spec_rack_commonlogger.rb index 9d212d5e..46a72e86 100644 --- a/test/spec_rack_commonlogger.rb +++ b/test/spec_rack_commonlogger.rb @@ -46,4 +46,16 @@ context "Rack::CommonLogger" do res.errors.should.not.be.empty res.errors.should =~ /"GET \/ " 200 - / end + + def length + self.class.length + end + + def self.length + 123 + end + + def self.obj + "hello world" + end end diff --git a/test/spec_rack_lint.rb b/test/spec_rack_lint.rb index 8c6419dd..9c5d5031 100644 --- a/test/spec_rack_lint.rb +++ b/test/spec_rack_lint.rb @@ -110,6 +110,28 @@ context "Rack::Lint" do Rack::Lint.new(nil).call(env("rack.input" => "")) }.should.raise(Rack::Lint::LintError). message.should.match(/does not respond to #gets/) + + lambda { + input = Object.new + def input.binmode? + false + end + Rack::Lint.new(nil).call(env("rack.input" => input)) + }.should.raise(Rack::Lint::LintError). + message.should.match(/is not opened in binary mode/) + + lambda { + input = Object.new + def input.external_encoding + result = Object.new + def result.name + "US-ASCII" + end + result + end + Rack::Lint.new(nil).call(env("rack.input" => input)) + }.should.raise(Rack::Lint::LintError). + message.should.match(/does not have ASCII-8BIT as its external encoding/) end specify "notices error errors" do @@ -432,46 +454,48 @@ context "Rack::Lint" do end specify "passes valid read calls" do + hello_str = "hello world" + hello_str.force_encoding("ASCII-8BIT") if hello_str.respond_to? :force_encoding lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] - }).call(env({"rack.input" => StringIO.new("hello world")})) + }).call(env({"rack.input" => StringIO.new(hello_str)})) }.should.not.raise(Rack::Lint::LintError) lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read(0) [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] - }).call(env({"rack.input" => StringIO.new("hello world")})) + }).call(env({"rack.input" => StringIO.new(hello_str)})) }.should.not.raise(Rack::Lint::LintError) lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read(1) [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] - }).call(env({"rack.input" => StringIO.new("hello world")})) + }).call(env({"rack.input" => StringIO.new(hello_str)})) }.should.not.raise(Rack::Lint::LintError) lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read(nil) [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] - }).call(env({"rack.input" => StringIO.new("hello world")})) + }).call(env({"rack.input" => StringIO.new(hello_str)})) }.should.not.raise(Rack::Lint::LintError) lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read(nil, '') [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] - }).call(env({"rack.input" => StringIO.new("hello world")})) + }).call(env({"rack.input" => StringIO.new(hello_str)})) }.should.not.raise(Rack::Lint::LintError) lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read(1, '') [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] - }).call(env({"rack.input" => StringIO.new("hello world")})) + }).call(env({"rack.input" => StringIO.new(hello_str)})) }.should.not.raise(Rack::Lint::LintError) end end diff --git a/test/spec_rack_request.rb b/test/spec_rack_request.rb index 81f05eba..fe000c24 100644 --- a/test/spec_rack_request.rb +++ b/test/spec_rack_request.rb @@ -37,6 +37,11 @@ context "Rack::Request" do req = Rack::Request.new \ Rack::MockRequest.env_for("/", "SERVER_NAME" => "example.org:9292") req.host.should.equal "example.org" + + env = Rack::MockRequest.env_for("/") + env.delete("SERVER_NAME") + req = Rack::Request.new(env) + req.host.should.equal "" end specify "can parse the query string" do @@ -424,6 +429,7 @@ Content-Transfer-Encoding: base64\r /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r --AaB03x--\r EOF + input.force_encoding("ASCII-8BIT") if input.respond_to? :force_encoding res = Rack::MockRequest.new(Rack::Lint.new(app)).get "/", "CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x", "CONTENT_LENGTH" => input.size.to_s, "rack.input" => StringIO.new(input) diff --git a/test/spec_rack_utils.rb b/test/spec_rack_utils.rb index 077ccc50..c3974299 100644 --- a/test/spec_rack_utils.rb +++ b/test/spec_rack_utils.rb @@ -12,6 +12,15 @@ context "Rack::Utils" do should.equal "q1%212%22%27w%245%267%2Fz8%29%3F%5C" end + specify "should escape correctly for multibyte characters" do + matz_name = "\xE3\x81\xBE\xE3\x81\xA4\xE3\x82\x82\xE3\x81\xA8".unpack("a*")[0] # Matsumoto + matz_name.force_encoding("UTF-8") if matz_name.respond_to? :force_encoding + Rack::Utils.escape(matz_name).should.equal '%E3%81%BE%E3%81%A4%E3%82%82%E3%81%A8' + matz_name_sep = "\xE3\x81\xBE\xE3\x81\xA4 \xE3\x82\x82\xE3\x81\xA8".unpack("a*")[0] # Matsu moto + matz_name_sep.force_encoding("UTF-8") if matz_name_sep.respond_to? :force_encoding + Rack::Utils.escape(matz_name_sep).should.equal '%E3%81%BE%E3%81%A4+%E3%82%82%E3%81%A8' + end + specify "should unescape correctly" do Rack::Utils.unescape("fo%3Co%3Ebar").should.equal "fo<o>bar" Rack::Utils.unescape("a+space").should.equal "a space" @@ -228,6 +237,37 @@ context "Rack::Utils::HeaderHash" do h = Rack::Utils::HeaderHash.new("foo" => ["bar", "baz"]) h.to_hash.should.equal({ "foo" => "bar\nbaz" }) end + + specify "should replace hashes correctly" do + h = Rack::Utils::HeaderHash.new("Foo-Bar" => "baz") + j = {"foo" => "bar"} + h.replace(j) + h["foo"].should.equal "bar" + end + + specify "should be able to delete the given key case-sensitively" do + h = Rack::Utils::HeaderHash.new("foo" => "bar") + h.delete("foo") + h["foo"].should.be.nil + h["FOO"].should.be.nil + end + + specify "should be able to delete the given key case-insensitively" do + h = Rack::Utils::HeaderHash.new("foo" => "bar") + h.delete("FOO") + h["foo"].should.be.nil + h["FOO"].should.be.nil + end + + specify "should return the deleted value when #delete is called on an existing key" do + h = Rack::Utils::HeaderHash.new("foo" => "bar") + h.delete("Foo").should.equal("bar") + end + + specify "should return nil when #delete is called on a non-existant key" do + h = Rack::Utils::HeaderHash.new("foo" => "bar") + h.delete("Hello").should.be.nil + end end context "Rack::Utils::Context" do |