diff options
42 files changed, 554 insertions, 81 deletions
diff --git a/.travis.yml b/.travis.yml index fa8db9d0..cf999939 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,30 @@ -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.5 + - 2.3.1 - ruby-head - rbx-2 - - jruby - - jruby-9000 + - jruby-9.0.4.0 - jruby-head notifications: @@ -17,5 +33,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 @@ -1,3 +1,19 @@ +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: @@ -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/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 4a02eed1..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 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/common_logger.rb b/lib/rack/common_logger.rb index 1ec8266d..ae410430 100644 --- a/lib/rack/common_logger.rb +++ b/lib/rack/common_logger.rb @@ -48,7 +48,7 @@ module Rack 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, diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb index c656432e..f0fa5e4f 100644 --- a/lib/rack/deflater.rb +++ b/lib/rack/deflater.rb @@ -22,7 +22,7 @@ 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 { |*, body| body.map(&:bytesize).reduce(0, :+) > 512 } + # e.g use Rack::Deflater, :if => lambda { |env, status, headers, body| body.map(&:bytesize).reduce(0, :+) > 512 } # 'include' - a list of content types that should be compressed def initialize(app, options = {}) @app = app 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 0ecc55ab..a0041062 100644 --- a/lib/rack/etag.rb +++ b/lib/rack/etag.rb @@ -65,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/file.rb b/lib/rack/file.rb index 5b755f56..0a257b3d 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, { 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/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/mock.rb b/lib/rack/mock.rb index a2fc2401..4ebc4df1 100644 --- a/lib/rack/mock.rb +++ b/lib/rack/mock.rb @@ -128,7 +128,7 @@ module Rack end end - empty_str = ''.force_encoding(Encoding::ASCII_8BIT) + empty_str = String.new.force_encoding(Encoding::ASCII_8BIT) opts[:input] ||= empty_str if String === opts[:input] rack_input = StringIO.new(opts[:input]) @@ -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 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..d8cb3670 100644 --- a/lib/rack/multipart/parser.rb +++ b/lib/rack/multipart/parser.rb @@ -8,7 +8,7 @@ module Rack BUFSIZE = 16384 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 @@ -347,6 +345,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 50dba475..5bf3eb17 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 @@ -160,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 @@ -437,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/server.rb b/lib/rack/server.rb index 690f1096..1f37aacb 100644 --- a/lib/rack/server.rb +++ b/lib/rack/server.rb @@ -1,4 +1,5 @@ require 'optparse' +require 'fileutils' module Rack @@ -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 f8536f46..ca1a2628 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 @@ -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 @@ -198,7 +210,7 @@ module Rack :sidbits => 128, :cookie_only => true, :secure_random => ::SecureRandom - } + }.freeze attr_reader :key, :default_options, :sid_secure 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 616edbeb..7b842125 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -248,12 +248,23 @@ module Rack rfc2822(value[:expires].clone.gmtime) 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, '' @@ -550,6 +561,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', @@ -597,5 +609,12 @@ module Rack 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..259ae3ab 100644 --- a/rack.gemspec +++ b/rack.gemspec @@ -16,20 +16,18 @@ Also see http://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.author = 'Aaron Patterson' + s.email = 'tenderlove@ruby-lang.org' s.homepage = 'http://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 64f6d30f..aa9c0e0a 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -2,7 +2,10 @@ require 'minitest/autorun' module Rack class TestCase < Minitest::Test - if `which lighttpd` && !$?.success? + # Check for Lighttpd and launch it for tests if available. + `which lighttpd` + + if $?.success? begin # Keep this first. LIGHTTPD_PID = fork { 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_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..10ee2bd0 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 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_file.rb b/test/spec_file.rb index 2d0919a9..3106e629 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)) @@ -237,4 +242,10 @@ describe Rack::File do res['Content-Type'].must_equal 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 + end diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb index 9e8a6140..02b86bed 100644 --- a/test/spec_multipart.rb +++ b/test/spec_multipart.rb @@ -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 @@ -297,6 +305,12 @@ 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) diff --git a/test/spec_request.rb b/test/spec_request.rb index 2062e246..74f2fa87 100644 --- a/test/spec_request.rb +++ b/test/spec_request.rb @@ -1305,6 +1305,11 @@ EOF req.trusted_proxy?("2001:470:1f0b:18f8::1").must_equal 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 def params {:foo => "bar"} diff --git a/test/spec_response.rb b/test/spec_response.rb index de0670da..02e51435 100644 --- a/test/spec_response.rb +++ b/test/spec_response.rb @@ -115,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" 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_utils.rb b/test/spec_utils.rb index 17a12115..e5d4d244 100644 --- a/test/spec_utils.rb +++ b/test/spec_utils.rb @@ -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 diff --git a/test/spec_webrick.rb b/test/spec_webrick.rb index 8e0360d2..4a10c1ca 100644 --- a/test/spec_webrick.rb +++ b/test/spec_webrick.rb @@ -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 @@ -158,7 +171,7 @@ describe Rack::Handler::WEBrick do Rack::Lint.new(lambda{ |req| [ 200, - {"rack.hijack" => io_lambda}, + [ [ "rack.hijack", io_lambda ] ], [""] ] }) @@ -188,6 +201,7 @@ describe Rack::Handler::WEBrick do end after do + @status_thread.join @server.shutdown @thread.join end |