diff options
-rw-r--r-- | .travis.yml | 27 | ||||
-rw-r--r-- | HISTORY.md | 17 | ||||
-rw-r--r-- | lib/rack.rb | 2 | ||||
-rw-r--r-- | lib/rack/auth/digest/params.rb | 5 | ||||
-rw-r--r-- | lib/rack/common_logger.rb | 2 | ||||
-rw-r--r-- | lib/rack/directory.rb | 22 | ||||
-rw-r--r-- | lib/rack/etag.rb | 4 | ||||
-rw-r--r-- | lib/rack/file.rb | 11 | ||||
-rw-r--r-- | lib/rack/handler.rb | 2 | ||||
-rw-r--r-- | lib/rack/mock.rb | 1 | ||||
-rw-r--r-- | lib/rack/multipart/generator.rb | 2 | ||||
-rw-r--r-- | lib/rack/multipart/parser.rb | 2 | ||||
-rw-r--r-- | lib/rack/query_parser.rb | 11 | ||||
-rw-r--r-- | lib/rack/request.rb | 42 | ||||
-rw-r--r-- | lib/rack/urlmap.rb | 6 | ||||
-rw-r--r-- | lib/rack/utils.rb | 21 | ||||
-rw-r--r-- | test/helper.rb | 5 | ||||
-rw-r--r-- | test/spec_directory.rb | 15 | ||||
-rw-r--r-- | test/spec_etag.rb | 4 | ||||
-rw-r--r-- | test/spec_file.rb | 11 | ||||
-rw-r--r-- | test/spec_response.rb | 60 | ||||
-rw-r--r-- | test/spec_utils.rb | 15 | ||||
-rw-r--r-- | test/spec_webrick.rb | 14 |
23 files changed, 244 insertions, 57 deletions
diff --git a/.travis.yml b/.travis.yml index 5008c954..cf999939 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,27 @@ +language: ruby +sudo: false +cache: + - bundler + - apt + +services: + - memcached + +addons: + apt: + packages: + - lighttpd + - libfcgi-dev + before_install: - - sudo apt-get update > /dev/null - - sudo apt-get -y install lighttpd libfcgi-dev libmemcache-dev memcached -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.4 - - 2.3.0 + - 2.2.5 + - 2.3.1 - ruby-head - rbx-2 - jruby-9.0.4.0 @@ -20,3 +34,4 @@ notifications: matrix: allow_failures: - rvm: rbx-2 + - rvm: jruby-head @@ -1,9 +1,18 @@ Sun Dec 4 18:48:03 2015 Jeremy Daer <jeremydaer@gmail.com> - * "First-Party" cookies. Browsers omit First-Party cookies from - third-party requests, closing the door on many common CSRF attacks. - Pass `first_party: true` to enable: - response.set_cookie 'foo', value: 'bar', first_party: true + * 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> diff --git a/lib/rack.rb b/lib/rack.rb index 4a02eed1..00621178 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.0.rc1" # Return the Rack release as a dotted string. def self.release 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/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/mock.rb b/lib/rack/mock.rb index 04d27a61..4ebc4df1 100644 --- a/lib/rack/mock.rb +++ b/lib/rack/mock.rb @@ -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 567c868d..74a7ee67 100644 --- a/lib/rack/multipart/parser.rb +++ b/lib/rack/multipart/parser.rb @@ -26,7 +26,7 @@ module Rack str = if left < size @io.read left else - @io.read size + @io.read size end if str diff --git a/lib/rack/query_parser.rb b/lib/rack/query_parser.rb index 72a521f3..17b77128 100644 --- a/lib/rack/query_parser.rb +++ b/lib/rack/query_parser.rb @@ -82,7 +82,13 @@ module Rack 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 == ''.freeze params[k] = v @@ -96,7 +102,8 @@ 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) + first_key = child_key.gsub(/[\[\]]/, ' ').split.first + if params_hash_type?(params[k].last) && !params[k].last.key?(first_key) normalize_params(params[k].last, child_key, v, depth - 1) else params[k] << normalize_params(make_params, child_key, v, depth - 1) diff --git a/lib/rack/request.rb b/lib/rack/request.rb index a76f15cd..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 @@ -437,6 +420,31 @@ 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 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 d541608a..7b842125 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -248,13 +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]) - first_party = "; First-Party" if value[:first_party] + 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}#{first_party}" + "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}" case header when nil, '' @@ -599,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/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/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_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_response.rb b/test/spec_response.rb index f1028826..02e51435 100644 --- a/test/spec_response.rb +++ b/test/spec_response.rb @@ -115,16 +115,66 @@ describe Rack::Response do response["Set-Cookie"].must_equal "foo=bar" end - it "can set First-Party cookies" do + it "can set SameSite cookies with symbol value :lax" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :first_party => true} - response["Set-Cookie"].must_equal "foo=bar; First-Party" + 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 First-Party attribute given a #{non_truthy.inspect} value" do + it "omits SameSite attribute given a #{non_truthy.inspect} value" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :first_party => non_truthy} + response.set_cookie "foo", {:value => "bar", :same_site => non_truthy} response["Set-Cookie"].must_equal "foo=bar" end end diff --git a/test/spec_utils.rb b/test/spec_utils.rb index 17a12115..b24762c9 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'" @@ -300,13 +308,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 +333,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..9ae6103d 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 @@ -188,6 +201,7 @@ describe Rack::Handler::WEBrick do end after do + @status_thread.join @server.shutdown @thread.join end |