Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge remote-tracking branch 'upstream/master'

Conflicts:
	test/spec_deflater.rb
  • Loading branch information...
commit 8ac05587d382ce46009b1c0eed0198bdadf3342c 2 parents f48a75a + 0cba6a4
@jakubpawlowicz authored
Showing with 916 additions and 191 deletions.
  1. +4 −0 .travis.yml
  2. +9 −0 KNOWN-ISSUES
  3. +102 −4 README.rdoc
  4. +1 −1  Rakefile
  5. +69 −4 SPEC
  6. +1 −1  example/protectedlobster.rb
  7. +2 −2 lib/rack.rb
  8. +1 −1  lib/rack/auth/abstract/request.rb
  9. +1 −1  lib/rack/auth/basic.rb
  10. +1 −1  lib/rack/auth/digest/request.rb
  11. +6 −2 lib/rack/builder.rb
  12. +13 −2 lib/rack/cascade.rb
  13. +5 −0 lib/rack/config.rb
  14. +9 −10 lib/rack/file.rb
  15. +18 −5 lib/rack/handler.rb
  16. +1 −0  lib/rack/handler/webrick.rb
  17. +3 −0  lib/rack/head.rb
  18. +133 −7 lib/rack/lint.rb
  19. +3 −3 lib/rack/lobster.rb
  20. +2 −0  lib/rack/lock.rb
  21. +0 −2  lib/rack/methodoverride.rb
  22. +30 −1 lib/rack/mime.rb
  23. +2 −2 lib/rack/multipart.rb
  24. +10 −3 lib/rack/multipart/parser.rb
  25. +18 −24 lib/rack/request.rb
  26. +2 −2 lib/rack/response.rb
  27. +18 −4 lib/rack/sendfile.rb
  28. +21 −11 lib/rack/server.rb
  29. +16 −10 lib/rack/session/abstract/id.rb
  30. +1 −1  lib/rack/session/cookie.rb
  31. +66 −14 lib/rack/utils.rb
  32. +1 −1  rack.gemspec
  33. +7 −0 test/spec_builder.rb
  34. +8 −0 test/spec_cascade.rb
  35. +1 −1  test/spec_cgi.rb
  36. +3 −5 test/spec_chunked.rb
  37. +3 −6 test/spec_content_length.rb
  38. +0 −3  test/spec_deflater.rb
  39. +1 −1  test/spec_fastcgi.rb
  40. +21 −0 test/spec_file.rb
  41. +18 −7 test/spec_head.rb
  42. +6 −6 test/spec_lint.rb
  43. +5 −8 test/spec_lock.rb
  44. +4 −1 test/spec_methodoverride.rb
  45. +51 −0 test/spec_mime.rb
  46. +1 −1  test/spec_mongrel.rb
  47. +75 −0 test/spec_multipart.rb
  48. +3 −6 test/spec_nulllogger.rb
  49. +10 −3 test/spec_request.rb
  50. +34 −10 test/spec_response.rb
  51. +53 −12 test/spec_sendfile.rb
  52. +7 −1 test/spec_server.rb
  53. +35 −0 test/spec_utils.rb
  54. +1 −1  test/spec_webrick.rb
