summary refs log tree commit
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml8
-rw-r--r--.yardopts2
-rw-r--r--Rakefile4
-rw-r--r--SPEC2
-rw-r--r--lib/rack/auth/abstract/request.rb2
-rw-r--r--lib/rack/auth/basic.rb2
-rw-r--r--lib/rack/builder.rb21
-rw-r--r--lib/rack/chunked.rb13
-rw-r--r--lib/rack/commonlogger.rb16
-rw-r--r--lib/rack/content_length.rb6
-rw-r--r--lib/rack/deflater.rb2
-rw-r--r--lib/rack/etag.rb6
-rw-r--r--lib/rack/file.rb12
-rw-r--r--lib/rack/handler.rb4
-rw-r--r--lib/rack/head.rb9
-rw-r--r--lib/rack/lint.rb2
-rw-r--r--lib/rack/mock.rb13
-rw-r--r--lib/rack/multipart.rb2
-rw-r--r--lib/rack/multipart/parser.rb148
-rw-r--r--lib/rack/request.rb29
-rw-r--r--lib/rack/response.rb1
-rw-r--r--lib/rack/sendfile.rb13
-rw-r--r--lib/rack/session/cookie.rb4
-rw-r--r--lib/rack/utils.rb7
-rw-r--r--test/spec_builder.rb11
-rw-r--r--test/spec_chunked.rb16
-rw-r--r--test/spec_commonlogger.rb12
-rw-r--r--test/spec_content_length.rb4
-rw-r--r--test/spec_etag.rb9
-rw-r--r--test/spec_file.rb14
-rw-r--r--test/spec_head.rb2
-rw-r--r--test/spec_multipart.rb46
-rw-r--r--test/spec_request.rb24
-rw-r--r--test/spec_session_cookie.rb21
-rw-r--r--test/spec_utils.rb10
36 files changed, 397 insertions, 101 deletions
diff --git a/.gitignore b/.gitignore
index ae1afb4f..7a7ad3a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@ Gemfile.lock
 .rbx
 doc
 /.bundle
+/.yardoc
diff --git a/.travis.yml b/.travis.yml
index 68904373..90a799ad 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,12 +9,14 @@ rvm:
   - 1.9.2
   - 1.9.3
   - 2.0.0
+  - 2.1.0-rc1
+  - ruby-head
   - rbx
   - jruby
   - ree
-branches:
-  # The old 1.1, 1.2, and 1.3 branches aren't correctly setup yet.
-  only: master
+matrix:
+  allow_failures:
+    - rvm: rbx
 notifications:
   email: false
   irc: "irc.freenode.org#rack"
diff --git a/.yardopts b/.yardopts
new file mode 100644
index 00000000..f4d6aeba
--- /dev/null
+++ b/.yardopts
@@ -0,0 +1,2 @@
+-
+SPEC
diff --git a/Rakefile b/Rakefile
index bee94b09..801609e0 100644
--- a/Rakefile
+++ b/Rakefile
@@ -41,8 +41,8 @@ end
 
 desc "Make binaries executable"
 task :chmod do
-  Dir["bin/*"].each { |binary| File.chmod(0775, binary) }
-  Dir["test/cgi/test*"].each { |binary| File.chmod(0775, binary) }
+  Dir["bin/*"].each { |binary| File.chmod(0755, binary) }
+  Dir["test/cgi/test*"].each { |binary| File.chmod(0755, binary) }
 end
 
 desc "Generate a ChangeLog"
diff --git a/SPEC b/SPEC
index 3a6481d2..f7bfb3df 100644
--- a/SPEC
+++ b/SPEC
@@ -207,7 +207,7 @@ but only contain keys that consist of
 letters, digits, <tt>_</tt> or <tt>-</tt> and start with a letter.
 The values of the header must be Strings,
 consisting of lines (for multiple header values, e.g. multiple
-<tt>Set-Cookie</tt> values) separated by "\n".
+<tt>Set-Cookie</tt> values) separated by "\\n".
 The lines must not contain characters below 037.
 === The Content-Type
 There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
diff --git a/lib/rack/auth/abstract/request.rb b/lib/rack/auth/abstract/request.rb
index aa6bf8e2..80d1c272 100644
--- a/lib/rack/auth/abstract/request.rb
+++ b/lib/rack/auth/abstract/request.rb
@@ -21,7 +21,7 @@ module Rack
       end
 
       def scheme
-        @scheme ||= parts.first.downcase
+        @scheme ||= parts.first && parts.first.downcase
       end
 
       def params
diff --git a/lib/rack/auth/basic.rb b/lib/rack/auth/basic.rb
index 7dd9a99b..9c589214 100644
--- a/lib/rack/auth/basic.rb
+++ b/lib/rack/auth/basic.rb
@@ -41,7 +41,7 @@ module Rack
 
       class Request < Auth::AbstractRequest
         def basic?
-          !parts.first.nil? && "basic" == scheme
+          "basic" == scheme
         end
 
         def credentials
