summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml4
-rw-r--r--Gemfile10
-rw-r--r--KNOWN-ISSUES9
-rw-r--r--README.rdoc112
-rw-r--r--Rakefile2
-rw-r--r--SPEC73
-rw-r--r--example/protectedlobster.rb2
-rw-r--r--lib/rack.rb4
-rw-r--r--lib/rack/auth/abstract/request.rb2
-rw-r--r--lib/rack/auth/basic.rb2
-rw-r--r--lib/rack/auth/digest/request.rb2
-rw-r--r--lib/rack/body_proxy.rb10
-rw-r--r--lib/rack/builder.rb8
-rw-r--r--lib/rack/cascade.rb15
-rw-r--r--lib/rack/commonlogger.rb23
-rw-r--r--lib/rack/config.rb5
-rw-r--r--lib/rack/deflater.rb6
-rw-r--r--lib/rack/file.rb28
-rw-r--r--lib/rack/handler.rb23
-rw-r--r--lib/rack/handler/thin.rb9
-rw-r--r--lib/rack/handler/webrick.rb1
-rw-r--r--lib/rack/head.rb3
-rw-r--r--lib/rack/lint.rb140
-rw-r--r--lib/rack/lobster.rb6
-rw-r--r--lib/rack/lock.rb2
-rw-r--r--lib/rack/methodoverride.rb2
-rw-r--r--lib/rack/mime.rb31
-rw-r--r--lib/rack/multipart.rb4
-rw-r--r--lib/rack/multipart/parser.rb13
-rw-r--r--lib/rack/reloader.rb2
-rw-r--r--lib/rack/request.rb42
-rw-r--r--lib/rack/response.rb6
-rw-r--r--lib/rack/sendfile.rb22
-rw-r--r--lib/rack/server.rb32
-rw-r--r--lib/rack/session/abstract/id.rb115
-rw-r--r--lib/rack/session/cookie.rb2
-rw-r--r--lib/rack/static.rb99
-rw-r--r--lib/rack/utils.rb204
-rw-r--r--rack.gemspec7
-rw-r--r--test/cgi/assets/folder/test.js1
-rw-r--r--test/cgi/assets/fonts/font.eot1
-rw-r--r--test/cgi/assets/images/image.png1
-rw-r--r--test/cgi/assets/index.html1
-rw-r--r--test/cgi/assets/javascripts/app.js1
-rw-r--r--test/cgi/assets/stylesheets/app.css1
-rw-r--r--test/spec_auth_basic.rb8
-rw-r--r--test/spec_body_proxy.rb4
-rw-r--r--test/spec_builder.rb7
-rw-r--r--test/spec_cascade.rb8
-rw-r--r--test/spec_cgi.rb2
-rw-r--r--test/spec_chunked.rb8
-rw-r--r--test/spec_content_length.rb9
-rw-r--r--test/spec_deflater.rb17
-rw-r--r--test/spec_fastcgi.rb2
-rw-r--r--test/spec_file.rb29
-rw-r--r--test/spec_head.rb25
-rw-r--r--test/spec_lint.rb12
-rw-r--r--test/spec_lock.rb13
-rw-r--r--test/spec_methodoverride.rb5
-rw-r--r--test/spec_mime.rb51
-rw-r--r--test/spec_mongrel.rb2
-rw-r--r--test/spec_multipart.rb75
-rw-r--r--test/spec_nulllogger.rb9
-rw-r--r--test/spec_request.rb13
-rw-r--r--test/spec_response.rb50
-rw-r--r--test/spec_sendfile.rb65
-rw-r--r--test/spec_server.rb8
-rw-r--r--test/spec_static.rb67
-rw-r--r--test/spec_thin.rb7
-rw-r--r--test/spec_utils.rb35
-rw-r--r--test/spec_webrick.rb2
72 files changed, 1259 insertions, 363 deletions
diff --git a/.gitignore b/.gitignore
index 16e56b3c..ae1afb4f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ stage
Gemfile.lock
.rbx
doc
+/.bundle
diff --git a/.travis.yml b/.travis.yml
index 0dfd1e50..5336f3d2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,6 +8,7 @@ rvm:
- 1.8.7
- 1.9.2
- 1.9.3
+ - 2.0.0
- rbx
- jruby
- ree
@@ -17,3 +18,6 @@ branches:
notifications:
email: false
irc: "irc.freenode.org#rack"
+matrix:
+ allow_failures:
+ - rvm: 2.0.0
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 00000000..6f0697b2
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,10 @@
+source 'https://rubygems.org'
+
+gemspec
+
+group :extra do
+ gem 'ruby-fcgi'
+ gem 'memcache-client'
+ gem 'mongrel', '>= 1.2.0.pre2'
+ gem 'thin'
+end
diff --git a/KNOWN-ISSUES b/KNOWN-ISSUES
index a1af5dc1..e0373b2f 100644
--- a/KNOWN-ISSUES
+++ b/KNOWN-ISSUES
@@ -1,3 +1,12 @@
+= Known issues with Rack and ECMA-262
+
+* Many users expect the escape() function defined in ECMA-262 to be compatible
+ with URI. Confusion is especially strong because the documentation for the
+ escape function includes a reference to the URI specifications. ECMA-262
+ escape is not however a URI escape function, it is a javascript escape
+ function, and is not fully compatible. Most notably, for characters outside of
+ the BMP. Users should use the more correct encodeURI functions.
+
= Known issues with Rack and Web servers
* Lighttpd sets wrong SCRIPT_NAME and PATH_INFO if you mount your
diff --git a/README.rdoc b/README.rdoc
index 3e2dc53e..59b048e1 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -29,8 +29,10 @@ These web servers include Rack handlers in their distributions:
* Phusion Passenger (which is mod_rack for Apache and for nginx)
* Puma
* Rainbows!
+* Reel
* Unicorn
* unixrack
+* uWSGI
* Zbatery
Any valid Rack app will run the same on all these handlers, without
@@ -41,6 +43,7 @@ changing anything.
These frameworks include Rack adapters in their distributions:
* Camping
* Coset
+* Espresso
* Halcyon
* Mack
* Maveric
@@ -56,9 +59,6 @@ These frameworks include Rack adapters in their distributions:
* Wee
* ... and many others.
-Current links to these projects can be found at
-http://wiki.ramaze.net/Home#other-frameworks
-
== Available middleware
Between the server and the framework, Rack can be customized to your
@@ -134,7 +134,11 @@ at my site:
Testing Rack requires the bacon testing framework:
- gem install bacon
+ bundle install --without extra # to be able to run the fast tests
+
+Or:
+
+ bundle install # this assumes that you have installed native extensions!
There are two rake-based test tasks:
@@ -357,7 +361,7 @@ run on port 11211) and memcache-client installed.
* July 16, 2011: Sixteenth public release 1.3.2
* Fix for Rails and rack-test, Rack::Utils#escape calls to_s
-* Not Yet Released: Seventeenth public release 1.3.3
+* September 16, 2011: Seventeenth public release 1.3.3
* Fix bug with broken query parameters in Rack::ShowExceptions
* Rack::Request#cookies no longer swallows exceptions on broken input
* Prevents XSS attacks enabled by bug in Ruby 1.8's regexp engine
@@ -375,6 +379,10 @@ run on port 11211) and memcache-client installed.
* October 17, 2011: Twentieth public release 1.3.5
* Fix annoying warnings caused by the backport in 1.3.4
+* December 28th, 2011: Twenty first public release: 1.1.3.
+ * Security fix. http://www.ocert.org/advisories/ocert-2011-003.html
+ Further information here: http://jruby.org/2011/12/27/jruby-1-6-5-1
+
* December 28th, 2011: Twenty fourth public release 1.4.0
* Ruby 1.8.6 support has officially been dropped. Not all tests pass.
* Raise sane error messages for broken config.ru
@@ -410,11 +418,105 @@ run on port 11211) and memcache-client installed.
* Rack::Static no longer defaults to serving index files
* Rack.release was fixed
+* January 6th, 2013: Twenty sixth public release 1.1.4
+ * Add warnings when users do not provide a session secret
+
+* January 6th, 2013: Twenty seventh public release 1.2.6
+ * Add warnings when users do not provide a session secret
+ * Fix parsing performance for unquoted filenames
+
+* January 6th, 2013: Twenty eighth public release 1.3.7
+ * Add warnings when users do not provide a session secret
+ * Fix parsing performance for unquoted filenames
+ * Updated URI backports
+ * Fix URI backport version matching, and silence constant warnings
+ * Correct parameter parsing with empty values
+ * Correct rackup '-I' flag, to allow multiple uses
+ * Correct rackup pidfile handling
+ * Report rackup line numbers correctly
+ * Fix request loops caused by non-stale nonces with time limits
+ * Fix reloader on Windows
+ * Prevent infinite recursions from Response#to_ary
+ * Various middleware better conforms to the body close specification
+ * Updated language for the body close specification
+ * Additional notes regarding ECMA escape compatibility issues
+ * Fix the parsing of multiple ranges in range headers
+
+* January 6th, 2013: Twenty ninth public release 1.4.2
+ * Add warnings when users do not provide a session secret
+ * Fix parsing performance for unquoted filenames
+ * Updated URI backports
+ * Fix URI backport version matching, and silence constant warnings
+ * Correct parameter parsing with empty values
+ * Correct rackup '-I' flag, to allow multiple uses
+ * Correct rackup pidfile handling
+ * Report rackup line numbers correctly
+ * Fix request loops caused by non-stale nonces with time limits
+ * Fix reloader on Windows
+ * Prevent infinite recursions from Response#to_ary
+ * Various middleware better conforms to the body close specification
+ * Updated language for the body close specification
+ * Additional notes regarding ECMA escape compatibility issues
+ * Fix the parsing of multiple ranges in range headers
+ * Prevent errors from empty parameter keys
+ * Added PATCH verb to Rack::Request
+ * Various documentation updates
+ * Fix session merge semantics (fixes rack-test)
+ * Rack::Static :index can now handle multiple directories
+ * All tests now utilize Rack::Lint (special thanks to Lars Gierth)
+ * Rack::File cache_control parameter is now deprecated, and removed by 1.5
+ * Correct Rack::Directory script name escaping
+ * Rack::Static supports header rules for sophisticated configurations
+ * Multipart parsing now works without a Content-Length header
+ * New logos courtesy of Zachary Scott!
+ * Rack::BodyProxy now explicitly defines #each, useful for C extensions
+ * Cookies that are not URI escaped no longer cause exceptions
+
+* January 7th, 2013: Thirtieth public release 1.3.8
+ * Security: Prevent unbounded reads in large multipart boundaries
+
+* January 7th, 2013: Thirty first public release 1.4.3
+ * Security: Prevent unbounded reads in large multipart boundaries
+
+* January 13th, 2013: Thirty second public release 1.4.4, 1.3.9, 1.2.7, 1.1.5
+ * [SEC] Rack::Auth::AbstractRequest no longer symbolizes arbitrary strings
+ * Fixed erroneous test case in the 1.3.x series
+
+* January 21st, 2013: Thirty third public release 1.5.0
+ * Introduced hijack SPEC, for before-response and after-response hijacking
+ * SessionHash is no longer a Hash subclass
+ * Rack::File cache_control parameter is removed, in place of headers options
+ * Rack::Auth::AbstractRequest#scheme now yields strings, not symbols
+ * Rack::Utils cookie functions now format expires in RFC 2822 format
+ * Rack::File now has a default mime type
+ * rackup -b 'run Rack::File.new(".")', option provides command line configs
+ * Rack::Deflater will no longer double encode bodies
+ * Rack::Mime#match? provides convenience for Accept header matching
+ * Rack::Utils#q_values provides splitting for Accept headers
+ * Rack::Utils#best_q_match provides a helper for Accept headers
+ * Rack::Handler.pick provides convenience for finding available servers
+ * Puma added to the list of default servers (preferred over Webrick)
+ * Various middleware now correctly close body when replacing it
+ * Rack::Request#params is no longer persistent with only GET params
+ * Rack::Request#update_param and #delete_param provide persistent operations
+ * Rack::Request#trusted_proxy? now returns true for local unix sockets
+ * Rack::Response no longer forces Content-Types
+ * Rack::Sendfile provides local mapping configuration options
+ * Rack::Utils#rfc2109 provides old netscape style time output
+ * Updated HTTP status codes
+ * Ruby 1.8.6 likely no longer passes tests, and is no longer fully supported
+
== Contact
Please post bugs, suggestions and patches to
the bug tracker at <http://github.com/rack/rack/issues>.
+Please post security related bugs and suggestions to the core team at
+<https://groups.google.com/group/rack-core> or rack-core@googlegroups.com. This
+list is not public. Due to wide usage of the library, it is strongly preferred
+that we manage timing in order to provide viable patches at the time of
+disclosure. Your assistance in this matter is greatly appreciated.
+
Mailing list archives are available at
<http://groups.google.com/group/rack-devel>.
diff --git a/Rakefile b/Rakefile
index 7f4124c7..6bfe82ef 100644
--- a/Rakefile
+++ b/Rakefile
@@ -85,7 +85,7 @@ task :test => 'SPEC' do
specopts = ENV['TESTOPTS'] ||
"-q -t '^(?!Rack::Adapter|Rack::Session::Memcache|Rack::Server|Rack::Handler)'"
- sh "bacon -I./lib:./test #{opts} #{specopts}"
+ sh "bacon -w -I./lib:./test #{opts} #{specopts}"
end
desc "Run all the tests we run on CI"
diff --git a/SPEC b/SPEC
index 8f710bdd..9c31801f 100644
--- a/SPEC
+++ b/SPEC
@@ -60,6 +60,9 @@ Rack-specific variables:
<tt>rack.multithread</tt>:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise.
<tt>rack.multiprocess</tt>:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise.
<tt>rack.run_once</tt>:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar).
+<tt>rack.hijack?</tt>:: present and true if the server supports connection hijacking. See below, hijacking.
+<tt>rack.hijack</tt>:: an object responding to #call that must be called at least once before using rack.hijack_io. It is recommended #call return rack.hijack_io as well as setting it in env if necessary.
+<tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack has received #call, this will contain an object resembling an IO. See hijacking.
Additional environment specifications have approved to
standardized middleware APIs. None of these are required to
be implemented by the server.
@@ -90,6 +93,7 @@ There are the following restrictions:
* <tt>rack.url_scheme</tt> must either be +http+ or +https+.
* There must be a valid input stream in <tt>rack.input</tt>.
* There must be a valid error stream in <tt>rack.errors</tt>.
+* There may be a valid hijack stream in <tt>rack.hijack_io</tt>
* The <tt>REQUEST_METHOD</tt> must be a valid token.
* The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
* The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
@@ -129,12 +133,72 @@ The error stream must respond to +puts+, +write+ and +flush+.
* +flush+ must be called without arguments and must be called
in order to make the error appear for sure.
* +close+ must never be called on the error stream.
+=== Hijacking
+==== Request (before status)
+If rack.hijack? is true then rack.hijack must respond to #call.
+rack.hijack must return the io that will also be assigned (or is
+already present, in rack.hijack_io.
+
+rack.hijack_io must respond to:
+<tt>read, write, read_nonblock, write_nonblock, flush, close,
+close_read, close_write, closed?</tt>
+
+The semantics of these IO methods must be a best effort match to
+those of a normal ruby IO or Socket object, using standard
+arguments and raising standard exceptions. Servers are encouraged
+to simply pass on real IO objects, although it is recognized that
+this approach is not directly compatible with SPDY and HTTP 2.0.
+
+IO provided in rack.hijack_io should preference the
+IO::WaitReadable and IO::WaitWritable APIs wherever supported.
+
+There is a deliberate lack of full specification around
+rack.hijack_io, as semantics will change from server to server.
+Users are encouraged to utilize this API with a knowledge of their
+server choice, and servers may extend the functionality of
+hijack_io to provide additional features to users. The purpose of
+rack.hijack is for Rack to "get out of the way", as such, Rack only
+provides the minimum of specification and support.
+
+If rack.hijack? is false, then rack.hijack should not be set.
+
+If rack.hijack? is false, then rack.hijack_io should not be set.
+==== Response (after headers)
+It is also possible to hijack a response after the status and headers
+have been sent.
+In order to do this, an application may set the special header
+<tt>rack.hijack</tt> to an object that responds to <tt>call</tt>
+accepting an argument that conforms to the <tt>rack.hijack_io</tt>
+protocol.
+
+After the headers have been sent, and this hijack callback has been
+called, the application is now responsible for the remaining lifecycle
+of the IO. The application is also responsible for maintaining HTTP
+semantics. Of specific note, in almost all cases in the current SPEC,
+applications will have wanted to specify the header Connection:close in
+HTTP/1.1, and not Connection:keep-alive, as there is no protocol for
+returning hijacked sockets to the web server. For that purpose, use the
+body streaming API instead (progressively yielding strings via each).
+
+Servers must ignore the <tt>body</tt> part of the response tuple when
+the <tt>rack.hijack</tt> response API is in use.
+
+The special response header <tt>rack.hijack</tt> must only be set
+if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
+==== Conventions
+* Middleware should not use hijack unless it is handling the whole
+ response.
+* Middleware may wrap the IO object for the response pattern.
+* Middleware should not wrap the IO object for the request pattern. The
+ request pattern is intended to provide the hijacker with "raw tcp".
== The Response
=== The Status
This is an HTTP status. When parsed as integer (+to_i+), it must be
greater than or equal to 100.
=== The Headers
The header must respond to +each+, and yield values of key and value.
+Special headers starting "rack." are for communicating with the
+server, and must not be sent back to the client.
The header keys must be Strings.
The header must not contain a +Status+ key,
contain keys with <tt>:</tt> or newlines in their name,
@@ -146,9 +210,8 @@ consisting of lines (for multiple header values, e.g. multiple
<tt>Set-Cookie</tt> values) seperated by "\n".
The lines must not contain characters below 037.
=== The Content-Type
-There must be a <tt>Content-Type</tt>, except when the
-+Status+ is 1xx, 204, 205 or 304, in which case there must be none
-given.
+There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
+204, 205 or 304.
=== The Content-Length
There must not be a <tt>Content-Length</tt> header when the
+Status+ is 1xx, 204, 205 or 304.
@@ -157,7 +220,9 @@ The Body must respond to +each+
and must only yield String values.
The Body itself should not be an instance of String, as this will
break in Ruby 1.9.
-If the Body responds to +close+, it will be called after iteration.
+If the Body responds to +close+, it will be called after iteration. If
+the body is replaced by a middleware after action, the original body
+must be closed first, if it repsonds to close.
If the Body responds to +to_path+, it must return a String
identifying the location of a file whose contents are identical
to that produced by calling +each+; this may be used by the
diff --git a/example/protectedlobster.rb b/example/protectedlobster.rb
index 108b9d05..d904b4ce 100644
--- a/example/protectedlobster.rb
+++ b/example/protectedlobster.rb
@@ -11,4 +11,4 @@ protected_lobster.realm = 'Lobster 2.0'
pretty_protected_lobster = Rack::ShowStatus.new(Rack::ShowExceptions.new(protected_lobster))
-Rack::Handler::WEBrick.run pretty_protected_lobster, :Port => 9292
+Rack::Server.start :app => pretty_protected_lobster, :Port => 9292
diff --git a/lib/rack.rb b/lib/rack.rb
index acfcb5ac..0cfc501d 100644
--- a/lib/rack.rb
+++ b/lib/rack.rb
@@ -11,7 +11,7 @@
module Rack
# The Rack protocol version number implemented.
- VERSION = [1,1]
+ VERSION = [1,2]
# Return the Rack protocol version as a dotted string.
def self.version
@@ -20,7 +20,7 @@ module Rack
# Return the Rack release as a dotted string.
def self.release
- "1.4"
+ "1.5"
end
autoload :Builder, "rack/builder"
diff --git a/lib/rack/auth/abstract/request.rb b/lib/rack/auth/abstract/request.rb
index 9e15c720..aa6bf8e2 100644
--- a/lib/rack/auth/abstract/request.rb
+++ b/lib/rack/auth/abstract/request.rb
@@ -21,7 +21,7 @@ module Rack
end
def scheme
- @scheme ||= parts.first.downcase.to_sym
+ @scheme ||= parts.first.downcase
end
def params
diff --git a/lib/rack/auth/basic.rb b/lib/rack/auth/basic.rb
index 95572246..7dd9a99b 100644
--- a/lib/rack/auth/basic.rb
+++ b/lib/rack/auth/basic.rb
@@ -41,7 +41,7 @@ module Rack
class Request < Auth::AbstractRequest
def basic?
- :basic == scheme
+ !parts.first.nil? && "basic" == scheme
end
def credentials
diff --git a/lib/rack/auth/digest/request.rb b/lib/rack/auth/digest/request.rb
index 78d7bd61..706c651c 100644
--- a/lib/rack/auth/digest/request.rb
+++ b/lib/rack/auth/digest/request.rb
@@ -11,7 +11,7 @@ module Rack
end
def digest?
- :digest == scheme
+ "digest" == scheme
end
def correct_uri?
diff --git a/lib/rack/body_proxy.rb b/lib/rack/body_proxy.rb
index 64984e66..95a74626 100644
--- a/lib/rack/body_proxy.rb
+++ b/lib/rack/body_proxy.rb
@@ -5,6 +5,7 @@ module Rack
end
def respond_to?(*args)
+ return false if args.first.to_s =~ /^to_ary$/
super or @body.respond_to?(*args)
end
@@ -22,7 +23,16 @@ module Rack
@closed
end
+ # N.B. This method is a special case to address the bug described by #434.
+ # We are applying this special case for #each only. Future bugs of this
+ # class will be handled by requesting users to patch their ruby
+ # implementation, to save adding too many methods in this class.
+ def each(*args, &block)
+ @body.each(*args, &block)
+ end
+
def method_missing(*args, &block)
+ super if args.first.to_s =~ /^to_ary$/
@body.__send__(*args, &block)
end
end
diff --git a/lib/rack/builder.rb b/lib/rack/builder.rb
index 87743000..66dc7bd4 100644
--- a/lib/rack/builder.rb
+++ b/lib/rack/builder.rb
@@ -37,8 +37,7 @@ module Rack
options = opts.parse! $1.split(/\s+/)
end
cfgfile.sub!(/^__END__\n.*\Z/m, '')
- app = eval "Rack::Builder.new {\n" + cfgfile + "\n}.to_app",
- TOPLEVEL_BINDING, config, 0
+ app = new_from_string cfgfile, config
else
require config
app = Object.const_get(::File.basename(config, '.rb').capitalize)
@@ -46,6 +45,11 @@ module Rack
return app, options
end
+ def self.new_from_string(builder_script, file="(rackup)")
+ eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
+ TOPLEVEL_BINDING, file, 0
+ end
+
def initialize(default_app = nil,&block)
@use, @map, @run = [], nil, default_app
instance_eval(&block) if block_given?
diff --git a/lib/rack/cascade.rb b/lib/rack/cascade.rb
index 77c9724c..d104a4be 100644
--- a/lib/rack/cascade.rb
+++ b/lib/rack/cascade.rb
@@ -1,6 +1,6 @@
module Rack
- # Rack::Cascade tries an request on several apps, and returns the
- # first response that is not 404 (or in a list of configurable
+ # Rack::Cascade tries a request on several apps, and returns the
+ # first response that is not 404 or 405 (or in a list of configurable
# status codes).
class Cascade
@@ -19,8 +19,19 @@ module Rack
def call(env)
result = NotFound
+ last_body = nil
+
@apps.each do |app|
+ # The SPEC says that the body must be closed after it has been iterated
+ # by the server, or if it is replaced by a middleware action. Cascade
+ # replaces the body each time a cascade happens. It is assumed that nil
+ # does not respond to close, otherwise the previous application body
+ # will be closed. The final application body will not be closed, as it
+ # will be passed to the server as a result.
+ last_body.close if last_body.respond_to? :close
+
result = app.call(env)
+ last_body = result[2]
break unless @catch.include?(result[0].to_i)
end
diff --git a/lib/rack/commonlogger.rb b/lib/rack/commonlogger.rb
index 22d7f78a..7028615f 100644
--- a/lib/rack/commonlogger.rb
+++ b/lib/rack/commonlogger.rb
@@ -1,13 +1,26 @@
require 'rack/body_proxy'
module Rack
- # Rack::CommonLogger forwards every request to an +app+ given, and
- # logs a line in the Apache common log format to the +logger+, or
- # rack.errors by default.
+ # Rack::CommonLogger forwards every request to the given +app+, and
+ # logs a line in the
+ # {Apache common log format}[http://httpd.apache.org/docs/1.3/logs.html#common]
+ # to the +logger+.
+ #
+ # If +logger+ is nil, CommonLogger will fall back +rack.errors+, which is
+ # an instance of Rack::NullLogger.
+ #
+ # +logger+ can be any class, including the standard library Logger, and is
+ # expected to have a +write+ method, which accepts the CommonLogger::FORMAT.
+ # According to the SPEC, the error stream must also respond to +puts+
+ # (which takes a single argument that responds to +to_s+), and +flush+
+ # (which is called without arguments in order to make the error appear for
+ # sure)
class CommonLogger
# Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
- # lilith.local - - [07/Aug/2006 23:58:02] "GET / HTTP/1.1" 500 -
- # %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
+ #
+ # lilith.local - - [07/Aug/2006 23:58:02] "GET / HTTP/1.1" 500 -
+ #
+ # %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n}
def initialize(app, logger=nil)
diff --git a/lib/rack/config.rb b/lib/rack/config.rb
index c6d446c0..dc255d27 100644
--- a/lib/rack/config.rb
+++ b/lib/rack/config.rb
@@ -1,6 +1,11 @@
module Rack
# Rack::Config modifies the environment using the block given during
# initialization.
+ #
+ # Example:
+ # use Rack::Config do |env|
+ # env['my-key'] = 'some-value'
+ # end
class Config
def initialize(app, &block)
@app = app
diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb
index 94022776..9972e63d 100644
--- a/lib/rack/deflater.rb
+++ b/lib/rack/deflater.rb
@@ -46,6 +46,7 @@ module Rack
when "identity"
[status, headers, body]
when nil
+ body.close if body.respond_to?(:close)
message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
[406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, [message]]
end
@@ -65,6 +66,7 @@ module Rack
gzip.write(part)
gzip.flush
}
+ ensure
@body.close if @body.respond_to?(:close)
gzip.close
@writer = nil
@@ -91,9 +93,11 @@ module Rack
def each
deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS)
@body.each { |part| yield deflater.deflate(part, Zlib::SYNC_FLUSH) }
- @body.close if @body.respond_to?(:close)
yield deflater.finish
nil
+ ensure
+ @body.close if @body.respond_to?(:close)
+ deflater.close
end
end
end
diff --git a/lib/rack/file.rb b/lib/rack/file.rb
index 0a22d2d0..090a0015 100644
--- a/lib/rack/file.rb
+++ b/lib/rack/file.rb
@@ -21,16 +21,10 @@ module Rack
alias :to_path :path
- def initialize(root, headers={})
+ def initialize(root, headers={}, default_mime = 'text/plain')
@root = root
- # Allow a cache_control string for backwards compatibility
- if headers.instance_of? String
- warn \
- "Rack::File headers parameter replaces cache_control after Rack 1.5."
- @headers = { 'Cache-Control' => headers }
- else
- @headers = headers
- end
+ @headers = headers
+ @default_mime = default_mime
end
def call(env)
@@ -77,17 +71,15 @@ module Rack
def serving(env)
last_modified = F.mtime(@path).httpdate
return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
- response = [
- 200,
- {
- "Last-Modified" => last_modified,
- "Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain')
- },
- env["REQUEST_METHOD"] == "HEAD" ? [] : self
- ]
+
+ headers = { "Last-Modified" => last_modified }
+ mime = Mime.mime_type(F.extname(@path), @default_mime)
+ headers["Content-Type"] = mime if mime
# Set custom headers
- @headers.each { |field, content| response[1][field] = content } if @headers
+ @headers.each { |field, content| headers[field] = content } if @headers
+
+ response = [ 200, headers, env["REQUEST_METHOD"] == "HEAD" ? [] : self ]
# NOTE:
# We check via File::size? whether this file provides size info
diff --git a/lib/rack/handler.rb b/lib/rack/handler.rb
index 70316be9..155dbfab 100644
--- a/lib/rack/handler.rb
+++ b/lib/rack/handler.rb
@@ -26,6 +26,23 @@ module Rack
raise load_error || name_error
end
+ # Select first available Rack handler given an `Array` of server names.
+ # Raises `LoadError` if no handler was found.
+ #
+ # > pick ['thin', 'webrick']
+ # => Rack::Handler::WEBrick
+ def self.pick(server_names)
+ server_names = Array(server_names)
+ server_names.each do |server_name|
+ begin
+ return get(server_name.to_s)
+ rescue LoadError, NameError
+ end
+ end
+
+ raise LoadError, "Couldn't find handler for: #{server_names.join(', ')}."
+ end
+
def self.default(options = {})
# Guess.
if ENV.include?("PHP_FCGI_CHILDREN")
@@ -37,11 +54,7 @@ module Rack
elsif ENV.include?("REQUEST_METHOD")
Rack::Handler::CGI
else
- begin
- Rack::Handler::Thin
- rescue LoadError
- Rack::Handler::WEBrick
- end
+ pick ['thin', 'puma', 'webrick']
end
end
diff --git a/lib/rack/handler/thin.rb b/lib/rack/handler/thin.rb
index fc110ff2..dc269725 100644
--- a/lib/rack/handler/thin.rb
+++ b/lib/rack/handler/thin.rb
@@ -6,9 +6,12 @@ module Rack
module Handler
class Thin
def self.run(app, options={})
- server = ::Thin::Server.new(options[:Host] || '0.0.0.0',
- options[:Port] || 8080,
- app)
+ host = options.delete(:Host) || '0.0.0.0'
+ port = options.delete(:Port) || 8080
+ args = [host, port, app, options]
+ # Thin versions below 0.8.0 do not support additional options
+ args.pop if ::Thin::VERSION::MAJOR < 1 && ::Thin::VERSION::MINOR < 8
+ server = ::Thin::Server.new(*args)
yield server if block_given?
server.start
end
diff --git a/lib/rack/handler/webrick.rb b/lib/rack/handler/webrick.rb
index f86b20b6..487a0ea1 100644
--- a/lib/rack/handler/webrick.rb
+++ b/lib/rack/handler/webrick.rb
@@ -7,6 +7,7 @@ module Rack
class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
def self.run(app, options={})
options[:BindAddress] = options.delete(:Host) if options[:Host]
+ options[:Port] ||= 8080
@server = ::WEBrick::HTTPServer.new(options)
@server.mount "/", Rack::Handler::WEBrick, app
yield @server if block_given?
diff --git a/lib/rack/head.rb b/lib/rack/head.rb
index deab822a..7ffead6c 100644
--- a/lib/rack/head.rb
+++ b/lib/rack/head.rb
@@ -1,6 +1,8 @@
module Rack
class Head
+ # Rack::Head returns an empty body for all HEAD requests. It leaves
+ # all other requests unchanged.
def initialize(app)
@app = app
end
@@ -9,6 +11,7 @@ class Head
status, headers, body = @app.call(env)
if env["REQUEST_METHOD"] == "HEAD"
+ body.close if body.respond_to? :close
[status, headers, []]
else
[status, headers, body]
diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb
index 3cd28e5e..338f7f09 100644
--- a/lib/rack/lint.rb
+++ b/lib/rack/lint.rb
@@ -1,4 +1,5 @@
require 'rack/utils'
+require 'forwardable'
module Rack
# Rack::Lint validates your application and the requests and
@@ -50,6 +51,9 @@ module Rack
check_status status
## the *headers*,
check_headers headers
+
+ check_hijack_response headers, env
+
## and the *body*.
check_content_type status, headers
check_content_length status, headers
@@ -121,6 +125,9 @@ module Rack
## <tt>rack.multithread</tt>:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise.
## <tt>rack.multiprocess</tt>:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise.
## <tt>rack.run_once</tt>:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar).
+ ## <tt>rack.hijack?</tt>:: present and true if the server supports connection hijacking. See below, hijacking.
+ ## <tt>rack.hijack</tt>:: an object responding to #call that must be called at least once before using rack.hijack_io. It is recommended #call return rack.hijack_io as well as setting it in env if necessary.
+ ## <tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack has received #call, this will contain an object resembling an IO. See hijacking.
##
## Additional environment specifications have approved to
@@ -227,6 +234,8 @@ module Rack
check_input env["rack.input"]
## * There must be a valid error stream in <tt>rack.errors</tt>.
check_error env["rack.errors"]
+ ## * There may be a valid hijack stream in <tt>rack.hijack_io</tt>
+ check_hijack env
## * The <tt>REQUEST_METHOD</tt> must be a valid token.
assert("REQUEST_METHOD unknown: #{env["REQUEST_METHOD"]}") {
@@ -417,6 +426,121 @@ module Rack
end
end
+ class HijackWrapper
+ include Assertion
+ extend Forwardable
+
+ REQUIRED_METHODS = [
+ :read, :write, :read_nonblock, :write_nonblock, :flush, :close,
+ :close_read, :close_write, :closed?
+ ]
+
+ def_delegators :@io, *REQUIRED_METHODS
+
+ def initialize(io)
+ @io = io
+ REQUIRED_METHODS.each do |meth|
+ assert("rack.hijack_io must respond to #{meth}") { io.respond_to? meth }
+ end
+ end
+ end
+
+ ## === Hijacking
+ #
+ # AUTHORS: n.b. The trailing whitespace between paragraphs is important and
+ # should not be removed. The whitespace creates paragraphs in the RDoc
+ # output.
+ #
+ ## ==== Request (before status)
+ def check_hijack(env)
+ if env['rack.hijack?']
+ ## If rack.hijack? is true then rack.hijack must respond to #call.
+ original_hijack = env['rack.hijack']
+ assert("rack.hijack must respond to call") { original_hijack.respond_to?(:call) }
+ env['rack.hijack'] = proc do
+ ## rack.hijack must return the io that will also be assigned (or is
+ ## already present, in rack.hijack_io.
+ io = original_hijack.call
+ HijackWrapper.new(io)
+ ##
+ ## rack.hijack_io must respond to:
+ ## <tt>read, write, read_nonblock, write_nonblock, flush, close,
+ ## close_read, close_write, closed?</tt>
+ ##
+ ## The semantics of these IO methods must be a best effort match to
+ ## those of a normal ruby IO or Socket object, using standard
+ ## arguments and raising standard exceptions. Servers are encouraged
+ ## to simply pass on real IO objects, although it is recognized that
+ ## this approach is not directly compatible with SPDY and HTTP 2.0.
+ ##
+ ## IO provided in rack.hijack_io should preference the
+ ## IO::WaitReadable and IO::WaitWritable APIs wherever supported.
+ ##
+ ## There is a deliberate lack of full specification around
+ ## rack.hijack_io, as semantics will change from server to server.
+ ## Users are encouraged to utilize this API with a knowledge of their
+ ## server choice, and servers may extend the functionality of
+ ## hijack_io to provide additional features to users. The purpose of
+ ## rack.hijack is for Rack to "get out of the way", as such, Rack only
+ ## provides the minimum of specification and support.
+ env['rack.hijack_io'] = HijackWrapper.new(env['rack.hijack_io'])
+ io
+ end
+ else
+ ##
+ ## If rack.hijack? is false, then rack.hijack should not be set.
+ assert("rack.hijack? is false, but rack.hijack is present") { env['rack.hijack'].nil? }
+ ##
+ ## If rack.hijack? is false, then rack.hijack_io should not be set.
+ assert("rack.hijack? is false, but rack.hijack_io is present") { env['rack.hijack_io'].nil? }
+ end
+ end
+
+ ## ==== Response (after headers)
+ ## It is also possible to hijack a response after the status and headers
+ ## have been sent.
+ def check_hijack_response(headers, env)
+ ## In order to do this, an application may set the special header
+ ## <tt>rack.hijack</tt> to an object that responds to <tt>call</tt>
+ ## accepting an argument that conforms to the <tt>rack.hijack_io</tt>
+ ## protocol.
+ ##
+ ## After the headers have been sent, and this hijack callback has been
+ ## called, the application is now responsible for the remaining lifecycle
+ ## of the IO. The application is also responsible for maintaining HTTP
+ ## semantics. Of specific note, in almost all cases in the current SPEC,
+ ## applications will have wanted to specify the header Connection:close in
+ ## HTTP/1.1, and not Connection:keep-alive, as there is no protocol for
+ ## returning hijacked sockets to the web server. For that purpose, use the
+ ## body streaming API instead (progressively yielding strings via each).
+ ##
+ ## Servers must ignore the <tt>body</tt> part of the response tuple when
+ ## the <tt>rack.hijack</tt> response API is in use.
+
+ if env['rack.hijack?'] && headers['rack.hijack']
+ assert('rack.hijack header must respond to #call') {
+ headers['rack.hijack'].respond_to? :call
+ }
+ original_hijack = headers['rack.hijack']
+ headers['rack.hijack'] = proc do |io|
+ original_hijack.call HijackWrapper.new(io)
+ end
+ else
+ ##
+ ## The special response header <tt>rack.hijack</tt> must only be set
+ ## if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
+ assert('rack.hijack header must not be present if server does not support hijacking') {
+ headers['rack.hijack'].nil?
+ }
+ end
+ end
+ ## ==== Conventions
+ ## * Middleware should not use hijack unless it is handling the whole
+ ## response.
+ ## * Middleware may wrap the IO object for the response pattern.
+ ## * Middleware should not wrap the IO object for the request pattern. The
+ ## request pattern is intended to provide the hijacker with "raw tcp".
+
## == The Response
## === The Status
@@ -433,6 +557,10 @@ module Rack
header.respond_to? :each
}
header.each { |key, value|
+ ## Special headers starting "rack." are for communicating with the
+ ## server, and must not be sent back to the client.
+ next if key =~ /^rack\..+$/
+
## The header keys must be Strings.
assert("header key must be a string, was #{key.class}") {
key.kind_of? String
@@ -464,9 +592,8 @@ module Rack
## === The Content-Type
def check_content_type(status, headers)
headers.each { |key, value|
- ## There must be a <tt>Content-Type</tt>, except when the
- ## +Status+ is 1xx, 204, 205 or 304, in which case there must be none
- ## given.
+ ## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
+ ## 204, 205 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
@@ -474,9 +601,6 @@ module Rack
return
end
}
- assert("No Content-Type header found") {
- Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
- }
end
## === The Content-Length
@@ -529,7 +653,9 @@ module Rack
## The Body itself should not be an instance of String, as this will
## break in Ruby 1.9.
##
- ## If the Body responds to +close+, it will be called after iteration.
+ ## If the Body responds to +close+, it will be called after iteration. If
+ ## the body is replaced by a middleware after action, the original body
+ ## must be closed first, if it repsonds to close.
# XXX howto: assert("Body has not been closed") { @closed }
diff --git a/lib/rack/lobster.rb b/lib/rack/lobster.rb
index f63f419a..d1a7f7bc 100644
--- a/lib/rack/lobster.rb
+++ b/lib/rack/lobster.rb
@@ -59,7 +59,7 @@ end
if $0 == __FILE__
require 'rack'
require 'rack/showexceptions'
- Rack::Handler::WEBrick.run \
- Rack::ShowExceptions.new(Rack::Lint.new(Rack::Lobster.new)),
- :Port => 9292
+ Rack::Server.start(
+ :app => Rack::ShowExceptions.new(Rack::Lint.new(Rack::Lobster.new)), :Port => 9292
+ )
end
diff --git a/lib/rack/lock.rb b/lib/rack/lock.rb
index 4954e2e2..b3139c03 100644
--- a/lib/rack/lock.rb
+++ b/lib/rack/lock.rb
@@ -2,6 +2,8 @@ require 'thread'
require 'rack/body_proxy'
module Rack
+ # Rack::Lock locks every request inside a mutex, so that every request
+ # will effectively be executed synchronously.
class Lock
FLAG = 'rack.multithread'.freeze
diff --git a/lib/rack/methodoverride.rb b/lib/rack/methodoverride.rb
index 48d6afdd..1bdaca84 100644
--- a/lib/rack/methodoverride.rb
+++ b/lib/rack/methodoverride.rb
@@ -26,8 +26,6 @@ module Rack
method = req.POST[METHOD_OVERRIDE_PARAM_KEY] ||
env[HTTP_METHOD_OVERRIDE_HEADER]
method.to_s.upcase
- rescue EOFError
- ""
end
end
end
diff --git a/lib/rack/mime.rb b/lib/rack/mime.rb
index d2e908c1..5d050221 100644
--- a/lib/rack/mime.rb
+++ b/lib/rack/mime.rb
@@ -18,6 +18,35 @@ module Rack
end
module_function :mime_type
+ # Returns true if the given value is a mime match for the given mime match
+ # specification, false otherwise.
+ #
+ # Rack::Mime.match?('text/html', 'text/*') => true
+ # Rack::Mime.match?('text/plain', '*') => true
+ # Rack::Mime.match?('text/html', 'application/json') => false
+
+ def match?(value, matcher)
+ v1, v2 = value.split('/', 2)
+ m1, m2 = matcher.split('/', 2)
+
+ if m1 == '*'
+ if m2.nil? || m2 == '*'
+ return true
+ elsif m2 == v2
+ return true
+ else
+ return false
+ end
+ end
+
+ return false if v1 != m1
+
+ return true if m2.nil? || m2 == '*'
+
+ m2 == v2
+ end
+ module_function :match?
+
# List of most common mime-types, selected various sources
# according to their usefulness in a webserving scope for Ruby
# users.
@@ -598,7 +627,7 @@ module Rack
".wmv" => "video/x-ms-wmv",
".wmx" => "video/x-ms-wmx",
".wmz" => "application/x-ms-wmz",
- ".woff" => "application/octet-stream",
+ ".woff" => "application/font-woff",
".wpd" => "application/vnd.wordperfect",
".wpl" => "application/vnd.ms-wpl",
".wps" => "application/vnd.ms-works",
diff --git a/lib/rack/multipart.rb b/lib/rack/multipart.rb
index 37771067..68492480 100644
--- a/lib/rack/multipart.rb
+++ b/lib/rack/multipart.rb
@@ -12,7 +12,7 @@ module Rack
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
- DISPPARM = /;\s*(#{TOKEN})=("(?:\\"|[^"])*"|#{TOKEN})*/
+ DISPPARM = /;\s*(#{TOKEN})=("(?:\\"|[^"])*"|#{TOKEN})/
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
@@ -31,4 +31,4 @@ module Rack
end
end
-end \ No newline at end of file
+end
diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb
index 21986ccf..1315c7b3 100644
--- a/lib/rack/multipart/parser.rb
+++ b/lib/rack/multipart/parser.rb
@@ -70,9 +70,16 @@ module Rack
def fast_forward_to_first_boundary
loop do
- read_buffer = @io.gets
- break if read_buffer == full_boundary
- raise EOFError, "bad content body" if read_buffer.nil?
+ content = @io.read(BUFSIZE)
+ raise EOFError, "bad content body" unless content
+ @buf << content
+
+ while @buf.gsub!(/\A([^\n]*\n)/, '')
+ read_buffer = $1
+ return if read_buffer == full_boundary
+ end
+
+ raise EOFError, "bad content body" if Utils.bytesize(@buf) >= BUFSIZE
end
end
diff --git a/lib/rack/reloader.rb b/lib/rack/reloader.rb
index a06de23a..5f643592 100644
--- a/lib/rack/reloader.rb
+++ b/lib/rack/reloader.rb
@@ -101,7 +101,7 @@ module Rack
return unless file
stat = ::File.stat(file)
return file, stat if stat.file?
- rescue Errno::ENOENT, Errno::ENOTDIR
+ rescue Errno::ENOENT, Errno::ENOTDIR, Errno::ESRCH
@cache.delete(file) and false
end
end
diff --git a/lib/rack/request.rb b/lib/rack/request.rb
index 130e3675..6a22c309 100644
--- a/lib/rack/request.rb
+++ b/lib/rack/request.rb
@@ -98,10 +98,8 @@ module Rack
port.to_i
elsif port = @env['HTTP_X_FORWARDED_PORT']
port.to_i
- elsif ssl?
- 443
elsif @env.has_key?("HTTP_X_FORWARDED_HOST")
- 80
+ DEFAULT_PORTS[scheme]
else
@env["SERVER_PORT"].to_i
end
@@ -118,25 +116,25 @@ module Rack
# Checks the HTTP request method (or verb) to see if it was of type DELETE
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
-
+
# Checks the HTTP request method (or verb) to see if it was of type HEAD
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
-
+
# Checks the HTTP request method (or verb) to see if it was of type PATCH
def patch?; request_method == "PATCH" end
-
+
# Checks the HTTP request method (or verb) to see if it was of type POST
def post?; request_method == "POST" end
-
+
# Checks the HTTP request method (or verb) to see if it was of type PUT
def put?; request_method == "PUT" end
-
+
# Checks the HTTP request method (or verb) to see if it was of type TRACE
def trace?; request_method == "TRACE" end
@@ -157,6 +155,10 @@ module Rack
'multipart/mixed'
]
+ # Default ports depending on scheme. Used to decide whether or not
+ # to include the port in a generated URI.
+ DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
+
# Determine whether the request body contains form-data by checking
# the request Content-Type for one of the media-types:
# "application/x-www-form-urlencoded" or "multipart/form-data". The
@@ -297,12 +299,10 @@ module Rack
# the Cookie header such that those with more specific Path attributes
# precede those with less specific. Ordering with respect to other
# attributes (e.g., Domain) is unspecified.
- Utils.parse_query(string, ';,').each { |k,v| hash[k] = Array === v ? v.first : v }
+ cookies = Utils.parse_query(string, ';,') { |s| Rack::Utils.unescape(s) rescue s }
+ cookies.each { |k,v| hash[k] = Array === v ? v.first : v }
@env["rack.request.cookie_string"] = string
hash
- rescue => error
- error.message.replace "cannot parse Cookie header: #{error.message}"
- raise
end
def xhr?
@@ -310,14 +310,8 @@ module Rack
end
def base_url
- url = scheme + "://"
- url << host
-
- if scheme == "https" && port != 443 ||
- scheme == "http" && port != 80
- url << ":#{port}"
- end
-
+ url = "#{scheme}://#{host}"
+ url << ":#{port}" if port != DEFAULT_PORTS[scheme]
url
end
@@ -346,13 +340,13 @@ module Rack
end
def trusted_proxy?(ip)
- ip =~ /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|^::1$|^fd[0-9a-f]{2}:.+|^localhost$|^unix$/i
+ ip =~ /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|^::1$|^fd[0-9a-f]{2}:.+|^localhost$|^unix$|^unix:/i
end
def ip
remote_addrs = split_ip_addresses(@env['REMOTE_ADDR'])
remote_addrs = reject_trusted_ip_addresses(remote_addrs)
-
+
return remote_addrs.first if remote_addrs.any?
forwarded_ips = split_ip_addresses(@env['HTTP_X_FORWARDED_FOR'])
diff --git a/lib/rack/response.rb b/lib/rack/response.rb
index a9973f45..2beba7a8 100644
--- a/lib/rack/response.rb
+++ b/lib/rack/response.rb
@@ -21,8 +21,7 @@ module Rack
def initialize(body=[], status=200, header={})
@status = status.to_i
- @header = Utils::HeaderHash.new("Content-Type" => "text/html").
- merge(header)
+ @header = Utils::HeaderHash.new.merge(header)
@chunked = "chunked" == @header['Transfer-Encoding']
@writer = lambda { |x| @body << x }
@@ -74,9 +73,10 @@ module Rack
if [204, 205, 304].include?(status.to_i)
header.delete "Content-Type"
header.delete "Content-Length"
+ close
[status.to_i, header, []]
else
- [status.to_i, header, self]
+ [status.to_i, header, BodyProxy.new(self){}]
end
end
alias to_a finish # For *response
diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb
index c82145ae..bc04ca2f 100644
--- a/lib/rack/sendfile.rb
+++ b/lib/rack/sendfile.rb
@@ -89,13 +89,23 @@ module Rack
# RequestHeader Set X-Sendfile-Type X-Sendfile
# ProxyPassReverse / http://localhost:8001/
# XSendFile on
+ #
+ # === Mapping parameter
+ #
+ # The third parameter allows for an overriding extension of the
+ # X-Accel-Mapping header. Mappings should be provided in tuples of internal to
+ # external. The internal values may contain regular expression syntax, they
+ # will be matched with case indifference.
class Sendfile
F = ::File
- def initialize(app, variation=nil)
+ def initialize(app, variation=nil, mappings=[])
@app = app
@variation = variation
+ @mappings = mappings.map do |internal, external|
+ [/^#{internal}/i, external]
+ end
end
def call(env)
@@ -107,6 +117,7 @@ module Rack
if url = map_accel_path(env, path)
headers['Content-Length'] = '0'
headers[type] = url
+ body.close if body.respond_to?(:close)
body = []
else
env['rack.errors'].puts "X-Accel-Mapping header missing"
@@ -115,6 +126,7 @@ module Rack
path = F.expand_path(body.to_path)
headers['Content-Length'] = '0'
headers[type] = path
+ body.close if body.respond_to?(:close)
body = []
when '', nil
else
@@ -131,10 +143,12 @@ module Rack
env['HTTP_X_SENDFILE_TYPE']
end
- def map_accel_path(env, file)
- if mapping = env['HTTP_X_ACCEL_MAPPING']
+ def map_accel_path(env, path)
+ if mapping = @mappings.find { |internal,_| internal =~ path }
+ path.sub(*mapping)
+ elsif mapping = env['HTTP_X_ACCEL_MAPPING']
internal, external = mapping.split('=', 2).map{ |p| p.strip }
- file.sub(/^#{internal}/i, external)
+ path.sub(/^#{internal}/i, external)
end
end
end
diff --git a/lib/rack/server.rb b/lib/rack/server.rb
index 16185296..b385d4bd 100644
--- a/lib/rack/server.rb
+++ b/lib/rack/server.rb
@@ -17,6 +17,10 @@ module Rack
lineno += 1
}
+ opts.on("-b", "--builder BUILDER_LINE", "evaluate a BUILDER_LINE of code as a builder script") { |line|
+ options[:builder] = line
+ }
+
opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") {
options[:debug] = true
}
@@ -192,15 +196,7 @@ module Rack
end
def app
- @app ||= begin
- if !::File.exist? options[:config]
- abort "configuration #{options[:config]} not found"
- end
-
- app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
- self.options.merge! options
- app
- end
+ @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
end
def self.logging_middleware
@@ -273,6 +269,20 @@ module Rack
end
private
+ def build_app_and_options_from_config
+ if !::File.exist? options[:config]
+ abort "configuration #{options[:config]} not found"
+ end
+
+ app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
+ self.options.merge! options
+ app
+ end
+
+ def build_app_from_string
+ Rack::Builder.new_from_string(self.options[:builder])
+ end
+
def parse_options(args)
options = default_options
@@ -294,8 +304,8 @@ module Rack
middleware[options[:environment]].reverse_each do |middleware|
middleware = middleware.call(self) if middleware.respond_to?(:call)
next unless middleware
- klass = middleware.shift
- app = klass.new(app, *middleware)
+ klass, *args = middleware
+ app = klass.new(app, *args)
end
app
end
diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb
index ed74afd6..ab76ca78 100644
--- a/lib/rack/session/abstract/id.rb
+++ b/lib/rack/session/abstract/id.rb
@@ -18,89 +18,85 @@ module Rack
ENV_SESSION_KEY = 'rack.session'.freeze
ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
- # Thin wrapper around Hash that allows us to lazily load session id into session_options.
+ # SessionHash is responsible to lazily load the session from store.
- class OptionsHash < Hash #:nodoc:
- def initialize(by, env, default_options)
- @by = by
- @env = env
- @session_id_loaded = false
- merge!(default_options)
- end
+ class SessionHash
+ include Enumerable
+ attr_writer :id
- def [](key)
- load_session_id! if key == :id && session_id_not_loaded?
- super
+ def initialize(store, env)
+ @store = store
+ @env = env
+ @loaded = false
end
- private
-
- def session_id_not_loaded?
- !(@session_id_loaded || key?(:id))
+ def id
+ return @id if @loaded or instance_variable_defined?(:@id)
+ @id = @store.send(:extract_session_id, @env)
end
- def load_session_id!
- self[:id] = @by.send(:extract_session_id, @env)
- @session_id_loaded = true
+ def options
+ @env[ENV_SESSION_OPTIONS_KEY]
end
- end
- # SessionHash is responsible to lazily load the session from store.
-
- class SessionHash < Hash
- def initialize(by, env)
- super()
- @by = by
- @env = env
- @loaded = false
+ def each(&block)
+ load_for_read!
+ @data.each(&block)
end
def [](key)
load_for_read!
- super(key.to_s)
+ @data[key.to_s]
end
+ alias :fetch :[]
def has_key?(key)
load_for_read!
- super(key.to_s)
+ @data.has_key?(key.to_s)
end
alias :key? :has_key?
alias :include? :has_key?
def []=(key, value)
load_for_write!
- super(key.to_s, value)
+ @data[key.to_s] = value
end
+ alias :store :[]=
def clear
load_for_write!
- super
+ @data.clear
end
def destroy
- clear
- options = @env[ENV_SESSION_OPTIONS_KEY]
- options[:id] = @by.send(:destroy_session, @env, options[:id], options)
+ clear
+ @id = @store.send(:destroy_session, @env, id, options)
end
def to_hash
load_for_read!
- Hash[self].delete_if { |k,v| v.nil? }
+ @data.dup
end
def update(hash)
load_for_write!
- super(stringify_keys(hash))
+ @data.update(stringify_keys(hash))
+ end
+ alias :merge! :update
+
+ def replace(hash)
+ load_for_write!
+ @data.replace(stringify_keys(hash))
end
def delete(key)
load_for_write!
- super(key.to_s)
+ @data.delete(key.to_s)
end
def inspect
if loaded?
- super
+ @data.inspect
else
"#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
end
@@ -108,7 +104,8 @@ module Rack
def exists?
return @exists if instance_variable_defined?(:@exists)
- @exists = @by.send(:session_exists?, @env)
+ @data = {}
+ @exists = @store.send(:session_exists?, @env)
end
def loaded?
@@ -117,12 +114,7 @@ module Rack
def empty?
load_for_read!
- super
- end
-
- def merge!(hash)
- load_for_write!
- super
+ @data.empty?
end
private
@@ -136,9 +128,8 @@ module Rack
end
def load!
- id, session = @by.send(:load_session, @env)
- @env[ENV_SESSION_OPTIONS_KEY][:id] = id
- replace(stringify_keys(session))
+ @id, session = @store.send(:load_session, @env)
+ @data = stringify_keys(session)
@loaded = true
end
@@ -242,8 +233,8 @@ module Rack
def prepare_session(env)
session_was = env[ENV_SESSION_KEY]
- env[ENV_SESSION_KEY] = SessionHash.new(self, env)
- env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options)
+ env[ENV_SESSION_KEY] = session_class.new(self, env)
+ env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
env[ENV_SESSION_KEY].merge! session_was if session_was
end
@@ -265,10 +256,10 @@ module Rack
sid
end
- # Returns the current session id from the OptionsHash.
+ # Returns the current session id from the SessionHash.
def current_session_id(env)
- env[ENV_SESSION_OPTIONS_KEY][:id]
+ env[ENV_SESSION_KEY].id
end
# Check if the session exists or not.
@@ -291,7 +282,7 @@ module Rack
end
def loaded_session?(session)
- !session.is_a?(SessionHash) || session.loaded?
+ !session.is_a?(session_class) || session.loaded?
end
def forced_session_update?(session, options)
@@ -314,21 +305,21 @@ module Rack
# response with the session's id.
def commit_session(env, status, headers, body)
- session = env['rack.session']
- options = env['rack.session.options']
+ session = env[ENV_SESSION_KEY]
+ options = session.options
if options[:drop] || options[:renew]
- session_id = destroy_session(env, options[:id] || generate_sid, options)
+ session_id = destroy_session(env, session.id || generate_sid, options)
return [status, headers, body] unless session_id
end
return [status, headers, body] unless commit_session?(env, session, options)
session.send(:load!) unless loaded_session?(session)
- session = session.to_hash
- session_id ||= options[:id] || generate_sid
+ session_id ||= session.id
+ session_data = session.to_hash.delete_if { |k,v| v.nil? }
- if not data = set_session(env, session_id, session, options)
+ if not data = set_session(env, session_id, session_data, options)
env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
elsif options[:defer] and not options[:renew]
env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE
@@ -352,6 +343,12 @@ module Rack
end
end
+ # Allow subclasses to prepare_session for different Session classes
+
+ def session_class
+ SessionHash
+ end
+
# All thread safety and session retrival proceedures should occur here.
# Should return [session_id, session].
# If nil is provided as the session id, generation of a new valid id
diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb
index 50bdb782..56469135 100644
--- a/lib/rack/session/cookie.rb
+++ b/lib/rack/session/cookie.rb
@@ -98,7 +98,7 @@ module Rack
private
- def load_session(env)
+ def get_session(env, sid)
data = unpacked_cookie_data(env)
data = persistent_session_id!(data)
[data["session_id"], data]
diff --git a/lib/rack/static.rb b/lib/rack/static.rb
index 28754360..46bc66da 100644
--- a/lib/rack/static.rb
+++ b/lib/rack/static.rb
@@ -28,21 +28,56 @@ module Rack
# use Rack::Static, :urls => [""], :root => 'public', :index =>
# 'index.html'
#
- # Set a fixed Cache-Control header for all served files:
+ # Set custom HTTP Headers for based on rules:
#
- # use Rack::Static, :root => 'public', :cache_control => 'public'
+ # use Rack::Static, :root => 'public',
+ # :header_rules => [
+ # [rule, {header_field => content, header_field => content}],
+ # [rule, {header_field => content}]
+ # ]
#
- # Set custom HTTP Headers for all served files:
+ # Rules for selecting files:
#
- # use Rack::Static, :root => 'public', :headers =>
- # {'Cache-Control' => 'public, max-age=31536000',
- # 'Access-Control-Allow-Origin' => '*'}
+ # 1) All files
+ # Provide the :all symbol
+ # :all => Matches every file
#
- # Note: If both :headers => {'Cache-Control' => 'public, max-age=42'}
- # and :cache_control => 'public, max-age=38' are being provided
- # the :headers setting takes precedence
+ # 2) Folders
+ # Provide the folder path as a string
+ # '/folder' or '/folder/subfolder' => Matches files in a certain folder
+ #
+ # 3) File Extensions
+ # Provide the file extensions as an array
+ # ['css', 'js'] or %w(css js) => Matches files ending in .css or .js
+ #
+ # 4) Regular Expressions / Regexp
+ # Provide a regular expression
+ # %r{\.(?:css|js)\z} => Matches files ending in .css or .js
+ # /\.(?:eot|ttf|otf|woff|svg)\z/ => Matches files ending in
+ # the most common web font formats (.eot, .ttf, .otf, .woff, .svg)
+ # Note: This Regexp is available as a shortcut, using the :fonts rule
+ #
+ # 5) Font Shortcut
+ # Provide the :fonts symbol
+ # :fonts => Uses the Regexp rule stated right above to match all common web font endings
+ #
+ # Rule Ordering:
+ # Rules are applied in the order that they are provided.
+ # List rather general rules above special ones.
+ #
+ # Complete example use case including HTTP header rules:
+ #
+ # use Rack::Static, :root => 'public',
+ # :header_rules => [
+ # # Cache all static files in public caches (e.g. Rack::Cache)
+ # # as well as in the browser
+ # [:all, {'Cache-Control' => 'public, max-age=31536000'}],
+ #
+ # # Provide web fonts with cross-origin access-control-headers
+ # # Firefox requires this when serving assets using a Content Delivery Network
+ # [:fonts, {'Access-Control-Allow-Origin' => '*'}]
+ # ]
#
-
class Static
def initialize(app, options={})
@@ -50,11 +85,14 @@ module Rack
@urls = options[:urls] || ["/favicon.ico"]
@index = options[:index]
root = options[:root] || Dir.pwd
- headers = options[:headers] || {}
- # Allow for legacy :cache_control option
- # while prioritizing :headers => {'Cache-Control' => ''} settings
- headers['Cache-Control'] ||= options[:cache_control] if options[:cache_control]
- @file_server = Rack::File.new(root, headers)
+
+ # 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]
+ @headers = {}
+
+ @file_server = Rack::File.new(root, @headers)
end
def overwrite_file_path(path)
@@ -74,11 +112,42 @@ module Rack
if can_serve(path)
env["PATH_INFO"] = (path =~ /\/$/ ? path + @index : @urls[path]) if overwrite_file_path(path)
+ @path = env["PATH_INFO"]
+ apply_header_rules
@file_server.call(env)
else
@app.call(env)
end
end
+ # Convert HTTP header rules to HTTP headers
+ def apply_header_rules
+ @header_rules.each do |rule, headers|
+ apply_rule(rule, headers)
+ end
+ end
+
+ def apply_rule(rule, headers)
+ case rule
+ when :all # All files
+ set_headers(headers)
+ when :fonts # Fonts Shortcut
+ set_headers(headers) if @path.match(/\.(?:ttf|otf|eot|woff|svg)\z/)
+ when String # Folder
+ path = ::Rack::Utils.unescape(@path)
+ set_headers(headers) if (path.start_with?(rule) || path.start_with?('/' + rule))
+ when Array # Extension/Extensions
+ extensions = rule.join('|')
+ set_headers(headers) if @path.match(/\.(#{extensions})\z/)
+ when Regexp # Flexible Regexp
+ set_headers(headers) if @path.match(rule)
+ else
+ end
+ end
+
+ def set_headers(headers)
+ headers.each { |field, content| @headers[field] = content }
+ end
+
end
end
diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb
index 415571bd..3ffdb3a2 100644
--- a/lib/rack/utils.rb
+++ b/lib/rack/utils.rb
@@ -3,9 +3,9 @@ require 'fileutils'
require 'set'
require 'tempfile'
require 'rack/multipart'
+require 'time'
major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
-ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
if major == 1 && minor < 9
require 'rack/backports/uri/common_18'
@@ -63,13 +63,14 @@ module Rack
# and ';' characters. You can also use this to parse
# cookies by changing the characters used in the second
# parameter (which defaults to '&;').
- def parse_query(qs, d = nil)
+ def parse_query(qs, d = nil, &unescaper)
+ unescaper ||= method(:unescape)
+
params = KeySpaceConstrainedParams.new
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
next if p.empty?
- k, v = p.split('=', 2).map { |x| unescape(x) }
- next unless k || v
+ k, v = p.split('=', 2).map(&unescaper)
if cur = params[k]
if cur.class == Array
@@ -166,6 +167,31 @@ module Rack
end
module_function :build_nested_query
+ def q_values(q_value_header)
+ q_value_header.to_s.split(/\s*,\s*/).map do |part|
+ value, parameters = part.split(/\s*;\s*/, 2)
+ quality = 1.0
+ if md = /\Aq=([\d.]+)/.match(parameters)
+ quality = md[1].to_f
+ end
+ [value, quality]
+ end
+ end
+ module_function :q_values
+
+ def best_q_match(q_value_header, available_mimes)
+ values = q_values(q_value_header)
+
+ values.map do |req_mime, quality|
+ match = available_mimes.first { |am| Rack::Mime.match?(am, req_mime) }
+ next unless match
+ [match, quality]
+ end.compact.sort_by do |match, quality|
+ (match.split('/', 2).count('*') * -10) + quality
+ end.last.first
+ end
+ module_function :best_q_match
+
ESCAPE_HTML = {
"&" => "&amp;",
"<" => "&lt;",
@@ -223,8 +249,30 @@ module Rack
when Hash
domain = "; domain=" + value[:domain] if value[:domain]
path = "; path=" + value[:path] if value[:path]
- # According to RFC 2109, we need dashes here.
- # N.B.: cgi.rb uses spaces...
+ 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 delimeted 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]
secure = "; secure" if value[:secure]
@@ -234,7 +282,7 @@ module Rack
value = [value] unless Array === value
cookie = escape(key) + "=" +
value.map { |v| escape v }.join("&") +
- "#{domain}#{path}#{expires}#{secure}#{httponly}"
+ "#{domain}#{path}#{max_age}#{expires}#{secure}#{httponly}"
case header["Set-Cookie"]
when nil, ''
@@ -273,6 +321,7 @@ module Rack
set_cookie_header!(header, key,
{:value => '', :path => nil, :domain => nil,
+ :max_age => '0',
:expires => Time.at(0) }.merge(value))
nil
@@ -292,6 +341,11 @@ module Rack
end
module_function :bytesize
+ def rfc2822(time)
+ time.rfc2822
+ end
+ module_function :rfc2822
+
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
# of '% %b %Y'.
# It assumes that the time is in GMT to comply to the RFC 2109.
@@ -301,12 +355,12 @@ module Rack
# Do not use %a and %b from Time.strptime, it would use localized names for
# weekday and month.
#
- def rfc2822(time)
+ def rfc2109(time)
wday = Time::RFC2822_DAY_NAME[time.wday]
mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
end
- module_function :rfc2822
+ module_function :rfc2109
# Parses the "Range:" header, if present, into an array of Range objects.
# Returns nil if the header is missing or syntactically invalid.
@@ -314,16 +368,16 @@ module Rack
def byte_ranges(env, size)
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
http_range = env['HTTP_RANGE']
- return nil unless http_range
+ return nil unless http_range && http_range =~ /bytes=([^;]+)/
ranges = []
- http_range.split(/,\s*/).each do |range_spec|
- matches = range_spec.match(/bytes=(\d*)-(\d*)/)
- return nil unless matches
- r0,r1 = matches[1], matches[2]
+ $1.split(/,\s*/).each do |range_spec|
+ return nil unless range_spec =~ /(\d*)-(\d*)/
+ r0,r1 = $1, $2
if r0.empty?
return nil if r1.empty?
# suffix-byte-range-spec, represents trailing suffix of file
- r0 = [size - r1.to_i, 0].max
+ r0 = size - r1.to_i
+ r0 = 0 if r0 < 0
r1 = size - 1
else
r0 = r0.to_i
@@ -472,62 +526,72 @@ module Rack
# Every standard HTTP code mapped to the appropriate message.
# Generated with:
- # curl -s http://www.iana.org/assignments/http-status-codes | \
- # ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
- # puts " #{m[1]} => \x27#{m[2].strip}x27,"'
+ # irb -ropen-uri -rnokogiri
+ # > Nokogiri::XML(open("http://www.iana.org/assignments/http-status-codes/http-status-codes.xml")).css("record").each{|r|
+ # puts "#{r.css('value').text} => '#{r.css('description').text}'"}
HTTP_STATUS_CODES = {
- 100 => 'Continue',
- 101 => 'Switching Protocols',
- 102 => 'Processing',
- 200 => 'OK',
- 201 => 'Created',
- 202 => 'Accepted',
- 203 => 'Non-Authoritative Information',
- 204 => 'No Content',
- 205 => 'Reset Content',
- 206 => 'Partial Content',
- 207 => 'Multi-Status',
- 226 => 'IM Used',
- 300 => 'Multiple Choices',
- 301 => 'Moved Permanently',
- 302 => 'Found',
- 303 => 'See Other',
- 304 => 'Not Modified',
- 305 => 'Use Proxy',
- 306 => 'Reserved',
- 307 => 'Temporary Redirect',
- 400 => 'Bad Request',
- 401 => 'Unauthorized',
- 402 => 'Payment Required',
- 403 => 'Forbidden',
- 404 => 'Not Found',
- 405 => 'Method Not Allowed',
- 406 => 'Not Acceptable',
- 407 => 'Proxy Authentication Required',
- 408 => 'Request Timeout',
- 409 => 'Conflict',
- 410 => 'Gone',
- 411 => 'Length Required',
- 412 => 'Precondition Failed',
- 413 => 'Request Entity Too Large',
- 414 => 'Request-URI Too Long',
- 415 => 'Unsupported Media Type',
- 416 => 'Requested Range Not Satisfiable',
- 417 => 'Expectation Failed',
- 418 => "I'm a Teapot",
- 422 => 'Unprocessable Entity',
- 423 => 'Locked',
- 424 => 'Failed Dependency',
- 426 => 'Upgrade Required',
- 500 => 'Internal Server Error',
- 501 => 'Not Implemented',
- 502 => 'Bad Gateway',
- 503 => 'Service Unavailable',
- 504 => 'Gateway Timeout',
- 505 => 'HTTP Version Not Supported',
- 506 => 'Variant Also Negotiates',
- 507 => 'Insufficient Storage',
- 510 => 'Not Extended',
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 102 => 'Processing',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status',
+ 208 => 'Already Reported',
+ 226 => 'IM Used',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 306 => 'Reserved',
+ 307 => 'Temporary Redirect',
+ 308 => 'Permanent Redirect',
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Long',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Requested Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 422 => 'Unprocessable Entity',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 425 => 'Reserved for WebDAV advanced collections expired proposal',
+ 426 => 'Upgrade Required',
+ 427 => 'Unassigned',
+ 428 => 'Precondition Required',
+ 429 => 'Too Many Requests',
+ 430 => 'Unassigned',
+ 431 => 'Request Header Fields Too Large',
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported',
+ 506 => 'Variant Also Negotiates (Experimental)',
+ 507 => 'Insufficient Storage',
+ 508 => 'Loop Detected',
+ 509 => 'Unassigned',
+ 510 => 'Not Extended',
+ 511 => 'Network Authentication Required'
}
# Responses with HTTP status codes that should not have an entity body
diff --git a/rack.gemspec b/rack.gemspec
index 89be19c1..cf438685 100644
--- a/rack.gemspec
+++ b/rack.gemspec
@@ -1,6 +1,6 @@
Gem::Specification.new do |s|
s.name = "rack"
- s.version = "1.4.1"
+ s.version = "1.5.0"
s.platform = Gem::Platform::RUBY
s.summary = "a modular Ruby webserver interface"
@@ -29,9 +29,4 @@ EOF
s.add_development_dependency 'bacon'
s.add_development_dependency 'rake'
-
- s.add_development_dependency 'ruby-fcgi'
- s.add_development_dependency 'memcache-client'
- s.add_development_dependency 'mongrel', '>= 1.2.0.pre2'
- s.add_development_dependency 'thin'
end
diff --git a/test/cgi/assets/folder/test.js b/test/cgi/assets/folder/test.js
new file mode 100644
index 00000000..6874e45a
--- /dev/null
+++ b/test/cgi/assets/folder/test.js
@@ -0,0 +1 @@
+### TestFile ###
diff --git a/test/cgi/assets/fonts/font.eot b/test/cgi/assets/fonts/font.eot
new file mode 100644
index 00000000..6874e45a
--- /dev/null
+++ b/test/cgi/assets/fonts/font.eot
@@ -0,0 +1 @@
+### TestFile ###
diff --git a/test/cgi/assets/images/image.png b/test/cgi/assets/images/image.png
new file mode 100644
index 00000000..6874e45a
--- /dev/null
+++ b/test/cgi/assets/images/image.png
@@ -0,0 +1 @@
+### TestFile ###
diff --git a/test/cgi/assets/index.html b/test/cgi/assets/index.html
new file mode 100644
index 00000000..6874e45a
--- /dev/null
+++ b/test/cgi/assets/index.html
@@ -0,0 +1 @@
+### TestFile ###
diff --git a/test/cgi/assets/javascripts/app.js b/test/cgi/assets/javascripts/app.js
new file mode 100644
index 00000000..6874e45a
--- /dev/null
+++ b/test/cgi/assets/javascripts/app.js
@@ -0,0 +1 @@
+### TestFile ###
diff --git a/test/cgi/assets/stylesheets/app.css b/test/cgi/assets/stylesheets/app.css
new file mode 100644
index 00000000..6874e45a
--- /dev/null
+++ b/test/cgi/assets/stylesheets/app.css
@@ -0,0 +1 @@
+### TestFile ###
diff --git a/test/spec_auth_basic.rb b/test/spec_auth_basic.rb
index 145f6b9d..af8e779b 100644
--- a/test/spec_auth_basic.rb
+++ b/test/spec_auth_basic.rb
@@ -66,6 +66,14 @@ describe Rack::Auth::Basic do
end
end
+ should 'return 400 Bad Request for a malformed authorization header' do
+ request 'HTTP_AUTHORIZATION' => '' do |response|
+ response.should.be.a.client_error
+ response.status.should.equal 400
+ response.should.not.include 'WWW-Authenticate'
+ end
+ end
+
it 'takes realm as optional constructor arg' do
app = Rack::Auth::Basic.new(unprotected_app, realm) { true }
realm.should == app.realm
diff --git a/test/spec_body_proxy.rb b/test/spec_body_proxy.rb
index 2f9f040b..c65e10d2 100644
--- a/test/spec_body_proxy.rb
+++ b/test/spec_body_proxy.rb
@@ -62,4 +62,8 @@ describe Rack::BodyProxy do
proxy.close
closed.should.equal true
end
+
+ should 'provide an #each method' do
+ Rack::BodyProxy.method_defined?(:each).should.equal true
+ end
end
diff --git a/test/spec_builder.rb b/test/spec_builder.rb
index 511a6472..a2fd5685 100644
--- a/test/spec_builder.rb
+++ b/test/spec_builder.rb
@@ -204,4 +204,11 @@ describe Rack::Builder do
Rack::MockRequest.new(app).get("/").body.to_s.should.equal '1'
end
end
+
+ describe 'new_from_string' do
+ it "builds a rack app from string" do
+ app, = Rack::Builder.new_from_string "run lambda{|env| [200, {'Content-Type' => 'text/plane'}, ['OK']] }"
+ Rack::MockRequest.new(app).get("/").body.to_s.should.equal 'OK'
+ end
+ end
end
diff --git a/test/spec_cascade.rb b/test/spec_cascade.rb
index 8db570d2..38a18daa 100644
--- a/test/spec_cascade.rb
+++ b/test/spec_cascade.rb
@@ -50,4 +50,12 @@ describe Rack::Cascade do
cascade << app3
Rack::MockRequest.new(cascade).get('/foo').should.be.ok
end
+
+ should "close the body on cascade" do
+ body = StringIO.new
+ closer = lambda { |env| [404, {}, body] }
+ cascade = Rack::Cascade.new([closer, app3], [404])
+ Rack::MockRequest.new(cascade).get("/foo").should.be.ok
+ body.should.be.closed
+ end
end
diff --git a/test/spec_cgi.rb b/test/spec_cgi.rb
index ebfee023..8c90ec81 100644
--- a/test/spec_cgi.rb
+++ b/test/spec_cgi.rb
@@ -43,7 +43,7 @@ describe Rack::Handler::CGI do
should "have rack headers" do
GET("/test")
- response["rack.version"].should.equal([1,1])
+ response["rack.version"].should.equal([1,2])
response["rack.multithread"].should.be.false
response["rack.multiprocess"].should.be.true
response["rack.run_once"].should.be.true
diff --git a/test/spec_chunked.rb b/test/spec_chunked.rb
index 1cd91f06..12f21581 100644
--- a/test/spec_chunked.rb
+++ b/test/spec_chunked.rb
@@ -3,18 +3,16 @@ require 'rack/lint'
require 'rack/mock'
describe Rack::Chunked do
- ::Enumerator = ::Enumerable::Enumerator unless Object.const_defined?(:Enumerator)
-
def chunked(app)
proc do |env|
app = Rack::Chunked.new(app)
response = Rack::Lint.new(app).call(env)
# we want to use body like an array, but it only has #each
- response[2] = Enumerator.new(response[2]).to_a
+ response[2] = response[2].to_enum.to_a
response
end
end
-
+
before do
@env = Rack::MockRequest.
env_for('/', 'HTTP_VERSION' => '1.1', 'REQUEST_METHOD' => 'GET')
@@ -43,7 +41,7 @@ describe Rack::Chunked do
response.headers.should.not.include 'Content-Length'
response.headers['Transfer-Encoding'].should.equal 'chunked'
response.body.encoding.to_s.should.equal "ASCII-8BIT"
- response.body.should.equal "c\r\n\xFE\xFFH\x00e\x00l\x00l\x00o\x00\r\n2\r\n \x00\r\na\r\nW\x00o\x00r\x00l\x00d\x00\r\n0\r\n\r\n"
+ response.body.should.equal "c\r\n\xFE\xFFH\x00e\x00l\x00l\x00o\x00\r\n2\r\n \x00\r\na\r\nW\x00o\x00r\x00l\x00d\x00\r\n0\r\n\r\n".force_encoding("BINARY")
end if RUBY_VERSION >= "1.9"
should 'not modify response when Content-Length header present' do
diff --git a/test/spec_content_length.rb b/test/spec_content_length.rb
index 03d4ee79..4b80a0f4 100644
--- a/test/spec_content_length.rb
+++ b/test/spec_content_length.rb
@@ -1,19 +1,16 @@
-require 'enumerator'
require 'rack/content_length'
require 'rack/lint'
require 'rack/mock'
describe Rack::ContentLength do
- ::Enumerator = ::Enumerable::Enumerator unless Object.const_defined?(:Enumerator)
-
def content_length(app)
Rack::Lint.new Rack::ContentLength.new(app)
end
-
+
def request
Rack::MockRequest.env_for
end
-
+
should "set Content-Length on Array bodies if none is set" do
app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
response = content_length(app).call(request)
@@ -81,6 +78,6 @@ describe Rack::ContentLength do
response = content_length(app).call(request)
expected = %w[one two three]
response[1]['Content-Length'].should.equal expected.join.size.to_s
- Enumerator.new(response[2]).to_a.should.equal expected
+ response[2].to_enum.to_a.should.equal expected
end
end
diff --git a/test/spec_deflater.rb b/test/spec_deflater.rb
index 378cc51a..6f5137ca 100644
--- a/test/spec_deflater.rb
+++ b/test/spec_deflater.rb
@@ -1,4 +1,3 @@
-require 'enumerator'
require 'stringio'
require 'time' # for Time#httpdate
require 'rack/deflater'
@@ -7,12 +6,10 @@ require 'rack/mock'
require 'zlib'
describe Rack::Deflater do
- ::Enumerator = ::Enumerable::Enumerator unless Object.const_defined?(:Enumerator)
-
def deflater(app)
Rack::Lint.new Rack::Deflater.new(app)
end
-
+
def build_response(status, body, accept_encoding, headers = {})
body = [body] if body.respond_to? :to_str
app = lambda do |env|
@@ -129,7 +126,7 @@ describe Rack::Deflater do
response[0].should.equal(200)
response[1].should.equal({ "Vary" => "Accept-Encoding", "Content-Type" => "text/plain" })
- Enumerator.new(response[2]).to_a.should.equal(["Hello world!"])
+ response[2].to_enum.to_a.should.equal(["Hello world!"])
end
should "be able to skip when there is no response entity body" do
@@ -137,19 +134,19 @@ describe Rack::Deflater do
response[0].should.equal(304)
response[1].should.equal({})
- Enumerator.new(response[2]).to_a.should.equal([])
+ response[2].to_enum.to_a.should.equal([])
end
should "handle the lack of an acceptable encoding" do
response1 = build_response(200, "Hello world!", "identity;q=0", "PATH_INFO" => "/")
response1[0].should.equal(406)
response1[1].should.equal({"Content-Type" => "text/plain", "Content-Length" => "71"})
- Enumerator.new(response1[2]).to_a.should.equal(["An acceptable encoding for the requested resource / could not be found."])
+ response1[2].to_enum.to_a.should.equal(["An acceptable encoding for the requested resource / could not be found."])
response2 = build_response(200, "Hello world!", "identity;q=0", "SCRIPT_NAME" => "/foo", "PATH_INFO" => "/bar")
response2[0].should.equal(406)
response2[1].should.equal({"Content-Type" => "text/plain", "Content-Length" => "78"})
- Enumerator.new(response2[2]).to_a.should.equal(["An acceptable encoding for the requested resource /foo/bar could not be found."])
+ response2[2].to_enum.to_a.should.equal(["An acceptable encoding for the requested resource /foo/bar could not be found."])
end
should "handle gzip response with Last-Modified header" do
@@ -182,7 +179,7 @@ describe Rack::Deflater do
response[0].should.equal(200)
response[1].should.not.include "Content-Encoding"
- Enumerator.new(response[2]).to_a.join.should.equal("Hello World!")
+ response[2].to_enum.to_a.join.should.equal("Hello World!")
end
should "do nothing when Content-Encoding already present" do
@@ -191,7 +188,7 @@ describe Rack::Deflater do
response = deflater(app).call(request)
response[0].should.equal(200)
- Enumerator.new(response[2]).to_a.join.should.equal("Hello World!")
+ response[2].to_enum.to_a.join.should.equal("Hello World!")
end
should "deflate when Content-Encoding is identity" do
diff --git a/test/spec_fastcgi.rb b/test/spec_fastcgi.rb
index 5897f35b..14594a20 100644
--- a/test/spec_fastcgi.rb
+++ b/test/spec_fastcgi.rb
@@ -48,7 +48,7 @@ describe Rack::Handler::FastCGI do
should "have rack headers" do
GET("/test.fcgi")
- response["rack.version"].should.equal [1,1]
+ response["rack.version"].should.equal [1,2]
response["rack.multithread"].should.be.false
response["rack.multiprocess"].should.be.true
response["rack.run_once"].should.be.false
diff --git a/test/spec_file.rb b/test/spec_file.rb
index 2505db6a..c9d7a1b9 100644
--- a/test/spec_file.rb
+++ b/test/spec_file.rb
@@ -145,14 +145,6 @@ describe Rack::File do
res["Content-Range"].should.equal "bytes */193"
end
- should "support legacy cache control options provided as string" do
- env = Rack::MockRequest.env_for("/cgi/test")
- status, heads, _ = file(DOCROOT, 'public, max-age=38').call(env)
-
- status.should.equal 200
- heads['Cache-Control'].should.equal 'public, max-age=38'
- end
-
should "support custom http headers" do
env = Rack::MockRequest.env_for("/cgi/test")
status, heads, _ = file(DOCROOT, 'Cache-Control' => 'public, max-age=38',
@@ -197,4 +189,25 @@ describe Rack::File do
res['Content-Length'].should.equal "193"
end
+ should "default to a mime type of text/plain" do
+ req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT)))
+ res = req.get "/cgi/test"
+ res.should.be.successful
+ res['Content-Type'].should.equal "text/plain"
+ end
+
+ should "allow the default mime type to be set" do
+ req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, 'application/octet-stream')))
+ res = req.get "/cgi/test"
+ res.should.be.successful
+ res['Content-Type'].should.equal "application/octet-stream"
+ end
+
+ should "not set Content-Type if the mime type is not set" do
+ req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, nil)))
+ res = req.get "/cgi/test"
+ res.should.be.successful
+ res['Content-Type'].should.equal nil
+ end
+
end
diff --git a/test/spec_head.rb b/test/spec_head.rb
index 77487a33..18f9a76a 100644
--- a/test/spec_head.rb
+++ b/test/spec_head.rb
@@ -1,32 +1,43 @@
-require 'enumerator'
require 'rack/head'
require 'rack/lint'
require 'rack/mock'
describe Rack::Head do
+
def test_response(headers = {})
- app = lambda { |env| [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]] }
+ body = StringIO.new "foo"
+ app = lambda do |env|
+ [200, {"Content-type" => "test/plain", "Content-length" => "3"}, body]
+ end
request = Rack::MockRequest.env_for("/", headers)
response = Rack::Lint.new(Rack::Head.new(app)).call(request)
- return response
+ return response, body
end
should "pass GET, POST, PUT, DELETE, OPTIONS, TRACE requests" do
%w[GET POST PUT DELETE OPTIONS TRACE].each do |type|
- resp = test_response("REQUEST_METHOD" => type)
+ resp, _ = test_response("REQUEST_METHOD" => type)
resp[0].should.equal(200)
resp[1].should.equal({"Content-type" => "test/plain", "Content-length" => "3"})
- Enumerator.new(resp[2]).to_a.should.equal(["foo"])
+ resp[2].to_enum.to_a.should.equal(["foo"])
end
end
should "remove body from HEAD requests" do
- resp = test_response("REQUEST_METHOD" => "HEAD")
+ resp, _ = test_response("REQUEST_METHOD" => "HEAD")
+
+ resp[0].should.equal(200)
+ resp[1].should.equal({"Content-type" => "test/plain", "Content-length" => "3"})
+ resp[2].to_enum.to_a.should.equal([])
+ end
+ should "close the body when it is removed" do
+ resp, body = test_response("REQUEST_METHOD" => "HEAD")
resp[0].should.equal(200)
resp[1].should.equal({"Content-type" => "test/plain", "Content-length" => "3"})
- Enumerator.new(resp[2]).to_a.should.equal([])
+ resp[2].to_enum.to_a.should.equal([])
+ body.should.be.closed
end
end
diff --git a/test/spec_lint.rb b/test/spec_lint.rb
index 6eda1828..f824113f 100644
--- a/test/spec_lint.rb
+++ b/test/spec_lint.rb
@@ -234,12 +234,12 @@ describe Rack::Lint do
end
should "notice content-type errors" do
- lambda {
- Rack::Lint.new(lambda { |env|
- [200, {"Content-length" => "0"}, []]
- }).call(env({}))
- }.should.raise(Rack::Lint::LintError).
- message.should.match(/No Content-Type/)
+ # lambda {
+ # Rack::Lint.new(lambda { |env|
+ # [200, {"Content-length" => "0"}, []]
+ # }).call(env({}))
+ # }.should.raise(Rack::Lint::LintError).
+ # message.should.match(/No Content-Type/)
[100, 101, 204, 205, 304].each do |status|
lambda {
diff --git a/test/spec_lock.rb b/test/spec_lock.rb
index bda77e93..0cbb5447 100644
--- a/test/spec_lock.rb
+++ b/test/spec_lock.rb
@@ -1,4 +1,3 @@
-require 'enumerator'
require 'rack/lint'
require 'rack/lock'
require 'rack/mock'
@@ -36,13 +35,11 @@ module LockHelpers
end
describe Rack::Lock do
- ::Enumerator = ::Enumerable::Enumerator unless Object.const_defined?(:Enumerator)
-
extend LockHelpers
-
+
describe 'Proxy' do
extend LockHelpers
-
+
should 'delegate each' do
env = Rack::MockRequest.env_for("/")
response = Class.new {
@@ -115,11 +112,11 @@ describe Rack::Lock do
env = Rack::MockRequest.env_for("/")
body = [200, {"Content-Type" => "text/plain"}, %w{ hi mom }]
app = lock_app(lambda { |inner_env| body })
-
+
res = app.call(env)
res[0].should.equal body[0]
res[1].should.equal body[1]
- Enumerator.new(res[2]).to_a.should.equal ["hi", "mom"]
+ res[2].to_enum.to_a.should.equal ["hi", "mom"]
end
should "call synchronize on lock" do
@@ -142,7 +139,7 @@ describe Rack::Lock do
should "unlock if the app throws" do
lock = Lock.new
env = Rack::MockRequest.env_for("/")
- app = lock_app(lambda {|env| throw :bacon }, lock)
+ app = lock_app(lambda {|_| throw :bacon }, lock)
lambda { app.call(env) }.should.throw(:bacon)
lock.synchronized.should.equal false
end
diff --git a/test/spec_methodoverride.rb b/test/spec_methodoverride.rb
index 6547fea2..16f5f283 100644
--- a/test/spec_methodoverride.rb
+++ b/test/spec_methodoverride.rb
@@ -65,7 +65,10 @@ EOF
"CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x",
"CONTENT_LENGTH" => input.size.to_s,
:method => "POST", :input => input)
- app.call env
+ begin
+ app.call env
+ rescue EOFError
+ end
env["REQUEST_METHOD"].should.equal "POST"
end
diff --git a/test/spec_mime.rb b/test/spec_mime.rb
new file mode 100644
index 00000000..231bf35b
--- /dev/null
+++ b/test/spec_mime.rb
@@ -0,0 +1,51 @@
+require 'rack/mime'
+
+describe Rack::Mime do
+
+ it "should return the fallback mime-type for files with no extension" do
+ fallback = 'image/jpg'
+ Rack::Mime.mime_type(File.extname('no_ext'), fallback).should.equal fallback
+ end
+
+ it "should always return 'application/octet-stream' for unknown file extensions" do
+ unknown_ext = File.extname('unknown_ext.abcdefg')
+ Rack::Mime.mime_type(unknown_ext).should.equal 'application/octet-stream'
+ end
+
+ it "should return the mime-type for a given extension" do
+ # sanity check. it would be infeasible test every single mime-type.
+ Rack::Mime.mime_type(File.extname('image.jpg')).should.equal 'image/jpeg'
+ end
+
+ it "should support null fallbacks" do
+ Rack::Mime.mime_type('.nothing', nil).should.equal nil
+ end
+
+ it "should match exact mimes" do
+ Rack::Mime.match?('text/html', 'text/html').should.equal true
+ Rack::Mime.match?('text/html', 'text/meme').should.equal false
+ Rack::Mime.match?('text', 'text').should.equal true
+ Rack::Mime.match?('text', 'binary').should.equal false
+ end
+
+ it "should match class wildcard mimes" do
+ Rack::Mime.match?('text/html', 'text/*').should.equal true
+ Rack::Mime.match?('text/plain', 'text/*').should.equal true
+ Rack::Mime.match?('application/json', 'text/*').should.equal false
+ Rack::Mime.match?('text/html', 'text').should.equal true
+ end
+
+ it "should match full wildcards" do
+ Rack::Mime.match?('text/html', '*').should.equal true
+ Rack::Mime.match?('text/plain', '*').should.equal true
+ Rack::Mime.match?('text/html', '*/*').should.equal true
+ Rack::Mime.match?('text/plain', '*/*').should.equal true
+ end
+
+ it "should match type wildcard mimes" do
+ Rack::Mime.match?('text/html', '*/html').should.equal true
+ Rack::Mime.match?('text/plain', '*/plain').should.equal true
+ end
+
+end
+
diff --git a/test/spec_mongrel.rb b/test/spec_mongrel.rb
index 6160327e..e162cdc9 100644
--- a/test/spec_mongrel.rb
+++ b/test/spec_mongrel.rb
@@ -36,7 +36,7 @@ describe Rack::Handler::Mongrel do
should "have rack headers" do
GET("/test")
- response["rack.version"].should.equal [1,1]
+ response["rack.version"].should.equal [1,2]
response["rack.multithread"].should.be.true
response["rack.multiprocess"].should.be.false
response["rack.run_once"].should.be.false
diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb
index 38c0d289..ca3f9747 100644
--- a/test/spec_multipart.rb
+++ b/test/spec_multipart.rb
@@ -48,6 +48,59 @@ describe Rack::Multipart do
params['profile']['bio'].should.include 'hello'
end
+ should "reject insanely long boundaries" do
+ # using a pipe since a tempfile can use up too much space
+ rd, wr = IO.pipe
+
+ # we only call rewind once at start, so make sure it succeeds
+ # and doesn't hit ESPIPE
+ 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
+ begin
+ wr.write("--AaB03x")
+
+ # make the initial boundary a few gigs long
+ longer = "0123456789" * 1024 * 1024
+ (1024 * 1024).times { wr.write(longer) }
+
+ wr.write("\r\n")
+ wr.write('Content-Disposition: form-data; name="a"; filename="a.txt"')
+ wr.write("\r\n")
+ wr.write("Content-Type: text/plain\r\n")
+ wr.write("\r\na")
+ wr.write("--AaB03x--\r\n")
+ wr.close
+ rescue => err # this is EPIPE if Rack shuts us down
+ err
+ end
+ end
+
+ fixture = {
+ "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
+ "CONTENT_LENGTH" => rd.length.to_s,
+ :input => rd,
+ }
+
+ env = Rack::MockRequest.env_for '/', fixture
+ lambda {
+ Rack::Multipart.parse_multipart(env)
+ }.should.raise(EOFError)
+ rd.close
+
+ err = thr.value
+ err.should.be.instance_of Errno::EPIPE
+ wr.close
+ end
+
should "parse multipart upload with text file" do
env = Rack::MockRequest.env_for("/", multipart_fixture(:text))
params = Rack::Multipart.parse_multipart(env)
@@ -367,4 +420,26 @@ EOF
params = Rack::Multipart.parse_multipart(env)
params['profile']['bio'].should.include 'hello'
end
+
+ should "parse very long unquoted multipart file names" do
+ data = <<-EOF
+--AaB03x\r
+Content-Type: text/plain\r
+Content-Disposition: attachment; name=file; filename=#{'long' * 100}\r
+\r
+contents\r
+--AaB03x--\r
+ EOF
+
+ options = {
+ "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
+ "CONTENT_LENGTH" => data.length.to_s,
+ :input => StringIO.new(data)
+ }
+ env = Rack::MockRequest.env_for("/", options)
+ params = Rack::Utils::Multipart.parse_multipart(env)
+
+ params["file"][:filename].should.equal('long' * 100)
+ end
+
end
diff --git a/test/spec_nulllogger.rb b/test/spec_nulllogger.rb
index b120df64..88ba5216 100644
--- a/test/spec_nulllogger.rb
+++ b/test/spec_nulllogger.rb
@@ -1,23 +1,20 @@
-require 'enumerator'
require 'rack/lint'
require 'rack/mock'
require 'rack/nulllogger'
describe Rack::NullLogger do
- ::Enumerator = ::Enumerable::Enumerator unless Object.const_defined?(:Enumerator)
-
should "act as a noop logger" do
app = lambda { |env|
env['rack.logger'].warn "b00m"
[200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]]
}
-
+
logger = Rack::Lint.new(Rack::NullLogger.new(app))
-
+
res = logger.call(Rack::MockRequest.env_for)
res[0..1].should.equal [
200, {'Content-Type' => 'text/plain'}
]
- Enumerator.new(res[2]).to_a.should.equal ["Hello, World!"]
+ res[2].to_enum.to_a.should.equal ["Hello, World!"]
end
end
diff --git a/test/spec_request.rb b/test/spec_request.rb
index 67e8338c..748115d8 100644
--- a/test/spec_request.rb
+++ b/test/spec_request.rb
@@ -508,9 +508,9 @@ describe Rack::Request do
req2.params.should.equal({})
end
- should "raise any errors on every request" do
+ should "pass through non-uri escaped cookies as-is" do
req = Rack::Request.new Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=%")
- 2.times { proc { req.cookies }.should.raise(ArgumentError) }
+ req.cookies["foo"].should == "%"
end
should "parse cookies according to RFC 2109" do
@@ -569,7 +569,10 @@ describe Rack::Request do
should.equal "http://example.org:8080/"
Rack::Request.new(Rack::MockRequest.env_for("https://example.org/")).url.
should.equal "https://example.org/"
-
+ Rack::Request.new(Rack::MockRequest.env_for("coffee://example.org/")).url.
+ should.equal "coffee://example.org/"
+ Rack::Request.new(Rack::MockRequest.env_for("coffee://example.org:443/")).url.
+ should.equal "coffee://example.org:443/"
Rack::Request.new(Rack::MockRequest.env_for("https://example.com:8080/foo?foo")).url.
should.equal "https://example.com:8080/foo?foo"
end
@@ -1001,6 +1004,10 @@ EOF
'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
res.body.should.equal '3.4.5.6'
+ res = mock.get '/',
+ 'REMOTE_ADDR' => 'unix:/tmp/foo',
+ 'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
+ res.body.should.equal '3.4.5.6'
end
class MyRequest < Rack::Request
diff --git a/test/spec_response.rb b/test/spec_response.rb
index 395bf91f..7ba1e0e1 100644
--- a/test/spec_response.rb
+++ b/test/spec_response.rb
@@ -6,7 +6,7 @@ describe Rack::Response do
response = Rack::Response.new
status, header, body = response.finish
status.should.equal 200
- header.should.equal "Content-Type" => "text/html"
+ header.should.equal({})
body.each { |part|
part.should.equal ""
}
@@ -14,7 +14,7 @@ describe Rack::Response do
response = Rack::Response.new
status, header, body = *response
status.should.equal 200
- header.should.equal "Content-Type" => "text/html"
+ header.should.equal({})
body.each { |part|
part.should.equal ""
}
@@ -37,7 +37,7 @@ describe Rack::Response do
it "can set and read headers" do
response = Rack::Response.new
- response["Content-Type"].should.equal "text/html"
+ response["Content-Type"].should.equal nil
response["Content-Type"] = "text/plain"
response["Content-Type"].should.equal "text/plain"
end
@@ -65,12 +65,12 @@ describe Rack::Response do
response["Set-Cookie"].should.equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"].join("\n")
end
- it "formats the Cookie expiration date accordingly to RFC 2109" do
+ it "formats the Cookie expiration date accordingly to RFC 6265" do
response = Rack::Response.new
response.set_cookie "foo", {:value => "bar", :expires => Time.now+10}
response["Set-Cookie"].should.match(
- /expires=..., \d\d-...-\d\d\d\d \d\d:\d\d:\d\d .../)
+ /expires=..., \d\d ... \d\d\d\d \d\d:\d\d:\d\d .../)
end
it "can set secure cookies" do
@@ -92,7 +92,7 @@ describe Rack::Response do
response.delete_cookie "foo"
response["Set-Cookie"].should.equal [
"foo2=bar2",
- "foo=; expires=Thu, 01-Jan-1970 00:00:00 GMT"
+ "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
].join("\n")
end
@@ -102,10 +102,10 @@ describe Rack::Response do
response.set_cookie "foo", {:value => "bar", :domain => ".example.com"}
response["Set-Cookie"].should.equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"].join("\n")
response.delete_cookie "foo", :domain => ".example.com"
- response["Set-Cookie"].should.equal ["foo=bar; domain=sample.example.com", "foo=; domain=.example.com; expires=Thu, 01-Jan-1970 00:00:00 GMT"].join("\n")
+ response["Set-Cookie"].should.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.delete_cookie "foo", :domain => "sample.example.com"
- response["Set-Cookie"].should.equal ["foo=; domain=.example.com; expires=Thu, 01-Jan-1970 00:00:00 GMT",
- "foo=; domain=sample.example.com; expires=Thu, 01-Jan-1970 00:00:00 GMT"].join("\n")
+ response["Set-Cookie"].should.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")
end
it "can delete cookies with the same name with different paths" do
@@ -117,7 +117,7 @@ describe Rack::Response do
response.delete_cookie "foo", :path => "/path"
response["Set-Cookie"].should.equal ["foo=bar; path=/",
- "foo=; path=/path; expires=Thu, 01-Jan-1970 00:00:00 GMT"].join("\n")
+ "foo=; path=/path; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n")
end
it "can do redirects" do
@@ -280,4 +280,34 @@ describe Rack::Response do
res.close
res.body.should.be.closed
end
+
+ it "calls close on #body when 204, 205, or 304" do
+ res = Rack::Response.new
+ res.body = StringIO.new
+ res.finish
+ res.body.should.not.be.closed
+
+ res.status = 204
+ _, _, b = res.finish
+ res.body.should.be.closed
+ b.should.not.equal res.body
+
+ res.body = StringIO.new
+ res.status = 205
+ _, _, b = res.finish
+ res.body.should.be.closed
+ b.should.not.equal res.body
+
+ res.body = StringIO.new
+ res.status = 304
+ _, _, b = res.finish
+ res.body.should.be.closed
+ b.should.not.equal res.body
+ end
+
+ it "wraps the body from #to_ary to prevent infinite loops" do
+ res = Rack::Response.new
+ res.finish.last.should.not.respond_to?(:to_ary)
+ lambda { res.finish.last.to_ary }.should.raise(NoMethodError)
+ end
end
diff --git a/test/spec_sendfile.rb b/test/spec_sendfile.rb
index bb9ad87b..2d03a8e3 100644
--- a/test/spec_sendfile.rb
+++ b/test/spec_sendfile.rb
@@ -2,6 +2,7 @@ require 'fileutils'
require 'rack/lint'
require 'rack/sendfile'
require 'rack/mock'
+require 'tmpdir'
describe Rack::File do
should "respond to #to_path" do
@@ -11,9 +12,9 @@ end
describe Rack::Sendfile do
def sendfile_body
- FileUtils.touch "/tmp/rack_sendfile"
+ FileUtils.touch File.join(Dir.tmpdir, "rack_sendfile")
res = ['Hello World']
- def res.to_path ; "/tmp/rack_sendfile" ; end
+ def res.to_path ; File.join(Dir.tmpdir, "rack_sendfile") ; end
res
end
@@ -21,14 +22,20 @@ describe Rack::Sendfile do
lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] }
end
- def sendfile_app(body=sendfile_body)
- Rack::Lint.new Rack::Sendfile.new(simple_app(body))
+ def sendfile_app(body, mappings = [])
+ Rack::Lint.new Rack::Sendfile.new(simple_app(body), nil, mappings)
end
- @request = Rack::MockRequest.new(sendfile_app)
+ def request(headers={}, body=sendfile_body, mappings=[])
+ yield Rack::MockRequest.new(sendfile_app(body, mappings)).get('/', headers)
+ end
- def request(headers={})
- yield @request.get('/', headers)
+ def open_file(path)
+ Class.new(File) do
+ unless method_defined?(:to_path)
+ alias :to_path :path
+ end
+ end.open(path, 'w+')
end
it "does nothing when no X-Sendfile-Type header present" do
@@ -44,7 +51,7 @@ describe Rack::Sendfile do
response.should.be.ok
response.body.should.be.empty
response.headers['Content-Length'].should.equal '0'
- response.headers['X-Sendfile'].should.equal '/tmp/rack_sendfile'
+ response.headers['X-Sendfile'].should.equal File.join(Dir.tmpdir, "rack_sendfile")
end
end
@@ -53,14 +60,14 @@ describe Rack::Sendfile do
response.should.be.ok
response.body.should.be.empty
response.headers['Content-Length'].should.equal '0'
- response.headers['X-Lighttpd-Send-File'].should.equal '/tmp/rack_sendfile'
+ response.headers['X-Lighttpd-Send-File'].should.equal File.join(Dir.tmpdir, "rack_sendfile")
end
end
it "sets X-Accel-Redirect response header and discards body" do
headers = {
'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect',
- 'HTTP_X_ACCEL_MAPPING' => '/tmp/=/foo/bar/'
+ 'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/foo/bar/"
}
request headers do |response|
response.should.be.ok
@@ -80,10 +87,44 @@ describe Rack::Sendfile do
end
it 'does nothing when body does not respond to #to_path' do
- @request = Rack::MockRequest.new(sendfile_app(['Not a file...']))
- request 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' do |response|
+ request({'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile'}, ['Not a file...']) do |response|
response.body.should.equal 'Not a file...'
response.headers.should.not.include 'X-Sendfile'
end
end
+
+ it "sets X-Accel-Redirect response header and discards body when initialized with multiple mappings" 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'
+
+ mappings = [
+ ["#{dir1}/", '/foo/bar/'],
+ ["#{dir2}/", '/wibble/']
+ ]
+
+ request({'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect'}, first_body, mappings) do |response|
+ response.should.be.ok
+ response.body.should.be.empty
+ response.headers['Content-Length'].should.equal '0'
+ response.headers['X-Accel-Redirect'].should.equal '/foo/bar/rack_sendfile'
+ end
+
+ request({'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect'}, second_body, mappings) do |response|
+ response.should.be.ok
+ response.body.should.be.empty
+ response.headers['Content-Length'].should.equal '0'
+ response.headers['X-Accel-Redirect'].should.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 7ce6d101..82495bd8 100644
--- a/test/spec_server.rb
+++ b/test/spec_server.rb
@@ -19,7 +19,13 @@ describe Rack::Server do
it "overrides :config if :app is passed in" do
server = Rack::Server.new(:app => "FOO")
- server.app.should == "FOO"
+ server.app.should.equal "FOO"
+ end
+
+ should "prefer to use :builder when it is passed in" do
+ server = Rack::Server.new(:builder => "run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['success']] }")
+ server.app.class.should.equal Proc
+ Rack::MockRequest.new(server.app).get("/").body.to_s.should.equal 'success'
end
should "not include Rack::Lint in deployment or none environments" do
diff --git a/test/spec_static.rb b/test/spec_static.rb
index 5e722e6a..fed1df25 100644
--- a/test/spec_static.rb
+++ b/test/spec_static.rb
@@ -70,7 +70,7 @@ describe Rack::Static do
res.body.should == "Hello World"
end
- it "supports serving fixed cache-control" do
+ it "supports serving fixed cache-control (legacy option)" do
opts = OPTIONS.merge(:cache_control => 'public')
request = Rack::MockRequest.new(static(DummyApp.new, opts))
res = request.get("/cgi/test")
@@ -78,19 +78,64 @@ describe Rack::Static do
res.headers['Cache-Control'].should == 'public'
end
- it "supports serving custom http headers" do
- opts = OPTIONS.merge(:headers => {'Cache-Control' => 'public, max-age=42',
- 'Access-Control-Allow-Origin' => '*'})
- request = Rack::MockRequest.new(static(DummyApp.new, opts))
- res = request.get("/cgi/test")
+ HEADER_OPTIONS = {:urls => ["/cgi"], :root => root, :header_rules => [
+ [:all, {'Cache-Control' => 'public, max-age=100'}],
+ [:fonts, {'Cache-Control' => 'public, max-age=200'}],
+ [%w(png jpg), {'Cache-Control' => 'public, max-age=300'}],
+ ['/cgi/assets/folder/', {'Cache-Control' => 'public, max-age=400'}],
+ ['cgi/assets/javascripts', {'Cache-Control' => 'public, max-age=500'}],
+ [/\.(css|erb)\z/, {'Cache-Control' => 'public, max-age=600'}]
+ ]}
+ @header_request = Rack::MockRequest.new(static(DummyApp.new, HEADER_OPTIONS))
+
+ it "supports header rule :all" do
+ # Headers for all files via :all shortcut
+ res = @header_request.get('/cgi/assets/index.html')
res.should.be.ok
- res.headers['Cache-Control'].should == 'public, max-age=42'
- res.headers['Access-Control-Allow-Origin'].should == '*'
+ res.headers['Cache-Control'].should == 'public, max-age=100'
+ end
+
+ it "supports header rule :fonts" do
+ # Headers for web fonts via :fonts shortcut
+ res = @header_request.get('/cgi/assets/fonts/font.eot')
+ res.should.be.ok
+ res.headers['Cache-Control'].should == 'public, max-age=200'
end
- it "allows headers hash to take priority over fixed cache-control" do
- opts = OPTIONS.merge(:cache_control => 'public, max-age=38',
- :headers => {'Cache-Control' => 'public, max-age=42'})
+ it "supports file extension header rules provided as an Array" do
+ # Headers for file extensions via array
+ res = @header_request.get('/cgi/assets/images/image.png')
+ res.should.be.ok
+ res.headers['Cache-Control'].should == 'public, max-age=300'
+ end
+
+ it "supports folder rules provided as a String" do
+ # Headers for files in folder via string
+ res = @header_request.get('/cgi/assets/folder/test.js')
+ res.should.be.ok
+ res.headers['Cache-Control'].should == 'public, max-age=400'
+ end
+
+ it "supports folder header rules provided as a String not starting with a slash" do
+ res = @header_request.get('/cgi/assets/javascripts/app.js')
+ res.should.be.ok
+ res.headers['Cache-Control'].should == 'public, max-age=500'
+ end
+
+ it "supports flexible header rules provided as Regexp" do
+ # Flexible Headers via Regexp
+ res = @header_request.get('/cgi/assets/stylesheets/app.css')
+ res.should.be.ok
+ res.headers['Cache-Control'].should == 'public, max-age=600'
+ end
+
+ it "prioritizes header rules over fixed cache-control setting (legacy option)" do
+ opts = OPTIONS.merge(
+ :cache_control => 'public, max-age=24',
+ :header_rules => [
+ [:all, {'Cache-Control' => 'public, max-age=42'}]
+ ])
+
request = Rack::MockRequest.new(static(DummyApp.new, opts))
res = request.get("/cgi/test")
res.should.be.ok
diff --git a/test/spec_thin.rb b/test/spec_thin.rb
index f762d11e..15a1ab54 100644
--- a/test/spec_thin.rb
+++ b/test/spec_thin.rb
@@ -11,7 +11,7 @@ describe Rack::Handler::Thin do
Thin::Logging.silent = true
@thread = Thread.new do
- Rack::Handler::Thin.run(@app, :Host => @host='127.0.0.1', :Port => @port=9204) do |server|
+ Rack::Handler::Thin.run(@app, :Host => @host='127.0.0.1', :Port => @port=9204, :tag => "tag") do |server|
@server = server
end
end
@@ -77,8 +77,13 @@ describe Rack::Handler::Thin do
response["rack.url_scheme"].should.equal "http"
end
+ should "set tag for server" do
+ @server.tag.should.equal 'tag'
+ end
+
@server.stop!
@thread.kill
+
end
rescue LoadError
diff --git a/test/spec_utils.rb b/test/spec_utils.rb
index 6bda7bbc..de28a4f2 100644
--- a/test/spec_utils.rb
+++ b/test/spec_utils.rb
@@ -272,6 +272,29 @@ describe Rack::Utils do
Rack::Utils.build_query(key => nil).should.equal Rack::Utils.escape(key)
end
+ should "parse q-values" do
+ # XXX handle accept-extension
+ Rack::Utils.q_values("foo;q=0.5,bar,baz;q=0.9").should.equal [
+ [ 'foo', 0.5 ],
+ [ 'bar', 1.0 ],
+ [ 'baz', 0.9 ]
+ ]
+ end
+
+ should "select best quality match" do
+ Rack::Utils.best_q_match("text/html", %w[text/html]).should.equal "text/html"
+
+ # More specific matches are preferred
+ Rack::Utils.best_q_match("text/*;q=0.5,text/html;q=1.0", %w[text/html]).should.equal "text/html"
+
+ # Higher quality matches are preferred
+ Rack::Utils.best_q_match("text/*;q=0.5,text/plain;q=1.0", %w[text/plain text/html]).should.equal "text/plain"
+
+ # All else equal, the available mimes are preferred in order
+ Rack::Utils.best_q_match("text/*", %w[text/html text/plain]).should.equal "text/html"
+ Rack::Utils.best_q_match("text/plain,text/html", %w[text/html text/plain]).should.equal "text/html"
+ end
+
should "escape html entities [&><'\"/]" do
Rack::Utils.escape_html("foo").should.equal "foo"
Rack::Utils.escape_html("f&o").should.equal "f&amp;o"
@@ -342,6 +365,14 @@ describe Rack::Utils do
should "return status code for symbol" do
Rack::Utils.status_code(:ok).should.equal 200
end
+
+ should "return rfc2822 format from rfc2822 helper" do
+ Rack::Utils.rfc2822(Time.at(0).gmtime).should == "Thu, 01 Jan 1970 00:00:00 -0000"
+ end
+
+ should "return rfc2109 format from rfc2109 helper" do
+ Rack::Utils.rfc2109(Time.at(0).gmtime).should == "Thu, 01-Jan-1970 00:00:00 GMT"
+ end
end
describe Rack::Utils, "byte_range" do
@@ -365,6 +396,10 @@ describe Rack::Utils, "byte_range" do
Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=499-499"},500).should.equal [(499..499)]
end
+ should "parse several byte ranges" do
+ Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=500-600,601-999"},1000).should.equal [(500..600),(601..999)]
+ end
+
should "truncate byte ranges" do
Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123-999"},500).should.equal [(123..499)]
Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=600-999"},500).should.equal []
diff --git a/test/spec_webrick.rb b/test/spec_webrick.rb
index d121c69b..5d647724 100644
--- a/test/spec_webrick.rb
+++ b/test/spec_webrick.rb
@@ -33,7 +33,7 @@ describe Rack::Handler::WEBrick do
should "have rack headers" do
GET("/test")
- response["rack.version"].should.equal [1,1]
+ response["rack.version"].should.equal [1,2]
response["rack.multithread"].should.be.true
response["rack.multiprocess"].should.be.false
response["rack.run_once"].should.be.false