View
4 .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
View
9 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
View
106 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
@@ -361,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
@@ -379,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
@@ -414,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>.
View
2  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"
View
73 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
View
2  example/protectedlobster.rb
@@ -11,4 +11,4 @@
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
View
4 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 @@ def self.version
# Return the Rack release as a dotted string.
def self.release
- "1.4"
+ "1.5"
end
autoload :Builder, "rack/builder"
View
2  lib/rack/auth/abstract/request.rb
@@ -21,7 +21,7 @@ def parts
end
def scheme
- @scheme ||= parts.first.downcase.to_sym
+ @scheme ||= parts.first.downcase
end
def params
View
2  lib/rack/auth/basic.rb
@@ -41,7 +41,7 @@ def valid?(auth)
class Request < Auth::AbstractRequest
def basic?
- !parts.first.nil? && :basic == scheme
+ !parts.first.nil? && "basic" == scheme
end
def credentials
View
2  lib/rack/auth/digest/request.rb
@@ -11,7 +11,7 @@ def method
end
def digest?
- :digest == scheme
+ "digest" == scheme
end
def correct_uri?
View
8 lib/rack/builder.rb
@@ -37,8 +37,7 @@ def self.parse_file(config, opts = Server::Options.new)
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 @@ def self.parse_file(config, opts = Server::Options.new)
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?
View
15 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 @@ def initialize(apps, catch=[404, 405])
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
View
5 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
View
19 lib/rack/file.rb
@@ -21,9 +21,10 @@ class File
alias :to_path :path
- def initialize(root, headers={})
+ def initialize(root, headers={}, default_mime = 'text/plain')
@root = root
@headers = headers
+ @default_mime = default_mime
end
def call(env)
@@ -70,17 +71,15 @@ def _call(env)
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
View
23 lib/rack/handler.rb
@@ -26,6 +26,23 @@ def self.get(server)
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 @@ def self.default(options = {})
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
View
1  lib/rack/handler/webrick.rb
@@ -7,6 +7,7 @@ module Handler
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?
View
3  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 @@ def call(env)
status, headers, body = @app.call(env)
if env["REQUEST_METHOD"] == "HEAD"
+ body.close if body.respond_to? :close
[status, headers, []]
else
[status, headers, body]
View
140 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 @@ def _call(env)
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 @@ def check_env(env)
## <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 @@ def check_env(env)
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 @@ def close(*args)
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 @@ def check_headers(header)
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 @@ def check_headers(header)
## === 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 @@ def check_content_type(status, headers)
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 @@ def each
## 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 }
View
6 lib/rack/lobster.rb
@@ -59,7 +59,7 @@ def call(env)
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
View
2  lib/rack/lock.rb
@@ -2,6 +2,8 @@
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
View
2  lib/rack/methodoverride.rb
@@ -26,8 +26,6 @@ def method_override(env)
method = req.POST[METHOD_OVERRIDE_PARAM_KEY] ||
env[HTTP_METHOD_OVERRIDE_HEADER]
method.to_s.upcase
- rescue EOFError
- ""
end
end
end
View
31 lib/rack/mime.rb
@@ -18,6 +18,35 @@ def mime_type(ext, fallback='application/octet-stream')
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 @@ def mime_type(ext, fallback='application/octet-stream')
".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",
View
4 lib/rack/multipart.rb
@@ -12,7 +12,7 @@ module Multipart
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 @@ def build_multipart(params, first = true)
end
end
-end
+end
View
13 lib/rack/multipart/parser.rb
@@ -70,9 +70,16 @@ def rx
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
View
42 lib/rack/request.rb
@@ -98,10 +98,8 @@ def port
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 @@ def path_info=(s); @env["PATH_INFO"] = s.to_s end
# 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 @@ def trace?; request_method == "TRACE" end
'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 @@ def cookies
# 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 @@ def xhr?
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 @@ def accept_encoding
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'])
View
4 lib/rack/response.rb
@@ -21,8 +21,7 @@ class Response
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,6 +73,7 @@ def finish(&block)
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, BodyProxy.new(self){}]
View
22 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 @@ def call(env)
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 @@ def call(env)
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 @@ def variation(env)
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
View
32 lib/rack/server.rb
@@ -17,6 +17,10 @@ def parse!(args)
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 @@ def default_options
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 @@ def server
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 @@ def build_app(app)
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
View
26 lib/rack/session/abstract/id.rb
@@ -24,15 +24,15 @@ class SessionHash
include Enumerable
attr_writer :id
- def initialize(by, env)
- @by = by
+ def initialize(store, env)
+ @store = store
@env = env
@loaded = false
end
def id
return @id if @loaded or instance_variable_defined?(:@id)
- @id = @by.send(:extract_session_id, @env)
+ @id = @store.send(:extract_session_id, @env)
end
def options
@@ -70,7 +70,7 @@ def clear
def destroy
clear
- @id = @by.send(:destroy_session, @env, id, options)
+ @id = @store.send(:destroy_session, @env, id, options)
end
def to_hash
@@ -105,7 +105,7 @@ def inspect
def exists?
return @exists if instance_variable_defined?(:@exists)
@data = {}
- @exists = @by.send(:session_exists?, @env)
+ @exists = @store.send(:session_exists?, @env)
end
def loaded?
@@ -128,7 +128,7 @@ def load_for_write!
end
def load!
- @id, session = @by.send(:load_session, @env)
+ @id, session = @store.send(:load_session, @env)
@data = stringify_keys(session)
@loaded = true
end
@@ -233,7 +233,7 @@ def generate_sid(secure = @sid_secure)
def prepare_session(env)
session_was = env[ENV_SESSION_KEY]
- env[ENV_SESSION_KEY] = SessionHash.new(self, env)
+ 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
@@ -256,7 +256,7 @@ def extract_session_id(env)
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_KEY].id
@@ -282,7 +282,7 @@ def commit_session?(env, session, options)
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)
@@ -316,7 +316,7 @@ def commit_session(env, status, headers, body)
return [status, headers, body] unless commit_session?(env, session, options)
session.send(:load!) unless loaded_session?(session)
- session_id ||= session.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_data, options)
@@ -343,6 +343,12 @@ def set_cookie(env, headers, cookie)
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
View
2  lib/rack/session/cookie.rb
@@ -98,7 +98,7 @@ def initialize(app, options={})
private
- def load_session(env)
+ def get_session(env, sid)
data = unpacked_cookie_data(env)
data = persistent_session_id!(data)
[data["session_id"], data]
View
80 lib/rack/utils.rb
@@ -3,9 +3,9 @@
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 @@ class << self
# 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 @@ def build_nested_query(value, prefix = nil)
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;",
@@ -224,8 +250,29 @@ def set_cookie_header!(header, key, value)
domain = "; domain=" + value[:domain] if value[:domain]
path = "; path=" + value[:path] if value[:path]
max_age = "; max-age=" + value[:max_age] if value[:max_age]
- # According to RFC 2109, we need dashes here.
- # N.B.: cgi.rb uses spaces...
+ # 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]
@@ -294,6 +341,11 @@ def bytesize(string)
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.
@@ -303,12 +355,12 @@ def bytesize(string)
# 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.
@@ -316,16 +368,16 @@ def rfc2822(time)
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
View
2  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"
View
7 test/spec_builder.rb
@@ -204,4 +204,11 @@ def config_file(name)
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
View
8 test/spec_cascade.rb
@@ -50,4 +50,12 @@ def cascade(*args)
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
View
2  test/spec_cgi.rb
@@ -43,7 +43,7 @@
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
View
8 test/spec_chunked.rb
@@ -3,18 +3,16 @@
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 @@ def chunked(app)
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
View
9 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 @@ def to_ary; end
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
View
3  test/spec_deflater.rb
@@ -1,4 +1,3 @@
-require 'enumerator'
require 'stringio'
require 'time' # for Time#httpdate
require 'rack/deflater'
@@ -7,8 +6,6 @@
require 'zlib'
describe Rack::Deflater do
- ::Enumerator = ::Enumerable::Enumerator unless Object.const_defined?(:Enumerator)
-
def build_response(status, body, accept_encoding, options = {})
body = [body] if body.respond_to? :to_str
app = lambda do |env|
View
2  test/spec_fastcgi.rb
@@ -48,7 +48,7 @@
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
View
21 test/spec_file.rb
@@ -189,4 +189,25 @@ def file(*args)
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
View
25 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
View
12 test/spec_lint.rb
@@ -234,12 +234,12 @@ def result.name
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 {
View
13 test/spec_lock.rb
@@ -1,4 +1,3 @@
-require 'enumerator'
require 'rack/lint'
require 'rack/lock'
require 'rack/mock'
@@ -36,13 +35,11 @@ def lock_app(app, lock = Lock.new)
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 @@ def close; @close_called = true; end
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 @@ def close; @close_called = true; end
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
View
5 test/spec_methodoverride.rb
@@ -65,7 +65,10 @@ def app
"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
View
51 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
+
View
2  test/spec_mongrel.rb
@@ -36,7 +36,7 @@
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
View
75 test/spec_multipart.rb
@@ -48,6 +48,59 @@ def multipart_file(name)
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 @@ def multipart_file(name)
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
View
9 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
View
13 test/spec_request.rb
@@ -508,9 +508,9 @@
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 @@
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
@@ -996,6 +999,10 @@
'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
View
44 test/spec_response.rb
@@ -6,7 +6,7 @@
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 @@
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 @@
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 @@
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 @@
response.delete_cookie "foo"
response["Set-Cookie"].should.equal [
"foo2=bar2",
- "foo=; max-age=0; 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 @@
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; max-age=0; 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; max-age=0; expires=Thu, 01-Jan-1970 00:00:00 GMT",
- "foo=; domain=sample.example.com; max-age=0; expires=Thu, 01-Jan-1970 00:00:00 GMT"].join("\n")
+ 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 @@
response.delete_cookie "foo", :path => "/path"
response["Set-Cookie"].should.equal ["foo=bar; path=/",
- "foo=; path=/path; max-age=0; 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
@@ -281,6 +281,30 @@ def object_with_each.each
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)
View
65 test/spec_sendfile.rb
@@ -2,6 +2,7 @@
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 @@
describe Rack::Sendfile do