diff --git a/lib/rack/builder.rb b/lib/rack/builder.rb
index 66dc7bd4..2aa37b68 100644
--- a/lib/rack/builder.rb
+++ b/lib/rack/builder.rb
@@ -51,7 +51,7 @@ module Rack
     end
 
     def initialize(default_app = nil,&block)
-      @use, @map, @run = [], nil, default_app
+      @use, @map, @run, @warmup = [], nil, default_app, nil
       instance_eval(&block) if block_given?
     end
 
@@ -104,6 +104,19 @@ module Rack
       @run = app
     end
 
+    # Takes a lambda or block that is used to warm-up the application.
+    #
+    #   warmup do |app|
+    #     client = Rack::MockRequest.new(app)
+    #     client.get('/')
+    #   end
+    #
+    #   use SomeMiddleware
+    #   run MyApp
+    def warmup(prc=nil, &block)
+      @warmup = prc || block
+    end
+
     # Creates a route within the application.
     #
     #   Rack::Builder.app do
@@ -131,7 +144,9 @@ module Rack
     def to_app
       app = @map ? generate_map(@run, @map) : @run
       fail "missing run or map statement" unless app
-      @use.reverse.inject(app) { |a,e| e[a] }
+      app = @use.reverse.inject(app) { |a,e| e[a] }
+      @warmup.call(app) if @warmup
+      app
     end
 
     def call(env)
@@ -142,7 +157,7 @@ module Rack
 
     def generate_map(default_app, mapping)
       mapped = default_app ? {'/' => default_app} : {}
-      mapping.each { |r,b| mapped[r] = self.class.new(default_app, &b) }
+      mapping.each { |r,b| mapped[r] = self.class.new(default_app, &b).to_app }
       URLMap.new(mapped)
     end
   end
diff --git a/lib/rack/chunked.rb b/lib/rack/chunked.rb
index a400756a..ea221fa9 100644
--- a/lib/rack/chunked.rb
+++ b/lib/rack/chunked.rb
@@ -39,11 +39,22 @@ module Rack
       @app = app
     end
 
+    # pre-HTTP/1.0 (informally "HTTP/0.9") HTTP requests did not have
+    # a version (nor response headers)
+    def chunkable_version?(ver)
+      case ver
+      when "HTTP/1.0", nil, "HTTP/0.9"
+        false
+      else
+        true
+      end
+    end
+
     def call(env)
       status, headers, body = @app.call(env)
       headers = HeaderHash.new(headers)
 
-      if env['HTTP_VERSION'] == 'HTTP/1.0' ||
+      if ! chunkable_version?(env['HTTP_VERSION']) ||
          STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
          headers['Content-Length'] ||
          headers['Transfer-Encoding']
diff --git a/lib/rack/commonlogger.rb b/lib/rack/commonlogger.rb
index b3968ac7..1c99045e 100644
--- a/lib/rack/commonlogger.rb
+++ b/lib/rack/commonlogger.rb
@@ -10,7 +10,7 @@ module Rack
   # an instance of Rack::NullLogger.
   #
   # +logger+ can be any class, including the standard library Logger, and is
-  # expected to have a +write+ method, which accepts the CommonLogger::FORMAT.
+  # expected to have either +write+ or +<<+ method, which accepts the CommonLogger::FORMAT.
   # According to the SPEC, the error stream must also respond to +puts+
   # (which takes a single argument that responds to +to_s+), and +flush+
   # (which is called without arguments in order to make the error appear for
@@ -42,11 +42,10 @@ module Rack
       now = Time.now
       length = extract_content_length(header)
 
-      logger = @logger || env['rack.errors']
-      logger.write FORMAT % [
+      msg = FORMAT % [
         env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
         env["REMOTE_USER"] || "-",
-        now.strftime("%d/%b/%Y %H:%M:%S %z"),
+        now.strftime("%d/%b/%Y:%H:%M:%S %z"),
         env["REQUEST_METHOD"],
         env["PATH_INFO"],
         env["QUERY_STRING"].empty? ? "" : "?"+env["QUERY_STRING"],
@@ -54,6 +53,15 @@ module Rack
         status.to_s[0..3],
         length,
         now - began_at ]
+
+      logger = @logger || env['rack.errors']
+      # Standard library logger doesn't support write but it supports << which actually
+      # calls to write on the log device without formatting
+      if logger.respond_to?(:write)
+        logger.write(msg)
+      else
+        logger << msg
+      end
     end
 
     def extract_content_length(headers)
diff --git a/lib/rack/content_length.rb b/lib/rack/content_length.rb
index 634bdc41..71bc919b 100644
--- a/lib/rack/content_length.rb
+++ b/lib/rack/content_length.rb
@@ -1,4 +1,5 @@
 require 'rack/utils'
+require 'rack/body_proxy'
 
 module Rack
 
@@ -22,7 +23,10 @@ module Rack
         obody = body
         body, length = [], 0
         obody.each { |part| body << part; length += bytesize(part) }
-        obody.close if obody.respond_to?(:close)
+
+        body = BodyProxy.new(body) do
+          obody.close if obody.respond_to?(:close)
+        end
 
         headers['Content-Length'] = length.to_s
       end
diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb
index fe2ac3db..2e55f97e 100644
--- a/lib/rack/deflater.rb
+++ b/lib/rack/deflater.rb
@@ -80,7 +80,6 @@ module Rack
           gzip.flush
         }
       ensure
