diff options
79 files changed, 1507 insertions, 402 deletions
diff --git a/.travis.yml b/.travis.yml index fa8db9d0..016b8829 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,31 @@ -before_install: sudo apt-get install lighttpd libfcgi-dev libmemcache-dev memcached -install: +language: ruby +sudo: false +cache: + - bundler + - apt + +services: + - memcached + +addons: + apt: + packages: + - lighttpd + - libfcgi-dev + +before_install: - gem env version | grep '^\(2\|1.\(8\|9\|[0-9][0-9]\)\)' || gem update --system - - bundle install --jobs=3 --retry=3 + - gem list -i bundler || gem install bundler + script: bundle exec rake ci + rvm: - - 2.2.3 + - 2.2.7 + - 2.3.4 + - 2.4.1 - ruby-head - rbx-2 - - jruby - - jruby-9000 + - jruby-9.0.4.0 - jruby-head notifications: @@ -17,5 +34,5 @@ notifications: matrix: allow_failures: - - rvm: jruby - rvm: rbx-2 + - rvm: jruby-head @@ -1,4 +1,4 @@ -Copyright (c) 2007-2015 Christian Neukirchen <purl.org/net/chneukirchen> +Copyright (c) 2007-2016 Christian Neukirchen <purl.org/net/chneukirchen> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to @@ -19,3 +19,7 @@ group :extra do gem 'memcache-client' gem 'thin', :platforms => c_platforms end + +group :doc do + gem 'rdoc' +end @@ -1,3 +1,50 @@ +Sun Dec 4 18:48:03 2015 Jeremy Daer <jeremydaer@gmail.com> + + * First-party "SameSite" cookies. Browsers omit SameSite cookies + from third-party requests, closing the door on many CSRF attacks. + + Pass `same_site: true` (or `:strict`) to enable: + response.set_cookie 'foo', value: 'bar', same_site: true + or `same_site: :lax` to use Lax enforcement: + response.set_cookie 'foo', value: 'bar', same_site: :lax + + Based on version 7 of the Same-site Cookies internet draft: + https://tools.ietf.org/html/draft-west-first-party-cookies-07 + + Thanks to Ben Toews (@mastahyeti) and Bob Long (@bobjflong) for + updating to drafts 5 and 7. + +Tue Nov 3 16:17:26 2015 Aaron Patterson <tenderlove@ruby-lang.org> + + * Add `Rack::Events` middleware for adding event based middleware: + middleware that does not care about the response body, but only cares + about doing work at particular points in the request / response + lifecycle. + +Thu Oct 8 14:58:46 2015 Aaron Patterson <tenderlove@ruby-lang.org> + + * Add `Rack::Request#authority` to calculate the authority under which + the response is being made (this will be handy for h2 pushes). + +Tue Oct 6 13:19:04 2015 Aaron Patterson <tenderlove@ruby-lang.org> + + * Add `Rack::Response::Helpers#cache_control` and `cache_control=`. + Use this for setting cache control headers on your response objects. + +Tue Oct 6 13:12:21 2015 Aaron Patterson <tenderlove@ruby-lang.org> + + * Add `Rack::Response::Helpers#etag` and `etag=`. Use this for + setting etag values on the response. + +Sun Oct 3 18:25:03 2015 Jeremy Daer <jeremydaer@gmail.com> + + * Introduce `Rack::Response::Helpers#add_header` to add a value to a + multi-valued response header. Implemented in terms of other + `Response#*_header` methods, so it's available to any response-like + class that includes the `Helpers` module. + + * Add `Rack::Request#add_header` to match. + Fri Sep 4 18:34:53 2015 Aaron Patterson <tenderlove@ruby-lang.org> * `Rack::Session::Abstract::ID` IS DEPRECATED. Please switch to @@ -31,6 +78,17 @@ Thu Aug 27 15:43:48 2015 Aaron Patterson <tenderlove@ruby-lang.org> * Tempfiles are automatically closed in the case that there were too many posted. +Thu Aug 27 11:00:03 2015 Aaron Patterson <tenderlove@ruby-lang.org> + + * Added methods for manipulating response headers that don't assume + they're stored as a Hash. Response-like classes may include the + Rack::Response::Helpers module if they define these methods: + + * Rack::Response#has_header? + * Rack::Response#get_header + * Rack::Response#set_header + * Rack::Response#delete_header + Mon Aug 24 18:05:23 2015 Aaron Patterson <tenderlove@ruby-lang.org> * Introduce Util.get_byte_ranges that will parse the value of the @@ -55,10 +113,12 @@ Thu Aug 20 16:20:58 2015 Aaron Patterson <tenderlove@ruby-lang.org> data set as CGI parameters, and just any arbitrary data the user wants to associate with a particular request. New methods: - * Rack::Request#get_header - * Rack::Request#set_header * Rack::Request#has_header? + * Rack::Request#get_header + * Rack::Request#fetch_header * Rack::Request#each_header + * Rack::Request#set_header + * Rack::Request#delete_header Thu Jun 18 16:00:05 2015 Aaron Patterson <tenderlove@ruby-lang.org> diff --git a/README.rdoc b/README.rdoc index bedcda99..8c1e2f01 100644 --- a/README.rdoc +++ b/README.rdoc @@ -41,9 +41,11 @@ These frameworks include Rack adapters in their distributions: * Coset * Espresso * Halcyon +* Hanami * Mack * Maveric * Merb +* Padrino * Racktools::SimpleApplication * Ramaze * Ruby on Rails @@ -295,7 +297,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. == Links -Rack:: <http://rack.github.io/> +Rack:: <https://rack.github.io/> Official Rack repositories:: <https://github.com/rack> Rack Bug Tracking:: <https://github.com/rack/rack/issues> rack-devel mailing list:: <https://groups.google.com/group/rack-devel> @@ -36,7 +36,7 @@ task :officialrelease_really => %w[SPEC dist gem] do end def release - "rack-#{File.read("rack.gemspec")[/s.version *= *"(.*?)"/, 1]}" + "rack-" + File.read('lib/rack.rb')[/RELEASE += +([\"\'])([\d][\w\.]+)\1/, 2] end desc "Make binaries executable" diff --git a/SECURITY_POLICY.md b/SECURITY_POLICY.md index 1ae1a95f..844d6969 100644 --- a/SECURITY_POLICY.md +++ b/SECURITY_POLICY.md @@ -64,4 +64,4 @@ No one outside the core team, the initial reporter or vendor-sec will be notifie ## Comments on this Policy -If you have any suggestions to improve this policy, please send an email the core teamat [rack-core@googlegroups.com](https://groups.google.com/group/rack-core). +If you have any suggestions to improve this policy, please send an email the core team at [rack-core@googlegroups.com](https://groups.google.com/group/rack-core). @@ -237,10 +237,10 @@ consisting of lines (for multiple header values, e.g. multiple 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, -204, 205 or 304. +204 or 304. === The Content-Length There must not be a <tt>Content-Length</tt> header when the -+Status+ is 1xx, 204, 205 or 304. ++Status+ is 1xx, 204 or 304. === The Body The Body must respond to +each+ and must only yield String values. diff --git a/example/protectedlobster.rb b/example/protectedlobster.rb index d904b4ce..26b23661 100644 --- a/example/protectedlobster.rb +++ b/example/protectedlobster.rb @@ -4,7 +4,7 @@ require 'rack/lobster' lobster = Rack::Lobster.new protected_lobster = Rack::Auth::Basic.new(lobster) do |username, password| - 'secret' == password + Rack::Utils.secure_compare('secret', password) end protected_lobster.realm = 'Lobster 2.0' diff --git a/example/protectedlobster.ru b/example/protectedlobster.ru index b0da62f0..1ba48702 100644 --- a/example/protectedlobster.ru +++ b/example/protectedlobster.ru @@ -2,7 +2,7 @@ require 'rack/lobster' use Rack::ShowExceptions use Rack::Auth::Basic, "Lobster 2.0" do |username, password| - 'secret' == password + Rack::Utils.secure_compare('secret', password) end run Rack::Lobster.new diff --git a/lib/rack.rb b/lib/rack.rb index c66bc641..f1417d2d 100644 --- a/lib/rack.rb +++ b/lib/rack.rb @@ -18,7 +18,7 @@ module Rack VERSION.join(".") end - RELEASE = "2.0.0.alpha" + RELEASE = "2.0.1" # Return the Rack release as a dotted string. def self.release @@ -43,6 +43,7 @@ module Rack SET_COOKIE = 'Set-Cookie'.freeze TRANSFER_ENCODING = 'Transfer-Encoding'.freeze HTTP_COOKIE = 'HTTP_COOKIE'.freeze + ETAG = 'ETag'.freeze # HTTP method verbs GET = 'GET'.freeze diff --git a/lib/rack/auth/abstract/request.rb b/lib/rack/auth/abstract/request.rb index 80d1c272..b738cc98 100644 --- a/lib/rack/auth/abstract/request.rb +++ b/lib/rack/auth/abstract/request.rb @@ -13,7 +13,11 @@ module Rack end def provided? - !authorization_key.nil? + !authorization_key.nil? && valid? + end + + def valid? + !@env[authorization_key].nil? end def parts diff --git a/lib/rack/auth/digest/params.rb b/lib/rack/auth/digest/params.rb index f35a7bab..2b226e62 100644 --- a/lib/rack/auth/digest/params.rb +++ b/lib/rack/auth/digest/params.rb @@ -17,7 +17,7 @@ module Rack end def self.split_header_value(str) - str.scan( /(\w+\=(?:"[^\"]+"|[^,]+))/n ).collect{ |v| v[0] } + str.scan(/\w+\=(?:"[^\"]+"|[^,]+)/n) end def initialize @@ -38,7 +38,7 @@ module Rack def to_s map do |k, v| - "#{k}=" + (UNQUOTED.include?(k) ? v.to_s : quote(v)) + "#{k}=" << (UNQUOTED.include?(k) ? v.to_s : quote(v)) end.join(', ') end @@ -50,4 +50,3 @@ module Rack end end end - diff --git a/lib/rack/builder.rb b/lib/rack/builder.rb index 5250aff3..f11c66bc 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, @warmup = [], nil, default_app, nil + @use, @map, @run, @warmup, @freeze_app = [], nil, default_app, nil, false instance_eval(&block) if block_given? end @@ -141,10 +141,17 @@ module Rack @map[path] = block end + # Freeze the app (set using run) and all middleware instances when building the application + # in to_app. + def freeze_app + @freeze_app = true + end + def to_app app = @map ? generate_map(@run, @map) : @run fail "missing run or map statement" unless app - app = @use.reverse.inject(app) { |a,e| e[a] } + app.freeze if @freeze_app + app = @use.reverse.inject(app) { |a,e| e[a].tap { |x| x.freeze if @freeze_app } } @warmup.call(app) if @warmup app end diff --git a/lib/rack/chunked.rb b/lib/rack/chunked.rb index 4b8f270e..3076931c 100644 --- a/lib/rack/chunked.rb +++ b/lib/rack/chunked.rb @@ -24,7 +24,7 @@ module Rack size = chunk.bytesize next if size == 0 - chunk = chunk.dup.force_encoding(Encoding::BINARY) + chunk = chunk.b yield [size.to_s(16), term, chunk, term].join end yield TAIL diff --git a/lib/rack/common_logger.rb b/lib/rack/common_logger.rb index 1ec8266d..7855f0c3 100644 --- a/lib/rack/common_logger.rb +++ b/lib/rack/common_logger.rb @@ -29,7 +29,7 @@ module Rack end def call(env) - began_at = Time.now + began_at = Utils.clock_time status, header, body = @app.call(env) header = Utils::HeaderHash.new(header) body = BodyProxy.new(body) { log(env, status, header, began_at) } @@ -39,20 +39,19 @@ module Rack private def log(env, status, header, began_at) - now = Time.now length = extract_content_length(header) msg = FORMAT % [ env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-", env["REMOTE_USER"] || "-", - now.strftime("%d/%b/%Y:%H:%M:%S %z"), + Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"), env[REQUEST_METHOD], env[PATH_INFO], - env[QUERY_STRING].empty? ? "" : "?"+env[QUERY_STRING], + env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}", env[HTTP_VERSION], status.to_s[0..3], length, - now - began_at ] + Utils.clock_time - began_at ] logger = @logger || env[RACK_ERRORS] # Standard library logger doesn't support write but it supports << which actually diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb index 62a11243..d1fb73ab 100644 --- a/lib/rack/deflater.rb +++ b/lib/rack/deflater.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require "zlib" require "time" # for Time.httpdate require 'rack/utils' @@ -8,7 +9,6 @@ module Rack # Currently supported compression algorithms: # # * gzip - # * deflate # * identity (no transformation) # # The middleware automatically detects when compression is supported @@ -22,13 +22,18 @@ module Rack # [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 } + # e.g use Rack::Deflater, :if => lambda { |*, body| sum=0; body.each { |i| sum += i.length }; sum > 512 } # 'include' - a list of content types that should be compressed + # 'sync' - determines if the stream is going to be flused after every chunk. + # Flushing after every chunk reduces latency for + # time-sensitive streaming applications, but hurts + # compression and throughput. Defaults to `true'. def initialize(app, options = {}) @app = app @condition = options[:if] @compressible_types = options[:include] + @sync = options[:sync] == false ? false : true end def call(env) @@ -41,7 +46,7 @@ module Rack request = Request.new(env) - encoding = Utils.select_best_encoding(%w(gzip deflate identity), + encoding = Utils.select_best_encoding(%w(gzip identity), request.accept_encoding) # Set the Vary HTTP header. @@ -53,37 +58,33 @@ module Rack case encoding when "gzip" headers['Content-Encoding'] = "gzip" - headers.delete(CONTENT_LENGTH) - mtime = headers.key?("Last-Modified") ? - Time.httpdate(headers["Last-Modified"]) : Time.now - [status, headers, GzipStream.new(body, mtime)] - when "deflate" - headers['Content-Encoding'] = "deflate" - headers.delete(CONTENT_LENGTH) - [status, headers, DeflateStream.new(body)] + headers.delete('Content-Length') + mtime = headers["Last-Modified"] + mtime = Time.httpdate(mtime).to_i if mtime + [status, headers, GzipStream.new(body, mtime, @sync)] when "identity" [status, headers, body] when nil message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found." bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) } - [406, {CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s}, bp] + [406, {'Content-Type' => "text/plain", 'Content-Length' => message.length.to_s}, bp] end end class GzipStream - def initialize(body, mtime) + def initialize(body, mtime, sync) + @sync = sync @body = body @mtime = mtime - @closed = false end def each(&block) @writer = block gzip =::Zlib::GzipWriter.new(self) - gzip.mtime = @mtime + gzip.mtime = @mtime if @mtime @body.each { |part| gzip.write(part) - gzip.flush + gzip.flush if @sync } ensure gzip.close @@ -95,39 +96,8 @@ module Rack end def close - return if @closed - @closed = true - @body.close if @body.respond_to?(:close) - end - end - - class DeflateStream - DEFLATE_ARGS = [ - Zlib::DEFAULT_COMPRESSION, - # drop the zlib header which causes both Safari and IE to choke - -Zlib::MAX_WBITS, - Zlib::DEF_MEM_LEVEL, - Zlib::DEFAULT_STRATEGY - ] - - def initialize(body) - @body = body - @closed = false - end - - def each - deflator = ::Zlib::Deflate.new(*DEFLATE_ARGS) - @body.each { |part| yield deflator.deflate(part, Zlib::SYNC_FLUSH) } - yield fin = deflator.finish - ensure - deflator.finish unless fin - deflator.close - end - - def close - return if @closed - @closed = true @body.close if @body.respond_to?(:close) + @body = nil end end @@ -137,13 +107,13 @@ module Rack # 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['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][/[^;]*/])) + 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) diff --git a/lib/rack/directory.rb b/lib/rack/directory.rb index 554f9c33..89cfe807 100644 --- a/lib/rack/directory.rb +++ b/lib/rack/directory.rb @@ -59,13 +59,21 @@ table { width:100%%; } def initialize(root, app=nil) @root = ::File.expand_path(root) @app = app || Rack::File.new(@root) + @head = Rack::Head.new(lambda { |env| get env }) end def call(env) + # strip body if this is a HEAD call + @head.call env + end + + def get(env) script_name = env[SCRIPT_NAME] path_info = Utils.unescape_path(env[PATH_INFO]) - if forbidden = check_forbidden(path_info) + if bad_request = check_bad_request(path_info) + bad_request + elsif forbidden = check_forbidden(path_info) forbidden else path = ::File.join(@root, path_info) @@ -73,6 +81,16 @@ table { width:100%%; } end end + def check_bad_request(path_info) + return if Utils.valid_path?(path_info) + + body = "Bad Request\n" + size = body.bytesize + return [400, {CONTENT_TYPE => "text/plain", + CONTENT_LENGTH => size.to_s, + "X-Cascade" => "pass"}, [body]] + end + def check_forbidden(path_info) return unless path_info.include? ".." @@ -155,7 +173,7 @@ table { width:100%%; } return format % (int.to_f / size) if int >= size end - int.to_s + 'B' + "#{int}B" end end end diff --git a/lib/rack/etag.rb b/lib/rack/etag.rb index 88973131..5a8c6452 100644 --- a/lib/rack/etag.rb +++ b/lib/rack/etag.rb @@ -1,4 +1,5 @@ -require 'digest/md5' +require 'rack' +require 'digest/sha2' module Rack # Automatically sets the ETag header on all String bodies. @@ -11,7 +12,7 @@ module Rack # used when Etag is absent and a directive when it is present. The first # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate" class ETag - ETAG_STRING = 'ETag'.freeze + ETAG_STRING = Rack::ETAG DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL) @@ -64,10 +65,10 @@ module Rack body.each do |part| parts << part - (digest ||= Digest::MD5.new) << part unless part.empty? + (digest ||= Digest::SHA256.new) << part unless part.empty? end - [digest && digest.hexdigest, parts] + [digest && digest.hexdigest.byteslice(0, 32), parts] end end end diff --git a/lib/rack/events.rb b/lib/rack/events.rb new file mode 100644 index 00000000..3782a22e --- /dev/null +++ b/lib/rack/events.rb @@ -0,0 +1,154 @@ +require 'rack/response' +require 'rack/body_proxy' + +module Rack + ### This middleware provides hooks to certain places in the request / + #response lifecycle. This is so that middleware that don't need to filter + #the response data can safely leave it alone and not have to send messages + #down the traditional "rack stack". + # + # The events are: + # + # * on_start(request, response) + # + # This event is sent at the start of the request, before the next + # middleware in the chain is called. This method is called with a request + # object, and a response object. Right now, the response object is always + # nil, but in the future it may actually be a real response object. + # + # * on_commit(request, response) + # + # The response has been committed. The application has returned, but the + # response has not been sent to the webserver yet. This method is always + # called with a request object and the response object. The response + # object is constructed from the rack triple that the application returned. + # Changes may still be made to the response object at this point. + # + # * on_send(request, response) + # + # The webserver has started iterating over the response body and presumably + # has started sending data over the wire. This method is always called with + # a request object and the response object. The response object is + # constructed from the rack triple that the application returned. Changes + # SHOULD NOT be made to the response object as the webserver has already + # started sending data. Any mutations will likely result in an exception. + # + # * on_finish(request, response) + # + # The webserver has closed the response, and all data has been written to + # the response socket. The request and response object should both be + # read-only at this point. The body MAY NOT be available on the response + # object as it may have been flushed to the socket. + # + # * on_error(request, response, error) + # + # An exception has occurred in the application or an `on_commit` event. + # This method will get the request, the response (if available) and the + # exception that was raised. + # + # ## Order + # + # `on_start` is called on the handlers in the order that they were passed to + # the constructor. `on_commit`, on_send`, `on_finish`, and `on_error` are + # called in the reverse order. `on_finish` handlers are called inside an + # `ensure` block, so they are guaranteed to be called even if something + # raises an exception. If something raises an exception in a `on_finish` + # method, then nothing is guaranteed. + + class Events + module Abstract + def on_start req, res + end + + def on_commit req, res + end + + def on_send req, res + end + + def on_finish req, res + end + + def on_error req, res, e + end + end + + class EventedBodyProxy < Rack::BodyProxy # :nodoc: + attr_reader :request, :response + + def initialize body, request, response, handlers, &block + super(body, &block) + @request = request + @response = response + @handlers = handlers + end + + def each + @handlers.reverse_each { |handler| handler.on_send request, response } + super + end + end + + class BufferedResponse < Rack::Response::Raw # :nodoc: + attr_reader :body + + def initialize status, headers, body + super(status, headers) + @body = body + end + + def to_a; [status, headers, body]; end + end + + def initialize app, handlers + @app = app + @handlers = handlers + end + + def call env + request = make_request env + on_start request, nil + + begin + status, headers, body = @app.call request.env + response = make_response status, headers, body + on_commit request, response + rescue StandardError => e + on_error request, response, e + on_finish request, response + raise + end + + body = EventedBodyProxy.new(body, request, response, @handlers) do + on_finish request, response + end + [response.status, response.headers, body] + end + + private + + def on_error request, response, e + @handlers.reverse_each { |handler| handler.on_error request, response, e } + end + + def on_commit request, response + @handlers.reverse_each { |handler| handler.on_commit request, response } + end + + def on_start request, response + @handlers.each { |handler| handler.on_start request, nil } + end + + def on_finish request, response + @handlers.reverse_each { |handler| handler.on_finish request, response } + end + + def make_request env + Rack::Request.new env + end + + def make_response status, headers, body + BufferedResponse.new status, headers, body + end + end +end diff --git a/lib/rack/file.rb b/lib/rack/file.rb index 5b755f56..09eb0afb 100644 --- a/lib/rack/file.rb +++ b/lib/rack/file.rb @@ -2,6 +2,7 @@ require 'time' require 'rack/utils' require 'rack/mime' require 'rack/request' +require 'rack/head' module Rack # Rack::File serves files below the +root+ directory given, according to the @@ -22,17 +23,24 @@ module Rack @root = root @headers = headers @default_mime = default_mime + @head = Rack::Head.new(lambda { |env| get env }) end def call(env) + # HEAD requests drop the response body, including 4xx error messages. + @head.call env + end + + def get(env) request = Rack::Request.new env unless ALLOWED_VERBS.include? request.request_method return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER}) end path_info = Utils.unescape_path request.path_info - clean_path_info = Utils.clean_path_info(path_info) + return fail(400, "Bad Request") unless Utils.valid_path?(path_info) + clean_path_info = Utils.clean_path_info(path_info) path = ::File.join(@root, clean_path_info) available = begin @@ -131,6 +139,7 @@ module Rack def fail(status, body, headers = {}) body += "\n" + [ status, { @@ -149,7 +158,7 @@ module Rack def filesize path # If response_body is present, use its size. - return Rack::Utils.bytesize(response_body) if response_body + return response_body.bytesize if response_body # We check via File::size? whether this file provides size info # via stat (e.g. /proc files often don't), otherwise we have to diff --git a/lib/rack/handler.rb b/lib/rack/handler.rb index adaac5d6..70a77fa9 100644 --- a/lib/rack/handler.rb +++ b/lib/rack/handler.rb @@ -52,7 +52,7 @@ module Rack elsif ENV.include?("RACK_HANDLER") self.get(ENV["RACK_HANDLER"]) else - pick ['thin', 'puma', 'webrick'] + pick ['puma', 'thin', 'webrick'] end end diff --git a/lib/rack/handler/scgi.rb b/lib/rack/handler/scgi.rb index e056a01d..beda9c3e 100644 --- a/lib/rack/handler/scgi.rb +++ b/lib/rack/handler/scgi.rb @@ -41,7 +41,8 @@ module Rack env[QUERY_STRING] ||= "" env[SCRIPT_NAME] = "" - rack_input = StringIO.new(input_body, encoding: Encoding::BINARY) + rack_input = StringIO.new(input_body) + rack_input.set_encoding(Encoding::BINARY) env.update( RACK_VERSION => Rack::VERSION, diff --git a/lib/rack/handler/webrick.rb b/lib/rack/handler/webrick.rb index 95aa8927..d0fcd213 100644 --- a/lib/rack/handler/webrick.rb +++ b/lib/rack/handler/webrick.rb @@ -86,10 +86,11 @@ module Rack status, headers, body = @app.call(env) begin res.status = status.to_i + io_lambda = nil headers.each { |k, vs| - next if k.downcase == RACK_HIJACK - - if k.downcase == "set-cookie" + if k == RACK_HIJACK + io_lambda = vs + elsif k.downcase == "set-cookie" res.cookies.concat vs.split("\n") else # Since WEBrick won't accept repeated headers, @@ -98,7 +99,6 @@ module Rack end } - io_lambda = headers[RACK_HIJACK] if io_lambda rd, wr = IO.pipe res.body = rd diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb index 54d37822..683ba684 100644 --- a/lib/rack/lint.rb +++ b/lib/rack/lint.rb @@ -659,7 +659,7 @@ module Rack def check_content_type(status, headers) headers.each { |key, value| ## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx, - ## 204, 205 or 304. + ## 204 or 304. if key.downcase == "content-type" assert("Content-Type header found in #{status} response, not allowed") { not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i @@ -674,7 +674,7 @@ module Rack headers.each { |key, value| if key.downcase == 'content-length' ## There must not be a <tt>Content-Length</tt> header when the - ## +Status+ is 1xx, 204, 205 or 304. + ## +Status+ is 1xx, 204 or 304. assert("Content-Length header found in #{status} response, not allowed") { not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i } diff --git a/lib/rack/lock.rb b/lib/rack/lock.rb index 923dca59..b5a41e8e 100644 --- a/lib/rack/lock.rb +++ b/lib/rack/lock.rb @@ -11,12 +11,21 @@ module Rack def call(env) @mutex.lock + @env = env + @old_rack_multithread = env[RACK_MULTITHREAD] begin - response = @app.call(env.merge(RACK_MULTITHREAD => false)) - returned = response << BodyProxy.new(response.pop) { @mutex.unlock } + response = @app.call(env.merge!(RACK_MULTITHREAD => false)) + returned = response << BodyProxy.new(response.pop) { unlock } ensure - @mutex.unlock unless returned + unlock unless returned end end + + private + + def unlock + @mutex.unlock + @env[RACK_MULTITHREAD] = @old_rack_multithread + end end end diff --git a/lib/rack/method_override.rb b/lib/rack/method_override.rb index f5637771..06df21f7 100644 --- a/lib/rack/method_override.rb +++ b/lib/rack/method_override.rb @@ -38,6 +38,9 @@ module Rack def method_override_param(req) req.POST[METHOD_OVERRIDE_PARAM_KEY] rescue Utils::InvalidParameterError, Utils::ParameterTypeError + req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params" + rescue EOFError + req.get_header(RACK_ERRORS).puts "Bad request content body" end end end diff --git a/lib/rack/mock.rb b/lib/rack/mock.rb index a2fc2401..914bf3b5 100644 --- a/lib/rack/mock.rb +++ b/lib/rack/mock.rb @@ -91,13 +91,13 @@ module Rack env = DEFAULT_ENV.dup - env[REQUEST_METHOD] = opts[:method] ? opts[:method].to_s.upcase : GET - env[SERVER_NAME] = uri.host || "example.org" - env[SERVER_PORT] = uri.port ? uri.port.to_s : "80" - env[QUERY_STRING] = uri.query.to_s - env[PATH_INFO] = (!uri.path || uri.path.empty?) ? "/" : uri.path - env[RACK_URL_SCHEME] = uri.scheme || "http" - env[HTTPS] = env[RACK_URL_SCHEME] == "https" ? "on" : "off" + env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b + env[SERVER_NAME] = (uri.host || "example.org").b + env[SERVER_PORT] = (uri.port ? uri.port.to_s : "80").b + env[QUERY_STRING] = (uri.query.to_s).b + env[PATH_INFO] = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b + env[RACK_URL_SCHEME] = (uri.scheme || "http").b + env[HTTPS] = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b env[SCRIPT_NAME] = opts[:script_name] || "" @@ -128,7 +128,7 @@ module Rack end end - empty_str = ''.force_encoding(Encoding::ASCII_8BIT) + empty_str = String.new opts[:input] ||= empty_str if String === opts[:input] rack_input = StringIO.new(opts[:input]) @@ -139,7 +139,7 @@ module Rack rack_input.set_encoding(Encoding::BINARY) env[RACK_INPUT] = rack_input - env["CONTENT_LENGTH"] ||= env[RACK_INPUT].length.to_s + env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size) opts.each { |field, value| env[field] = value if String === field @@ -163,7 +163,6 @@ module Rack def initialize(status, headers, body, errors=StringIO.new("")) @original_headers = headers @errors = errors.string if errors.respond_to?(:string) - @body_string = nil super(body, status, headers) end @@ -191,7 +190,7 @@ module Rack end def empty? - [201, 204, 205, 304].include? status + [201, 204, 304].include? status end end end diff --git a/lib/rack/multipart/generator.rb b/lib/rack/multipart/generator.rb index 6367135f..f0b70a8d 100644 --- a/lib/rack/multipart/generator.rb +++ b/lib/rack/multipart/generator.rb @@ -22,7 +22,7 @@ module Rack else content_for_other(file, name) end - end.join + "--#{MULTIPART_BOUNDARY}--\r" + end.join << "--#{MULTIPART_BOUNDARY}--\r" end private diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb index fe3381b9..c02e26f6 100644 --- a/lib/rack/multipart/parser.rb +++ b/lib/rack/multipart/parser.rb @@ -5,10 +5,10 @@ module Rack class MultipartPartLimitError < Errno::EMFILE; end class Parser - BUFSIZE = 16384 + BUFSIZE = 1_048_576 TEXT_PLAIN = "text/plain" TEMPFILE_FACTORY = lambda { |filename, content_type| - Tempfile.new(["RackMultipart", ::File.extname(filename)]) + Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))]) } class BoundedIO # :nodoc: @@ -26,7 +26,7 @@ module Rack str = if left < size @io.read left else - @io.read size + @io.read size end if str @@ -100,8 +100,6 @@ module Rack # Generic multipart cases, not coming from a form data = {:type => content_type, :name => name, :tempfile => body, :head => head} - elsif !filename && data.empty? - return end yield data @@ -137,7 +135,7 @@ module Rack klass = TempfilePart @open_files += 1 else - body = ''.force_encoding(Encoding::ASCII_8BIT) + body = String.new klass = BufferPart end @@ -167,15 +165,15 @@ module Rack attr_reader :state def initialize(boundary, tempfile, bufsize, query_parser) - @buf = "".force_encoding(Encoding::ASCII_8BIT) + @buf = String.new @query_parser = query_parser @params = query_parser.make_params @boundary = "--#{boundary}" - @boundary_size = @boundary.bytesize + EOL.size @bufsize = bufsize @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n + @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max @full_boundary = @boundary @end_boundary = @boundary + '--' @state = :FAST_FORWARD @@ -265,15 +263,17 @@ module Rack end def handle_mime_body - if @buf =~ rx + if i = @buf.index(rx) # Save the rest. - if i = @buf.index(rx) - @collector.on_mime_body @mime_index, @buf.slice!(0, i) - @buf.slice!(0, 2) # Remove \r\n after the content - end + @collector.on_mime_body @mime_index, @buf.slice!(0, i) + @buf.slice!(0, 2) # Remove \r\n after the content @state = :CONSUME_TOKEN @mime_index += 1 else + # Save the read body part. + if @rx_max_size < @buf.size + @collector.on_mime_body @mime_index, @buf.slice!(0, @buf.size - @rx_max_size) + end :want_read end end @@ -347,6 +347,7 @@ module Rack k,v = param.split('=', 2) k.strip! v.strip! + v = v[1..-2] if v[0] == '"' && v[-1] == '"' encoding = Encoding.find v if k == CHARSET end end diff --git a/lib/rack/query_parser.rb b/lib/rack/query_parser.rb index fe8fe2d7..be74bc06 100644 --- a/lib/rack/query_parser.rb +++ b/lib/rack/query_parser.rb @@ -79,16 +79,22 @@ module Rack raise RangeError if depth <= 0 name =~ %r(\A[\[\]]*([^\[\]]+)\]*) - k = $1 || '' - after = $' || '' + k = $1 || ''.freeze + after = $' || ''.freeze - return if k.empty? + if k.empty? + if !v.nil? && name == "[]".freeze + return Array(v) + else + return + end + end - if after == "" + if after == ''.freeze params[k] = v - elsif after == "[" + elsif after == "[".freeze params[name] = v - elsif after == "[]" + elsif after == "[]".freeze params[k] ||= [] raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) params[k] << v @@ -96,7 +102,7 @@ module Rack child_key = $1 params[k] ||= [] 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) + if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key) normalize_params(params[k].last, child_key, v, depth - 1) else params[k] << normalize_params(make_params, child_key, v, depth - 1) @@ -107,7 +113,7 @@ module Rack params[k] = normalize_params(params[k], after, v, depth - 1) end - return params + params end def make_params @@ -128,6 +134,18 @@ module Rack obj.kind_of?(@params_class) end + def params_hash_has_key?(hash, key) + return false if key =~ /\[\]/ + + key.split(/[\[\]]+/).inject(hash) do |h, part| + next h if part == '' + return false unless params_hash_type?(h) && h.key?(part) + h[part] + end + + true + end + def unescape(s) Utils.unescape(s) end diff --git a/lib/rack/reloader.rb b/lib/rack/reloader.rb index 5f643592..296dd6a1 100644 --- a/lib/rack/reloader.rb +++ b/lib/rack/reloader.rb @@ -26,6 +26,7 @@ module Rack @last = (Time.now - cooldown) @cache = {} @mtimes = {} + @reload_mutex = Mutex.new extend backend end @@ -33,7 +34,7 @@ module Rack def call(env) if @cooldown and Time.now > @last + @cooldown if Thread.list.size > 1 - Thread.exclusive{ reload! } + @reload_mutex.synchronize{ reload! } else reload! end diff --git a/lib/rack/request.rb b/lib/rack/request.rb index e97f302f..28712ef6 100644 --- a/lib/rack/request.rb +++ b/lib/rack/request.rb @@ -16,23 +16,6 @@ module Rack super(env) end - # shortcut for <tt>request.params[key]</tt> - def [](key) - params[key.to_s] - end - - # shortcut for <tt>request.params[key] = value</tt> - # - # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params. - def []=(key, value) - params[key.to_s] = value - end - - # like Hash#values_at - def values_at(*keys) - keys.map{|key| params[key] } - end - def params @params ||= super end @@ -57,6 +40,12 @@ module Rack super() end + # Predicate method to test to see if `name` has been set as request + # specific data + def has_header?(name) + @env.key? name + end + # Get a request specific value for `name`. def get_header(name) @env[name] @@ -68,9 +57,9 @@ module Rack @env.fetch(name, &block) end - # Delete a request specific value for `name`. - def delete_header(name) - @env.delete name + # Loops through each key / value pair in the request specific data. + def each_header(&block) + @env.each(&block) end # Set a request specific value for `name` to `v` @@ -78,15 +67,28 @@ module Rack @env[name] = v end - # Predicate method to test to see if `name` has been set as request - # specific data - def has_header?(name) - @env.key? name + # Add a header that may have multiple values. + # + # Example: + # request.add_header 'Accept', 'image/png' + # request.add_header 'Accept', '*/*' + # + # assert_equal 'image/png,*/*', request.get_header('Accept') + # + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 + def add_header key, v + if v.nil? + get_header key + elsif has_header? key + set_header key, "#{get_header key},#{v}" + else + set_header key, v + end end - # Loops through each key / value pair in the request specific data. - def each_header(&block) - @env.each(&block) + # Delete a request specific value for `name`. + def delete_header(name) + @env.delete name end def initialize_copy(other) @@ -96,7 +98,7 @@ module Rack module Helpers # The set of form-data media-types. Requests that do not indicate - # one of the media types presents in this list will not be eligible + # one of the media types present in this list will not be eligible # for form-data / param parsing. FORM_DATA_MEDIA_TYPES = [ 'application/x-www-form-urlencoded', @@ -104,7 +106,7 @@ module Rack ] # The set of media-types. Requests that do not indicate - # one of the media types presents in this list will not be eligible + # one of the media types present in this list will not be eligible # for param parsing like soap attachments or generic multiparts PARSEABLE_DATA_MEDIA_TYPES = [ 'multipart/related', @@ -141,7 +143,7 @@ module Rack def session fetch_header(RACK_SESSION) do |k| - set_header RACK_SESSION, {} + set_header RACK_SESSION, default_session end end @@ -155,10 +157,10 @@ module Rack def delete?; request_method == DELETE end # Checks the HTTP request method (or verb) to see if it was of type GET - def get?; request_method == GET end + def get?; request_method == GET end # Checks the HTTP request method (or verb) to see if it was of type HEAD - def head?; request_method == HEAD end + def head?; request_method == HEAD end # Checks the HTTP request method (or verb) to see if it was of type OPTIONS def options?; request_method == OPTIONS end @@ -195,6 +197,10 @@ module Rack end end + def authority + get_header(SERVER_NAME) + ':' + get_header(SERVER_PORT) + end + def cookies hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k| set_header(k, {}) @@ -414,8 +420,35 @@ module Rack ip =~ /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i end + # shortcut for <tt>request.params[key]</tt> + def [](key) + if $VERBOSE + warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead") + end + + params[key.to_s] + end + + # shortcut for <tt>request.params[key] = value</tt> + # + # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params. + def []=(key, value) + if $VERBOSE + warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead") + end + + params[key.to_s] = value + end + + # like Hash#values_at + def values_at(*keys) + keys.map { |key| params[key] } + end + private + def default_session; {}; end + def parse_http_accept_header(header) header.to_s.split(/\s*,\s*/).map do |part| attribute, parameters = part.split(/\s*;\s*/, 2) diff --git a/lib/rack/response.rb b/lib/rack/response.rb index 78c9f473..a9f0c2a3 100644 --- a/lib/rack/response.rb +++ b/lib/rack/response.rb @@ -60,7 +60,7 @@ module Rack def finish(&block) @block = block - if [204, 205, 304].include?(status.to_i) + if [204, 304].include?(status.to_i) delete_header CONTENT_TYPE delete_header CONTENT_LENGTH close @@ -99,7 +99,7 @@ module Rack @block == nil && @body.empty? end - def have_header?(key); headers.key? key; end + def has_header?(key); headers.key? key; end def get_header(key); headers[key]; end def set_header(key, v); headers[key] = v; end def delete_header(key); headers.delete key; end @@ -132,7 +132,26 @@ module Rack def redirect?; [301, 302, 303, 307, 308].include? status; end def include?(header) - have_header? header + has_header? header + end + + # Add a header that may have multiple values. + # + # Example: + # response.add_header 'Vary', 'Accept-Encoding' + # response.add_header 'Vary', 'Cookie' + # + # assert_equal 'Accept-Encoding,Cookie', response.get_header('Vary') + # + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 + def add_header key, v + if v.nil? + get_header key + elsif has_header? key + set_header key, "#{get_header key},#{v}" + else + set_header key, v + end end def content_type @@ -176,6 +195,22 @@ module Rack def set_cookie_header= v set_header SET_COOKIE, v end + + def cache_control + get_header CACHE_CONTROL + end + + def cache_control= v + set_header CACHE_CONTROL, v + end + + def etag + get_header ETAG + end + + def etag= v + set_header ETAG, v + end end include Helpers @@ -183,14 +218,15 @@ module Rack class Raw include Helpers - attr_reader :status, :headers + attr_reader :headers + attr_accessor :status def initialize status, headers @status = status @headers = headers end - def have_header?(key); headers.key? key; end + def has_header?(key); headers.key? key; end def get_header(key); headers[key]; end def set_header(key, v); headers[key] = v; end def delete_header(key); headers.delete key; end diff --git a/lib/rack/runtime.rb b/lib/rack/runtime.rb index 7752bee8..bb15bdb1 100644 --- a/lib/rack/runtime.rb +++ b/lib/rack/runtime.rb @@ -1,3 +1,5 @@ +require 'rack/utils' + module Rack # Sets an "X-Runtime" response header, indicating the response # time of the request, in seconds @@ -16,9 +18,9 @@ module Rack end def call(env) - start_time = clock_time + start_time = Utils.clock_time status, headers, body = @app.call(env) - request_time = clock_time - start_time + request_time = Utils.clock_time - start_time unless headers.has_key?(@header_name) headers[@header_name] = FORMAT_STRING % request_time @@ -26,17 +28,5 @@ module Rack [status, headers, body] end - - private - - if defined?(Process::CLOCK_MONOTONIC) - def clock_time - Process.clock_gettime(Process::CLOCK_MONOTONIC) - end - else - def clock_time - Time.now.to_f - end - end end end diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb index bdb7cf2f..34c1a84e 100644 --- a/lib/rack/sendfile.rb +++ b/lib/rack/sendfile.rb @@ -150,8 +150,12 @@ module Rack if mapping = @mappings.find { |internal,_| internal =~ path } path.sub(*mapping) elsif mapping = env['HTTP_X_ACCEL_MAPPING'] - internal, external = mapping.split('=', 2).map(&:strip) - path.sub(/^#{internal}/i, external) + mapping.split(',').map(&:strip).each do |m| + internal, external = m.split('=', 2).map(&:strip) + new_path = path.sub(/^#{internal}/i, external) + return new_path unless path == new_path + end + path end end end diff --git a/lib/rack/server.rb b/lib/rack/server.rb index 690f1096..ce965144 100644 --- a/lib/rack/server.rb +++ b/lib/rack/server.rb @@ -1,4 +1,5 @@ require 'optparse' +require 'fileutils' module Rack @@ -46,7 +47,7 @@ module Rack opts.separator "" opts.separator "Rack options:" - opts.on("-s", "--server SERVER", "serve using SERVER (thin/puma/webrick/mongrel)") { |s| + opts.on("-s", "--server SERVER", "serve using SERVER (thin/puma/webrick)") { |s| options[:server] = s } @@ -359,7 +360,7 @@ module Rack def write_pid ::File.open(options[:pid], ::File::CREAT | ::File::EXCL | ::File::WRONLY ){ |f| f.write("#{Process.pid}") } - at_exit { ::File.delete(options[:pid]) if ::File.exist?(options[:pid]) } + at_exit { ::FileUtils.rm_f(options[:pid]) } rescue Errno::EEXIST check_pid! retry diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb index fe11d902..1bb8d5d0 100644 --- a/lib/rack/session/abstract/id.rb +++ b/lib/rack/session/abstract/id.rb @@ -18,6 +18,8 @@ module Rack include Enumerable attr_writer :id + Unspecified = Object.new + def self.find(req) req.get_header RACK_SESSION end @@ -42,7 +44,7 @@ module Rack end def options - @req.get_header RACK_SESSION_OPTIONS + @req.session_options end def each(&block) @@ -54,7 +56,15 @@ module Rack load_for_read! @data[key.to_s] end - alias :fetch :[] + + def fetch(key, default=Unspecified, &block) + load_for_read! + if default == Unspecified + @data.fetch(key.to_s, &block) + else + @data.fetch(key.to_s, default, &block) + end + end def has_key?(key) load_for_read! @@ -124,10 +134,12 @@ module Rack end def keys + load_for_read! @data.keys end def values + load_for_read! @data.values end @@ -165,7 +177,7 @@ module Rack # * :key determines the name of the cookie, by default it is # 'rack.session' # * :path, :domain, :expire_after, :secure, and :httponly set the related - # cookie options as by Rack::Response#add_cookie + # cookie options as by Rack::Response#set_cookie # * :skip will not a set a cookie in the response nor update the session state # * :defer will not set a cookie in the response but still update the session # state if it is used with a backend @@ -198,7 +210,7 @@ module Rack :sidbits => 128, :cookie_only => true, :secure_random => ::SecureRandom - } + }.freeze attr_reader :key, :default_options, :sid_secure @@ -351,6 +363,7 @@ module Rack set_cookie(req, res, cookie.merge!(options)) end end + public :commit_session # Sets the cookie back to the client with session id. We skip the cookie # setting if the value didn't change (sid is the same) or expires was given. @@ -395,7 +408,7 @@ module Rack class ID < Persisted def self.inherited(klass) - k = klass.ancestors.find { |kl| kl.superclass == ID } + k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID } unless k.instance_variable_defined?(:"@_rack_warned") warn "#{klass} is inheriting from #{ID}. Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE k.instance_variable_set(:"@_rack_warned", true) diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb index bd047163..71bb96f4 100644 --- a/lib/rack/session/cookie.rb +++ b/lib/rack/session/cookie.rb @@ -105,6 +105,8 @@ module Rack def initialize(app, options={}) @secrets = options.values_at(:secret, :old_secret).compact + @hmac = options.fetch(:hmac, OpenSSL::Digest::SHA1) + warn <<-MSG unless secure?(options) SECURITY WARNING: No secret option provided to Rack::Session::Cookie. This poses a security threat. It is strongly recommended that you @@ -180,7 +182,7 @@ module Rack end def generate_hmac(data, secret) - OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, data) + OpenSSL::HMAC.hexdigest(@hmac.new, secret, data) end def secure?(options) diff --git a/lib/rack/static.rb b/lib/rack/static.rb index 90d61009..17f47649 100644 --- a/lib/rack/static.rb +++ b/lib/rack/static.rb @@ -93,7 +93,7 @@ module Rack # HTTP Headers @header_rules = options[:header_rules] || [] # Allow for legacy :cache_control option while prioritizing global header_rules setting - @header_rules.insert(0, [:all, {CACHE_CONTROL => options[:cache_control]}]) if options[:cache_control] + @header_rules.unshift([:all, {CACHE_CONTROL => options[:cache_control]}]) if options[:cache_control] @file_server = Rack::File.new(root) end diff --git a/lib/rack/urlmap.rb b/lib/rack/urlmap.rb index 572b2151..510b4b50 100644 --- a/lib/rack/urlmap.rb +++ b/lib/rack/urlmap.rb @@ -47,11 +47,13 @@ module Rack server_name = env[SERVER_NAME] server_port = env[SERVER_PORT] + is_same_server = casecmp?(http_host, server_name) || + casecmp?(http_host, "#{server_name}:#{server_port}") + @mapping.each do |host, location, match, app| unless casecmp?(http_host, host) \ || casecmp?(server_name, host) \ - || (!host && (casecmp?(http_host, server_name) || - casecmp?(http_host, "#{server_name}:#{server_port}"))) + || (!host && is_same_server) next end diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb index 8eab6828..98ceee8a 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: true require 'uri' require 'fileutils' require 'set' @@ -77,6 +78,17 @@ module Rack self.default_query_parser = self.default_query_parser.new_space_limit(v) end + if defined?(Process::CLOCK_MONOTONIC) + def clock_time + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + else + def clock_time + Time.now.to_f + end + end + module_function :clock_time + def parse_query(qs, d = nil, &unescaper) Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper) end @@ -210,39 +222,26 @@ module Rack domain = "; domain=#{value[:domain]}" if value[:domain] path = "; path=#{value[:path]}" if value[:path] max_age = "; max-age=#{value[:max_age]}" if value[:max_age] - # There is an RFC mess in the area of date formatting for Cookies. Not - # only are there contradicting RFCs and examples within RFC text, but - # there are also numerous conflicting names of fields and partially - # cross-applicable specifications. - # - # These are best described in RFC 2616 3.3.1. This RFC text also - # specifies that RFC 822 as updated by RFC 1123 is preferred. That is a - # fixed length format with space-date delimited fields. - # - # See also RFC 1123 section 5.2.14. - # - # RFC 6265 also specifies "sane-cookie-date" as RFC 1123 date, defined - # in RFC 2616 3.3.1. RFC 6265 also gives examples that clearly denote - # the space delimited format. These formats are compliant with RFC 2822. - # - # For reference, all involved RFCs are: - # RFC 822 - # RFC 1123 - # RFC 2109 - # RFC 2616 - # RFC 2822 - # RFC 2965 - # RFC 6265 - expires = "; expires=" + - rfc2822(value[:expires].clone.gmtime) if value[:expires] + expires = "; expires=#{value[:expires].httpdate}" if value[:expires] secure = "; secure" if value[:secure] httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only]) + same_site = + case value[:same_site] + when false, nil + nil + when :lax, 'Lax', :Lax + '; SameSite=Lax'.freeze + when true, :strict, 'Strict', :Strict + '; SameSite=Strict'.freeze + else + raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}" + end value = value[:value] end value = [value] unless Array === value cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \ - "#{path}#{max_age}#{expires}#{secure}#{httponly}" + "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}" case header when nil, '' @@ -251,6 +250,8 @@ module Rack [header, cookie].join("\n") when Array (header + [cookie]).join("\n") + else + raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}" end end module_function :add_cookie_to_header @@ -486,9 +487,9 @@ module Rack # Every standard HTTP code mapped to the appropriate message. # Generated with: - # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \ - # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \ - # puts "#{m[1]} => \x27#{m[2].strip}\x27,"' + # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \ + # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \ + # puts "#{m[1]} => \x27#{m[2].strip}\x27,"' HTTP_STATUS_CODES = { 100 => 'Continue', 101 => 'Switching Protocols', @@ -537,6 +538,7 @@ module Rack 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable for Legal Reasons', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', @@ -551,7 +553,7 @@ module Rack } # Responses with HTTP status codes that should not have an entity body - STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 205 << 304) + STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304) SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message| [message.downcase.gsub(/\s|-|'/, '_').to_sym, code] @@ -580,9 +582,16 @@ module Rack clean.unshift '/' if parts.empty? || parts.first.empty? - ::File.join(*clean) + ::File.join clean end module_function :clean_path_info + NULL_BYTE = "\0".freeze + + def valid_path?(path) + path.valid_encoding? && !path.include?(NULL_BYTE) + end + module_function :valid_path? + end end diff --git a/rack.gemspec b/rack.gemspec index 7ec3f607..ec2b79f6 100644 --- a/rack.gemspec +++ b/rack.gemspec @@ -12,24 +12,22 @@ the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call. -Also see http://rack.github.io/. +Also see https://rack.github.io/. EOF s.files = Dir['{bin/*,contrib/*,example/*,lib/**/*,test/**/*}'] + - %w(COPYING KNOWN-ISSUES rack.gemspec Rakefile README.rdoc SPEC) + %w(COPYING rack.gemspec Rakefile README.rdoc SPEC) s.bindir = 'bin' s.executables << 'rackup' s.require_path = 'lib' - s.extra_rdoc_files = ['README.rdoc', 'KNOWN-ISSUES', 'HISTORY.md'] + s.extra_rdoc_files = ['README.rdoc', 'HISTORY.md'] s.test_files = Dir['test/spec_*.rb'] s.author = 'Christian Neukirchen' s.email = 'chneukirchen@gmail.com' - s.homepage = 'http://rack.github.io/' + s.homepage = 'https://rack.github.io/' s.required_ruby_version = '>= 2.2.2' - s.add_dependency 'json' - s.add_development_dependency 'minitest', "~> 5.0" s.add_development_dependency 'minitest-sprint' s.add_development_dependency 'concurrent-ruby' diff --git a/test/helper.rb b/test/helper.rb index c664583d..aa9c0e0a 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -1,23 +1,34 @@ require 'minitest/autorun' module Rack - class TestCase - # Keep this first. - PID = fork { - ENV['RACK_ENV'] = 'deployment' - ENV['RUBYLIB'] = [ - ::File.expand_path('../../lib', __FILE__), - ENV['RUBYLIB'], - ].compact.join(':') + class TestCase < Minitest::Test + # Check for Lighttpd and launch it for tests if available. + `which lighttpd` - Dir.chdir(::File.expand_path("../cgi", __FILE__)) do - exec "lighttpd -D -f lighttpd.conf" - end - } + if $?.success? + begin + # Keep this first. + LIGHTTPD_PID = fork { + ENV['RACK_ENV'] = 'deployment' + ENV['RUBYLIB'] = [ + ::File.expand_path('../../lib', __FILE__), + ENV['RUBYLIB'], + ].compact.join(':') - Minitest.after_run do - Process.kill 15, PID - Process.wait(PID) + Dir.chdir(::File.expand_path("../cgi", __FILE__)) do + exec "lighttpd -D -f lighttpd.conf" + end + } + rescue NotImplementedError + warn "Your Ruby doesn't support Kernel#fork. Skipping Rack::Handler::CGI and ::FastCGI tests." + else + Minitest.after_run do + Process.kill 15, LIGHTTPD_PID + Process.wait LIGHTTPD_PID + end + end + else + warn "Lighttpd isn't installed. Skipping Rack::Handler::CGI and FastCGI tests. Install lighttpd to run them." end end end diff --git a/test/multipart/filename_with_null_byte b/test/multipart/filename_with_null_byte new file mode 100644 index 00000000..961d44c4 --- /dev/null +++ b/test/multipart/filename_with_null_byte @@ -0,0 +1,7 @@ +--AaB03x
+Content-Type: image/jpeg
+Content-Disposition: attachment; name="files"; filename="flowers.exe%00.jpg"
+Content-Description: a complete map of the human genome
+
+contents
+--AaB03x--
diff --git a/test/multipart/unity3d_wwwform b/test/multipart/unity3d_wwwform new file mode 100644 index 00000000..1089a690 --- /dev/null +++ b/test/multipart/unity3d_wwwform @@ -0,0 +1,11 @@ +--AaB03x
+Content-Type: text/plain; charset="utf-8"
+Content-disposition: form-data; name="user_sid"
+
+bbf14f82-d2aa-4c07-9fb8-ca6714a7ea97
+--AaB03x
+Content-Type: image/png; charset=UTF-8
+Content-disposition: form-data; name="file";
+filename="b67879ed-bfed-4491-a8cc-f99cca769f94.png"
+
+--AaB03x
diff --git a/test/spec_auth_basic.rb b/test/spec_auth_basic.rb index 1e19bf66..45d28576 100644 --- a/test/spec_auth_basic.rb +++ b/test/spec_auth_basic.rb @@ -75,6 +75,13 @@ describe Rack::Auth::Basic do end end + it 'return 401 Bad Request for a nil authorization header' do + request 'HTTP_AUTHORIZATION' => nil do |response| + response.must_be :client_error? + response.status.must_equal 401 + end + end + it 'takes realm as optional constructor arg' do app = Rack::Auth::Basic.new(unprotected_app, realm) { true } realm.must_equal app.realm diff --git a/test/spec_body_proxy.rb b/test/spec_body_proxy.rb index 9df6f1d7..4db447a0 100644 --- a/test/spec_body_proxy.rb +++ b/test/spec_body_proxy.rb @@ -1,7 +1,6 @@ require 'minitest/autorun' require 'rack/body_proxy' require 'stringio' -require 'ostruct' describe Rack::BodyProxy do it 'call each on the wrapped body' do @@ -58,7 +57,7 @@ describe Rack::BodyProxy do end it 'not respond to :to_ary' do - body = OpenStruct.new(:to_ary => true) + body = Object.new.tap { |o| def o.to_ary() end } body.respond_to?(:to_ary).must_equal true proxy = Rack::BodyProxy.new(body) { } diff --git a/test/spec_builder.rb b/test/spec_builder.rb index cb0bbbd4..326f6b6c 100644 --- a/test/spec_builder.rb +++ b/test/spec_builder.rb @@ -174,6 +174,27 @@ describe Rack::Builder do Rack::MockRequest.new(app).get("/").must_be :server_error? end + it "supports #freeze_app for freezing app and middleware" do + app = builder do + freeze_app + use Rack::ShowExceptions + use(Class.new do + def initialize(app) @app = app end + def call(env) @a = 1 if env['PATH_INFO'] == '/a'; @app.call(env) end + end) + o = Object.new + def o.call(env) + @a = 1 if env['PATH_INFO'] == '/b'; + [200, {}, []] + end + run o + end + + Rack::MockRequest.new(app).get("/a").must_be :server_error? + Rack::MockRequest.new(app).get("/b").must_be :server_error? + Rack::MockRequest.new(app).get("/c").status.must_equal 200 + end + it 'complains about a missing run' do proc do Rack::Lint.new Rack::Builder.app { use Rack::ShowExceptions } diff --git a/test/spec_cgi.rb b/test/spec_cgi.rb index b89ed9af..77020c2f 100644 --- a/test/spec_cgi.rb +++ b/test/spec_cgi.rb @@ -1,5 +1,7 @@ require 'helper' -begin + +if defined? LIGHTTPD_PID + require File.expand_path('../testrequest', __FILE__) require 'rack/handler/cgi' @@ -79,8 +81,4 @@ describe Rack::Handler::CGI do end end -rescue RuntimeError - $stderr.puts "Skipping Rack::Handler::CGI tests (lighttpd is required). Install lighttpd and try again." -rescue NotImplementedError - $stderr.puts "Your Ruby implemenation or platform does not support fork. Skipping Rack::Handler::CGI tests." -end +end # if defined? LIGHTTPD_PID diff --git a/test/spec_chunked.rb b/test/spec_chunked.rb index 7bbcfd92..dc6e8c9d 100644 --- a/test/spec_chunked.rb +++ b/test/spec_chunked.rb @@ -92,7 +92,7 @@ describe Rack::Chunked do body.join.must_equal 'Hello World!' end - [100, 204, 205, 304].each do |status_code| + [100, 204, 304].each do |status_code| it "not modify response when status code is #{status_code}" do app = lambda { |env| [status_code, {}, []] } status, headers, _ = chunked(app).call(@env) diff --git a/test/spec_conditional_get.rb b/test/spec_conditional_get.rb index fd69c375..58f37ad5 100644 --- a/test/spec_conditional_get.rb +++ b/test/spec_conditional_get.rb @@ -33,7 +33,7 @@ describe Rack::ConditionalGet do it "set a 304 status and truncate body when If-None-Match hits" do app = conditional_get(lambda { |env| - [200, {'Etag'=>'1234'}, ['TEST']] }) + [200, {'ETag'=>'1234'}, ['TEST']] }) response = Rack::MockRequest.new(app). get("/", 'HTTP_IF_NONE_MATCH' => '1234') @@ -57,7 +57,7 @@ describe Rack::ConditionalGet do it "set a 304 status and truncate body when both If-None-Match and If-Modified-Since hits" do timestamp = Time.now.httpdate app = conditional_get(lambda { |env| - [200, {'Last-Modified'=>timestamp, 'Etag'=>'1234'}, ['TEST']] }) + [200, {'Last-Modified'=>timestamp, 'ETag'=>'1234'}, ['TEST']] }) response = Rack::MockRequest.new(app). get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp, 'HTTP_IF_NONE_MATCH' => '1234') diff --git a/test/spec_content_length.rb b/test/spec_content_length.rb index 261e4505..89752bbe 100644 --- a/test/spec_content_length.rb +++ b/test/spec_content_length.rb @@ -36,13 +36,13 @@ describe Rack::ContentLength do it "not set Content-Length on 304 responses" do app = lambda { |env| [304, {}, []] } response = content_length(app).call(request) - response[1]['Content-Length'].must_equal nil + response[1]['Content-Length'].must_be_nil end it "not set Content-Length when Transfer-Encoding is chunked" do app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Transfer-Encoding' => 'chunked'}, []] } response = content_length(app).call(request) - response[1]['Content-Length'].must_equal nil + response[1]['Content-Length'].must_be_nil end # Using "Connection: close" for this is fairly contended. It might be useful @@ -51,7 +51,7 @@ describe Rack::ContentLength do # should "not force a Content-Length when Connection:close" do # app = lambda { |env| [200, {'Connection' => 'close'}, []] } # response = content_length(app).call({}) - # response[1]['Content-Length'].must_equal nil + # response[1]['Content-Length'].must_be_nil # end it "close bodies that need to be closed" do @@ -64,7 +64,7 @@ describe Rack::ContentLength do app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] } response = content_length(app).call(request) - body.closed.must_equal nil + body.closed.must_be_nil response[2].close body.closed.must_equal true end diff --git a/test/spec_content_type.rb b/test/spec_content_type.rb index 281879f3..daf75355 100644 --- a/test/spec_content_type.rb +++ b/test/spec_content_type.rb @@ -41,6 +41,6 @@ describe Rack::ContentType do it "not set Content-Type on 304 responses" do app = lambda { |env| [304, {}, []] } response = content_type(app, "text/html").call(request) - response[1]['Content-Type'].must_equal nil + response[1]['Content-Type'].must_be_nil end end diff --git a/test/spec_deflater.rb b/test/spec_deflater.rb index ba7ec5d3..a5e91285 100644 --- a/test/spec_deflater.rb +++ b/test/spec_deflater.rb @@ -44,6 +44,8 @@ describe Rack::Deflater do [accept_encoding, accept_encoding.dup] end + start = Time.now.to_i + # build response status, headers, body = build_response( options['app_status'] || expected_status, @@ -67,6 +69,13 @@ describe Rack::Deflater do when 'gzip' io = StringIO.new(body_text) gz = Zlib::GzipReader.new(io) + mtime = gz.mtime.to_i + if last_mod = headers['Last-Modified'] + Time.httpdate(last_mod).to_i.must_equal mtime + else + mtime.must_be(:<=, Time.now.to_i) + mtime.must_be(:>=, start.to_i) + end tmp = gz.read gz.close tmp @@ -81,13 +90,22 @@ describe Rack::Deflater do yield(status, headers, body) if block_given? end + # automatic gzip detection (streamable) + def auto_inflater + Zlib::Inflate.new(32 + Zlib::MAX_WBITS) + end + + def deflate_or_gzip + {'deflate, gzip' => 'gzip'} + end + it '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 - verify(200, 'foobar', 'deflate', { 'app_body' => app_body }) do |status, headers, body| + verify(200, 'foobar', deflate_or_gzip, { 'app_body' => app_body }) do |status, headers, body| headers.must_equal({ - 'Content-Encoding' => 'deflate', + 'Content-Encoding' => 'gzip', 'Vary' => 'Accept-Encoding', 'Content-Type' => 'text/plain' }) @@ -98,15 +116,15 @@ describe Rack::Deflater do app_body = Object.new class << app_body; def each; yield('foo'); yield('bar'); end; end - verify(200, app_body, 'deflate', { 'skip_body_verify' => true }) do |status, headers, body| + verify(200, app_body, deflate_or_gzip, { 'skip_body_verify' => true }) do |status, headers, body| headers.must_equal({ - 'Content-Encoding' => 'deflate', + 'Content-Encoding' => 'gzip', 'Vary' => 'Accept-Encoding', 'Content-Type' => 'text/plain' }) buf = [] - inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS) + inflater = auto_inflater body.each { |part| buf << inflater.inflate(part) } buf << inflater.finish @@ -118,32 +136,33 @@ describe Rack::Deflater do app_body = Object.new class << app_body; def each; yield('foo'); yield('bar'); end; end opts = { 'skip_body_verify' => true } - verify(200, app_body, 'deflate', opts) do |status, headers, body| + verify(200, app_body, 'gzip', opts) do |status, headers, body| headers.must_equal({ - 'Content-Encoding' => 'deflate', + 'Content-Encoding' => 'gzip', 'Vary' => 'Accept-Encoding', 'Content-Type' => 'text/plain' }) buf = [] - inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS) + inflater = auto_inflater FakeDisconnect = Class.new(RuntimeError) assert_raises(FakeDisconnect, "not Zlib::DataError not raised") do body.each do |part| - buf << inflater.inflate(part) + tmp = inflater.inflate(part) + buf << tmp if tmp.bytesize > 0 raise FakeDisconnect end end - assert_raises(Zlib::BufError) { inflater.finish } + inflater.finish buf.must_equal(%w(foo)) end end # TODO: This is really just a special case of the above... it 'be able to deflate String bodies' do - verify(200, 'Hello world!', 'deflate') do |status, headers, body| + verify(200, 'Hello world!', deflate_or_gzip) do |status, headers, body| headers.must_equal({ - 'Content-Encoding' => 'deflate', + 'Content-Encoding' => 'gzip', 'Vary' => 'Accept-Encoding', 'Content-Type' => 'text/plain' }) @@ -280,7 +299,7 @@ describe Rack::Deflater do 'Content-Encoding' => 'identity' } } - verify(200, 'Hello World!', 'deflate', options) + verify(200, 'Hello World!', deflate_or_gzip, options) end it "deflate if content-type matches :include" do @@ -334,7 +353,7 @@ describe Rack::Deflater do :if => lambda { |env, status, headers, body| true } } } - verify(200, 'Hello World!', 'deflate', options) + verify(200, 'Hello World!', deflate_or_gzip, options) end it "not deflate if :if lambda evaluates to false" do @@ -362,4 +381,38 @@ describe Rack::Deflater do verify(200, response, 'gzip', options) end + + it 'will honor sync: false to avoid unnecessary flushing' do + app_body = Object.new + class << app_body + def each + (0..20).each { |i| yield "hello\n".freeze } + end + end + + options = { + 'deflater_options' => { :sync => false }, + 'app_body' => app_body, + 'skip_body_verify' => true, + } + verify(200, app_body, deflate_or_gzip, options) do |status, headers, body| + headers.must_equal({ + 'Content-Encoding' => 'gzip', + 'Vary' => 'Accept-Encoding', + 'Content-Type' => 'text/plain' + }) + + buf = '' + raw_bytes = 0 + inflater = auto_inflater + body.each do |part| + raw_bytes += part.bytesize + buf << inflater.inflate(part) + end + buf << inflater.finish + expect = "hello\n" * 21 + buf.must_equal expect + raw_bytes.must_be(:<, expect.bytesize) + end + end end diff --git a/test/spec_directory.rb b/test/spec_directory.rb index 1a97e9e5..42bdea9f 100644 --- a/test/spec_directory.rb +++ b/test/spec_directory.rb @@ -63,6 +63,13 @@ describe Rack::Directory do assert_match(res, /passed!/) end + it "serve uri with URL encoded null byte (%00) in filenames" do + res = Rack::MockRequest.new(Rack::Lint.new(app)) + .get("/cgi/test%00") + + res.must_be :bad_request? + end + it "not allow directory traversal" do res = Rack::MockRequest.new(Rack::Lint.new(app)). get("/cgi/../test") @@ -130,4 +137,12 @@ describe Rack::Directory do res = mr.get("/script-path/cgi/test+directory/test+file") res.must_be :ok? end + + it "return error when file not found for head request" do + res = Rack::MockRequest.new(Rack::Lint.new(app)). + head("/cgi/missing") + + res.must_be :not_found? + res.body.must_be :empty? + end end diff --git a/test/spec_etag.rb b/test/spec_etag.rb index 03680602..74795759 100644 --- a/test/spec_etag.rb +++ b/test/spec_etag.rb @@ -22,13 +22,13 @@ describe Rack::ETag do it "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'].must_equal "W/\"65a8e27d8879283831b664bd8b7f0ad4\"" + response[1]['ETag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\"" end it "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'].must_equal "W/\"65a8e27d8879283831b664bd8b7f0ad4\"" + response[1]['ETag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\"" end it "set Cache-Control to 'max-age=0, private, must-revalidate' (default) if none is set" do @@ -58,7 +58,7 @@ describe Rack::ETag do it "not set Cache-Control if directive isn't present" do app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] } response = etag(app, nil, nil).call(request) - response[1]['Cache-Control'].must_equal nil + response[1]['Cache-Control'].must_be_nil end it "not change ETag if it is already set" do diff --git a/test/spec_events.rb b/test/spec_events.rb new file mode 100644 index 00000000..7fc7b055 --- /dev/null +++ b/test/spec_events.rb @@ -0,0 +1,133 @@ +require 'helper' +require 'rack/events' + +module Rack + class TestEvents < Rack::TestCase + class EventMiddleware + attr_reader :events + + def initialize events + @events = events + end + + def on_start req, res + events << [self, __method__] + end + + def on_commit req, res + events << [self, __method__] + end + + def on_send req, res + events << [self, __method__] + end + + def on_finish req, res + events << [self, __method__] + end + + def on_error req, res, e + events << [self, __method__] + end + end + + def test_events_fire + events = [] + ret = [200, {}, []] + app = lambda { |env| events << [app, :call]; ret } + se = EventMiddleware.new events + e = Events.new app, [se] + triple = e.call({}) + response_body = [] + triple[2].each { |x| response_body << x } + triple[2].close + triple[2] = response_body + assert_equal ret, triple + assert_equal [[se, :on_start], + [app, :call], + [se, :on_commit], + [se, :on_send], + [se, :on_finish], + ], events + end + + def test_send_and_finish_are_not_run_until_body_is_sent + events = [] + ret = [200, {}, []] + app = lambda { |env| events << [app, :call]; ret } + se = EventMiddleware.new events + e = Events.new app, [se] + triple = e.call({}) + assert_equal [[se, :on_start], + [app, :call], + [se, :on_commit], + ], events + end + + def test_send_is_called_on_each + events = [] + ret = [200, {}, []] + app = lambda { |env| events << [app, :call]; ret } + se = EventMiddleware.new events + e = Events.new app, [se] + triple = e.call({}) + triple[2].each { |x| } + assert_equal [[se, :on_start], + [app, :call], + [se, :on_commit], + [se, :on_send], + ], events + end + + def test_finish_is_called_on_close + events = [] + ret = [200, {}, []] + app = lambda { |env| events << [app, :call]; ret } + se = EventMiddleware.new events + e = Events.new app, [se] + triple = e.call({}) + triple[2].each { |x| } + triple[2].close + assert_equal [[se, :on_start], + [app, :call], + [se, :on_commit], + [se, :on_send], + [se, :on_finish], + ], events + end + + def test_finish_is_called_in_reverse_order + events = [] + ret = [200, {}, []] + app = lambda { |env| events << [app, :call]; ret } + se1 = EventMiddleware.new events + se2 = EventMiddleware.new events + se3 = EventMiddleware.new events + + e = Events.new app, [se1, se2, se3] + triple = e.call({}) + triple[2].each { |x| } + triple[2].close + + groups = events.group_by { |x| x.last } + assert_equal groups[:on_start].map(&:first), groups[:on_finish].map(&:first).reverse + assert_equal groups[:on_commit].map(&:first), groups[:on_finish].map(&:first) + assert_equal groups[:on_send].map(&:first), groups[:on_finish].map(&:first) + end + + def test_finish_is_called_if_there_is_an_exception + events = [] + ret = [200, {}, []] + app = lambda { |env| raise } + se = EventMiddleware.new events + e = Events.new app, [se] + assert_raises(RuntimeError) do + e.call({}) + end + assert_equal [[se, :on_start], + [se, :on_error], + [se, :on_finish], + ], events + end + end +end diff --git a/test/spec_fastcgi.rb b/test/spec_fastcgi.rb index ef7c3c3f..5a48327b 100644 --- a/test/spec_fastcgi.rb +++ b/test/spec_fastcgi.rb @@ -1,5 +1,7 @@ require 'helper' -begin + +if defined? LIGHTTPD_PID + require File.expand_path('../testrequest', __FILE__) require 'rack/handler/fastcgi' @@ -11,10 +13,6 @@ describe Rack::Handler::FastCGI do @port = 9203 end - if `which lighttpd` && !$?.success? - raise "lighttpd not found" - end - it "respond" do sleep 1 GET("/test") @@ -84,8 +82,4 @@ describe Rack::Handler::FastCGI do end end -rescue RuntimeError - $stderr.puts "Skipping Rack::Handler::FastCGI tests (lighttpd is required). Install lighttpd and try again." -rescue LoadError - $stderr.puts "Skipping Rack::Handler::FastCGI tests (FCGI is required). `gem install fcgi` and try again." -end +end # if defined? LIGHTTPD_PID diff --git a/test/spec_file.rb b/test/spec_file.rb index 2d0919a9..48c0ab90 100644 --- a/test/spec_file.rb +++ b/test/spec_file.rb @@ -68,6 +68,11 @@ describe Rack::File do assert_match(res, /ruby/) end + it "serve uri with URL encoded null byte (%00) in filenames" do + res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test%00") + res.must_be :bad_request? + end + it "allow safe directory traversal" do req = Rack::MockRequest.new(file(DOCROOT)) @@ -179,8 +184,8 @@ describe Rack::File do status, heads, _ = file(DOCROOT).call(env) status.must_equal 200 - heads['Cache-Control'].must_equal nil - heads['Access-Control-Allow-Origin'].must_equal nil + heads['Cache-Control'].must_be_nil + heads['Access-Control-Allow-Origin'].must_be_nil end it "only support GET, HEAD, and OPTIONS requests" do @@ -234,7 +239,26 @@ describe Rack::File do req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, nil))) res = req.get "/cgi/test" res.must_be :successful? - res['Content-Type'].must_equal nil + res['Content-Type'].must_be_nil + end + + it "return error when file not found for head request" do + res = Rack::MockRequest.new(file(DOCROOT)).head("/cgi/missing") + res.must_be :not_found? + res.body.must_be :empty? + end + + class MyFile < Rack::File + def response_body + "hello world" + end + end + + it "behaves gracefully if response_body is present" do + file = Rack::Lint.new MyFile.new(DOCROOT) + res = Rack::MockRequest.new(file).get("/cgi/test") + + res.must_be :ok? end end diff --git a/test/spec_lint.rb b/test/spec_lint.rb index 6d1c2c45..d99c1aa3 100644 --- a/test/spec_lint.rb +++ b/test/spec_lint.rb @@ -269,7 +269,7 @@ describe Rack::Lint do # }.must_raise(Rack::Lint::LintError). # message.must_match(/No Content-Type/) - [100, 101, 204, 205, 304].each do |status| + [100, 101, 204, 304].each do |status| lambda { Rack::Lint.new(lambda { |env| [status, {"Content-type" => "text/plain", "Content-length" => "0"}, []] @@ -280,7 +280,7 @@ describe Rack::Lint do end it "notice content-length errors" do - [100, 101, 204, 205, 304].each do |status| + [100, 101, 204, 304].each do |status| lambda { Rack::Lint.new(lambda { |env| [status, {"Content-length" => "0"}, []] diff --git a/test/spec_lock.rb b/test/spec_lock.rb index aa3efa54..c6f7c05e 100644 --- a/test/spec_lock.rb +++ b/test/spec_lock.rb @@ -147,7 +147,8 @@ describe Rack::Lock do }, false) env = Rack::MockRequest.env_for("/") env['rack.multithread'].must_equal true - app.call(env) + _, _, body = app.call(env) + body.close env['rack.multithread'].must_equal true end @@ -191,4 +192,13 @@ describe Rack::Lock do lambda { app.call(env) }.must_raise Exception lock.synchronized.must_equal false end + + it "not replace the environment" do + env = Rack::MockRequest.env_for("/") + app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, [inner_env.object_id.to_s]] }) + + _, _, body = app.call(env) + + body.to_enum.to_a.must_equal [env.object_id.to_s] + end end diff --git a/test/spec_media_type.rb b/test/spec_media_type.rb index ef054364..1d9f0fc3 100644 --- a/test/spec_media_type.rb +++ b/test/spec_media_type.rb @@ -8,7 +8,7 @@ describe Rack::MediaType do before { @content_type = nil } it '#type is nil' do - Rack::MediaType.type(@content_type).must_equal nil + Rack::MediaType.type(@content_type).must_be_nil end it '#params is empty' do diff --git a/test/spec_method_override.rb b/test/spec_method_override.rb index 14ace0b1..bb72af9f 100644 --- a/test/spec_method_override.rb +++ b/test/spec_method_override.rb @@ -66,14 +66,27 @@ EOF "CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x", "CONTENT_LENGTH" => input.size.to_s, :method => "POST", :input => input) - begin - app.call env - rescue EOFError - end + app.call env env["REQUEST_METHOD"].must_equal "POST" end + it "writes error to RACK_ERRORS when given invalid multipart form data" do + input = <<EOF +--AaB03x\r +content-disposition: form-data; name="huge"; filename="huge"\r +EOF + env = Rack::MockRequest.env_for("/", + "CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x", + "CONTENT_LENGTH" => input.size.to_s, + Rack::RACK_ERRORS => StringIO.new, + :method => "POST", :input => input) + Rack::MethodOverride.new(proc { [200, {"Content-Type" => "text/plain"}, []] }).call env + + env[Rack::RACK_ERRORS].rewind + env[Rack::RACK_ERRORS].read.must_match /Bad request content body/ + end + it "not modify REQUEST_METHOD for POST requests when the params are unparseable" do env = Rack::MockRequest.env_for("/", :method => "POST", :input => "(%bad-params%)") app.call env diff --git a/test/spec_mime.rb b/test/spec_mime.rb index cd40b4b5..569233b4 100644 --- a/test/spec_mime.rb +++ b/test/spec_mime.rb @@ -19,7 +19,7 @@ describe Rack::Mime do end it "should support null fallbacks" do - Rack::Mime.mime_type('.nothing', nil).must_equal nil + Rack::Mime.mime_type('.nothing', nil).must_be_nil end it "should match exact mimes" do diff --git a/test/spec_mock.rb b/test/spec_mock.rb index f27f7d6e..c7992321 100644 --- a/test/spec_mock.rb +++ b/test/spec_mock.rb @@ -84,6 +84,15 @@ describe Rack::MockRequest do it "set content length" do env = Rack::MockRequest.env_for("/", :input => "foo") env["CONTENT_LENGTH"].must_equal "3" + + env = Rack::MockRequest.env_for("/", :input => StringIO.new("foo")) + env["CONTENT_LENGTH"].must_equal "3" + + env = Rack::MockRequest.env_for("/", :input => Tempfile.new("name").tap { |t| t << "foo" }) + env["CONTENT_LENGTH"].must_equal "3" + + env = Rack::MockRequest.env_for("/", :input => IO.pipe.first) + env["CONTENT_LENGTH"].must_be_nil end it "allow posting" do @@ -211,6 +220,23 @@ describe Rack::MockRequest do Rack::MockRequest.new(capp).get('/', :lint => true) called.must_equal true end + + it "defaults encoding to ASCII 8BIT" do + req = Rack::MockRequest.env_for("/foo") + + keys = [ + Rack::REQUEST_METHOD, + Rack::SERVER_NAME, + Rack::SERVER_PORT, + Rack::QUERY_STRING, + Rack::PATH_INFO, + Rack::HTTPS, + Rack::RACK_URL_SCHEME + ] + keys.each do |k| + assert_equal Encoding::ASCII_8BIT, req[k].encoding + end + end end describe Rack::MockResponse do @@ -273,3 +299,70 @@ describe Rack::MockResponse do }.must_raise Rack::MockRequest::FatalWarning end end + +describe Rack::MockResponse, 'headers' do + before do + @res = Rack::MockRequest.new(app).get('') + @res.set_header 'FOO', '1' + end + + it 'has_header?' do + lambda { @res.has_header? nil }.must_raise NoMethodError + + @res.has_header?('FOO').must_equal true + @res.has_header?('Foo').must_equal true + end + + it 'get_header' do + lambda { @res.get_header nil }.must_raise NoMethodError + + @res.get_header('FOO').must_equal '1' + @res.get_header('Foo').must_equal '1' + end + + it 'set_header' do + lambda { @res.set_header nil, '1' }.must_raise NoMethodError + + @res.set_header('FOO', '2').must_equal '2' + @res.get_header('FOO').must_equal '2' + + @res.set_header('Foo', '3').must_equal '3' + @res.get_header('Foo').must_equal '3' + @res.get_header('FOO').must_equal '3' + + @res.set_header('FOO', nil).must_be_nil + @res.get_header('FOO').must_be_nil + @res.has_header?('FOO').must_equal true + end + + it 'add_header' do + lambda { @res.add_header nil, '1' }.must_raise NoMethodError + + # Sets header on first addition + @res.add_header('FOO', '1').must_equal '1,1' + @res.get_header('FOO').must_equal '1,1' + + # Ignores nil additions + @res.add_header('FOO', nil).must_equal '1,1' + @res.get_header('FOO').must_equal '1,1' + + # Converts additions to strings + @res.add_header('FOO', 2).must_equal '1,1,2' + @res.get_header('FOO').must_equal '1,1,2' + + # Respects underlying case-sensitivity + @res.add_header('Foo', 'yep').must_equal '1,1,2,yep' + @res.get_header('Foo').must_equal '1,1,2,yep' + @res.get_header('FOO').must_equal '1,1,2,yep' + end + + it 'delete_header' do + lambda { @res.delete_header nil }.must_raise NoMethodError + + @res.delete_header('FOO').must_equal '1' + @res.has_header?('FOO').must_equal false + + @res.has_header?('Foo').must_equal false + @res.delete_header('Foo').must_be_nil + end +end diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb index 9e8a6140..40bab4cd 100644 --- a/test/spec_multipart.rb +++ b/test/spec_multipart.rb @@ -27,7 +27,7 @@ describe Rack::Multipart do it "return nil if content type is not multipart" do env = Rack::MockRequest.env_for("/", "CONTENT_TYPE" => 'application/x-www-form-urlencoded') - Rack::Multipart.parse_multipart(env).must_equal nil + Rack::Multipart.parse_multipart(env).must_be_nil end it "parse multipart content when content type present but filename is not" do @@ -72,6 +72,13 @@ describe Rack::Multipart do end end + it "handles quoted encodings" do + # See #905 + env = Rack::MockRequest.env_for("/", multipart_fixture(:unity3d_wwwform)) + params = Rack::Multipart.parse_multipart(env) + params['user_sid'].encoding.must_equal Encoding::UTF_8 + end + it "raise RangeError if the key space is exhausted" do env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename)) @@ -88,6 +95,7 @@ describe Rack::Multipart do env['CONTENT_TYPE'] = "multipart/form-data; boundary=----WebKitFormBoundaryWLHCs9qmcJJoyjKR" params = Rack::Multipart.parse_multipart(env) params['profile']['bio'].must_include 'hello' + params['profile'].keys.must_include 'public_email' end it "reject insanely long boundaries" do @@ -99,11 +107,6 @@ describe Rack::Multipart do def rd.rewind; end wr.sync = true - # mock out length to make this pipe look like a Tempfile - def rd.length - 1024 * 1024 * 8 - end - # write to a pipe in a background thread, this will write a lot # unless Rack (properly) shuts down the read end thr = Thread.new do @@ -128,7 +131,7 @@ describe Rack::Multipart do fixture = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x", - "CONTENT_LENGTH" => rd.length.to_s, + "CONTENT_LENGTH" => (1024 * 1024 * 8).to_s, :input => rd, } @@ -297,11 +300,17 @@ describe Rack::Multipart do params["files"][:filename].must_equal "bob's flowers.jpg" end + it "parse multipart form with a null byte in the filename" do + env = Rack::MockRequest.env_for '/', multipart_fixture(:filename_with_null_byte) + params = Rack::Multipart.parse_multipart(env) + params["files"][:filename].must_equal "flowers.exe\u0000.jpg" + end + it "not include file params if no file was selected" do env = Rack::MockRequest.env_for("/", multipart_fixture(:none)) params = Rack::Multipart.parse_multipart(env) params["submit-name"].must_equal "Larry" - params["files"].must_equal nil + params["files"].must_be_nil params.keys.wont_include "files" end @@ -549,7 +558,7 @@ Content-Type: image/jpeg\r it "return nil if no UploadedFiles were used" do data = Rack::Multipart.build_multipart("people" => [{"submit-name" => "Larry", "files" => "contents"}]) - data.must_equal nil + data.must_be_nil end it "raise ArgumentError if params is not a Hash" do diff --git a/test/spec_request.rb b/test/spec_request.rb index 44fcfd45..bdad68fa 100644 --- a/test/spec_request.rb +++ b/test/spec_request.rb @@ -12,11 +12,31 @@ class RackRequestTest < Minitest::Spec refute_same req.env, req.dup.env end + it 'can check if something has been set' do + req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) + refute req.has_header?("FOO") + end + it "can get a key from the env" do req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) assert_equal "example.com", req.get_header("SERVER_NAME") end + it 'can calculate the authority' do + req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) + assert_equal "example.com:8080", req.authority + end + + it 'can calculate the authority without a port' do + req = make_request(Rack::MockRequest.env_for("http://example.com/")) + assert_equal "example.com:80", req.authority + end + + it 'can calculate the authority without a port on ssl' do + req = make_request(Rack::MockRequest.env_for("https://example.com/")) + assert_equal "example.com:443", req.authority + end + it 'yields to the block if no value has been set' do req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) yielded = false @@ -29,17 +49,6 @@ class RackRequestTest < Minitest::Spec assert_equal "bar", req.get_header("FOO") end - it 'can set values in the env' do - req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) - req.set_header("FOO", "BAR") - assert_equal "BAR", req.get_header("FOO") - end - - it 'can check if something has been set' do - req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) - refute req.has_header?("FOO") - end - it 'can iterate over values' do req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) req.set_header 'foo', 'bar' @@ -50,6 +59,25 @@ class RackRequestTest < Minitest::Spec assert_equal 'bar', hash['foo'] end + it 'can set values in the env' do + req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) + req.set_header("FOO", "BAR") + assert_equal "BAR", req.get_header("FOO") + end + + it 'can add to multivalued headers in the env' do + req = make_request(Rack::MockRequest.env_for('http://example.com:8080/')) + + assert_equal '1', req.add_header('FOO', '1') + assert_equal '1', req.get_header('FOO') + + assert_equal '1,2', req.add_header('FOO', '2') + assert_equal '1,2', req.get_header('FOO') + + assert_equal '1,2', req.add_header('FOO', nil) + assert_equal '1,2', req.get_header('FOO') + end + it 'can delete env values' do req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) req.set_header 'foo', 'bar' @@ -448,7 +476,7 @@ class RackRequestTest < Minitest::Spec req = make_request \ Rack::MockRequest.env_for("/") - req.referer.must_equal nil + req.referer.must_be_nil end it "extract user agent correctly" do @@ -458,25 +486,25 @@ class RackRequestTest < Minitest::Spec req = make_request \ Rack::MockRequest.env_for("/") - req.user_agent.must_equal nil + req.user_agent.must_be_nil end it "treat missing content type as nil" do req = make_request \ Rack::MockRequest.env_for("/") - req.content_type.must_equal nil + req.content_type.must_be_nil end it "treat empty content type as nil" do req = make_request \ Rack::MockRequest.env_for("/", "CONTENT_TYPE" => "") - req.content_type.must_equal nil + req.content_type.must_be_nil end it "return nil media type for empty content type" do req = make_request \ Rack::MockRequest.env_for("/", "CONTENT_TYPE" => "") - req.media_type.must_equal nil + req.media_type.must_be_nil end it "cache, but invalidates the cache" do @@ -1268,13 +1296,18 @@ EOF req.trusted_proxy?('unix').must_equal 0 req.trusted_proxy?('unix:/tmp/sock').must_equal 0 - req.trusted_proxy?("unix.example.org").must_equal nil - req.trusted_proxy?("example.org\n127.0.0.1").must_equal nil - req.trusted_proxy?("127.0.0.1\nexample.org").must_equal nil - req.trusted_proxy?("11.0.0.1").must_equal nil - req.trusted_proxy?("172.15.0.1").must_equal nil - req.trusted_proxy?("172.32.0.1").must_equal nil - req.trusted_proxy?("2001:470:1f0b:18f8::1").must_equal nil + req.trusted_proxy?("unix.example.org").must_be_nil + req.trusted_proxy?("example.org\n127.0.0.1").must_be_nil + req.trusted_proxy?("127.0.0.1\nexample.org").must_be_nil + req.trusted_proxy?("11.0.0.1").must_be_nil + req.trusted_proxy?("172.15.0.1").must_be_nil + req.trusted_proxy?("172.32.0.1").must_be_nil + req.trusted_proxy?("2001:470:1f0b:18f8::1").must_be_nil + end + + it "sets the default session to an empty hash" do + req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) + assert_equal Hash.new, req.session end class MyRequest < Rack::Request @@ -1339,8 +1372,8 @@ EOF include Rack::Request::Helpers extend Forwardable - def_delegators :@req, :get_header, :fetch_header, :delete_header, - :set_header, :has_header?, :each_header + def_delegators :@req, :has_header?, :get_header, :fetch_header, + :each_header, :set_header, :add_header, :delete_header def_delegators :@req, :[], :[]=, :values_at diff --git a/test/spec_response.rb b/test/spec_response.rb index 73634b9e..4fd7d2b3 100644 --- a/test/spec_response.rb +++ b/test/spec_response.rb @@ -4,6 +4,22 @@ require 'rack/response' require 'stringio' describe Rack::Response do + it 'has cache-control methods' do + response = Rack::Response.new + cc = 'foo' + response.cache_control = cc + assert_equal cc, response.cache_control + assert_equal cc, response.to_a[2]['Cache-Control'] + end + + it 'has an etag method' do + response = Rack::Response.new + etag = 'foo' + response.etag = etag + assert_equal etag, response.etag + assert_equal etag, response.to_a[2]['ETag'] + end + it "have sensible default values" do response = Rack::Response.new status, header, body = response.finish @@ -39,7 +55,7 @@ describe Rack::Response do it "can set and read headers" do response = Rack::Response.new - response["Content-Type"].must_equal nil + response["Content-Type"].must_be_nil response["Content-Type"] = "text/plain" response["Content-Type"].must_equal "text/plain" end @@ -99,6 +115,70 @@ describe Rack::Response do response["Set-Cookie"].must_equal "foo=bar" end + it "can set SameSite cookies with symbol value :lax" do + response = Rack::Response.new + response.set_cookie "foo", {:value => "bar", :same_site => :lax} + response["Set-Cookie"].must_equal "foo=bar; SameSite=Lax" + end + + it "can set SameSite cookies with symbol value :Lax" do + response = Rack::Response.new + response.set_cookie "foo", {:value => "bar", :same_site => :lax} + response["Set-Cookie"].must_equal "foo=bar; SameSite=Lax" + end + + it "can set SameSite cookies with string value 'Lax'" do + response = Rack::Response.new + response.set_cookie "foo", {:value => "bar", :same_site => "Lax"} + response["Set-Cookie"].must_equal "foo=bar; SameSite=Lax" + end + + it "can set SameSite cookies with boolean value true" do + response = Rack::Response.new + response.set_cookie "foo", {:value => "bar", :same_site => true} + response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict" + end + + it "can set SameSite cookies with symbol value :strict" do + response = Rack::Response.new + response.set_cookie "foo", {:value => "bar", :same_site => :strict} + response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict" + end + + it "can set SameSite cookies with symbol value :Strict" do + response = Rack::Response.new + response.set_cookie "foo", {:value => "bar", :same_site => :Strict} + response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict" + end + + it "can set SameSite cookies with string value 'Strict'" do + response = Rack::Response.new + response.set_cookie "foo", {:value => "bar", :same_site => "Strict"} + response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict" + end + + it "validates the SameSite option value" do + response = Rack::Response.new + lambda { + response.set_cookie "foo", {:value => "bar", :same_site => "Foo"} + }.must_raise(ArgumentError). + message.must_match(/Invalid SameSite value: "Foo"/) + end + + it "can set SameSite cookies with symbol value" do + response = Rack::Response.new + response.set_cookie "foo", {:value => "bar", :same_site => :Strict} + response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict" + end + + [ nil, false ].each do |non_truthy| + it "omits SameSite attribute given a #{non_truthy.inspect} value" do + response = Rack::Response.new + response.set_cookie "foo", {:value => "bar", :same_site => non_truthy} + response["Set-Cookie"].must_equal "foo=bar" + end + end + it "can delete cookies" do response = Rack::Response.new response.set_cookie "foo", "bar" @@ -106,7 +186,7 @@ describe Rack::Response do response.delete_cookie "foo" response["Set-Cookie"].must_equal [ "foo2=bar2", - "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000" + "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" ].join("\n") end @@ -116,10 +196,10 @@ describe Rack::Response do response.set_cookie "foo", {:value => "bar", :domain => ".example.com"} response["Set-Cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"].join("\n") response.delete_cookie "foo", :domain => ".example.com" - response["Set-Cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n") + response["Set-Cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n") response.delete_cookie "foo", :domain => "sample.example.com" - response["Set-Cookie"].must_equal ["foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000", - "foo=; domain=sample.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n") + response["Set-Cookie"].must_equal ["foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT", + "foo=; domain=sample.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n") end it "can delete cookies with the same name with different paths" do @@ -131,7 +211,7 @@ describe Rack::Response do response.delete_cookie "foo", :path => "/path" response["Set-Cookie"].must_equal ["foo=bar; path=/", - "foo=; path=/path; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n") + "foo=; path=/path; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n") end it "can do redirects" do @@ -193,8 +273,8 @@ describe Rack::Response do _, header, body = r.finish str = ""; body.each { |part| str << part } str.must_be :empty? - header["Content-Type"].must_equal nil - header['Content-Length'].must_equal nil + header["Content-Type"].must_be_nil + header['Content-Length'].must_be_nil lambda { Rack::Response.new(Object.new) @@ -330,7 +410,7 @@ describe Rack::Response do res.body.must_be :closed? end - it "calls close on #body when 204, 205, or 304" do + it "calls close on #body when 204 or 304" do res = Rack::Response.new res.body = StringIO.new res.finish @@ -344,7 +424,7 @@ describe Rack::Response do res.body = StringIO.new res.status = 205 _, _, b = res.finish - res.body.must_be :closed? + res.body.wont_be :closed? b.wont_equal res.body res.body = StringIO.new @@ -360,3 +440,71 @@ describe Rack::Response do lambda { res.finish.last.to_ary }.must_raise NoMethodError end end + +describe Rack::Response, 'headers' do + before do + @response = Rack::Response.new([], 200, { 'Foo' => '1' }) + end + + it 'has_header?' do + lambda { @response.has_header? nil }.must_raise NoMethodError + + @response.has_header?('Foo').must_equal true + @response.has_header?('foo').must_equal true + end + + it 'get_header' do + lambda { @response.get_header nil }.must_raise NoMethodError + + @response.get_header('Foo').must_equal '1' + @response.get_header('foo').must_equal '1' + end + + it 'set_header' do + lambda { @response.set_header nil, '1' }.must_raise NoMethodError + + @response.set_header('Foo', '2').must_equal '2' + @response.has_header?('Foo').must_equal true + @response.get_header('Foo').must_equal('2') + + @response.set_header('Foo', nil).must_be_nil + @response.has_header?('Foo').must_equal true + @response.get_header('Foo').must_be_nil + end + + it 'add_header' do + lambda { @response.add_header nil, '1' }.must_raise NoMethodError + + # Add a value to an existing header + @response.add_header('Foo', '2').must_equal '1,2' + @response.get_header('Foo').must_equal '1,2' + + # Add nil to an existing header + @response.add_header('Foo', nil).must_equal '1,2' + @response.get_header('Foo').must_equal '1,2' + + # Add nil to a nonexistent header + @response.add_header('Bar', nil).must_be_nil + @response.has_header?('Bar').must_equal false + @response.get_header('Bar').must_be_nil + + # Add a value to a nonexistent header + @response.add_header('Bar', '1').must_equal '1' + @response.has_header?('Bar').must_equal true + @response.get_header('Bar').must_equal '1' + end + + it 'delete_header' do + lambda { @response.delete_header nil }.must_raise NoMethodError + + @response.delete_header('Foo').must_equal '1' + (!!@response.has_header?('Foo')).must_equal false + + @response.delete_header('Foo').must_be_nil + @response.has_header?('Foo').must_equal false + + @response.set_header('Foo', 1) + @response.delete_header('foo').must_equal 1 + @response.has_header?('Foo').must_equal false + end +end diff --git a/test/spec_sendfile.rb b/test/spec_sendfile.rb index 18689857..1eb9413c 100644 --- a/test/spec_sendfile.rb +++ b/test/spec_sendfile.rb @@ -122,4 +122,39 @@ describe Rack::Sendfile do FileUtils.remove_entry_secure dir2 end end + + it "sets X-Accel-Redirect response header and discards body when initialized with multiple mappings via header" do + begin + dir1 = Dir.mktmpdir + dir2 = Dir.mktmpdir + + first_body = open_file(File.join(dir1, 'rack_sendfile')) + first_body.puts 'hello world' + + second_body = open_file(File.join(dir2, 'rack_sendfile')) + second_body.puts 'goodbye world' + + headers = { + 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect', + 'HTTP_X_ACCEL_MAPPING' => "#{dir1}/=/foo/bar/, #{dir2}/=/wibble/" + } + + request(headers, first_body) do |response| + response.must_be :ok? + response.body.must_be :empty? + response.headers['Content-Length'].must_equal '0' + response.headers['X-Accel-Redirect'].must_equal '/foo/bar/rack_sendfile' + end + + request(headers, second_body) do |response| + response.must_be :ok? + response.body.must_be :empty? + response.headers['Content-Length'].must_equal '0' + response.headers['X-Accel-Redirect'].must_equal '/wibble/rack_sendfile' + end + ensure + FileUtils.remove_entry_secure dir1 + FileUtils.remove_entry_secure dir2 + end + end end diff --git a/test/spec_server.rb b/test/spec_server.rb index a3690bce..4864a87a 100644 --- a/test/spec_server.rb +++ b/test/spec_server.rb @@ -77,7 +77,7 @@ describe Rack::Server do o, ENV["REQUEST_METHOD"] = ENV["REQUEST_METHOD"], 'foo' server = Rack::Server.new(:app => 'foo') server.server.name =~ /CGI/ - Rack::Server.logging_middleware.call(server).must_equal nil + Rack::Server.logging_middleware.call(server).must_be_nil ensure ENV['REQUEST_METHOD'] = o end @@ -85,7 +85,7 @@ describe Rack::Server do it "be quiet if said so" do server = Rack::Server.new(:app => "FOO", :quiet => true) - Rack::Server.logging_middleware.call(server).must_equal nil + Rack::Server.logging_middleware.call(server).must_be_nil end it "use a full path to the pidfile" do diff --git a/test/spec_session_abstract_session_hash.rb b/test/spec_session_abstract_session_hash.rb new file mode 100644 index 00000000..76b34a01 --- /dev/null +++ b/test/spec_session_abstract_session_hash.rb @@ -0,0 +1,45 @@ +require 'minitest/autorun' +require 'rack/session/abstract/id' + +describe Rack::Session::Abstract::SessionHash do + attr_reader :hash + + def setup + super + store = Class.new do + def load_session(req) + ["id", {foo: :bar, baz: :qux}] + end + def session_exists?(req) + true + end + end + @hash = Rack::Session::Abstract::SessionHash.new(store.new, nil) + end + + it "returns keys" do + assert_equal ["foo", "baz"], hash.keys + end + + it "returns values" do + assert_equal [:bar, :qux], hash.values + end + + describe "#fetch" do + it "returns value for a matching key" do + assert_equal :bar, hash.fetch(:foo) + end + + it "works with a default value" do + assert_equal :default, hash.fetch(:unknown, :default) + end + + it "works with a block" do + assert_equal :default, hash.fetch(:unkown) { :default } + end + + it "it raises when fetching unknown keys without defaults" do + lambda { hash.fetch(:unknown) }.must_raise KeyError + end + end +end diff --git a/test/spec_session_cookie.rb b/test/spec_session_cookie.rb index 2b382b50..9201a729 100644 --- a/test/spec_session_cookie.rb +++ b/test/spec_session_cookie.rb @@ -98,18 +98,18 @@ describe Rack::Session::Cookie do it 'rescues failures on decode' do coder = Rack::Session::Cookie::Base64::Marshal.new - coder.decode('lulz').must_equal nil + coder.decode('lulz').must_be_nil end end describe 'JSON' do - it 'marshals and base64 encodes' do + it 'JSON and base64 encodes' do coder = Rack::Session::Cookie::Base64::JSON.new obj = %w[fuuuuu] coder.encode(obj).must_equal [::JSON.dump(obj)].pack('m') end - it 'marshals and base64 decodes' do + it 'JSON and base64 decodes' do coder = Rack::Session::Cookie::Base64::JSON.new str = [::JSON.dump(%w[fuuuuu])].pack('m') coder.decode(str).must_equal ::JSON.parse(str.unpack('m').first) @@ -117,7 +117,7 @@ describe Rack::Session::Cookie do it 'rescues failures on decode' do coder = Rack::Session::Cookie::Base64::JSON.new - coder.decode('lulz').must_equal nil + coder.decode('lulz').must_be_nil end end @@ -139,7 +139,7 @@ describe Rack::Session::Cookie do it 'rescues failures on decode' do coder = Rack::Session::Cookie::Base64::ZipJSON.new - coder.decode('lulz').must_equal nil + coder.decode('lulz').must_be_nil end end end @@ -311,6 +311,22 @@ describe Rack::Session::Cookie do response.body.must_equal '{"counter"=>2}' end + it "supports custom digest class" do + app = [incrementor, { :secret => "test", hmac: OpenSSL::Digest::SHA256 }] + + response = response_for(:app => app) + response = response_for(:app => app, :cookie => response) + response.body.must_equal '{"counter"=>2}' + + response = response_for(:app => app, :cookie => response) + response.body.must_equal '{"counter"=>3}' + + app = [incrementor, { :secret => "other" }] + + response = response_for(:app => app, :cookie => response) + response.body.must_equal '{"counter"=>1}' + end + it "can handle Rack::Lint middleware" do response = response_for(:app => incrementor) diff --git a/test/spec_session_memcache.rb b/test/spec_session_memcache.rb index 08ef5f70..93a03d12 100644 --- a/test/spec_session_memcache.rb +++ b/test/spec_session_memcache.rb @@ -35,9 +35,9 @@ begin Rack::Session::Memcache.new(incrementor) it "faults on no connection" do - lambda{ + lambda { Rack::Session::Memcache.new(incrementor, :memcache_server => 'nosuchserver') - }.should.raise + }.must_raise(RuntimeError).message.must_equal 'No memcache servers' end it "connects to existing server" do @@ -143,7 +143,7 @@ begin res1.body.must_equal '{"counter"=>1}' res2 = dreq.get("/", "HTTP_COOKIE" => cookie) - res2["Set-Cookie"].must_equal nil + res2["Set-Cookie"].must_be_nil res2.body.must_equal '{"counter"=>2}' res3 = req.get("/", "HTTP_COOKIE" => cookie) @@ -183,7 +183,7 @@ begin creq = Rack::MockRequest.new(count) res0 = dreq.get("/") - res0["Set-Cookie"].must_equal nil + res0["Set-Cookie"].must_be_nil res0.body.must_equal '{"counter"=>1}' res0 = creq.get("/") @@ -201,7 +201,7 @@ begin creq = Rack::MockRequest.new(count) res0 = sreq.get("/") - res0["Set-Cookie"].must_equal nil + res0["Set-Cookie"].must_be_nil res0.body.must_equal '{"counter"=>1}' res0 = creq.get("/") diff --git a/test/spec_session_pool.rb b/test/spec_session_pool.rb index 5eaadfff..2d061691 100644 --- a/test/spec_session_pool.rb +++ b/test/spec_session_pool.rb @@ -138,7 +138,7 @@ describe Rack::Session::Pool do dreq = Rack::MockRequest.new(defer) res1 = dreq.get("/") - res1["Set-Cookie"].must_equal nil + res1["Set-Cookie"].must_be_nil res1.body.must_equal '{"counter"=>1}' pool.pool.size.must_equal 1 end diff --git a/test/spec_static.rb b/test/spec_static.rb index f0a47171..634f8acf 100644 --- a/test/spec_static.rb +++ b/test/spec_static.rb @@ -97,7 +97,7 @@ describe Rack::Static do it "serves regular files if client accepts gzip encoding and gzip files are not present" do res = @gzip_request.get("/cgi/rackup_stub.rb", 'HTTP_ACCEPT_ENCODING'=>'deflate, gzip') res.must_be :ok? - res.headers['Content-Encoding'].must_equal nil + res.headers['Content-Encoding'].must_be_nil res.headers['Content-Type'].must_equal 'text/x-script.ruby' res.body.must_match(/ruby/) end @@ -105,7 +105,7 @@ describe Rack::Static do it "serves regular files if client does not accept gzip encoding" do res = @gzip_request.get("/cgi/test") res.must_be :ok? - res.headers['Content-Encoding'].must_equal nil + res.headers['Content-Encoding'].must_be_nil res.headers['Content-Type'].must_equal 'text/plain' res.body.must_match(/ruby/) end diff --git a/test/spec_utils.rb b/test/spec_utils.rb index 76198fb8..143ad30a 100644 --- a/test/spec_utils.rb +++ b/test/spec_utils.rb @@ -64,7 +64,7 @@ describe Rack::Utils do end it "not hang on escaping long strings that end in % (http://redmine.ruby-lang.org/issues/5149)" do - timeout(1) do + Timeout.timeout(1) do lambda { URI.decode_www_form_component "A string that causes catastrophic backtracking as it gets longer %" }.must_raise ArgumentError @@ -206,6 +206,14 @@ describe Rack::Utils do Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][w]=a&x[y][][z]=2&x[y][][w]=3"). must_equal "x" => {"y" => [{"z" => "1", "w" => "a"}, {"z" => "2", "w" => "3"}]} + Rack::Utils.parse_nested_query("x[][y]=1&x[][z][w]=a&x[][y]=2&x[][z][w]=b"). + must_equal "x" => [{"y" => "1", "z" => {"w" => "a"}}, {"y" => "2", "z" => {"w" => "b"}}] + Rack::Utils.parse_nested_query("x[][z][w]=a&x[][y]=1&x[][z][w]=b&x[][y]=2"). + must_equal "x" => [{"y" => "1", "z" => {"w" => "a"}}, {"y" => "2", "z" => {"w" => "b"}}] + + Rack::Utils.parse_nested_query("data[books][][data][page]=1&data[books][][data][page]=2"). + must_equal "data" => { "books" => [{ "data" => { "page" => "1"}}, { "data" => { "page" => "2"}}] } + lambda { Rack::Utils.parse_nested_query("x[y]=1&x[y]z=2") }. must_raise(Rack::Utils::ParameterTypeError). message.must_equal "expected Hash (got String) for param `y'" @@ -223,6 +231,18 @@ describe Rack::Utils do message.must_equal "invalid byte sequence in UTF-8" end + it "only moves to a new array when the full key has been seen" do + Rack::Utils.parse_nested_query("x[][y][][z]=1&x[][y][][w]=2"). + must_equal "x" => [{"y" => [{"z" => "1", "w" => "2"}]}] + + Rack::Utils.parse_nested_query( + "x[][id]=1&x[][y][a]=5&x[][y][b]=7&x[][z][id]=3&x[][z][w]=0&x[][id]=2&x[][y][a]=6&x[][y][b]=8&x[][z][id]=4&x[][z][w]=0" + ).must_equal "x" => [ + {"id" => "1", "y" => {"a" => "5", "b" => "7"}, "z" => {"id" => "3", "w" => "0"}}, + {"id" => "2", "y" => {"a" => "6", "b" => "8"}, "z" => {"id" => "4", "w" => "0"}}, + ] + end + it "allow setting the params hash class to use for parsing query strings" do begin default_parser = Rack::Utils.default_query_parser @@ -300,13 +320,15 @@ describe Rack::Utils do must_equal 'x[y][][z]=1&x[y][][z]=2' Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1', 'w' => 'a' }, { 'z' => '2', 'w' => '3' }] }). must_equal 'x[y][][z]=1&x[y][][w]=a&x[y][][z]=2&x[y][][w]=3' + Rack::Utils.build_nested_query({"foo" => ["1", ["2"]]}). + must_equal 'foo[]=1&foo[][]=2' lambda { Rack::Utils.build_nested_query("foo=bar") }. must_raise(ArgumentError). message.must_equal "value must be a Hash" end - should 'perform the inverse function of #parse_nested_query' do + it 'performs the inverse function of #parse_nested_query' do [{"foo" => nil, "bar" => ""}, {"foo" => "bar", "baz" => ""}, {"foo" => ["1", "2"]}, @@ -323,7 +345,8 @@ describe Rack::Utils do {"x" => {"y" => [{"v" => {"w" => "1"}}]}}, {"x" => {"y" => [{"z" => "1", "v" => {"w" => "2"}}]}}, {"x" => {"y" => [{"z" => "1"}, {"z" => "2"}]}}, - {"x" => {"y" => [{"z" => "1", "w" => "a"}, {"z" => "2", "w" => "3"}]}} + {"x" => {"y" => [{"z" => "1", "w" => "a"}, {"z" => "2", "w" => "3"}]}}, + {"foo" => ["1", ["2"]]}, ].each { |params| qs = Rack::Utils.build_nested_query(params) Rack::Utils.parse_nested_query(qs).must_equal params @@ -345,23 +368,6 @@ describe Rack::Utils do Rack::Utils.build_query(key => nil).must_equal Rack::Utils.escape(key) end - it "parse cookies" do - env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "zoo=m") - Rack::Utils.parse_cookies(env).must_equal({"zoo" => "m"}) - - env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=%") - Rack::Utils.parse_cookies(env).must_equal({"foo" => "%"}) - - env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar;foo=car") - Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar"}) - - env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar;quux=h&m") - Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar", "quux" => "h&m"}) - - env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar").freeze - Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar"}) - end - it "parse q-values" do # XXX handle accept-extension Rack::Utils.q_values("foo;q=0.5,bar,baz;q=0.9").must_equal [ @@ -388,7 +394,7 @@ describe Rack::Utils do Rack::Utils.best_q_match("text/plain,text/html", %w[text/html text/plain]).must_equal "text/html" # When there are no matches, return nil: - Rack::Utils.best_q_match("application/json", %w[text/html text/plain]).must_equal nil + Rack::Utils.best_q_match("application/json", %w[text/html text/plain]).must_be_nil end it "escape html entities [&><'\"/]" do @@ -421,9 +427,9 @@ describe Rack::Utils do Rack::Utils.select_best_encoding(a, b) end - helper.call(%w(), [["x", 1]]).must_equal nil - helper.call(%w(identity), [["identity", 0.0]]).must_equal nil - helper.call(%w(identity), [["*", 0.0]]).must_equal nil + helper.call(%w(), [["x", 1]]).must_be_nil + helper.call(%w(identity), [["identity", 0.0]]).must_be_nil + helper.call(%w(identity), [["*", 0.0]]).must_be_nil helper.call(%w(identity), [["compress", 1.0], ["gzip", 1.0]]).must_equal "identity" @@ -483,17 +489,64 @@ describe Rack::Utils do end end +describe Rack::Utils, "cookies" do + it "parses cookies" do + env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "zoo=m") + Rack::Utils.parse_cookies(env).must_equal({"zoo" => "m"}) + + env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=%") + Rack::Utils.parse_cookies(env).must_equal({"foo" => "%"}) + + env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar;foo=car") + Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar"}) + + env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar;quux=h&m") + Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar", "quux" => "h&m"}) + + env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar").freeze + Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar"}) + end + + it "adds new cookies to nil header" do + Rack::Utils.add_cookie_to_header(nil, 'name', 'value').must_equal 'name=value' + end + + it "adds new cookies to blank header" do + header = '' + Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal 'name=value' + header.must_equal '' + end + + it "adds new cookies to string header" do + header = 'existing-cookie' + Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal "existing-cookie\nname=value" + header.must_equal 'existing-cookie' + end + + it "adds new cookies to array header" do + header = %w[ existing-cookie ] + Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal "existing-cookie\nname=value" + header.must_equal %w[ existing-cookie ] + end + + it "adds new cookies to an unrecognized header" do + lambda { + Rack::Utils.add_cookie_to_header(Object.new, 'name', 'value') + }.must_raise ArgumentError + end +end + describe Rack::Utils, "byte_range" do it "ignore missing or syntactically invalid byte ranges" do - Rack::Utils.byte_ranges({},500).must_equal nil - Rack::Utils.byte_ranges({"HTTP_RANGE" => "foobar"},500).must_equal nil - Rack::Utils.byte_ranges({"HTTP_RANGE" => "furlongs=123-456"},500).must_equal nil - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes="},500).must_equal nil - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-"},500).must_equal nil - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123,456"},500).must_equal nil + Rack::Utils.byte_ranges({},500).must_be_nil + Rack::Utils.byte_ranges({"HTTP_RANGE" => "foobar"},500).must_be_nil + Rack::Utils.byte_ranges({"HTTP_RANGE" => "furlongs=123-456"},500).must_be_nil + Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes="},500).must_be_nil + Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-"},500).must_be_nil + Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123,456"},500).must_be_nil # A range of non-positive length is syntactically invalid and ignored: - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=456-123"},500).must_equal nil - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=456-455"},500).must_equal nil + Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=456-123"},500).must_be_nil + Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=456-455"},500).must_be_nil end it "parse simple byte ranges" do diff --git a/test/spec_webrick.rb b/test/spec_webrick.rb index 8e0360d2..e3050f6f 100644 --- a/test/spec_webrick.rb +++ b/test/spec_webrick.rb @@ -1,6 +1,6 @@ require 'minitest/autorun' require 'rack/mock' -require 'concurrent/atomic/count_down_latch' +require 'concurrent/atomic/event' require File.expand_path('../testrequest', __FILE__) Thread.abort_on_exception = true @@ -17,6 +17,19 @@ describe Rack::Handler::WEBrick do Rack::Lint.new(TestRequest.new) @thread = Thread.new { @server.start } trap(:INT) { @server.shutdown } + @status_thread = Thread.new do + seconds = 10 + wait_time = 0.1 + until is_running? || seconds <= 0 + seconds -= wait_time + sleep wait_time + end + raise "Server never reached status 'Running'" unless is_running? + end + end + + def is_running? + @server.status == :Running end it "respond" do @@ -106,8 +119,7 @@ describe Rack::Handler::WEBrick do end it "provide a .run" do - block_ran = false - latch = Concurrent::CountDownLatch.new 1 + latch = Concurrent::Event.new t = Thread.new do Rack::Handler::WEBrick.run(lambda {}, @@ -116,10 +128,9 @@ describe Rack::Handler::WEBrick do :Port => 9210, :Logger => WEBrick::Log.new(nil, WEBrick::BasicLog::WARN), :AccessLog => []}) { |server| - block_ran = true assert_kind_of WEBrick::HTTPServer, server @s = server - latch.count_down + latch.set } end @@ -158,7 +169,7 @@ describe Rack::Handler::WEBrick do Rack::Lint.new(lambda{ |req| [ 200, - {"rack.hijack" => io_lambda}, + [ [ "rack.hijack", io_lambda ] ], [""] ] }) @@ -182,12 +193,13 @@ describe Rack::Handler::WEBrick do Net::HTTP.start(@host, @port){ |http| res = http.get("/chunked") res["Transfer-Encoding"].must_equal "chunked" - res["Content-Length"].must_equal nil + res["Content-Length"].must_be_nil res.body.must_equal "chunked" } end after do + @status_thread.join @server.shutdown @thread.join end |