diff options
38 files changed, 342 insertions, 121 deletions
diff --git a/.travis.yml b/.travis.yml index 5336f3d2..68904373 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,3 @@ branches: notifications: email: false irc: "irc.freenode.org#rack" -matrix: - allow_failures: - - rvm: 2.0.0 diff --git a/KNOWN-ISSUES b/KNOWN-ISSUES index e0373b2f..ceb2e61f 100644 --- a/KNOWN-ISSUES +++ b/KNOWN-ISSUES @@ -28,3 +28,17 @@ Since lighttpd 1.4.23, you also can use the "fix-root-scriptname" flag in fastcgi.server. + += Known conflicts regarding parameter parsing + + * Many users have differing opinions about parameter parsing. The current + parameter parsers in Rack are based on a combination of the HTTP and CGI + specs, and are intended to round-trip encoding and decoding. There are some + choices that may be viewed as deficiencies, specifically: + - Rack does not create implicit arrays for multiple instances of a parameter + - Rack returns nil when a value is not given + - Rack does not support multi-type keys in parameters + These issues or choices, will not be fixed before 2.0, if at all. They are + very major breaking changes. Users are free to write alternative parameter + parsers, and their own Request and Response wrappers. Moreover, users are + encouraged to do so. diff --git a/README.rdoc b/README.rdoc index 02c1193f..7a3c8d58 100644 --- a/README.rdoc +++ b/README.rdoc @@ -555,12 +555,17 @@ The Rack Core Team, consisting of * Christian Neukirchen (chneukirchen) * James Tucker (raggi) * Josh Peek (josh) +* José Valim (josevalim) * Michael Fellinger (manveru) -* Ryan Tomayko (rtomayko) -* Scytrin dai Kinthra (scytrin) * Aaron Patterson (tenderlove) +* Santiago Pastorino (spastorino) * Konstantin Haase (rkh) +and the Rack Alumnis + +* Ryan Tomayko (rtomayko) +* Scytrin dai Kinthra (scytrin) + would like to thank: * Adrian Madrid, for the LiteSpeed handler. @@ -614,7 +619,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. == Links -Rack:: <http://rack.github.com/> +Rack:: <http://rack.github.io/> Official Rack repositories:: <http://github.com/rack> Rack Bug Tracking:: <http://github.com/rack/rack/issues> rack-devel mailing list:: <http://groups.google.com/group/rack-devel> @@ -207,7 +207,7 @@ but only contain keys that consist of letters, digits, <tt>_</tt> or <tt>-</tt> and start with a letter. The values of the header must be Strings, consisting of lines (for multiple header values, e.g. multiple -<tt>Set-Cookie</tt> values) seperated by "\n". +<tt>Set-Cookie</tt> values) separated by "\n". The lines must not contain characters below 037. === The Content-Type There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx, @@ -222,7 +222,7 @@ 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 is replaced by a middleware after action, the original body -must be closed first, if it repsonds to close. +must be closed first, if it responds 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/lib/rack/auth/digest/md5.rb b/lib/rack/auth/digest/md5.rb index b5871d77..ddee35de 100644 --- a/lib/rack/auth/digest/md5.rb +++ b/lib/rack/auth/digest/md5.rb @@ -96,7 +96,7 @@ module Rack def valid_digest?(auth) pw = @authenticator.call(auth.username) - pw && digest(auth, pw) == auth.response + pw && Rack::Utils.secure_compare(digest(auth, pw), auth.response) end def md5(data) diff --git a/lib/rack/backports/uri/common_18.rb b/lib/rack/backports/uri/common_18.rb index 0ea1998d..ca3a6360 100644 --- a/lib/rack/backports/uri/common_18.rb +++ b/lib/rack/backports/uri/common_18.rb @@ -46,7 +46,7 @@ module URI # Decode given +str+ of URL-encoded form data. # - # This decods + to SP. + # This decodes + to SP. # # See URI.encode_www_form_component, URI.decode_www_form def self.decode_www_form_component(str, enc=nil) diff --git a/lib/rack/commonlogger.rb b/lib/rack/commonlogger.rb index 7028615f..b3968ac7 100644 --- a/lib/rack/commonlogger.rb +++ b/lib/rack/commonlogger.rb @@ -18,7 +18,7 @@ module Rack 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 - + # lilith.local - - [07/Aug/2006 23:58:02 -0400] "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} @@ -46,7 +46,7 @@ module Rack logger.write FORMAT % [ env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-", env["REMOTE_USER"] || "-", - now.strftime("%d/%b/%Y %H:%M:%S"), + now.strftime("%d/%b/%Y %H:%M:%S %z"), env["REQUEST_METHOD"], env["PATH_INFO"], env["QUERY_STRING"].empty? ? "" : "?"+env["QUERY_STRING"], diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb index 2f219f0e..fe2ac3db 100644 --- a/lib/rack/deflater.rb +++ b/lib/rack/deflater.rb @@ -68,6 +68,7 @@ module Rack def initialize(body, mtime) @body = body @mtime = mtime + @closed = false end def each(&block) @@ -79,7 +80,7 @@ module Rack gzip.flush } ensure - @body.close if @body.respond_to?(:close) + close gzip.close @writer = nil end @@ -87,6 +88,12 @@ module Rack def write(data) @writer.call(data) end + + def close + return if @closed + @closed = true + @body.close if @body.respond_to?(:close) + end end class DeflateStream @@ -100,16 +107,23 @@ module Rack def initialize(body) @body = body + @closed = false end def each - deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS) - @body.each { |part| yield deflater.deflate(part, Zlib::SYNC_FLUSH) } - yield deflater.finish + deflator = ::Zlib::Deflate.new(*DEFLATE_ARGS) + @body.each { |part| yield deflator.deflate(part, Zlib::SYNC_FLUSH) } + yield deflator.finish nil ensure + close + deflator.close + end + + def close + return if @closed + @closed = true @body.close if @body.respond_to?(:close) - deflater.close end end end diff --git a/lib/rack/directory.rb b/lib/rack/directory.rb index e90ee082..602f2d44 100644 --- a/lib/rack/directory.rb +++ b/lib/rack/directory.rb @@ -135,8 +135,8 @@ table { width:100%%; } end def each - show_path = @path.sub(/^#{@root}/,'') - files = @files.map{|f| DIR_FILE % f }*"\n" + show_path = Rack::Utils.escape_html(@path.sub(/^#{@root}/,'')) + files = @files.map{|f| DIR_FILE % DIR_FILE_escape(*f) }*"\n" page = DIR_PAGE % [ show_path, show_path , files ] page.each_line{|l| yield l } end @@ -157,5 +157,11 @@ table { width:100%%; } int.to_s + 'B' end + + private + # Assumes url is already escaped. + def DIR_FILE_escape url, *html + [url, *html.map { |e| Utils.escape_html(e) }] + end end end diff --git a/lib/rack/handler/fastcgi.rb b/lib/rack/handler/fastcgi.rb index 340e3613..b26fabc3 100644 --- a/lib/rack/handler/fastcgi.rb +++ b/lib/rack/handler/fastcgi.rb @@ -30,8 +30,11 @@ module Rack end def self.valid_options + environment = ENV['RACK_ENV'] || 'development' + default_host = environment == 'development' ? 'localhost' : '0.0.0.0' + { - "Host=HOST" => "Hostname to listen on (default: localhost)", + "Host=HOST" => "Hostname to listen on (default: #{default_host})", "Port=PORT" => "Port to listen on (default: 8080)", "File=PATH" => "Creates a Domain socket at PATH instead of a TCP socket. Ignores Host and Port if set.", } diff --git a/lib/rack/handler/mongrel.rb b/lib/rack/handler/mongrel.rb index 1a702fd2..20be86b1 100644 --- a/lib/rack/handler/mongrel.rb +++ b/lib/rack/handler/mongrel.rb @@ -7,8 +7,11 @@ module Rack module Handler class Mongrel < ::Mongrel::HttpHandler def self.run(app, options={}) + environment = ENV['RACK_ENV'] || 'development' + default_host = environment == 'development' ? 'localhost' : '0.0.0.0' + server = ::Mongrel::HttpServer.new( - options[:Host] || '0.0.0.0', + options[:Host] || default_host, options[:Port] || 8080, options[:num_processors] || 950, options[:throttle] || 0, @@ -39,8 +42,11 @@ module Rack end def self.valid_options + environment = ENV['RACK_ENV'] || 'development' + default_host = environment == 'development' ? 'localhost' : '0.0.0.0' + { - "Host=HOST" => "Hostname to listen on (default: localhost)", + "Host=HOST" => "Hostname to listen on (default: #{default_host})", "Port=PORT" => "Port to listen on (default: 8080)", "Processors=N" => "Number of concurrent processors to accept (default: 950)", "Timeout=N" => "Time before a request is dropped for inactivity (default: 60)", diff --git a/lib/rack/handler/scgi.rb b/lib/rack/handler/scgi.rb index a4fe6cea..40e86fb9 100644 --- a/lib/rack/handler/scgi.rb +++ b/lib/rack/handler/scgi.rb @@ -17,8 +17,11 @@ module Rack end def self.valid_options + environment = ENV['RACK_ENV'] || 'development' + default_host = environment == 'development' ? 'localhost' : '0.0.0.0' + { - "Host=HOST" => "Hostname to listen on (default: localhost)", + "Host=HOST" => "Hostname to listen on (default: #{default_host})", "Port=PORT" => "Port to listen on (default: 8080)", } end diff --git a/lib/rack/handler/thin.rb b/lib/rack/handler/thin.rb index dc269725..704db06c 100644 --- a/lib/rack/handler/thin.rb +++ b/lib/rack/handler/thin.rb @@ -6,7 +6,10 @@ module Rack module Handler class Thin def self.run(app, options={}) - host = options.delete(:Host) || '0.0.0.0' + environment = ENV['RACK_ENV'] || 'development' + default_host = environment == 'development' ? 'localhost' : '0.0.0.0' + + host = options.delete(:Host) || default_host port = options.delete(:Port) || 8080 args = [host, port, app, options] # Thin versions below 0.8.0 do not support additional options @@ -17,8 +20,11 @@ module Rack end def self.valid_options + environment = ENV['RACK_ENV'] || 'development' + default_host = environment == 'development' ? 'localhost' : '0.0.0.0' + { - "Host=HOST" => "Hostname to listen on (default: localhost)", + "Host=HOST" => "Hostname to listen on (default: #{default_host})", "Port=PORT" => "Port to listen on (default: 8080)", } end diff --git a/lib/rack/handler/webrick.rb b/lib/rack/handler/webrick.rb index 487a0ea1..f76679b4 100644 --- a/lib/rack/handler/webrick.rb +++ b/lib/rack/handler/webrick.rb @@ -6,8 +6,12 @@ module Rack module Handler class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet def self.run(app, options={}) - options[:BindAddress] = options.delete(:Host) if options[:Host] + environment = ENV['RACK_ENV'] || 'development' + default_host = environment == 'development' ? 'localhost' : '0.0.0.0' + + options[:BindAddress] = options.delete(:Host) || default_host options[:Port] ||= 8080 + options[:OutputBufferSize] = 5 @server = ::WEBrick::HTTPServer.new(options) @server.mount "/", Rack::Handler::WEBrick, app yield @server if block_given? @@ -15,8 +19,11 @@ module Rack end def self.valid_options + environment = ENV['RACK_ENV'] || 'development' + default_host = environment == 'development' ? 'localhost' : '0.0.0.0' + { - "Host=HOST" => "Hostname to listen on (default: localhost)", + "Host=HOST" => "Hostname to listen on (default: #{default_host})", "Port=PORT" => "Port to listen on (default: 8080)", } end @@ -46,7 +53,11 @@ module Rack "rack.multiprocess" => false, "rack.run_once" => false, - "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http" + "rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http", + + "rack.hijack?" => true, + "rack.hijack" => lambda { raise NotImplementedError, "only partial hijack is supported."}, + "rack.hijack_io" => nil, }) env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"] @@ -61,6 +72,8 @@ module Rack begin res.status = status.to_i headers.each { |k, vs| + next if k.downcase == "rack.hijack" + if k.downcase == "set-cookie" res.cookies.concat vs.split("\n") else @@ -69,9 +82,18 @@ module Rack res[k] = vs.split("\n").join(", ") end } - body.each { |part| - res.body << part - } + + io_lambda = headers["rack.hijack"] + if io_lambda + rd, wr = IO.pipe + res.body = rd + res.chunked = true + io_lambda.call wr + else + body.each { |part| + res.body << part + } + end ensure body.close if body.respond_to? :close end diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb index fd21f775..0c6e1a35 100644 --- a/lib/rack/lint.rb +++ b/lib/rack/lint.rb @@ -584,7 +584,7 @@ module Rack assert("a header value must be a String, but the value of " + "'#{key}' is a #{value.class}") { value.kind_of? String } ## consisting of lines (for multiple header values, e.g. multiple - ## <tt>Set-Cookie</tt> values) seperated by "\n". + ## <tt>Set-Cookie</tt> values) separated by "\n". value.split("\n").each { |item| ## The lines must not contain characters below 037. assert("invalid header value #{key}: #{item.inspect}") { @@ -660,7 +660,7 @@ module Rack ## ## 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. + ## must be closed first, if it responds to close. # XXX howto: assert("Body has not been closed") { @closed } diff --git a/lib/rack/methodoverride.rb b/lib/rack/methodoverride.rb index 1bdaca84..449961ce 100644 --- a/lib/rack/methodoverride.rb +++ b/lib/rack/methodoverride.rb @@ -1,6 +1,6 @@ module Rack class MethodOverride - HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS PATCH) + HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK) METHOD_OVERRIDE_PARAM_KEY = "_method".freeze HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE".freeze diff --git a/lib/rack/mime.rb b/lib/rack/mime.rb index 5d050221..2879c05f 100644 --- a/lib/rack/mime.rb +++ b/lib/rack/mime.rb @@ -29,21 +29,7 @@ module Rack 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 + (m1 == '*' || v1 == m1) && (m2.nil? || m2 == '*' || m2 == v2) end module_function :match? diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb index 1315c7b3..a8019f98 100644 --- a/lib/rack/multipart/parser.rb +++ b/lib/rack/multipart/parser.rb @@ -87,7 +87,6 @@ module Rack head = nil body = '' filename = content_type = name = nil - content = nil until head && @buf =~ rx if !head && i = @buf.index(EOL+EOL) @@ -137,6 +136,9 @@ module Rack if filename && filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ } filename = Utils.unescape(filename) end + if filename.respond_to?(:valid_encoding?) && !filename.valid_encoding? + filename = filename.chars.select { |char| char.valid_encoding? }.join + end if filename && filename !~ /\\[^\\"]/ filename = filename.gsub(/\\(.)/, '\1') end diff --git a/lib/rack/request.rb b/lib/rack/request.rb index e8734d76..f3a048ab 100644 --- a/lib/rack/request.rb +++ b/lib/rack/request.rb @@ -100,6 +100,8 @@ module Rack port.to_i elsif @env.has_key?("HTTP_X_FORWARDED_HOST") DEFAULT_PORTS[scheme] + elsif @env.has_key?("HTTP_X_FORWARDED_PROTO") + DEFAULT_PORTS[@env['HTTP_X_FORWARDED_PROTO']] else @env["SERVER_PORT"].to_i end @@ -126,6 +128,9 @@ module Rack # 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 LINK + def link?; request_method == "LINK" end + # Checks the HTTP request method (or verb) to see if it was of type PATCH def patch?; request_method == "PATCH" end @@ -137,6 +142,9 @@ module Rack # Checks the HTTP request method (or verb) to see if it was of type TRACE def trace?; request_method == "TRACE" end + + # Checks the HTTP request method (or verb) to see if it was of type UNLINK + def unlink?; request_method == "UNLINK" end # The set of form-data media-types. Requests that do not indicate @@ -199,7 +207,6 @@ module Rack elsif @env["rack.request.form_input"].eql? @env["rack.input"] @env["rack.request.form_hash"] elsif form_data? || parseable_data? - @env["rack.request.form_input"] = @env["rack.input"] unless @env["rack.request.form_hash"] = parse_multipart(env) form_vars = @env["rack.input"].read @@ -212,6 +219,7 @@ module Rack @env["rack.input"].rewind end + @env["rack.request.form_input"] = @env["rack.input"] @env["rack.request.form_hash"] else {} diff --git a/lib/rack/response.rb b/lib/rack/response.rb index 2beba7a8..2076aff0 100644 --- a/lib/rack/response.rb +++ b/lib/rack/response.rb @@ -122,6 +122,7 @@ module Rack def ok?; status == 200; end def bad_request?; status == 400; end + def unauthorized?; status == 401; end def forbidden?; status == 403; end def not_found?; status == 404; end def method_not_allowed?; status == 405; end diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb index bc04ca2f..c247a3bc 100644 --- a/lib/rack/sendfile.rb +++ b/lib/rack/sendfile.rb @@ -22,7 +22,7 @@ module Rack # # Nginx supports the X-Accel-Redirect header. This is similar to X-Sendfile # but requires parts of the filesystem to be mapped into a private URL - # hierarachy. + # hierarchy. # # The following example shows the Nginx configuration required to create # a private "/files/" area, enable X-Accel-Redirect, and pass the special diff --git a/lib/rack/server.rb b/lib/rack/server.rb index dfaed3fc..be7014c6 100644 --- a/lib/rack/server.rb +++ b/lib/rack/server.rb @@ -66,7 +66,7 @@ module Rack options[:daemonize] = d ? true : false } - opts.on("-P", "--pid FILE", "file to store PID (default: rack.pid)") { |f| + opts.on("-P", "--pid FILE", "file to store PID") { |f| options[:pid] = ::File.expand_path(f) } @@ -185,11 +185,14 @@ module Rack end def default_options + environment = ENV['RACK_ENV'] || 'development' + default_host = environment == 'development' ? 'localhost' : '0.0.0.0' + { - :environment => ENV['RACK_ENV'] || "development", + :environment => environment, :pid => nil, :Port => 9292, - :Host => "0.0.0.0", + :Host => default_host, :AccessLog => [], :config => "config.ru" } @@ -350,6 +353,8 @@ module Rack return :exited unless ::File.exist?(options[:pid]) pid = ::File.read(options[:pid]).to_i + return :dead if pid == 0 + Process.kill(0, pid) :running rescue Errno::ESRCH diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb index 0e0ad351..e9edeb7f 100644 --- a/lib/rack/session/abstract/id.rb +++ b/lib/rack/session/abstract/id.rb @@ -289,7 +289,7 @@ module Rack value && !value.empty? end - # Session should be commited if it was loaded, any of specific options like :renew, :drop + # Session should be committed if it was loaded, any of specific options like :renew, :drop # or :expire_after was given and the security permissions match. Skips if skip is given. def commit_session?(env, session, options) @@ -342,7 +342,7 @@ module Rack 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 + env["rack.errors"].puts("Deferring cookie for #{session_id}") if $VERBOSE else cookie = Hash.new cookie[:value] = data @@ -369,7 +369,7 @@ module Rack SessionHash end - # All thread safety and session retrival proceedures should occur here. + # All thread safety and session retrieval procedures should occur here. # Should return [session_id, session]. # If nil is provided as the session id, generation of a new valid id # should occur within. @@ -378,7 +378,7 @@ module Rack raise '#get_session not implemented.' end - # All thread safety and session storage proceedures should occur here. + # All thread safety and session storage procedures should occur here. # Must return the session id if the session was saved successfully, or # false if the session could not be saved. @@ -386,7 +386,7 @@ module Rack raise '#set_session not implemented.' end - # All thread safety and session destroy proceedures should occur here. + # All thread safety and session destroy procedures should occur here. # Should return a new session id or nil if options[:drop] def destroy_session(env, sid, options) diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb index 5aa80cb6..22e33d1f 100644 --- a/lib/rack/session/cookie.rb +++ b/lib/rack/session/cookie.rb @@ -1,4 +1,5 @@ require 'openssl' +require 'zlib' require 'rack/request' require 'rack/response' require 'rack/session/abstract/id' @@ -78,6 +79,19 @@ module Rack ::Rack::Utils::OkJson.decode(super(str)) rescue nil end end + + class ZipJSON < Base64 + def encode(obj) + super(Zlib::Deflate.deflate(::Rack::Utils::OkJson.encode(obj))) + end + + def decode(str) + return unless str + ::Rack::Utils::OkJson.decode(Zlib::Inflate.inflate(super(str))) + rescue + nil + end + end end # Use no encoding for session cookies @@ -86,12 +100,6 @@ module Rack def decode(str); str; end end - # Reverse string encoding. (trollface) - class Reverse - def encode(str); str.reverse; end - def decode(str); str.reverse; end - end - attr_reader :coder def initialize(app, options={}) diff --git a/lib/rack/showstatus.rb b/lib/rack/showstatus.rb index 5a9506f2..6892a5b7 100644 --- a/lib/rack/showstatus.rb +++ b/lib/rack/showstatus.rb @@ -96,7 +96,7 @@ TEMPLATE = <<'HTML' </table> </div> <div id="info"> - <p><%= detail %></p> + <p><%=h detail %></p> </div> <div id="explanation"> diff --git a/lib/rack/static.rb b/lib/rack/static.rb index 46bc66da..41aec7f3 100644 --- a/lib/rack/static.rb +++ b/lib/rack/static.rb @@ -90,9 +90,8 @@ module Rack @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) + @file_server = Rack::File.new(root) end def overwrite_file_path(path) @@ -112,42 +111,40 @@ 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) + path = env["PATH_INFO"] + response = @file_server.call(env) + + headers = response[1] + applicable_rules(path).each do |rule, new_headers| + new_headers.each { |field, content| headers[field] = content } + end + + response 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 + def applicable_rules(path) + @header_rules.find_all do |rule, new_headers| + case rule + when :all + true + when :fonts + path =~ /\.(?:ttf|otf|eot|woff|svg)\z/ + when String + path = ::Rack::Utils.unescape(path) + path.start_with?(rule) || path.start_with?('/' + rule) + when Array + path =~ /\.(#{rule.join('|')})\z/ + when Regexp + path =~ rule + else + false + end 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 561e46e5..43bbef37 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -203,7 +203,7 @@ module Rack if //.respond_to?(:encoding) ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys) else - # On 1.8, there is a kcode = 'u' bug that allows for XSS otherwhise + # On 1.8, there is a kcode = 'u' bug that allows for XSS otherwise # TODO doesn't apply to jruby, so a better condition above might be preferable? ESCAPE_HTML_PATTERN = /#{Regexp.union(*ESCAPE_HTML.keys)}/n end @@ -234,10 +234,8 @@ module Rack encoding_candidates.push("identity") end - expanded_accept_encoding.find_all { |m, q| - q == 0.0 - }.each { |m, _| - encoding_candidates.delete(m) + expanded_accept_encoding.each { |m, q| + encoding_candidates.delete(m) if q == 0.0 } return (encoding_candidates & available_encodings)[0] @@ -276,7 +274,7 @@ module Rack expires = "; expires=" + rfc2822(value[:expires].clone.gmtime) if value[:expires] secure = "; secure" if value[:secure] - httponly = "; HttpOnly" if value[:httponly] + httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only]) value = value[:value] end value = [value] unless Array === value @@ -350,7 +348,7 @@ module Rack # of '% %b %Y'. # It assumes that the time is in GMT to comply to the RFC 2109. # - # NOTE: I'm not sure the RFC says it requires GMT, but is ambigous enough + # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough # that I'm certain someone implemented only that option. # Do not use %a and %b from Time.strptime, it would use localized names for # weekday and month. @@ -538,9 +536,9 @@ module Rack # Every standard HTTP code mapped to the appropriate message. # Generated with: - # 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}'"} + # ruby -ropen-uri -rnokogiri -e "Nokogiri::XML(open( + # 'http://www.iana.org/assignments/http-status-codes/http-status-codes.xml')).css('record').each{|r| + # name = r.css('description').text; puts %Q[#{r.css('value').text} => '#{name}',] unless name == 'Unassigned' }" HTTP_STATUS_CODES = { 100 => 'Continue', 101 => 'Switching Protocols', @@ -585,12 +583,9 @@ module Rack 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', @@ -601,7 +596,6 @@ module Rack 506 => 'Variant Also Negotiates (Experimental)', 507 => 'Insufficient Storage', 508 => 'Loop Detected', - 509 => 'Unassigned', 510 => 'Not Extended', 511 => 'Network Authentication Required' } diff --git a/rack.gemspec b/rack.gemspec index 388d6eda..6ab4dd57 100644 --- a/rack.gemspec +++ b/rack.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "rack" - s.version = "1.5.2" + s.version = "1.6.0.alpha" # Ahead of 1.5, modify before release s.platform = Gem::Platform::RUBY s.summary = "a modular Ruby webserver interface" s.license = "MIT" @@ -12,7 +12,7 @@ the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call. -Also see http://rack.github.com/. +Also see http://rack.github.io/. EOF s.files = Dir['{bin/*,contrib/*,example/*,lib/**/*,test/**/*}'] + @@ -25,7 +25,7 @@ EOF s.author = 'Christian Neukirchen' s.email = 'chneukirchen@gmail.com' - s.homepage = 'http://rack.github.com/' + s.homepage = 'http://rack.github.io/' s.rubyforge_project = 'rack' s.add_development_dependency 'bacon' diff --git a/test/multipart/invalid_character b/test/multipart/invalid_character new file mode 100644 index 00000000..82467181 --- /dev/null +++ b/test/multipart/invalid_character @@ -0,0 +1,6 @@ +--AaB03x
+Content-Disposition: form-data; name="files"; filename="invalidÃ.txt"
+Content-Type: text/plain
+
+contents
+--AaB03x--
diff --git a/test/spec_builder.rb b/test/spec_builder.rb index a2fd5685..0774f597 100644 --- a/test/spec_builder.rb +++ b/test/spec_builder.rb @@ -180,8 +180,7 @@ describe Rack::Builder do end it "removes __END__ before evaluating app" do - app, options = Rack::Builder.parse_file config_file('end.ru') - options = nil # ignored, prevents warning + app, _ = Rack::Builder.parse_file config_file('end.ru') Rack::MockRequest.new(app).get("/").body.to_s.should.equal 'OK' end @@ -199,8 +198,7 @@ describe Rack::Builder do end it "sets __LINE__ correctly" do - app, options = Rack::Builder.parse_file config_file('line.ru') - options = nil # ignored, prevents warning + app, _ = Rack::Builder.parse_file config_file('line.ru') Rack::MockRequest.new(app).get("/").body.to_s.should.equal '1' end end diff --git a/test/spec_commonlogger.rb b/test/spec_commonlogger.rb index d88e19c3..9c96638c 100644 --- a/test/spec_commonlogger.rb +++ b/test/spec_commonlogger.rb @@ -47,6 +47,32 @@ describe Rack::CommonLogger do res.errors.should =~ /"GET \/ " 200 - / end + def with_mock_time(t = 0) + mc = class <<Time; self; end + mc.send :alias_method, :old_now, :now + mc.send :define_method, :now do + at(t) + end + yield + ensure + mc.send :alias_method, :now, :old_now + end + + should "log in common log format" do + log = StringIO.new + with_mock_time do + Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/") + end + + md = /- - - \[([^\]]+)\] "(\w+) \/ " (\d{3}) \d+ ([\d\.]+)/.match(log.string) + md.should.not == nil + time, method, status, duration = *md.captures + time.should == Time.at(0).strftime("%d/%b/%Y %H:%M:%S %z") + method.should == "GET" + status.should == "200" + (0..1).should.include?(duration.to_f) + end + def length 123 end diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb index bd0b07b6..fc180c24 100644 --- a/test/spec_multipart.rb +++ b/test/spec_multipart.rb @@ -166,6 +166,20 @@ describe Rack::Multipart do params["files"][:tempfile].read.should.equal "contents" end + should "parse multipart upload with filename with invalid characters" do + env = Rack::MockRequest.env_for("/", multipart_fixture(:invalid_character)) + params = Rack::Multipart.parse_multipart(env) + params["files"][:type].should.equal "text/plain" + params["files"][:filename].should.match(/invalid/) + head = "Content-Disposition: form-data; " + + "name=\"files\"; filename=\"invalid\xC3.txt\"\r\n" + + "Content-Type: text/plain\r\n" + head = head.force_encoding("ASCII-8BIT") if head.respond_to?(:force_encoding) + params["files"][:head].should.equal head + params["files"][:name].should.equal "files" + params["files"][:tempfile].read.should.equal "contents" + end + should "not include file params if no file was selected" do env = Rack::MockRequest.env_for("/", multipart_fixture(:none)) params = Rack::Multipart.parse_multipart(env) diff --git a/test/spec_request.rb b/test/spec_request.rb index 9649c5d2..039aae6b 100644 --- a/test/spec_request.rb +++ b/test/spec_request.rb @@ -93,6 +93,11 @@ describe Rack::Request do req = Rack::Request.new \ Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "example.org", "SERVER_PORT" => "9393") req.port.should.equal 80 + + req = Rack::Request.new \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost", "HTTP_X_FORWARDED_PROTO" => "https", "SERVER_PORT" => "80") + + req.port.should.equal 443 end should "figure out the correct host with port" do @@ -774,6 +779,20 @@ EOF lambda { req.POST }.should.raise(EOFError) end + should "consistently raise EOFError on bad multipart form data" do + input = <<EOF +--AaB03x\r +content-disposition: form-data; name="huge"; filename="huge"\r +EOF + req = Rack::Request.new Rack::MockRequest.env_for("/", + "CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x", + "CONTENT_LENGTH" => input.size, + :input => input) + + lambda { req.POST }.should.raise(EOFError) + lambda { req.POST }.should.raise(EOFError) + end + should "correctly parse the part name from Content-Id header" do input = <<EOF --AaB03x\r diff --git a/test/spec_response.rb b/test/spec_response.rb index 7ba1e0e1..12b8b7b3 100644 --- a/test/spec_response.rb +++ b/test/spec_response.rb @@ -85,6 +85,18 @@ describe Rack::Response do response["Set-Cookie"].should.equal "foo=bar; HttpOnly" end + it "can set http only cookies with :http_only" do + response = Rack::Response.new + response.set_cookie "foo", {:value => "bar", :http_only => true} + response["Set-Cookie"].should.equal "foo=bar; HttpOnly" + end + + it "can set prefers :httponly for http only cookie setting when :httponly and :http_only provided" do + response = Rack::Response.new + response.set_cookie "foo", {:value => "bar", :httponly => false, :http_only => true} + response["Set-Cookie"].should.equal "foo=bar" + end + it "can delete cookies" do response = Rack::Response.new response.set_cookie "foo", "bar" @@ -216,6 +228,11 @@ describe Rack::Response do res.should.be.client_error res.should.be.bad_request + res.status = 401 + res.should.not.be.successful + res.should.be.client_error + res.should.be.unauthorized + res.status = 404 res.should.not.be.successful res.should.be.client_error diff --git a/test/spec_session_cookie.rb b/test/spec_session_cookie.rb index 8256f762..f5d69b16 100644 --- a/test/spec_session_cookie.rb +++ b/test/spec_session_cookie.rb @@ -119,13 +119,35 @@ describe Rack::Session::Cookie do coder.decode('lulz').should.equal nil end end + + describe 'ZipJSON' do + it 'jsons, deflates, and base64 encodes' do + coder = Rack::Session::Cookie::Base64::ZipJSON.new + obj = %w[fuuuuu] + json = Rack::Utils::OkJson.encode(obj) + coder.encode(obj).should.equal [Zlib::Deflate.deflate(json)].pack('m') + end + + it 'base64 decodes, inflates, and decodes json' do + coder = Rack::Session::Cookie::Base64::ZipJSON.new + obj = %w[fuuuuu] + json = Rack::Utils::OkJson.encode(obj) + b64 = [Zlib::Deflate.deflate(json)].pack('m') + coder.decode(b64).should.equal obj + end + + it 'rescues failures on decode' do + coder = Rack::Session::Cookie::Base64::ZipJSON.new + coder.decode('lulz').should.equal nil + end + end end it "warns if no secret is given" do - cookie = Rack::Session::Cookie.new(incrementor) + Rack::Session::Cookie.new(incrementor) @warnings.first.should =~ /no secret/i @warnings.clear - cookie = Rack::Session::Cookie.new(incrementor, :secret => 'abc') + Rack::Session::Cookie.new(incrementor, :secret => 'abc') @warnings.should.be.empty? end diff --git a/test/spec_session_memcache.rb b/test/spec_session_memcache.rb index 6e5325b5..2b759806 100644 --- a/test/spec_session_memcache.rb +++ b/test/spec_session_memcache.rb @@ -274,7 +274,7 @@ begin session['counter'].should.equal 2 # meeeh tnum = rand(7).to_i+5 - r = Array.new(tnum) do |i| + r = Array.new(tnum) do app = Rack::Utils::Context.new pool, time_delta req = Rack::MockRequest.new app Thread.new(req) do |run| diff --git a/test/spec_showstatus.rb b/test/spec_showstatus.rb index 6f8e6fe1..5d97e8e5 100644 --- a/test/spec_showstatus.rb +++ b/test/spec_showstatus.rb @@ -1,6 +1,7 @@ require 'rack/showstatus' require 'rack/lint' require 'rack/mock' +require 'rack/utils' describe Rack::ShowStatus do def show_status(app) @@ -40,6 +41,24 @@ describe Rack::ShowStatus do res.should =~ /too meta/ end + should "escape error" do + detail = "<script>alert('hi \"')</script>" + req = Rack::MockRequest.new( + show_status( + lambda{|env| + env["rack.showstatus.detail"] = detail + [500, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []] + })) + + res = req.get("/", :lint => true) + res.should.be.not.empty + + res["Content-Type"].should.equal("text/html") + res.should =~ /500/ + res.should.not.include detail + res.body.should.include Rack::Utils.escape_html(detail) + end + should "not replace existing messages" do req = Rack::MockRequest.new( show_status( diff --git a/test/spec_webrick.rb b/test/spec_webrick.rb index 5d647724..b29a82d5 100644 --- a/test/spec_webrick.rb +++ b/test/spec_webrick.rb @@ -139,5 +139,28 @@ describe Rack::Handler::WEBrick do } end + should "support Rack partial hijack" do + io_lambda = lambda{ |io| + 5.times do + io.write "David\r\n" + end + io.close + } + + @server.mount "/partial", Rack::Handler::WEBrick, + Rack::Lint.new(lambda{ |req| + [ + 200, + {"rack.hijack" => io_lambda}, + [""] + ] + }) + + Net::HTTP.start(@host, @port){ |http| + res = http.get("/partial") + res.body.should.equal "David\r\nDavid\r\nDavid\r\nDavid\r\nDavid\r\n" + } + end + @server.shutdown end |