-        close
         gzip.close
         @writer = nil
       end
@@ -116,7 +115,6 @@ module Rack
         yield deflator.finish
         nil
       ensure
-        close
         deflator.close
       end
 
diff --git a/lib/rack/etag.rb b/lib/rack/etag.rb
index 5fa09abd..99a1a4c0 100644
--- a/lib/rack/etag.rb
+++ b/lib/rack/etag.rb
@@ -23,7 +23,11 @@ module Rack
       status, headers, body = @app.call(env)
 
       if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
-        digest, body = digest_body(body)
+        original_body = body
+        digest, new_body = digest_body(body)
+        body = Rack::BodyProxy.new(new_body) do
+          original_body.close if original_body.respond_to?(:close)
+        end
         headers['ETag'] = %("#{digest}") if digest
       end
 
diff --git a/lib/rack/file.rb b/lib/rack/file.rb
index ee58a1a7..820ecd60 100644
--- a/lib/rack/file.rb
+++ b/lib/rack/file.rb
@@ -13,7 +13,8 @@ module Rack
 
   class File
     SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
-    ALLOWED_VERBS = %w[GET HEAD]
+    ALLOWED_VERBS = %w[GET HEAD OPTIONS]
+    ALLOW_HEADER = ALLOWED_VERBS.join(', ')
 
     attr_accessor :root
     attr_accessor :path
@@ -35,7 +36,7 @@ module Rack
 
     def _call(env)
       unless ALLOWED_VERBS.include? env["REQUEST_METHOD"]
-        return fail(405, "Method Not Allowed")
+        return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER})
       end
 
       path_info = Utils.unescape(env["PATH_INFO"])
@@ -64,6 +65,9 @@ module Rack
     end
 
     def serving(env)
+      if env["REQUEST_METHOD"] == "OPTIONS"
+              return [200, {'Allow' => ALLOW_HEADER, 'Content-Length' => '0'}, []]
+      end
       last_modified = F.mtime(@path).httpdate
       return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
 
@@ -121,7 +125,7 @@ module Rack
 
     private
 
-    def fail(status, body)
+    def fail(status, body, headers = {})
       body += "\n"
       [
         status,
@@ -129,7 +133,7 @@ module Rack
           "Content-Type" => "text/plain",
           "Content-Length" => body.size.to_s,
           "X-Cascade" => "pass"
-        },
+        }.merge!(headers),
         [body]
       ]
     end
diff --git a/lib/rack/handler.rb b/lib/rack/handler.rb
index 155dbfab..8f649974 100644
--- a/lib/rack/handler.rb
+++ b/lib/rack/handler.rb
@@ -53,7 +53,9 @@ module Rack
         Rack::Handler::FastCGI
       elsif ENV.include?("REQUEST_METHOD")
         Rack::Handler::CGI
-      else
+     elsif ENV.include?("RACK_HANDLER")
+        self.get(ENV["RACK_HANDLER"])
+     else
         pick ['thin', 'puma', 'webrick']
       end
     end
diff --git a/lib/rack/head.rb b/lib/rack/head.rb
index 7ffead6c..72f3dbdd 100644
--- a/lib/rack/head.rb
+++ b/lib/rack/head.rb
@@ -1,3 +1,5 @@
+require 'rack/body_proxy'
+
 module Rack
 
 class Head
@@ -11,8 +13,11 @@ class Head
     status, headers, body = @app.call(env)
 
     if env["REQUEST_METHOD"] == "HEAD"
-      body.close if body.respond_to? :close
-      [status, headers, []]
+      [
+        status, headers, Rack::BodyProxy.new([]) do
+          body.close if body.respond_to? :close
+        end
+      ]
     else
       [status, headers, body]
     end
diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb
index 0c6e1a35..3978b70a 100644
--- a/lib/rack/lint.rb
+++ b/lib/rack/lint.rb
@@ -584,7 +584,7 @@ module Rack
         assert("a header value must be a String, but the value of " +
           "'#{key}' is a #{value.class}") { value.kind_of? String }
         ## consisting of lines (for multiple header values, e.g. multiple
