diff options
36 files changed, 397 insertions, 101 deletions
@@ -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 @@ -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" @@ -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"] |