diff options
author | James Tucker <jftucker@gmail.com> | 2014-08-03 14:21:41 -0300 |
---|---|---|
committer | James Tucker <jftucker@gmail.com> | 2014-08-03 14:21:41 -0300 |
commit | 9f52bb1db4b007066c9916881001d15c0a61bf66 (patch) | |
tree | 8de03740a1339dc367eb258e3e191abf00a91228 | |
parent | d8e2e2af6da57805d2f0906ce925ea150def31a0 (diff) | |
parent | 33075a489b85d43fc0be55d3503cc236f349e2f8 (diff) | |
download | rack-9f52bb1db4b007066c9916881001d15c0a61bf66.tar.gz |
Merge branch 'master' into pr/686
* master: (62 commits) build_nested_query includes integer values Rack::ETag correctly marks etags as Weak Fix yet another body close bug in Rack::Deflater Implement full Logger interface on NullLogger Revert "support empty string multipart filename" support empty string multipart filename multipart/form-data with files with no input name Fix parent type API regression introduced in #713 correct weird case regression from #714 UrlMap: Enable case-insensitive domain matching Raise specific exception if the parameters are invalid Fix media_type_params when Content-Type parameters contains quoted-strings Rack::Multipart::UploadedFile has file extensions multipart content-type match now case insensitive Undo test that falsely exemplifies production env default_middleware_by_environment should always returns empty array for unknown keys Remove rbx from Travis' allow_failures Fix rbx settings for Travis Use latest 2.1 on Travis Enable cleanup of Tempfiles from multipart form data by default ... Conflicts: lib/rack/request.rb
40 files changed, 1022 insertions, 329 deletions
diff --git a/.travis.yml b/.travis.yml index 588bf0ea..aa2eca43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,14 +9,11 @@ rvm: - 1.9.2 - 1.9.3 - 2.0.0 - - 2.1.0 + - 2.1 - ruby-head - - rbx + - rbx-2 - jruby - ree -matrix: - allow_failures: - - rvm: rbx notifications: email: false irc: "irc.freenode.org#rack" diff --git a/README.rdoc b/README.rdoc index 7a3c8d58..d5bd6d2e 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,4 +1,4 @@ -= Rack, a modular Ruby webserver interface {<img src="https://secure.travis-ci.org/rack/rack.png" alt="Build Status" />}[http://travis-ci.org/rack/rack] {<img src="https://gemnasium.com/rack/rack.png" alt="Dependency Status" />}[https://gemnasium.com/rack/rack] += Rack, a modular Ruby webserver interface {<img src="https://secure.travis-ci.org/rack/rack.svg" alt="Build Status" />}[http://travis-ci.org/rack/rack] {<img src="https://gemnasium.com/rack/rack.svg" alt="Dependency Status" />}[https://gemnasium.com/rack/rack] Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in @@ -33,6 +33,7 @@ These web servers include Rack handlers in their distributions: * Unicorn * unixrack * uWSGI +* yahns * Zbatery Any valid Rack app will run the same on all these handlers, without @@ -40,7 +40,17 @@ below. <tt>QUERY_STRING</tt>:: The portion of the request URL that follows the <tt>?</tt>, if any. May be empty, but is always required! -<tt>SERVER_NAME</tt>, <tt>SERVER_PORT</tt>:: When combined with <tt>SCRIPT_NAME</tt> and <tt>PATH_INFO</tt>, these variables can be used to complete the URL. Note, however, that <tt>HTTP_HOST</tt>, if present, should be used in preference to <tt>SERVER_NAME</tt> for reconstructing the request URL. <tt>SERVER_NAME</tt> and <tt>SERVER_PORT</tt> can never be empty strings, and so are always required. +<tt>SERVER_NAME</tt>, <tt>SERVER_PORT</tt>:: + When combined with <tt>SCRIPT_NAME</tt> and + <tt>PATH_INFO</tt>, these variables can be + used to complete the URL. Note, however, + that <tt>HTTP_HOST</tt>, if present, + should be used in preference to + <tt>SERVER_NAME</tt> for reconstructing + the request URL. + <tt>SERVER_NAME</tt> and <tt>SERVER_PORT</tt> + can never be empty strings, and so + are always required. <tt>HTTP_</tt> Variables:: Variables corresponding to the client-supplied HTTP request headers (i.e., variables whose @@ -49,24 +59,47 @@ below. variables should correspond with the presence or absence of the appropriate HTTP header in the - request. See <a href="https://tools.ietf.org/html/rfc3875#section-4.1.18"> - RFC3875 section 4.1.18</a> for specific behavior. + request. See + <a href="https://tools.ietf.org/html/rfc3875#section-4.1.18"> + RFC3875 section 4.1.18</a> for + specific behavior. In addition to this, the Rack environment must include these Rack-specific variables: -<tt>rack.version</tt>:: The Array representing this version of Rack. See Rack::VERSION, that corresponds to the version of this SPEC. -<tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the request URL. +<tt>rack.version</tt>:: The Array representing this version of Rack + See Rack::VERSION, that corresponds to + the version of this SPEC. +<tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the + request URL. <tt>rack.input</tt>:: See below, the input stream. <tt>rack.errors</tt>:: See below, the error stream. -<tt>rack.multithread</tt>:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise. -<tt>rack.multiprocess</tt>:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise. -<tt>rack.run_once</tt>:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar). -<tt>rack.hijack?</tt>:: present and true if the server supports connection hijacking. See below, hijacking. -<tt>rack.hijack</tt>:: an object responding to #call that must be called at least once before using rack.hijack_io. It is recommended #call return rack.hijack_io as well as setting it in env if necessary. -<tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack has received #call, this will contain an object resembling an IO. See hijacking. +<tt>rack.multithread</tt>:: true if the application object may be + simultaneously invoked by another thread + in the same process, false otherwise. +<tt>rack.multiprocess</tt>:: true if an equivalent application object + may be simultaneously invoked by another + process, false otherwise. +<tt>rack.run_once</tt>:: true if the server expects + (but does not guarantee!) that the + application will only be invoked this one + time during the life of its containing + process. Normally, this will only be true + for a server based on CGI + (or something similar). +<tt>rack.hijack?</tt>:: present and true if the server supports + connection hijacking. See below, hijacking. +<tt>rack.hijack</tt>:: an object responding to #call that must be + called at least once before using + rack.hijack_io. + It is recommended #call return rack.hijack_io + as well as setting it in env if necessary. +<tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack + has received #call, this will contain + an object resembling an IO. See hijacking. Additional environment specifications have approved to standardized middleware APIs. None of these are required to be implemented by the server. -<tt>rack.session</tt>:: A hash like interface for storing request session data. +<tt>rack.session</tt>:: A hash like interface for storing + request session data. The store must implement: store(key, value) (aliased as []=); fetch(key, default = nil) (aliased as []); @@ -110,15 +143,18 @@ must be opened in binary mode, for Ruby 1.9 compatibility. The input stream must respond to +gets+, +each+, +read+ and +rewind+. * +gets+ must be called without arguments and return a string, or +nil+ on EOF. -* +read+ behaves like IO#read. Its signature is <tt>read([length, [buffer]])</tt>. - If given, +length+ must be a non-negative Integer (>= 0) or +nil+, and +buffer+ must - be a String and may not be nil. If +length+ is given and not nil, then this method - reads at most +length+ bytes from the input stream. If +length+ is not given or nil, - then this method reads all data until EOF. - When EOF is reached, this method returns nil if +length+ is given and not nil, or "" - if +length+ is not given or is nil. - If +buffer+ is given, then the read data will be placed into +buffer+ instead of a - newly created String object. +* +read+ behaves like IO#read. + Its signature is <tt>read([length, [buffer]])</tt>. + If given, +length+ must be a non-negative Integer (>= 0) or +nil+, + and +buffer+ must be a String and may not be nil. + If +length+ is given and not nil, then this method reads at most + +length+ bytes from the input stream. + If +length+ is not given or nil, then this method reads + all data until EOF. + When EOF is reached, this method returns nil if +length+ is given + and not nil, or "" if +length+ is not given or is nil. + If +buffer+ is given, then the read data will be placed + into +buffer+ instead of a newly created String object. * +each+ must be called without arguments and only yield Strings. * +rewind+ must be called without arguments. It rewinds the input stream back to the beginning. It must not raise Errno::ESPIPE: diff --git a/lib/rack.rb b/lib/rack.rb index 57119df3..341514c5 100644 --- a/lib/rack.rb +++ b/lib/rack.rb @@ -53,6 +53,7 @@ module Rack autoload :ShowExceptions, "rack/showexceptions" autoload :ShowStatus, "rack/showstatus" autoload :Static, "rack/static" + autoload :TempfileReaper, "rack/tempfile_reaper" autoload :URLMap, "rack/urlmap" autoload :Utils, "rack/utils" autoload :Multipart, "rack/multipart" diff --git a/lib/rack/builder.rb b/lib/rack/builder.rb index a4f6c575..bda3be27 100644 --- a/lib/rack/builder.rb +++ b/lib/rack/builder.rb @@ -157,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/conditionalget.rb b/lib/rack/conditionalget.rb index ed87c54e..88573166 100644 --- a/lib/rack/conditionalget.rb +++ b/lib/rack/conditionalget.rb @@ -28,7 +28,10 @@ module Rack status = 304 headers.delete('Content-Type') headers.delete('Content-Length') - body = [] + original_body = body + body = Rack::BodyProxy.new([]) do + original_body.close if original_body.respond_to?(:close) + end end [status, headers, body] else diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb index 2e55f97e..9df510bd 100644 --- a/lib/rack/deflater.rb +++ b/lib/rack/deflater.rb @@ -17,19 +17,26 @@ module Rack # directive of 'no-transform' is present, or when the response status # code is one that doesn't allow an entity body. class Deflater - def initialize(app) + ## + # Creates Rack::Deflater middleware. + # + # [app] rack app instance + # [options] hash of deflater options, i.e. + # 'if' - a lambda enabling / disabling deflation based on returned boolean value + # e.g use Rack::Deflater, :if => lambda { |env, status, headers, body| body.length > 512 } + # 'include' - a list of content types that should be compressed + def initialize(app, options = {}) @app = app + + @condition = options[:if] + @compressible_types = options[:include] end def call(env) status, headers, body = @app.call(env) headers = Utils::HeaderHash.new(headers) - # Skip compressing empty entity body responses and responses with - # no-transform set. - if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) || - headers['Cache-Control'].to_s =~ /\bno-transform\b/ || - (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/) + unless should_deflate?(env, status, headers, body) return [status, headers, body] end @@ -58,9 +65,9 @@ module Rack when "identity" [status, headers, body] when nil - body.close if body.respond_to?(:close) message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found." - [406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, [message]] + bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) } + [406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, bp] end end @@ -124,5 +131,25 @@ module Rack @body.close if @body.respond_to?(:close) end end + + private + + def should_deflate?(env, status, headers, body) + # Skip compressing empty entity body responses and responses with + # no-transform set. + if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) || + headers['Cache-Control'].to_s =~ /\bno-transform\b/ || + (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/) + return false + end + + # Skip if @compressible_types are given and does not include request's content type + return false if @compressible_types && !(headers.has_key?('Content-Type') && @compressible_types.include?(headers['Content-Type'][/[^;]*/])) + + # Skip if @condition lambda is given and evaluates to false + return false if @condition && !@condition.call(env, status, headers, body) + + true + end end end diff --git a/lib/rack/etag.rb b/lib/rack/etag.rb index 99a1a4c0..fefe671f 100644 --- a/lib/rack/etag.rb +++ b/lib/rack/etag.rb @@ -28,7 +28,7 @@ module Rack body = Rack::BodyProxy.new(new_body) do original_body.close if original_body.respond_to?(:close) end - headers['ETag'] = %("#{digest}") if digest + headers['ETag'] = %(W/"#{digest}") if digest end unless headers['Cache-Control'] diff --git a/lib/rack/handler/webrick.rb b/lib/rack/handler/webrick.rb index f76679b4..023d8b27 100644 --- a/lib/rack/handler/webrick.rb +++ b/lib/rack/handler/webrick.rb @@ -2,6 +2,23 @@ require 'webrick' require 'stringio' require 'rack/content_length' +# This monkey patch allows for applications to perform their own chunking +# through WEBrick::HTTPResponse iff rack is set to true. +class WEBrick::HTTPResponse + attr_accessor :rack + + alias _rack_setup_header setup_header + def setup_header + app_chunking = rack && @header['transfer-encoding'] == 'chunked' + + @chunked = app_chunking if app_chunking + + _rack_setup_header + + @chunked = false if app_chunking + end +end + module Rack module Handler class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet @@ -39,6 +56,7 @@ module Rack end def service(req, res) + res.rack = true env = req.meta_vars env.delete_if { |k, v| v.nil? } diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb index 3978b70a..667c34a6 100644 --- a/lib/rack/lint.rb +++ b/lib/rack/lint.rb @@ -102,7 +102,17 @@ module Rack ## follows the <tt>?</tt>, if any. May be ## empty, but is always required! - ## <tt>SERVER_NAME</tt>, <tt>SERVER_PORT</tt>:: When combined with <tt>SCRIPT_NAME</tt> and <tt>PATH_INFO</tt>, these variables can be used to complete the URL. Note, however, that <tt>HTTP_HOST</tt>, if present, should be used in preference to <tt>SERVER_NAME</tt> for reconstructing the request URL. <tt>SERVER_NAME</tt> and <tt>SERVER_PORT</tt> can never be empty strings, and so are always required. + ## <tt>SERVER_NAME</tt>, <tt>SERVER_PORT</tt>:: + ## When combined with <tt>SCRIPT_NAME</tt> and + ## <tt>PATH_INFO</tt>, these variables can be + ## used to complete the URL. Note, however, + ## that <tt>HTTP_HOST</tt>, if present, + ## should be used in preference to + ## <tt>SERVER_NAME</tt> for reconstructing + ## the request URL. + ## <tt>SERVER_NAME</tt> and <tt>SERVER_PORT</tt> + ## can never be empty strings, and so + ## are always required. ## <tt>HTTP_</tt> Variables:: Variables corresponding to the ## client-supplied HTTP request @@ -112,29 +122,60 @@ module Rack ## variables should correspond with ## the presence or absence of the ## appropriate HTTP header in the - ## request. See <a href="https://tools.ietf.org/html/rfc3875#section-4.1.18"> - ## RFC3875 section 4.1.18</a> for specific behavior. + ## request. See + ## <a href="https://tools.ietf.org/html/rfc3875#section-4.1.18"> + ## RFC3875 section 4.1.18</a> for + ## specific behavior. ## In addition to this, the Rack environment must include these ## Rack-specific variables: - ## <tt>rack.version</tt>:: The Array representing this version of Rack. See Rack::VERSION, that corresponds to the version of this SPEC. - ## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the request URL. + ## <tt>rack.version</tt>:: The Array representing this version of Rack + ## See Rack::VERSION, that corresponds to + ## the version of this SPEC. + + ## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the + ## request URL. + ## <tt>rack.input</tt>:: See below, the input stream. + ## <tt>rack.errors</tt>:: See below, the error stream. - ## <tt>rack.multithread</tt>:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise. - ## <tt>rack.multiprocess</tt>:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise. - ## <tt>rack.run_once</tt>:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar). - ## <tt>rack.hijack?</tt>:: present and true if the server supports connection hijacking. See below, hijacking. - ## <tt>rack.hijack</tt>:: an object responding to #call that must be called at least once before using rack.hijack_io. It is recommended #call return rack.hijack_io as well as setting it in env if necessary. - ## <tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack has received #call, this will contain an object resembling an IO. See hijacking. - ## + + ## <tt>rack.multithread</tt>:: true if the application object may be + ## simultaneously invoked by another thread + ## in the same process, false otherwise. + + ## <tt>rack.multiprocess</tt>:: true if an equivalent application object + ## may be simultaneously invoked by another + ## process, false otherwise. + + ## <tt>rack.run_once</tt>:: true if the server expects + ## (but does not guarantee!) that the + ## application will only be invoked this one + ## time during the life of its containing + ## process. Normally, this will only be true + ## for a server based on CGI + ## (or something similar). + + ## <tt>rack.hijack?</tt>:: present and true if the server supports + ## connection hijacking. See below, hijacking. + + ## <tt>rack.hijack</tt>:: an object responding to #call that must be + ## called at least once before using + ## rack.hijack_io. + ## It is recommended #call return rack.hijack_io + ## as well as setting it in env if necessary. + + ## <tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack + ## has received #call, this will contain + ## an object resembling an IO. See hijacking. ## Additional environment specifications have approved to ## standardized middleware APIs. None of these are required to ## be implemented by the server. - ## <tt>rack.session</tt>:: A hash like interface for storing request session data. + ## <tt>rack.session</tt>:: A hash like interface for storing + ## request session data. ## The store must implement: if session = env['rack.session'] ## store(key, value) (aliased as []=); @@ -218,7 +259,6 @@ module Rack } } - ## ## There are the following restrictions: ## * <tt>rack.version</tt> must be an array of Integers. @@ -311,15 +351,23 @@ module Rack v end - ## * +read+ behaves like IO#read. Its signature is <tt>read([length, [buffer]])</tt>. - ## If given, +length+ must be a non-negative Integer (>= 0) or +nil+, and +buffer+ must - ## be a String and may not be nil. If +length+ is given and not nil, then this method - ## reads at most +length+ bytes from the input stream. If +length+ is not given or nil, - ## then this method reads all data until EOF. - ## When EOF is reached, this method returns nil if +length+ is given and not nil, or "" - ## if +length+ is not given or is nil. - ## If +buffer+ is given, then the read data will be placed into +buffer+ instead of a - ## newly created String object. + ## * +read+ behaves like IO#read. + ## Its signature is <tt>read([length, [buffer]])</tt>. + ## + ## If given, +length+ must be a non-negative Integer (>= 0) or +nil+, + ## and +buffer+ must be a String and may not be nil. + ## + ## If +length+ is given and not nil, then this method reads at most + ## +length+ bytes from the input stream. + ## + ## If +length+ is not given or nil, then this method reads + ## all data until EOF. + ## + ## When EOF is reached, this method returns nil if +length+ is given + ## and not nil, or "" if +length+ is not given or is nil. + ## + ## If +buffer+ is given, then the read data will be placed + ## into +buffer+ instead of a newly created String object. def read(*args) assert("rack.input#read called with too many arguments") { args.size <= 2 diff --git a/lib/rack/lobster.rb b/lib/rack/lobster.rb index d1a7f7bc..195bd945 100644 --- a/lib/rack/lobster.rb +++ b/lib/rack/lobster.rb @@ -32,9 +32,14 @@ module Rack def call(env) req = Request.new(env) if req.GET["flip"] == "left" - lobster = LobsterString.split("\n"). - map { |line| line.ljust(42).reverse }. - join("\n") + lobster = LobsterString.split("\n").map do |line| + line.ljust(42).reverse. + gsub('\\', 'TEMP'). + gsub('/', '\\'). + gsub('TEMP', '/'). + gsub('{','}'). + gsub('(',')') + end.join("\n") href = "?flip=right" elsif req.GET["flip"] == "crash" raise "Lobster crashed" diff --git a/lib/rack/methodoverride.rb b/lib/rack/methodoverride.rb index 449961ce..062f3d67 100644 --- a/lib/rack/methodoverride.rb +++ b/lib/rack/methodoverride.rb @@ -4,13 +4,14 @@ module Rack METHOD_OVERRIDE_PARAM_KEY = "_method".freeze HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE".freeze + ALLOWED_METHODS = ["POST"] def initialize(app) @app = app end def call(env) - if env["REQUEST_METHOD"] == "POST" + if allowed_methods.include?(env["REQUEST_METHOD"]) method = method_override(env) if HTTP_METHODS.include?(method) env["rack.methodoverride.original_method"] = env["REQUEST_METHOD"] @@ -23,9 +24,19 @@ module Rack def method_override(env) req = Request.new(env) - method = req.POST[METHOD_OVERRIDE_PARAM_KEY] || + method = method_override_param(req) || env[HTTP_METHOD_OVERRIDE_HEADER] method.to_s.upcase end + + private + + def allowed_methods + ALLOWED_METHODS + end + + def method_override_param(req) + req.POST[METHOD_OVERRIDE_PARAM_KEY] + end end end diff --git a/lib/rack/mock.rb b/lib/rack/mock.rb index 3ba314e4..3c02c1fe 100644 --- a/lib/rack/mock.rb +++ b/lib/rack/mock.rb @@ -77,9 +77,16 @@ module Rack body.close if body.respond_to?(:close) end + # For historical reasons, we're pinning to RFC 2396. It's easier for users + # and we get support from ruby 1.8 to 2.2 using this method. + def self.parse_uri_rfc2396(uri) + @parser ||= defined?(URI::RFC2396_Parser) ? URI::RFC2396_Parser.new : URI + @parser.parse(uri) + end + # Return the Rack environment used for a request to +uri+. def self.env_for(uri="", opts={}) - uri = URI(uri) + uri = parse_uri_rfc2396(uri) uri.path = "/#{uri.path}" unless uri.path[0] == ?/ env = DEFAULT_ENV.dup diff --git a/lib/rack/multipart.rb b/lib/rack/multipart.rb index d67ff051..7a44c4d4 100644 --- a/lib/rack/multipart.rb +++ b/lib/rack/multipart.rb @@ -9,7 +9,7 @@ module Rack EOL = "\r\n" MULTIPART_BOUNDARY = "AaB03x" - MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n + MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/ CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i DISPPARM = /;\s*(#{TOKEN})=("(?:\\"|[^"])*"|#{TOKEN})/ diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb index fa47fd16..22f9734b 100644 --- a/lib/rack/multipart/parser.rb +++ b/lib/rack/multipart/parser.rb @@ -16,10 +16,10 @@ module Rack content_length = env['CONTENT_LENGTH'] content_length = content_length.to_i if content_length - new($1, io, content_length) + new($1, io, content_length, env) end - def initialize(boundary, io, content_length) + def initialize(boundary, io, content_length, env) @buf = "" if @buf.respond_to? :force_encoding @@ -31,6 +31,7 @@ module Rack @io = io @content_length = content_length @boundary_size = Utils.bytesize(@boundary) + EOL.size + @env = env if @content_length @content_length -= @boundary_size @@ -111,8 +112,12 @@ module Rack filename = get_filename(head) + if name.nil? || name.empty? && filename + name = filename + end + if filename - body = Tempfile.new("RackMultipart") + (@env['rack.tempfiles'] ||= []) << body = Tempfile.new("RackMultipart") body.binmode if body.respond_to?(:binmode) end diff --git a/lib/rack/multipart/uploaded_file.rb b/lib/rack/multipart/uploaded_file.rb index 11932b17..1b56ad75 100644 --- a/lib/rack/multipart/uploaded_file.rb +++ b/lib/rack/multipart/uploaded_file.rb @@ -11,7 +11,7 @@ module Rack raise "#{path} file does not exist" unless ::File.exist?(path) @content_type = content_type @original_filename = ::File.basename(path) - @tempfile = Tempfile.new(@original_filename) + @tempfile = Tempfile.new([@original_filename, ::File.extname(path)]) @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding) @tempfile.binmode if binary FileUtils.copy_file(path, @tempfile.path) @@ -31,4 +31,4 @@ module Rack end end end -end
\ No newline at end of file +end diff --git a/lib/rack/nulllogger.rb b/lib/rack/nulllogger.rb index 77fb637d..2d5a2c97 100644 --- a/lib/rack/nulllogger.rb +++ b/lib/rack/nulllogger.rb @@ -9,10 +9,29 @@ module Rack @app.call(env) end - def info(progname = nil, &block); end + def info(progname = nil, &block); end def debug(progname = nil, &block); end - def warn(progname = nil, &block); end + def warn(progname = nil, &block); end def error(progname = nil, &block); end def fatal(progname = nil, &block); end + def unknown(progname = nil, &block); end + def info? ; end + def debug? ; end + def warn? ; end + def error? ; end + def fatal? ; end + def level ; end + def progname ; end + def datetime_format ; end + def formatter ; end + def sev_threshold ; end + def level=(level); end + def progname=(progname); end + def datetime_format=(datetime_format); end + def formatter=(formatter); end + def sev_threshold=(sev_threshold); end + def close ; end + def add(severity, message = nil, progname = nil, &block); end + def <<(msg); end end end diff --git a/lib/rack/request.rb b/lib/rack/request.rb index 551f7361..4f038384 100644 --- a/lib/rack/request.rb +++ b/lib/rack/request.rb @@ -52,7 +52,7 @@ module Rack return {} if content_type.nil? Hash[*content_type.split(/\s*[;,]\s*/)[1..-1]. collect { |s| s.split('=', 2) }. - map { |k,v| [k.downcase, v] }.flatten] + map { |k,v| [k.downcase, strip_doublequotes(v)] }.flatten] end # The character set of the request body if a "charset" media type @@ -354,12 +354,6 @@ module Rack forwarded_ips = split_ip_addresses(@env['HTTP_X_FORWARDED_FOR']) - if client_ip = @env['HTTP_CLIENT_IP'] - # If forwarded_ips doesn't include the client_ip, it might be an - # ip spoofing attempt, so we ignore HTTP_CLIENT_IP - return client_ip if forwarded_ips.include?(client_ip) - end - return reject_trusted_ip_addresses(forwarded_ips).last || @env["REMOTE_ADDR"] end @@ -377,7 +371,7 @@ module Rack when 'application/json' (qs && qs != '') ? ::Rack::Utils::OkJson.decode(qs) : {} else - Utils.parse_nested_query(qs) + Utils.parse_nested_query(qs, '&') end end @@ -395,5 +389,14 @@ module Rack [attribute, quality] end end + + private + def strip_doublequotes(s) + if s[0] == ?" && s[-1] == ?" + s[1..-2] + else + s + end + end end end diff --git a/lib/rack/response.rb b/lib/rack/response.rb index bd39da3b..12536710 100644 --- a/lib/rack/response.rb +++ b/lib/rack/response.rb @@ -129,6 +129,7 @@ module Rack def forbidden?; status == 403; end def not_found?; status == 404; end def method_not_allowed?; status == 405; end + def i_m_a_teapot?; status == 418; end def unprocessable?; status == 422; end def redirect?; [301, 302, 303, 307].include? status; end diff --git a/lib/rack/server.rb b/lib/rack/server.rb index be7014c6..d2f0b954 100644 --- a/lib/rack/server.rb +++ b/lib/rack/server.rb @@ -1,7 +1,10 @@ require 'optparse' + module Rack + class Server + class Options def parse!(args) options = {} @@ -166,7 +169,7 @@ module Rack # * :Port # the port to bind to (used by supporting Rack::Handler) # * :AccessLog - # webrick acess log options (or supporting Rack::Handler) + # webrick access log options (or supporting Rack::Handler) # * :debug # turn on debug output ($DEBUG = true) # * :warn @@ -202,29 +205,44 @@ module Rack @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config end - def self.logging_middleware - lambda { |server| - server.server.name =~ /CGI/ ? nil : [Rack::CommonLogger, $stderr] - } - end + class << self + def logging_middleware + lambda { |server| + server.server.name =~ /CGI/ ? nil : [Rack::CommonLogger, $stderr] + } + end - def self.middleware - @middleware ||= begin + def default_middleware_by_environment m = Hash.new {|h,k| h[k] = []} - m["deployment"].concat [ + m["deployment"] = [ [Rack::ContentLength], [Rack::Chunked], - logging_middleware + logging_middleware, + [Rack::TempfileReaper] ] - m["development"].concat m["deployment"] + [[Rack::ShowExceptions], [Rack::Lint]] + m["development"] = [ + [Rack::ContentLength], + [Rack::Chunked], + logging_middleware, + [Rack::ShowExceptions], + [Rack::Lint], + [Rack::TempfileReaper] + ] + m end + + # Aliased for backwards-compatibility + alias :middleware :default_middleware_by_environment end - def middleware - self.class.middleware + def default_middleware_by_environment + self.class.default_middleware_by_environment end + # Aliased for backwards-compatibility + alias :middleware :default_middleware_by_environment + def start &blk if options[:warn] $-w = true @@ -304,7 +322,8 @@ module Rack end def build_app(app) - middleware[options[:environment]].reverse_each do |middleware| + middlewares = default_middleware_by_environment[options[:environment]] + middlewares.reverse_each do |middleware| middleware = middleware.call(self) if middleware.respond_to?(:call) next unless middleware klass, *args = middleware @@ -364,4 +383,5 @@ module Rack end end + end diff --git a/lib/rack/showexceptions.rb b/lib/rack/showexceptions.rb index c91ca07c..731aea49 100644 --- a/lib/rack/showexceptions.rb +++ b/lib/rack/showexceptions.rb @@ -28,23 +28,32 @@ module Rack env["rack.errors"].puts(exception_string) env["rack.errors"].flush - if prefers_plain_text?(env) - content_type = "text/plain" - body = [exception_string] - else + if accepts_html?(env) content_type = "text/html" body = pretty(env, e) + else + content_type = "text/plain" + body = exception_string end - [500, - {"Content-Type" => content_type, - "Content-Length" => Rack::Utils.bytesize(body.join).to_s}, - body] + [ + 500, + { + "Content-Type" => content_type, + "Content-Length" => Rack::Utils.bytesize(body).to_s, + }, + [body], + ] + end + + def prefers_plaintext?(env) + !accepts_html(env) end - def prefers_plain_text?(env) - env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" && (!env["HTTP_ACCEPT"] || !env["HTTP_ACCEPT"].include?("text/html")) + def accepts_html?(env) + Rack::Utils.best_q_match(env["HTTP_ACCEPT"], %w[text/html]) end + private :accepts_html? def dump_exception(exception) string = "#{exception.class}: #{exception.message}\n" @@ -85,7 +94,7 @@ module Rack end }.compact - [@template.result(binding)] + @template.result(binding) end def h(obj) # :nodoc: diff --git a/lib/rack/tempfile_reaper.rb b/lib/rack/tempfile_reaper.rb new file mode 100644 index 00000000..1500b06a --- /dev/null +++ b/lib/rack/tempfile_reaper.rb @@ -0,0 +1,22 @@ +require 'rack/body_proxy' + +module Rack + + # Middleware tracks and cleans Tempfiles created throughout a request (i.e. Rack::Multipart) + # Ideas/strategy based on posts by Eric Wong and Charles Oliver Nutter + # https://groups.google.com/forum/#!searchin/rack-devel/temp/rack-devel/brK8eh-MByw/sw61oJJCGRMJ + class TempfileReaper + def initialize(app) + @app = app + end + + def call(env) + env['rack.tempfiles'] ||= [] + status, headers, body = @app.call(env) + body_proxy = BodyProxy.new(body) do + env['rack.tempfiles'].each { |f| f.close! } unless env['rack.tempfiles'].nil? + end + [status, headers, body_proxy] + end + end +end diff --git a/lib/rack/urlmap.rb b/lib/rack/urlmap.rb index d301ce9b..df9e7d6d 100644 --- a/lib/rack/urlmap.rb +++ b/lib/rack/urlmap.rb @@ -48,9 +48,10 @@ module Rack sPort = env['SERVER_PORT'] @mapping.each do |host, location, match, app| - unless hHost == host \ - || sName == host \ - || (!host && (hHost == sName || hHost == sName+':'+sPort)) + unless casecmp?(hHost, host) \ + || casecmp?(sName, host) \ + || (!host && (casecmp?(hHost, sName) || + casecmp?(hHost, sName+':'+sPort))) next end @@ -71,6 +72,19 @@ module Rack env['PATH_INFO'] = path env['SCRIPT_NAME'] = script_name end + + private + def casecmp?(v1, v2) + # if both nil, or they're the same string + return true if v1 == v2 + + # if either are nil... (but they're not the same) + return false if v1.nil? + return false if v2.nil? + + # otherwise check they're not case-insensitive the same + v1.casecmp(v2).zero? + end end end diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb index 6c2bf907..69a96eb9 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -22,6 +22,15 @@ module Rack # applications adopted from all kinds of Ruby libraries. module Utils + # ParameterTypeError is the error that is raised when incoming structural + # parameters (parsed by parse_nested_query) contain conflicting types. + class ParameterTypeError < TypeError; end + + # InvalidParameterError is the error that is raised when incoming structural + # parameters (parsed by parse_nested_query) contain invalid format or byte + # sequence. + class InvalidParameterError < ArgumentError; end + # URI escapes. (CGI style space to +) def escape(s) URI.encode_www_form_component(s) @@ -87,6 +96,11 @@ module Rack end module_function :parse_query + # parse_nested_query expands a query string into structural types. Supported + # types are Arrays, Hashes and basic value types. It is possible to supply + # query strings with parameters of conflicting types, in this case a + # ParameterTypeError is raised. Users are encouraged to return a 400 in this + # case. def parse_nested_query(qs, d = nil) params = KeySpaceConstrainedParams.new @@ -97,9 +111,14 @@ module Rack end return params.to_params_hash + rescue ArgumentError => e + raise InvalidParameterError, e.message end module_function :parse_nested_query + # normalize_params recursively expands parameters into structural types. If + # the structural types represented by two different parameter names are in + # conflict, a ParameterTypeError is raised. def normalize_params(params, name, v = nil) name =~ %r(\A[\[\]]*([^\[\]]+)\]*) k = $1 || '' @@ -113,12 +132,12 @@ module Rack 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) + raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) params[k] << v elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$) child_key = $1 params[k] ||= [] - raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) + raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) if params_hash_type?(params[k].last) && !params[k].last.key?(child_key) normalize_params(params[k].last, child_key, v) else @@ -126,7 +145,7 @@ module Rack end else params[k] ||= params.class.new - raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k]) + raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k]) params[k] = normalize_params(params[k], after, v) end @@ -159,12 +178,12 @@ module Rack when Hash value.map { |k, v| build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k)) - }.join("&") - when String + }.reject(&:empty?).join('&') + when nil + prefix + else raise ArgumentError, "value must be a Hash" if prefix.nil? "#{prefix}=#{escape(value)}" - else - prefix end end module_function :build_nested_query @@ -184,13 +203,14 @@ module Rack def best_q_match(q_value_header, available_mimes) values = q_values(q_value_header) - values.map do |req_mime, quality| - match = available_mimes.first { |am| Rack::Mime.match?(am, req_mime) } + matches = values.map do |req_mime, quality| + match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) } next unless match [match, quality] end.compact.sort_by do |match, quality| (match.split('/', 2).count('*') * -10) + quality - end.last.first + end.last + matches && matches.first end module_function :best_q_match @@ -532,7 +552,11 @@ module Rack hash.keys.each do |key| value = hash[key] if value.kind_of?(self.class) - hash[key] = value.to_params_hash + if value.object_id == self.object_id + hash[key] = hash + else + hash[key] = value.to_params_hash + end elsif value.kind_of?(Array) value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x} end @@ -587,6 +611,7 @@ module Rack 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', @@ -611,7 +636,7 @@ module Rack STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 205 << 304) SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message| - [message.downcase.gsub(/\s|-/, '_').to_sym, code] + [message.downcase.gsub(/\s|-|'/, '_').to_sym, code] }.flatten] def status_code(status) @@ -637,7 +662,7 @@ module Rack part == '..' ? clean.pop : clean << part end - clean.unshift '/' if parts.first.empty? + clean.unshift '/' if parts.empty? || parts.first.empty? ::File.join(*clean) end diff --git a/test/multipart/filename_and_no_name b/test/multipart/filename_and_no_name new file mode 100644 index 00000000..00d58153 --- /dev/null +++ b/test/multipart/filename_and_no_name @@ -0,0 +1,6 @@ +--AaB03x
+Content-Disposition: form-data; filename="file1.txt"
+Content-Type: text/plain
+
+contents
+--AaB03x--
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_deflater.rb b/test/spec_deflater.rb index 6f5137ca..1e921eff 100644 --- a/test/spec_deflater.rb +++ b/test/spec_deflater.rb @@ -6,199 +6,334 @@ require 'rack/mock' require 'zlib' describe Rack::Deflater do - def deflater(app) - Rack::Lint.new Rack::Deflater.new(app) - end - def build_response(status, body, accept_encoding, headers = {}) - body = [body] if body.respond_to? :to_str + def build_response(status, body, accept_encoding, options = {}) + body = [body] if body.respond_to? :to_str app = lambda do |env| - res = [status, {}, body] - res[1]["Content-Type"] = "text/plain" unless res[0] == 304 + res = [status, options['response_headers'] || {}, body] + res[1]['Content-Type'] = 'text/plain' unless res[0] == 304 res end - request = Rack::MockRequest.env_for("", headers.merge("HTTP_ACCEPT_ENCODING" => accept_encoding)) - response = deflater(app).call(request) - return response - end + request = Rack::MockRequest.env_for('', (options['request_headers'] || {}).merge('HTTP_ACCEPT_ENCODING' => accept_encoding)) + deflater = Rack::Lint.new Rack::Deflater.new(app, options['deflater_options'] || {}) - def inflate(buf) - inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS) - inflater.inflate(buf) << inflater.finish + deflater.call(request) end - should "be able to deflate bodies that respond to each" do - body = Object.new - class << body; def each; yield("foo"); yield("bar"); end; end - - response = build_response(200, body, "deflate") - - response[0].should.equal(200) - response[1].should.equal({ - "Content-Encoding" => "deflate", - "Vary" => "Accept-Encoding", - "Content-Type" => "text/plain" - }) - buf = '' - response[2].each { |part| buf << part } - inflate(buf).should.equal("foobar") - end - - should "flush deflated chunks to the client as they become ready" do - body = Object.new - class << body; def each; yield("foo"); yield("bar"); end; end + ## + # Constructs response object and verifies if it yields right results + # + # [expected_status] expected response status, e.g. 200, 304 + # [expected_body] expected response body + # [accept_encoing] what Accept-Encoding header to send and expect, e.g. + # 'deflate' - accepts and expects deflate encoding in response + # { 'gzip' => nil } - accepts gzip but expects no encoding in response + # [options] hash of request options, i.e. + # 'app_status' - what status dummy app should return (may be changed by deflater at some point) + # 'app_body' - what body dummy app should return (may be changed by deflater at some point) + # 'request_headers' - extra reqest headers to be sent + # 'response_headers' - extra response headers to be returned + # 'deflater_options' - options passed to deflater middleware + # [block] useful for doing some extra verification + def verify(expected_status, expected_body, accept_encoding, options = {}, &block) + accept_encoding, expected_encoding = if accept_encoding.kind_of?(Hash) + [accept_encoding.keys.first, accept_encoding.values.first] + else + [accept_encoding, accept_encoding.dup] + end - response = build_response(200, body, "deflate") + # build response + status, headers, body = build_response( + options['app_status'] || expected_status, + options['app_body'] || expected_body, + accept_encoding, + options + ) + + # verify status + status.should.equal(expected_status) + + # verify body + unless options['skip_body_verify'] + body_text = '' + body.each { |part| body_text << part } + + deflated_body = case expected_encoding + when 'deflate' + inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS) + inflater.inflate(body_text) << inflater.finish + when 'gzip' + io = StringIO.new(body_text) + gz = Zlib::GzipReader.new(io) + tmp = gz.read + gz.close + tmp + else + body_text + end + + deflated_body.should.equal(expected_body) + end - response[0].should.equal(200) - response[1].should.equal({ - "Content-Encoding" => "deflate", - "Vary" => "Accept-Encoding", - "Content-Type" => "text/plain" - }) - buf = [] - inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS) - response[2].each { |part| buf << inflater.inflate(part) } - buf << inflater.finish - buf.delete_if { |part| part.empty? } - buf.join.should.equal("foobar") + # yield full response verification + yield(status, headers, body) if block_given? end - # TODO: This is really just a special case of the above... - should "be able to deflate String bodies" do - response = build_response(200, "Hello world!", "deflate") + should 'be able to deflate bodies that respond to each' do + app_body = Object.new + class << app_body; def each; yield('foo'); yield('bar'); end; end - response[0].should.equal(200) - response[1].should.equal({ - "Content-Encoding" => "deflate", - "Vary" => "Accept-Encoding", - "Content-Type" => "text/plain" - }) - buf = '' - response[2].each { |part| buf << part } - inflate(buf).should.equal("Hello world!") + verify(200, 'foobar', 'deflate', { 'app_body' => app_body }) do |status, headers, body| + headers.should.equal({ + 'Content-Encoding' => 'deflate', + 'Vary' => 'Accept-Encoding', + 'Content-Type' => 'text/plain' + }) + end end - should "be able to gzip bodies that respond to each" do - body = Object.new - class << body; def each; yield("foo"); yield("bar"); end; end + should 'flush deflated chunks to the client as they become ready' do + app_body = Object.new + class << app_body; def each; yield('foo'); yield('bar'); end; end - response = build_response(200, body, "gzip") + verify(200, app_body, 'deflate', { 'skip_body_verify' => true }) do |status, headers, body| + headers.should.equal({ + 'Content-Encoding' => 'deflate', + 'Vary' => 'Accept-Encoding', + 'Content-Type' => 'text/plain' + }) - response[0].should.equal(200) - response[1].should.equal({ - "Content-Encoding" => "gzip", - "Vary" => "Accept-Encoding", - "Content-Type" => "text/plain" - }) + buf = [] + inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS) + body.each { |part| buf << inflater.inflate(part) } + buf << inflater.finish - buf = '' - response[2].each { |part| buf << part } - io = StringIO.new(buf) - gz = Zlib::GzipReader.new(io) - gz.read.should.equal("foobar") - gz.close + buf.delete_if { |part| part.empty? }.join.should.equal('foobar') + end end - should "flush gzipped chunks to the client as they become ready" do - body = Object.new - class << body; def each; yield("foo"); yield("bar"); end; end + # TODO: This is really just a special case of the above... + should 'be able to deflate String bodies' do + verify(200, 'Hello world!', 'deflate') do |status, headers, body| + headers.should.equal({ + 'Content-Encoding' => 'deflate', + 'Vary' => 'Accept-Encoding', + 'Content-Type' => 'text/plain' + }) + end + end - response = build_response(200, body, "gzip") + should 'be able to gzip bodies that respond to each' do + app_body = Object.new + class << app_body; def each; yield('foo'); yield('bar'); end; end - response[0].should.equal(200) - response[1].should.equal({ - "Content-Encoding" => "gzip", - "Vary" => "Accept-Encoding", - "Content-Type" => "text/plain" - }) - buf = [] - inflater = Zlib::Inflate.new(Zlib::MAX_WBITS + 32) - response[2].each { |part| buf << inflater.inflate(part) } - buf << inflater.finish - buf.delete_if { |part| part.empty? } - buf.join.should.equal("foobar") + verify(200, 'foobar', 'gzip', { 'app_body' => app_body }) do |status, headers, body| + headers.should.equal({ + 'Content-Encoding' => 'gzip', + 'Vary' => 'Accept-Encoding', + 'Content-Type' => 'text/plain' + }) + end end - should "be able to fallback to no deflation" do - response = build_response(200, "Hello world!", "superzip") + should 'flush gzipped chunks to the client as they become ready' do + app_body = Object.new + class << app_body; def each; yield('foo'); yield('bar'); end; end + + verify(200, app_body, 'gzip', { 'skip_body_verify' => true }) do |status, headers, body| + headers.should.equal({ + 'Content-Encoding' => 'gzip', + 'Vary' => 'Accept-Encoding', + 'Content-Type' => 'text/plain' + }) + + buf = [] + inflater = Zlib::Inflate.new(Zlib::MAX_WBITS + 32) + body.each { |part| buf << inflater.inflate(part) } + buf << inflater.finish - response[0].should.equal(200) - response[1].should.equal({ "Vary" => "Accept-Encoding", "Content-Type" => "text/plain" }) - response[2].to_enum.to_a.should.equal(["Hello world!"]) + buf.delete_if { |part| part.empty? }.join.should.equal('foobar') + end end - should "be able to skip when there is no response entity body" do - response = build_response(304, [], "gzip") + should 'be able to fallback to no deflation' do + verify(200, 'Hello world!', 'superzip') do |status, headers, body| + headers.should.equal({ + 'Vary' => 'Accept-Encoding', + 'Content-Type' => 'text/plain' + }) + end + end - response[0].should.equal(304) - response[1].should.equal({}) - response[2].to_enum.to_a.should.equal([]) + should 'be able to skip when there is no response entity body' do + verify(304, '', { 'gzip' => nil }, { 'app_body' => [] }) do |status, headers, body| + headers.should.equal({}) + end end - should "handle the lack of an acceptable encoding" do - response1 = build_response(200, "Hello world!", "identity;q=0", "PATH_INFO" => "/") - response1[0].should.equal(406) - response1[1].should.equal({"Content-Type" => "text/plain", "Content-Length" => "71"}) - response1[2].to_enum.to_a.should.equal(["An acceptable encoding for the requested resource / could not be found."]) + should 'handle the lack of an acceptable encoding' do + app_body = 'Hello world!' + not_found_body1 = 'An acceptable encoding for the requested resource / could not be found.' + not_found_body2 = 'An acceptable encoding for the requested resource /foo/bar could not be found.' + options1 = { + 'app_status' => 200, + 'app_body' => app_body, + 'request_headers' => { + 'PATH_INFO' => '/' + } + } + options2 = { + 'app_status' => 200, + 'app_body' => app_body, + 'request_headers' => { + 'PATH_INFO' => '/foo/bar' + } + } + + verify(406, not_found_body1, 'identity;q=0', options1) do |status, headers, body| + headers.should.equal({ + 'Content-Type' => 'text/plain', + 'Content-Length' => not_found_body1.length.to_s + }) + end - response2 = build_response(200, "Hello world!", "identity;q=0", "SCRIPT_NAME" => "/foo", "PATH_INFO" => "/bar") - response2[0].should.equal(406) - response2[1].should.equal({"Content-Type" => "text/plain", "Content-Length" => "78"}) - response2[2].to_enum.to_a.should.equal(["An acceptable encoding for the requested resource /foo/bar could not be found."]) + verify(406, not_found_body2, 'identity;q=0', options2) do |status, headers, body| + headers.should.equal({ + 'Content-Type' => 'text/plain', + 'Content-Length' => not_found_body2.length.to_s + }) + end end - should "handle gzip response with Last-Modified header" do + should 'handle gzip response with Last-Modified header' do last_modified = Time.now.httpdate + options = { + 'response_headers' => { + 'Content-Type' => 'text/plain', + 'Last-Modified' => last_modified + } + } + + verify(200, 'Hello World!', 'gzip', options) do |status, headers, body| + headers.should.equal({ + 'Content-Encoding' => 'gzip', + 'Vary' => 'Accept-Encoding', + 'Last-Modified' => last_modified, + 'Content-Type' => 'text/plain' + }) + end + end - app = lambda { |env| [200, { "Content-Type" => "text/plain", "Last-Modified" => last_modified }, ["Hello World!"]] } - request = Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => "gzip") - response = deflater(app).call(request) + should 'do nothing when no-transform Cache-Control directive present' do + options = { + 'response_headers' => { + 'Content-Type' => 'text/plain', + 'Cache-Control' => 'no-transform' + } + } + verify(200, 'Hello World!', { 'gzip' => nil }, options) do |status, headers, body| + headers.should.not.include 'Content-Encoding' + end + end + + should 'do nothing when Content-Encoding already present' do + options = { + 'response_headers' => { + 'Content-Type' => 'text/plain', + 'Content-Encoding' => 'gzip' + } + } + verify(200, 'Hello World!', { 'gzip' => nil }, options) + end - response[0].should.equal(200) - response[1].should.equal({ - "Content-Encoding" => "gzip", - "Vary" => "Accept-Encoding", - "Last-Modified" => last_modified, - "Content-Type" => "text/plain" - }) + should 'deflate when Content-Encoding is identity' do + options = { + 'response_headers' => { + 'Content-Type' => 'text/plain', + 'Content-Encoding' => 'identity' + } + } + verify(200, 'Hello World!', 'deflate', options) + end - buf = '' - response[2].each { |part| buf << part } - io = StringIO.new(buf) - gz = Zlib::GzipReader.new(io) - gz.read.should.equal("Hello World!") - gz.close + should "deflate if content-type matches :include" do + options = { + 'response_headers' => { + 'Content-Type' => 'text/plain' + }, + 'deflater_options' => { + :include => %w(text/plain) + } + } + verify(200, 'Hello World!', 'gzip', options) end - should "do nothing when no-transform Cache-Control directive present" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Cache-Control' => 'no-transform'}, ['Hello World!']] } - request = Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => "gzip") - response = deflater(app).call(request) + should "deflate if content-type is included it :include" do + options = { + 'response_headers' => { + 'Content-Type' => 'text/plain; charset=us-ascii' + }, + 'deflater_options' => { + :include => %w(text/plain) + } + } + verify(200, 'Hello World!', 'gzip', options) + end - response[0].should.equal(200) - response[1].should.not.include "Content-Encoding" - response[2].to_enum.to_a.join.should.equal("Hello World!") + should "not deflate if content-type is not set but given in :include" do + options = { + 'deflater_options' => { + :include => %w(text/plain) + } + } + verify(304, 'Hello World!', { 'gzip' => nil }, options) end - should "do nothing when Content-Encoding already present" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Content-Encoding' => 'gzip'}, ['Hello World!']] } - request = Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => "gzip") - response = deflater(app).call(request) + should "not deflate if content-type do not match :include" do + options = { + 'response_headers' => { + 'Content-Type' => 'text/plain' + }, + 'deflater_options' => { + :include => %w(text/json) + } + } + verify(200, 'Hello World!', { 'gzip' => nil }, options) + end - response[0].should.equal(200) - response[2].to_enum.to_a.join.should.equal("Hello World!") + should "deflate response if :if lambda evaluates to true" do + options = { + 'deflater_options' => { + :if => lambda { |env, status, headers, body| true } + } + } + verify(200, 'Hello World!', 'deflate', options) end - should "deflate when Content-Encoding is identity" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Content-Encoding' => 'identity'}, ['Hello World!']] } - request = Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => "deflate") - response = deflater(app).call(request) + should "not deflate if :if lambda evaluates to false" do + options = { + 'deflater_options' => { + :if => lambda { |env, status, headers, body| false } + } + } + verify(200, 'Hello World!', { 'gzip' => nil }, options) + end - response[0].should.equal(200) - buf = '' - response[2].each { |part| buf << part } - inflate(buf).should.equal("Hello World!") + should "check for Content-Length via :if" do + body = 'Hello World!' + body_len = body.length + options = { + 'response_headers' => { + 'Content-Length' => body_len.to_s + }, + 'deflater_options' => { + :if => lambda { |env, status, headers, body| + headers['Content-Length'].to_i >= body_len + } + } + } + + verify(200, body, 'gzip', options) end end diff --git a/test/spec_etag.rb b/test/spec_etag.rb index b8b8b637..c075d9d0 100644 --- a/test/spec_etag.rb +++ b/test/spec_etag.rb @@ -21,13 +21,13 @@ describe Rack::ETag do should "set ETag if none is set if status is 200" do app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] } response = etag(app).call(request) - response[1]['ETag'].should.equal "\"65a8e27d8879283831b664bd8b7f0ad4\"" + response[1]['ETag'].should.equal "W/\"65a8e27d8879283831b664bd8b7f0ad4\"" end should "set ETag if none is set if status is 201" do app = lambda { |env| [201, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] } response = etag(app).call(request) - response[1]['ETag'].should.equal "\"65a8e27d8879283831b664bd8b7f0ad4\"" + response[1]['ETag'].should.equal "W/\"65a8e27d8879283831b664bd8b7f0ad4\"" end should "set Cache-Control to 'max-age=0, private, must-revalidate' (default) if none is set" do diff --git a/test/spec_lobster.rb b/test/spec_lobster.rb index 56a54795..c6ec2b06 100644 --- a/test/spec_lobster.rb +++ b/test/spec_lobster.rb @@ -47,7 +47,7 @@ describe Rack::Lobster do should "be flippable" do res = lobster.get("/?flip=left") res.should.be.ok - res.body.should.include "(,,,(,,(,(" + res.body.should.include "),,,),,),)" end should "provide crashing for testing purposes" do diff --git a/test/spec_mock.rb b/test/spec_mock.rb index f49b1961..3ebd7776 100644 --- a/test/spec_mock.rb +++ b/test/spec_mock.rb @@ -30,6 +30,14 @@ describe Rack::MockRequest do env.should.include "rack.version" end + should "return an environment with a path" do + env = Rack::MockRequest.env_for("http://www.example.com/parse?location[]=1&location[]=2&age_group[]=2") + env["QUERY_STRING"].should.equal "location[]=1&location[]=2&age_group[]=2" + env["PATH_INFO"].should.equal "/parse" + env.should.be.kind_of Hash + env.should.include "rack.version" + end + should "provide sensible defaults" do res = Rack::MockRequest.new(app).request diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb index 069dc4d2..2acb6e0d 100644 --- a/test/spec_multipart.rb +++ b/test/spec_multipart.rb @@ -153,6 +153,18 @@ describe Rack::Multipart do params["files"][:tempfile].read.should.equal "contents" end + should "parse multipart upload with text file with no name field" do + env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_and_no_name)) + params = Rack::Multipart.parse_multipart(env) + params["file1.txt"][:type].should.equal "text/plain" + params["file1.txt"][:filename].should.equal "file1.txt" + params["file1.txt"][:head].should.equal "Content-Disposition: form-data; " + + "filename=\"file1.txt\"\r\n" + + "Content-Type: text/plain\r\n" + params["file1.txt"][:name].should.equal "file1.txt" + params["file1.txt"][:tempfile].read.should.equal "contents" + end + should "parse multipart upload with nested parameters" do env = Rack::MockRequest.env_for("/", multipart_fixture(:nested)) params = Rack::Multipart.parse_multipart(env) @@ -502,4 +514,28 @@ contents\r params["file"][:filename].should.equal('long' * 100) end + should "support mixed case metadata" do + file = multipart_file(:text) + data = File.open(file, 'rb') { |io| io.read } + + type = "Multipart/Form-Data; Boundary=AaB03x" + length = data.respond_to?(:bytesize) ? data.bytesize : data.size + + e = { "CONTENT_TYPE" => type, + "CONTENT_LENGTH" => length.to_s, + :input => StringIO.new(data) } + + env = Rack::MockRequest.env_for("/", e) + params = Rack::Multipart.parse_multipart(env) + params["submit-name"].should.equal "Larry" + params["submit-name-with-content"].should.equal "Berry" + params["files"][:type].should.equal "text/plain" + params["files"][:filename].should.equal "file1.txt" + params["files"][:head].should.equal "Content-Disposition: form-data; " + + "name=\"files\"; filename=\"file1.txt\"\r\n" + + "Content-Type: text/plain\r\n" + params["files"][:name].should.equal "files" + params["files"][:tempfile].read.should.equal "contents" + end + end diff --git a/test/spec_request.rb b/test/spec_request.rb index 7a6aa744..c4a7400a 100644 --- a/test/spec_request.rb +++ b/test/spec_request.rb @@ -130,6 +130,14 @@ describe Rack::Request do req.params.should.equal "foo" => "bar", "quux" => "bla" end + should "not truncate query strings containing semi-colons #543" do + req = Rack::Request.new(Rack::MockRequest.env_for("/?foo=bar&quux=b;la")) + req.query_string.should.equal "foo=bar&quux=b;la" + req.GET.should.equal "foo" => "bar", "quux" => "b;la" + req.POST.should.be.empty + req.params.should.equal "foo" => "bar", "quux" => "b;la" + end + should "limit the keys from the GET query string" do env = Rack::MockRequest.env_for("/?foo=bar") @@ -143,7 +151,7 @@ describe Rack::Request do end should "limit the key size per nested params hash" do - nested_query = Rack::MockRequest.env_for("/?foo[bar][baz][qux]=1") + nested_query = Rack::MockRequest.env_for("/?foo%5Bbar%5D%5Bbaz%5D%5Bqux%5D=1") plain_query = Rack::MockRequest.env_for("/?foo_bar__baz__qux_=1") old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 3 @@ -169,6 +177,18 @@ describe Rack::Request do req.params.should.equal req.GET.merge(req.POST) end + should "raise if input params has invalid %-encoding" do + mr = Rack::MockRequest.env_for("/?foo=quux", + "REQUEST_METHOD" => 'POST', + :input => "a%=1" + ) + req = Rack::Request.new mr + + lambda { req.POST }. + should.raise(Rack::Utils::InvalidParameterError). + message.should.equal "invalid %-encoding (a%)" + end + should "raise if rack.input is missing" do req = Rack::Request.new({}) lambda { req.POST }.should.raise(RuntimeError) @@ -613,7 +633,7 @@ describe Rack::Request do should "handle multiple media type parameters" do req = Rack::Request.new \ Rack::MockRequest.env_for("/", - "CONTENT_TYPE" => 'text/plain; foo=BAR,baz=bizzle dizzle;BLING=bam') + "CONTENT_TYPE" => 'text/plain; foo=BAR,baz=bizzle dizzle;BLING=bam;blong="boo";zump="zoo\"o";weird=lol"') req.should.not.be.form_data req.media_type_params.should.include 'foo' req.media_type_params['foo'].should.equal 'BAR' @@ -622,6 +642,9 @@ describe Rack::Request do req.media_type_params.should.not.include 'BLING' req.media_type_params.should.include 'bling' req.media_type_params['bling'].should.equal 'bam' + req.media_type_params['blong'].should.equal 'boo' + req.media_type_params['zump'].should.equal 'zoo\"o' + req.media_type_params['weird'].should.equal 'lol"' end should "parse with junk before boundry" do @@ -750,6 +773,31 @@ EOF req.POST["mean"][:tempfile].read.should.equal "--AaB03xha" end + should "record tempfiles from multipart form data in env[rack.tempfiles]" do + input = <<EOF +--AaB03x\r +content-disposition: form-data; name="fileupload"; filename="foo.jpg"\r +Content-Type: image/jpeg\r +Content-Transfer-Encoding: base64\r +\r +/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r +--AaB03x\r +content-disposition: form-data; name="fileupload"; filename="bar.jpg"\r +Content-Type: image/jpeg\r +Content-Transfer-Encoding: base64\r +\r +/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r +--AaB03x--\r +EOF + env = Rack::MockRequest.env_for("/", + "CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x", + "CONTENT_LENGTH" => input.size, + :input => input) + req = Rack::Request.new(env) + req.params + env['rack.tempfiles'].size.should.equal(2) + end + should "detect invalid multipart form data" do input = <<EOF --AaB03x\r @@ -1047,12 +1095,6 @@ EOF 'HTTP_CLIENT_IP' => '1.1.1.1' res.body.should.equal '1.1.1.1' - # Spoofing attempt - res = mock.get '/', - 'HTTP_X_FORWARDED_FOR' => '1.1.1.1', - 'HTTP_CLIENT_IP' => '2.2.2.2' - res.body.should.equal '1.1.1.1' - res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '8.8.8.8, 9.9.9.9' res.body.should.equal '9.9.9.9' @@ -1071,6 +1113,24 @@ EOF res.body.should.equal '3.4.5.6' end + should "not allow IP spoofing via Client-IP and X-Forwarded-For headers" do + mock = Rack::MockRequest.new(Rack::Lint.new(ip_app)) + + # IP Spoofing attempt: + # Client sends X-Forwarded-For: 6.6.6.6 + # Client-IP: 6.6.6.6 + # Load balancer adds X-Forwarded-For: 2.2.2.3, 192.168.0.7 + # App receives: X-Forwarded-For: 6.6.6.6 + # X-Forwarded-For: 2.2.2.3, 192.168.0.7 + # Client-IP: 6.6.6.6 + # Rack env: HTTP_X_FORWARDED_FOR: '6.6.6.6, 2.2.2.3, 192.168.0.7' + # HTTP_CLIENT_IP: '6.6.6.6' + res = mock.get '/', + 'HTTP_X_FORWARDED_FOR' => '6.6.6.6, 2.2.2.3, 192.168.0.7', + 'HTTP_CLIENT_IP' => '6.6.6.6' + res.body.should.equal '2.2.2.3' + end + should "regard local addresses as proxies" do req = Rack::Request.new(Rack::MockRequest.env_for("/")) req.trusted_proxy?('127.0.0.1').should.equal 0 @@ -1126,7 +1186,7 @@ EOF end should "raise TypeError every time if request parameters are broken" do - broken_query = Rack::MockRequest.env_for("/?foo[]=0&foo[bar]=1") + broken_query = Rack::MockRequest.env_for("/?foo%5B%5D=0&foo%5Bbar%5D=1") req = Rack::Request.new(broken_query) lambda{req.GET}.should.raise(TypeError) lambda{req.params}.should.raise(TypeError) diff --git a/test/spec_response.rb b/test/spec_response.rb index 031488bb..6b13c0c9 100644 --- a/test/spec_response.rb +++ b/test/spec_response.rb @@ -251,6 +251,11 @@ describe Rack::Response do res.should.be.client_error res.should.be.method_not_allowed + res.status = 418 + res.should.not.be.successful + res.should.be.client_error + res.should.be.i_m_a_teapot + res.status = 422 res.should.not.be.successful res.should.be.client_error diff --git a/test/spec_server.rb b/test/spec_server.rb index 44d4bcbb..01b4f562 100644 --- a/test/spec_server.rb +++ b/test/spec_server.rb @@ -30,14 +30,24 @@ describe Rack::Server do should "not include Rack::Lint in deployment or none environments" do server = Rack::Server.new(:app => 'foo') - server.middleware['deployment'].flatten.should.not.include(Rack::Lint) - server.middleware['none'].flatten.should.not.include(Rack::Lint) + server.default_middleware_by_environment['deployment'].flatten.should.not.include(Rack::Lint) + server.default_middleware_by_environment['none'].flatten.should.not.include(Rack::Lint) end should "not include Rack::ShowExceptions in deployment or none environments" do server = Rack::Server.new(:app => 'foo') - server.middleware['deployment'].flatten.should.not.include(Rack::ShowExceptions) - server.middleware['none'].flatten.should.not.include(Rack::ShowExceptions) + server.default_middleware_by_environment['deployment'].flatten.should.not.include(Rack::ShowExceptions) + server.default_middleware_by_environment['none'].flatten.should.not.include(Rack::ShowExceptions) + end + + should "always return an empty array for unknown environments" do + server = Rack::Server.new(:app => 'foo') + server.default_middleware_by_environment['production'].should.equal [] + end + + should "include Rack::TempfileReaper in deployment environment" do + server = Rack::Server.new(:app => 'foo') + server.middleware['deployment'].flatten.should.include(Rack::TempfileReaper) end should "support CGI" do @@ -53,7 +63,7 @@ describe Rack::Server do should "not force any middleware under the none configuration" do server = Rack::Server.new(:app => 'foo') - server.middleware['none'].should.be.empty + server.default_middleware_by_environment['none'].should.be.empty end should "use a full path to the pidfile" do diff --git a/test/spec_showexceptions.rb b/test/spec_showexceptions.rb index bdd5ce5b..7d50c59f 100644 --- a/test/spec_showexceptions.rb +++ b/test/spec_showexceptions.rb @@ -16,7 +16,7 @@ describe Rack::ShowExceptions do )) lambda{ - res = req.get("/") + res = req.get("/", "HTTP_ACCEPT" => "text/html") }.should.not.raise res.should.be.a.server_error @@ -26,7 +26,7 @@ describe Rack::ShowExceptions do res.should =~ /ShowExceptions/ end - it "responds with plain text on AJAX requests accepting anything but HTML" do + it "responds with HTML only to requests accepting HTML" do res = nil req = Rack::MockRequest.new( @@ -34,39 +34,32 @@ describe Rack::ShowExceptions do lambda{|env| raise RuntimeError, "It was never supposed to work" } )) - lambda{ - res = req.get("/", "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest") - }.should.not.raise - - res.should.be.a.server_error - res.status.should.equal 500 - - res.content_type.should.equal "text/plain" - - res.body.should.include "RuntimeError: It was never supposed to work\n" - res.body.should.include __FILE__ - end - - it "responds with HTML on AJAX requests accepting HTML" do - res = nil - - req = Rack::MockRequest.new( - show_exceptions( - lambda{|env| raise RuntimeError, "It was never supposed to work" } - )) - - lambda{ - res = req.get("/", "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", "HTTP_ACCEPT" => "text/html") - }.should.not.raise - - res.should.be.a.server_error - res.status.should.equal 500 - - res.content_type.should.equal "text/html" - - res.body.should.include "RuntimeError" - res.body.should.include "It was never supposed to work" - res.body.should.include Rack::Utils.escape_html(__FILE__) + [ + # Serve text/html when the client accepts text/html + ["text/html", ["/", {"HTTP_ACCEPT" => "text/html"}]], + ["text/html", ["/", {"HTTP_ACCEPT" => "*/*"}]], + # Serve text/plain when the client does not accept text/html + ["text/plain", ["/"]], + ["text/plain", ["/", {"HTTP_ACCEPT" => "application/json"}]] + ].each do |exmime, rargs| + lambda{ + res = req.get(*rargs) + }.should.not.raise + + res.should.be.a.server_error + res.status.should.equal 500 + + res.content_type.should.equal exmime + + res.body.should.include "RuntimeError" + res.body.should.include "It was never supposed to work" + + if exmime == "text/html" + res.body.should.include '</html>' + else + res.body.should.not.include '</html>' + end + end end it "handles exceptions without a backtrace" do @@ -79,7 +72,7 @@ describe Rack::ShowExceptions do ) lambda{ - res = req.get("/") + res = req.get("/", "HTTP_ACCEPT" => "text/html") }.should.not.raise res.should.be.a.server_error diff --git a/test/spec_tempfile_reaper.rb b/test/spec_tempfile_reaper.rb new file mode 100644 index 00000000..ac39d878 --- /dev/null +++ b/test/spec_tempfile_reaper.rb @@ -0,0 +1,63 @@ +require 'rack/tempfile_reaper' +require 'rack/lint' +require 'rack/mock' + +describe Rack::TempfileReaper do + class MockTempfile + attr_reader :closed + + def initialize + @closed = false + end + + def close! + @closed = true + end + end + + before do + @env = Rack::MockRequest.env_for + end + + def call(app) + Rack::Lint.new(Rack::TempfileReaper.new(app)).call(@env) + end + + should 'do nothing (i.e. not bomb out) without env[rack.tempfiles]' do + app = lambda { |_| [200, {}, ['Hello, World!']] } + response = call(app) + response[2].close + response[0].should.equal(200) + end + + should 'close env[rack.tempfiles] when body is closed' do + tempfile1, tempfile2 = MockTempfile.new, MockTempfile.new + @env['rack.tempfiles'] = [ tempfile1, tempfile2 ] + app = lambda { |_| [200, {}, ['Hello, World!']] } + call(app)[2].close + tempfile1.closed.should.equal true + tempfile2.closed.should.equal true + end + + should 'initialize env[rack.tempfiles] when not already present' do + tempfile = MockTempfile.new + app = lambda do |env| + env['rack.tempfiles'] << tempfile + [200, {}, ['Hello, World!']] + end + call(app)[2].close + tempfile.closed.should.equal true + end + + should 'append env[rack.tempfiles] when already present' do + tempfile1, tempfile2 = MockTempfile.new, MockTempfile.new + @env['rack.tempfiles'] = [ tempfile1 ] + app = lambda do |env| + env['rack.tempfiles'] << tempfile2 + [200, {}, ['Hello, World!']] + end + call(app)[2].close + tempfile1.closed.should.equal true + tempfile2.closed.should.equal true + end +end diff --git a/test/spec_urlmap.rb b/test/spec_urlmap.rb index 316c7254..2ef41cdc 100644 --- a/test/spec_urlmap.rb +++ b/test/spec_urlmap.rb @@ -210,4 +210,27 @@ describe Rack::URLMap do res["X-PathInfo"].should.equal "/http://example.org/bar" res["X-ScriptName"].should.equal "" end + + should "not be case sensitive with hosts" do + map = Rack::Lint.new(Rack::URLMap.new("http://example.org/" => lambda { |env| + [200, + { "Content-Type" => "text/plain", + "X-Position" => "root", + "X-PathInfo" => env["PATH_INFO"], + "X-ScriptName" => env["SCRIPT_NAME"] + }, [""]]} + )) + + res = Rack::MockRequest.new(map).get("http://example.org/") + res.should.be.ok + res["X-Position"].should.equal "root" + res["X-PathInfo"].should.equal "/" + res["X-ScriptName"].should.equal "" + + res = Rack::MockRequest.new(map).get("http://EXAMPLE.ORG/") + res.should.be.ok + res["X-Position"].should.equal "root" + res["X-PathInfo"].should.equal "/" + res["X-ScriptName"].should.equal "" + end end diff --git a/test/spec_utils.rb b/test/spec_utils.rb index c3867965..06ed5636 100644 --- a/test/spec_utils.rb +++ b/test/spec_utils.rb @@ -123,6 +123,17 @@ describe Rack::Utils do Rack::Utils.parse_query(",foo=bar;,", ";,").should.equal "foo" => "bar" end + should "not create infinite loops with cycle structures" do + ex = { "foo" => nil } + ex["foo"] = ex + + params = Rack::Utils::KeySpaceConstrainedParams.new + params['foo'] = params + lambda { + params.to_params_hash.to_s.should.equal ex.to_s + }.should.not.raise + end + should "parse nested query strings correctly" do Rack::Utils.parse_nested_query("foo"). should.equal "foo" => nil @@ -202,16 +213,22 @@ describe Rack::Utils do should.equal "x" => {"y" => [{"z" => "1", "w" => "a"}, {"z" => "2", "w" => "3"}]} lambda { Rack::Utils.parse_nested_query("x[y]=1&x[y]z=2") }. - should.raise(TypeError). + should.raise(Rack::Utils::ParameterTypeError). message.should.equal "expected Hash (got String) for param `y'" lambda { Rack::Utils.parse_nested_query("x[y]=1&x[]=1") }. - should.raise(TypeError). + should.raise(Rack::Utils::ParameterTypeError). message.should.match(/expected Array \(got [^)]*\) for param `x'/) lambda { Rack::Utils.parse_nested_query("x[y]=1&x[y][][w]=2") }. - should.raise(TypeError). + should.raise(Rack::Utils::ParameterTypeError). message.should.equal "expected Array (got String) for param `y'" + + if RUBY_VERSION.to_f > 1.9 + lambda { Rack::Utils.parse_nested_query("foo%81E=1") }. + should.raise(Rack::Utils::InvalidParameterError). + message.should.equal "invalid byte sequence in UTF-8" + end end should "build query strings correctly" do @@ -231,6 +248,8 @@ describe Rack::Utils do Rack::Utils.build_nested_query("foo" => "1", "bar" => "2"). should.be equal_query_to("foo=1&bar=2") + Rack::Utils.build_nested_query("foo" => 1, "bar" => 2). + should.be equal_query_to("foo=1&bar=2") Rack::Utils.build_nested_query("my weird field" => "q1!2\"'w$5&7/z8)?"). should.be equal_query_to("my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F") @@ -240,6 +259,14 @@ describe Rack::Utils do should.equal "foo[]=" Rack::Utils.build_nested_query("foo" => ["bar"]). should.equal "foo[]=bar" + Rack::Utils.build_nested_query('foo' => []). + should.equal '' + Rack::Utils.build_nested_query('foo' => {}). + should.equal '' + Rack::Utils.build_nested_query('foo' => 'bar', 'baz' => []). + should.equal 'foo=bar' + Rack::Utils.build_nested_query('foo' => 'bar', 'baz' => {}). + should.equal 'foo=bar' # The ordering of the output query string is unpredictable with 1.8's # unordered hash. Test that build_nested_query performs the inverse @@ -300,9 +327,15 @@ describe Rack::Utils do # Higher quality matches are preferred Rack::Utils.best_q_match("text/*;q=0.5,text/plain;q=1.0", %w[text/plain text/html]).should.equal "text/plain" + # Respect requested content type + Rack::Utils.best_q_match("application/json", %w[application/vnd.lotus-1-2-3 application/json]).should.equal "application/json" + # All else equal, the available mimes are preferred in order Rack::Utils.best_q_match("text/*", %w[text/html text/plain]).should.equal "text/html" Rack::Utils.best_q_match("text/plain,text/html", %w[text/html text/plain]).should.equal "text/html" + + # When there are no matches, return nil: + Rack::Utils.best_q_match("application/json", %w[text/html text/plain]).should.equal nil end should "escape html entities [&><'\"/]" do @@ -403,6 +436,10 @@ describe Rack::Utils do should "not clean directory traversal with encoded periods" do Rack::Utils.clean_path_info("/%2E%2E/README").should.equal "/%2E%2E/README" end + + should "clean slash only paths" do + Rack::Utils.clean_path_info("/").should.equal "/" + end end describe Rack::Utils, "byte_range" do diff --git a/test/spec_webrick.rb b/test/spec_webrick.rb index b29a82d5..497bfe20 100644 --- a/test/spec_webrick.rb +++ b/test/spec_webrick.rb @@ -162,5 +162,23 @@ describe Rack::Handler::WEBrick do } end + should "produce correct HTTP semantics with and without app chunking" do + @server.mount "/chunked", Rack::Handler::WEBrick, + Rack::Lint.new(lambda{ |req| + [ + 200, + {"Transfer-Encoding" => "chunked"}, + ["7\r\nchunked\r\n0\r\n\r\n"] + ] + }) + + Net::HTTP.start(@host, @port){ |http| + res = http.get("/chunked") + res["Transfer-Encoding"].should.equal "chunked" + res["Content-Length"].should.equal nil + res.body.should.equal "chunked" + } + end + @server.shutdown end |