-        ## <tt>Set-Cookie</tt> values) separated by "\n".
+        ## <tt>Set-Cookie</tt> values) separated by "\\n".
         value.split("\n").each { |item|
           ## The lines must not contain characters below 037.
           assert("invalid header value #{key}: #{item.inspect}") {
diff --git a/lib/rack/mock.rb b/lib/rack/mock.rb
index ac7ef08c..3ba314e4 100644
--- a/lib/rack/mock.rb
+++ b/lib/rack/mock.rb
@@ -53,12 +53,13 @@ module Rack
       @app = app
     end
 
-    def get(uri, opts={})    request("GET", uri, opts)    end
-    def post(uri, opts={})   request("POST", uri, opts)   end
-    def put(uri, opts={})    request("PUT", uri, opts)    end
-    def patch(uri, opts={})  request("PATCH", uri, opts)    end
-    def delete(uri, opts={}) request("DELETE", uri, opts) end
-    def head(uri, opts={})   request("HEAD", uri, opts)   end
+    def get(uri, opts={})     request("GET", uri, opts)     end
+    def post(uri, opts={})    request("POST", uri, opts)    end
+    def put(uri, opts={})     request("PUT", uri, opts)     end
+    def patch(uri, opts={})   request("PATCH", uri, opts)   end
+    def delete(uri, opts={})  request("DELETE", uri, opts)  end
+    def head(uri, opts={})    request("HEAD", uri, opts)    end
+    def options(uri, opts={}) request("OPTIONS", uri, opts) end
 
     def request(method="GET", uri="", opts={})
       env = self.class.env_for(uri, opts.merge(:method => method))
diff --git a/lib/rack/multipart.rb b/lib/rack/multipart.rb
index 68492480..d67ff051 100644
--- a/lib/rack/multipart.rb
+++ b/lib/rack/multipart.rb
@@ -22,7 +22,7 @@ module Rack
 
     class << self
       def parse_multipart(env)
-        Parser.new(env).parse
+        Parser.create(env).parse
       end
 
       def build_multipart(params, first = true)
diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb
index a8019f98..fa47fd16 100644
--- a/lib/rack/multipart/parser.rb
+++ b/lib/rack/multipart/parser.rb
@@ -5,13 +5,42 @@ module Rack
     class Parser
       BUFSIZE = 16384
 
-      def initialize(env)
-        @env = env
+      DUMMY = Struct.new(:parse).new
+
+      def self.create(env)
+        return DUMMY unless env['CONTENT_TYPE'] =~ MULTIPART
+
+        io = env['rack.input']
+        io.rewind
+
+        content_length = env['CONTENT_LENGTH']
+        content_length = content_length.to_i if content_length
+
+        new($1, io, content_length)
       end
 
-      def parse
-        return nil unless setup_parse
+      def initialize(boundary, io, content_length)
+        @buf            = ""
+
+        if @buf.respond_to? :force_encoding
+          @buf.force_encoding Encoding::ASCII_8BIT
+        end
+
+        @params         = Utils::KeySpaceConstrainedParams.new
+        @boundary       = "--#{boundary}"
+        @io             = io
+        @content_length = content_length
+        @boundary_size  = Utils.bytesize(@boundary) + EOL.size
+
+        if @content_length
+          @content_length -= @boundary_size
+        end
+
+        @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
+        @full_boundary = @boundary + EOL
+      end
 
+      def parse
         fast_forward_to_first_boundary
 
         loop do
@@ -26,9 +55,11 @@ module Rack
             @content_length = -1  if $1 == "--"
           end
 
-          filename, data = get_data(filename, body, content_type, name, head)
+          get_data(filename, body, content_type, name, head) do |data|
+            tag_multipart_encoding(filename, content_type, name, data)
 
-          Utils.normalize_params(@params, name, data) unless data.nil?
+            Utils.normalize_params(@params, name, data)
+          end
 
           # break if we're at the end of a buffer, but not if it is the end of a field
           break if (@buf.empty? && $1 != EOL) || @content_length == -1
@@ -40,33 +71,9 @@ module Rack
       end
 
       private
-      def setup_parse
-        return false unless @env['CONTENT_TYPE'] =~ MULTIPART
-
-        @boundary = "--#{$1}"
-
-        @buf = ""
-        @params = Utils::KeySpaceConstrainedParams.new
-
-        @io = @env['rack.input']
-        @io.rewind
-
-        @boundary_size = Utils.bytesize(@boundary) + EOL.size
-
-        if @content_length = @env['CONTENT_LENGTH']
-          @content_length = @content_length.to_i
-          @content_length -= @boundary_size
-        end
-        true
-      end
-
-      def full_boundary
-        @boundary + EOL
-      end
+      def full_boundary; @full_boundary; end
 
-      def rx
-        @rx ||= /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
-      end
+      def rx; @rx; end
 
       def fast_forward_to_first_boundary
         loop do
@@ -86,6 +93,11 @@ module Rack
       def get_current_head_and_filename_and_content_type_and_name_and_body
         head = nil
         body = ''
+
+        if body.respond_to? :force_encoding
+          body.force_encoding Encoding::ASCII_8BIT
+        end
+
         filename = content_type = name = nil
 
         until head && @buf =~ rx
@@ -124,32 +136,78 @@ module Rack
 
       def get_filename(head)
         filename = nil
-        if head =~ RFC2183
+        case head
+        when RFC2183
           filename = Hash[head.scan(DISPPARM)]['filename']
           filename = $1 if filename and filename =~ /^"(.*)"$/
-        elsif head =~ BROKEN_QUOTED
-          filename = $1
-        elsif head =~ BROKEN_UNQUOTED
+        when BROKEN_QUOTED, BROKEN_UNQUOTED
           filename = $1
         end
 
-        if filename && filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
+        return unless filename
+
+        if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
           filename = Utils.unescape(filename)
         end
-        if filename.respond_to?(:valid_encoding?) && !filename.valid_encoding?
-          filename = filename.chars.select { |char| char.valid_encoding? }.join
-        end
-        if filename && filename !~ /\\[^\\"]/
+
+        scrub_filename filename
+
+        if filename !~ /\\[^\\"]/
           filename = filename.gsub(/\\(.)/, '\1')
         end
         filename
       end
 
+      if "<3".respond_to? :valid_encoding?
+        def scrub_filename(filename)
+          unless filename.valid_encoding?
+            # FIXME: this force_encoding is for Ruby 2.0 and 1.9 support.
+            # We can remove it after they are dropped
+            filename.force_encoding(Encoding::ASCII_8BIT)
+            filename.encode!(:invalid => :replace, :undef => :replace)
+          end
+        end
+
+        CHARSET    = "charset"
+        TEXT_PLAIN = "text/plain"
+
+        def tag_multipart_encoding(filename, content_type, name, body)
+          name.force_encoding Encoding::UTF_8
+
+          return if filename
+
+          encoding = Encoding::UTF_8
+
+          if content_type
+            list         = content_type.split(';')
+            type_subtype = list.first
+            type_subtype.strip!
+            if TEXT_PLAIN == type_subtype
+              rest         = list.drop 1
+              rest.each do |param|
+                k,v = param.split('=', 2)
+                k.strip!
+                v.strip!
+                encoding = Encoding.find v if k == CHARSET
+              end
+            end
+          end
+
+          name.force_encoding encoding
+          body.force_encoding encoding
+        end
+      else
+        def scrub_filename(filename)
+        end
+        def tag_multipart_encoding(filename, content_type, name, body)
+        end
+      end
+
       def get_data(filename, body, content_type, name, head)
-        data = nil
+        data = body
         if filename == ""
           # filename is blank which means no file has been selected
-          return data
+          return
         elsif filename
           body.rewind
 
@@ -167,11 +225,9 @@ module Rack
           # Generic multipart cases, not coming from a form
           data = {:type => content_type,
                   :name => name, :tempfile => body, :head => head}
-        else
-          data = body
         end
 
-        [filename, data]
+        yield data
       end
     end
   end
diff --git a/lib/rack/request.rb b/lib/rack/request.rb
index 80d5e0db..ca6786f6 100644
--- a/lib/rack/request.rb
+++ b/lib/rack/request.rb
@@ -142,7 +142,7 @@ module Rack
 
     # Checks the HTTP request method (or verb) to see if it was of type TRACE
     def trace?;   request_method == "TRACE"   end
-    
+
     # Checks the HTTP request method (or verb) to see if it was of type UNLINK
     def unlink?;  request_method == "UNLINK"  end
 
@@ -192,8 +192,9 @@ module Rack
       if @env["rack.request.query_string"] == query_string
         @env["rack.request.query_hash"]
       else
+        p = parse_query(query_string)
         @env["rack.request.query_string"] = query_string
-        @env["rack.request.query_hash"]   = parse_query(query_string)
+        @env["rack.request.query_hash"]   = p
       end
     end
 
@@ -337,14 +338,11 @@ module Rack
     end
 
     def accept_encoding
-      @env["HTTP_ACCEPT_ENCODING"].to_s.split(/\s*,\s*/).map do |part|
-        encoding, parameters = part.split(/\s*;\s*/, 2)
-        quality = 1.0
-        if parameters and /\Aq=([\d.]+)/ =~ parameters
-          quality = $1.to_f
-        end
-        [encoding, quality]
-      end
+      parse_http_accept_header(@env["HTTP_ACCEPT_ENCODING"])
+    end
+
+    def accept_language
+      parse_http_accept_header(@env["HTTP_ACCEPT_LANGUAGE"])
     end
 
     def trusted_proxy?(ip)
@@ -384,5 +382,16 @@ module Rack
       def parse_multipart(env)
         Rack::Multipart.parse_multipart(env)
       end
+
+      def parse_http_accept_header(header)
+        header.to_s.split(/\s*,\s*/).map do |part|
+          attribute, parameters = part.split(/\s*;\s*/, 2)
+          quality = 1.0
+          if parameters and /\Aq=([\d.]+)/ =~ parameters
+            quality = $1.to_f
+          end
+          [attribute, quality]
+        end
+      end
   end
 end
diff --git a/lib/rack/response.rb b/lib/rack/response.rb
index 2076aff0..1acfce22 100644
--- a/lib/rack/response.rb
+++ b/lib/rack/response.rb
@@ -1,5 +1,6 @@
 require 'rack/request'
 require 'rack/utils'
+require 'rack/body_proxy'
 require 'time'
 
 module Rack
diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb
index 8a674904..8e2c3d67 100644
--- a/lib/rack/sendfile.rb
+++ b/lib/rack/sendfile.rb
@@ -1,4 +1,5 @@
 require 'rack/file'
+require 'rack/body_proxy'
 
 module Rack
 
@@ -117,8 +118,10 @@ module Rack
           if url = map_accel_path(env, path)
             headers['Content-Length'] = '0'
             headers[type] = url
-            body.close if body.respond_to?(:close)
-            body = []
+            obody = body
+            body = Rack::BodyProxy.new([]) do
+              obody.close if obody.respond_to?(:close)
+            end
           else
             env['rack.errors'].puts "X-Accel-Mapping header missing"
           end
@@ -126,8 +129,10 @@ module Rack
           path = F.expand_path(body.to_path)
           headers['Content-Length'] = '0'
           headers[type] = path
-          body.close if body.respond_to?(:close)
-          body = []
+          obody = body
+          body = Rack::BodyProxy.new([]) do
+            obody.close if obody.respond_to?(:close)
+          end
         when '', nil
         else
           env['rack.errors'].puts "Unknown x-sendfile variation: '#{type}'.\n"
diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb
index 22e33d1f..9bea586c 100644
--- a/lib/rack/session/cookie.rb
+++ b/lib/rack/session/cookie.rb
@@ -135,7 +135,9 @@ module Rack
           session_data = request.cookies[@key]
 
           if @secrets.size > 0 && session_data
-            session_data, digest = session_data.split("--")
+            digest, session_data = session_data.reverse.split("--", 2)
+            digest.reverse! if digest
+            session_data.reverse! if session_data
             session_data = nil unless digest_match?(session_data, digest)
           end
 
diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb
index 43bbef37..04fecf9d 100644
--- a/lib/rack/utils.rb
+++ b/lib/rack/utils.rb
@@ -109,6 +109,8 @@ module Rack
 
       if after == ""
         params[k] = v
+      elsif after == "["
+        params[name] = v
       elsif after == "[]"
         params[k] ||= []
         raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
@@ -394,6 +396,11 @@ module Rack
     module_function :byte_ranges
 
     # Constant time string comparison.
+    #
+    # NOTE: the values compared should be of fixed length, such as strings
+    # that have aready been processed by HMAC.  This should not be used
+    # on variable length plaintext strings because it could leak length info
+    # via timing attacks.
     def secure_compare(a, b)
       return false unless bytesize(a) == bytesize(b)
 
diff --git a/test/spec_builder.rb b/test/spec_builder.rb
index 0774f597..20ea6681 100644
--- a/test/spec_builder.rb
+++ b/test/spec_builder.rb
@@ -130,6 +130,17 @@ describe Rack::Builder do
     Rack::MockRequest.new(app).get("/foo").should.be.server_error
   end
 
+  it "yields the generated app to a block for warmup" do
+    warmed_up_app = nil
+
+    app = Rack::Builder.new do
+      warmup { |a| warmed_up_app = a }
+      run lambda { |env| [200, {}, []] }
+    end.to_app
+
+    warmed_up_app.should.equal app
+  end
+
   should "initialize apps once" do
     app = builder do
       class AppClass
diff --git a/test/spec_chunked.rb b/test/spec_chunked.rb
index 12f21581..0a6d9ff1 100644
--- a/test/spec_chunked.rb
+++ b/test/spec_chunked.rb
@@ -64,6 +64,22 @@ describe Rack::Chunked do
     body.join.should.equal 'Hello World!'
   end
 
+  should 'not modify response when client is ancient, pre-HTTP/1.0' do
+    app = lambda { |env| [200, {"Content-Type" => "text/plain"}, ['Hello', ' ', 'World!']] }
+    check = lambda do
+      status, headers, body = chunked(app).call(@env.dup)
+      status.should.equal 200
+      headers.should.not.include 'Transfer-Encoding'
+      body.join.should.equal 'Hello World!'
+    end
+
+    @env.delete('HTTP_VERSION') # unicorn will do this on pre-HTTP/1.0 requests
+    check.call
+
+    @env['HTTP_VERSION'] = 'HTTP/0.9' # not sure if this happens in practice
+    check.call
+  end
+
   should 'not modify response when Transfer-Encoding header already present' do
     app = lambda { |env|
       [200, {"Content-Type" => "text/plain", 'Transfer-Encoding' => 'identity'}, ['Hello', ' ', 'World!']]
diff --git a/test/spec_commonlogger.rb b/test/spec_commonlogger.rb
index 4d1c2cca..fd1f2521 100644
--- a/test/spec_commonlogger.rb
+++ b/test/spec_commonlogger.rb
@@ -2,6 +2,8 @@ require 'rack/commonlogger'
 require 'rack/lint'
 require 'rack/mock'
 
+require 'logger'
+
 describe Rack::CommonLogger do
   obj = 'foobar'
   length = obj.size
@@ -33,6 +35,14 @@ describe Rack::CommonLogger do
     log.string.should =~ /"GET \/ " 200 #{length} /
   end
 
+  should "work with standartd library logger" do
+    logdev = StringIO.new
+    log = Logger.new(logdev)
+    Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/")
+
+    logdev.string.should =~ /"GET \/ " 200 #{length} /
+  end
+
   should "log - content length if header is missing" do
     res = Rack::MockRequest.new(Rack::CommonLogger.new(app_without_length)).get("/")
 
@@ -67,7 +77,7 @@ describe Rack::CommonLogger do
     md = /- - - \[([^\]]+)\] "(\w+) \/ " (\d{3}) \d+ ([\d\.]+)/.match(log.string)
     md.should.not.equal nil
     time, method, status, duration = *md.captures
-    time.should.equal Time.at(0).strftime("%d/%b/%Y %H:%M:%S %z")
+    time.should.equal Time.at(0).strftime("%d/%b/%Y:%H:%M:%S %z")
     method.should.equal "GET"
     status.should.equal "200"
     (0..1).should.include?(duration.to_f)
diff --git a/test/spec_content_length.rb b/test/spec_content_length.rb
index 4b80a0f4..12c047fb 100644
--- a/test/spec_content_length.rb
+++ b/test/spec_content_length.rb
@@ -62,7 +62,9 @@ describe Rack::ContentLength do
     end.new(%w[one two three])
 
     app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] }
-    content_length(app).call(request)
+    response = content_length(app).call(request)
+    body.closed.should.equal nil
+    response[2].close
     body.closed.should.equal true
   end
 
diff --git a/test/spec_etag.rb b/test/spec_etag.rb
index d7f03504..b8b8b637 100644
--- a/test/spec_etag.rb
+++ b/test/spec_etag.rb
@@ -95,4 +95,13 @@ describe Rack::ETag do
     response = etag(app).call(request)
     response[1]['ETag'].should.be.nil
   end
+
+  should "close the original body" do
+    body = StringIO.new
+    app = lambda { |env| [200, {}, body] }
+    response = etag(app).call(request)
+    body.should.not.be.closed
+    response[2].close
+    body.should.be.closed
+  end
 end
diff --git a/test/spec_file.rb b/test/spec_file.rb
index c9d7a1b9..25c31ef8 100644
--- a/test/spec_file.rb
+++ b/test/spec_file.rb
@@ -164,24 +164,32 @@ describe Rack::File do
     heads['Access-Control-Allow-Origin'].should.equal nil
   end
 
-  should "only support GET and HEAD requests" do
+  should "only support GET, HEAD, and OPTIONS requests" do
     req = Rack::MockRequest.new(file(DOCROOT))
 
     forbidden = %w[post put patch delete]
     forbidden.each do |method|
-
       res = req.send(method, "/cgi/test")
       res.should.be.client_error
       res.should.be.method_not_allowed
+      res.headers['Allow'].split(/, */).sort.should == %w(GET HEAD OPTIONS)
     end
 
-    allowed = %w[get head]
+    allowed = %w[get head options]
     allowed.each do |method|
       res = req.send(method, "/cgi/test")
       res.should.be.successful
     end
   end
 
+  should "set Allow correctly for OPTIONS requests" do
+    req = Rack::MockRequest.new(file(DOCROOT))
+    res = req.options('/cgi/test')
+    res.should.be.successful
+    res.headers['Allow'].should.not.equal nil
+    res.headers['Allow'].split(/, */).sort.should == %w(GET HEAD OPTIONS)
+  end
+
   should "set Content-Length correctly for HEAD requests" do
     req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT)))
     res = req.head "/cgi/test"
diff --git a/test/spec_head.rb b/test/spec_head.rb
index 18f9a76a..78bc6ad7 100644
--- a/test/spec_head.rb
+++ b/test/spec_head.rb
@@ -38,6 +38,8 @@ describe Rack::Head do
     resp[0].should.equal(200)
     resp[1].should.equal({"Content-type" => "test/plain", "Content-length" => "3"})
     resp[2].to_enum.to_a.should.equal([])
+    body.should.not.be.closed
+    resp[2].close
     body.should.be.closed
   end
 end
diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb
index fc180c24..069dc4d2 100644
--- a/test/spec_multipart.rb
+++ b/test/spec_multipart.rb
@@ -30,6 +30,44 @@ describe Rack::Multipart do
     params["text"].should.equal "contents"
   end
 
+  if "<3".respond_to?(:force_encoding)
+  should "set US_ASCII encoding based on charset" do
+    env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename))
+    params = Rack::Multipart.parse_multipart(env)
+    params["text"].encoding.should.equal Encoding::US_ASCII
+
+    # I'm not 100% sure if making the param name encoding match the
+    # Content-Type charset is the right thing to do.  We should revisit this.
+    params.keys.each do |key|
+      key.encoding.should.equal Encoding::US_ASCII
+    end
+  end
+
+  should "set BINARY encoding on things without content type" do
+    env = Rack::MockRequest.env_for("/", multipart_fixture(:none))
+    params = Rack::Multipart.parse_multipart(env)
+    params["submit-name"].encoding.should.equal Encoding::UTF_8
+  end
+
+  should "set UTF8 encoding on names of things without content type" do
+    env = Rack::MockRequest.env_for("/", multipart_fixture(:none))
+    params = Rack::Multipart.parse_multipart(env)
+    params.keys.each do |key|
+      key.encoding.should.equal Encoding::UTF_8
+    end
+  end
+
+  should "default text to UTF8" do
+    env = Rack::MockRequest.env_for("/", multipart_fixture(:text))
+    params = Rack::Multipart.parse_multipart(env)
+    params['submit-name'].encoding.should.equal Encoding::UTF_8
+    params['submit-name-with-content'].encoding.should.equal Encoding::UTF_8
+    params.keys.each do |key|
+      key.encoding.should.equal Encoding::UTF_8
+    end
+  end
+  end
+
   should "raise RangeError if the key space is exhausted" do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename))
 
@@ -196,6 +234,14 @@ describe Rack::Multipart do
     params["files"].size.should.equal 252
   end
 
+  should "parse multipart/mixed" do
+    env = Rack::MockRequest.env_for("/", multipart_fixture(:mixed_files))
+    params = Rack::Utils::Multipart.parse_multipart(env)
+    params["foo"].should.equal "bar"
+    params["files"].should.be.instance_of String
+    params["files"].size.should.equal 252
+  end
+
   should "parse IE multipart upload and clean up filename" do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:ie))
     params = Rack::Multipart.parse_multipart(env)
diff --git a/test/spec_request.rb b/test/spec_request.rb
index a3f42379..f5210e57 100644
--- a/test/spec_request.rb
+++ b/test/spec_request.rb
@@ -938,6 +938,23 @@ EOF
     parser.call("gzip ; deflate").should.equal([["gzip", 1.0]])
   end
 
+  should "parse Accept-Language correctly" do
+    parser = lambda do |x|
+      Rack::Request.new(Rack::MockRequest.env_for("", "HTTP_ACCEPT_LANGUAGE" => x)).accept_language
+    end
+
+    parser.call(nil).should.equal([])
+
+    parser.call("fr, en").should.equal([["fr", 1.0], ["en", 1.0]])
+    parser.call("").should.equal([])
+    parser.call("*").should.equal([["*", 1.0]])
+    parser.call("fr;q=0.5, en;q=1.0").should.equal([["fr", 0.5], ["en", 1.0]])
+    parser.call("fr;q=1.0, en; q=0.5, *;q=0").should.equal([["fr", 1.0], ["en", 0.5], ["*", 0] ])
+
+    parser.call("fr ; q=0.9").should.equal([["fr", 0.9]])
+    parser.call("fr").should.equal([["fr", 1.0]])
+  end
+
   ip_app = lambda { |env|
     request = Rack::Request.new(env)
     response = Rack::Response.new
@@ -1098,6 +1115,13 @@ EOF
     req2.params.should.equal "foo" => "bar"
   end
 
+  should "raise TypeError every time if request parameters are broken" do
+    broken_query = Rack::MockRequest.env_for("/?foo[]=0&foo[bar]=1")
+    req = Rack::Request.new(broken_query)
+    lambda{req.GET}.should.raise(TypeError)
+    lambda{req.params}.should.raise(TypeError)
+  end
+
   (0x20...0x7E).collect { |a|
     b = a.chr
     c = CGI.escape(b)
diff --git a/test/spec_session_cookie.rb b/test/spec_session_cookie.rb
index f5d69b16..944fde02 100644
--- a/test/spec_session_cookie.rb
+++ b/test/spec_session_cookie.rb
@@ -386,4 +386,25 @@ describe Rack::Session::Cookie do
     response.body.should.match(/counter/)
     response.body.should.match(/foo/)
   end
+
+  it "allows more than one '--' in the cookie when calculating digests" do
+    @counter = 0
+    app = lambda do |env|
+      env["rack.session"]["message"] ||= ""
+      env["rack.session"]["message"] << "#{(@counter += 1).to_s}--"
+      hash = env["rack.session"].dup
+      hash.delete("session_id")
+      Rack::Response.new(hash["message"]).to_a
+    end
+    # another example of an unsafe coder is Base64.urlsafe_encode64
+    unsafe_coder = Class.new {
+      def encode(hash); hash.inspect end
+      def decode(str); eval(str) if str; end
+    }.new
+    _app = [ app, { :secret => "test", :coder => unsafe_coder } ]
+    response = response_for(:app => _app)
+    response.body.should.equal "1--"
+    response = response_for(:app => _app, :cookie => response)
+    response.body.should.equal "1--2--"
+  end
 end
diff --git a/test/spec_utils.rb b/test/spec_utils.rb
index 622b8ff5..fd276326 100644
--- a/test/spec_utils.rb
+++ b/test/spec_utils.rb
@@ -157,6 +157,16 @@ describe Rack::Utils do
       should.equal "foo" => [""]
     Rack::Utils.parse_nested_query("foo[]=bar").
       should.equal "foo" => ["bar"]
+    Rack::Utils.parse_nested_query("foo[]=bar&foo").
+      should.equal "foo" => nil
+    Rack::Utils.parse_nested_query("foo[]=bar&foo[").
+      should.equal "foo" => ["bar"], "foo[" => nil
+    Rack::Utils.parse_nested_query("foo[]=bar&foo[=baz").
+      should.equal "foo" => ["bar"], "foo[" => "baz"
+    Rack::Utils.parse_nested_query("foo[]=bar&foo[]").
+      should.equal "foo" => ["bar", nil]
+    Rack::Utils.parse_nested_query("foo[]=bar&foo[]=").
+      should.equal "foo" => ["bar", ""]
 
     Rack::Utils.parse_nested_query("foo[]=1&foo[]=2").
       should.equal "foo" => ["1", "2"]