Vendor Rack 1.1

Also clean up some View stuff.
This commit is contained in:
Jacques Distler 2009-12-26 14:00:18 -06:00
parent 77014652a3
commit a705709f9a
74 changed files with 3080 additions and 608 deletions

View file

@ -3,10 +3,6 @@
# Rack is freely distributable under the terms of an MIT-style license.
# See COPYING or http://www.opensource.org/licenses/mit-license.php.
path = File.expand_path(File.dirname(__FILE__))
$:.unshift(path) unless $:.include?(path)
# The Rack main module, serving as a namespace for all core Rack
# modules and classes.
#
@ -15,7 +11,7 @@ $:.unshift(path) unless $:.include?(path)
module Rack
# The Rack protocol version number implemented.
VERSION = [1,0]
VERSION = [1,1]
# Return the Rack protocol version as a dotted string.
def self.version
@ -24,7 +20,7 @@ module Rack
# Return the Rack release as a dotted string.
def self.release
"1.0"
"1.1"
end
autoload :Builder, "rack/builder"
@ -32,8 +28,10 @@ module Rack
autoload :Chunked, "rack/chunked"
autoload :CommonLogger, "rack/commonlogger"
autoload :ConditionalGet, "rack/conditionalget"
autoload :Config, "rack/config"
autoload :ContentLength, "rack/content_length"
autoload :ContentType, "rack/content_type"
autoload :ETag, "rack/etag"
autoload :File, "rack/file"
autoload :Deflater, "rack/deflater"
autoload :Directory, "rack/directory"
@ -42,10 +40,15 @@ module Rack
autoload :Head, "rack/head"
autoload :Lint, "rack/lint"
autoload :Lock, "rack/lock"
autoload :Logger, "rack/logger"
autoload :MethodOverride, "rack/methodoverride"
autoload :Mime, "rack/mime"
autoload :NullLogger, "rack/nulllogger"
autoload :Recursive, "rack/recursive"
autoload :Reloader, "rack/reloader"
autoload :Runtime, "rack/runtime"
autoload :Sendfile, "rack/sendfile"
autoload :Server, "rack/server"
autoload :ShowExceptions, "rack/showexceptions"
autoload :ShowStatus, "rack/showstatus"
autoload :Static, "rack/static"
@ -62,7 +65,6 @@ module Rack
autoload :Basic, "rack/auth/basic"
autoload :AbstractRequest, "rack/auth/abstract/request"
autoload :AbstractHandler, "rack/auth/abstract/handler"
autoload :OpenID, "rack/auth/openid"
module Digest
autoload :MD5, "rack/auth/digest/md5"
autoload :Nonce, "rack/auth/digest/nonce"

View file

@ -24,6 +24,23 @@ module Rack
# You can use +map+ to construct a Rack::URLMap in a convenient way.
class Builder
def self.parse_file(config, opts = Server::Options.new)
options = {}
if config =~ /\.ru$/
cfgfile = ::File.read(config)
if cfgfile[/^#\\(.*)/] && opts
options = opts.parse! $1.split(/\s+/)
end
cfgfile.sub!(/^__END__\n.*/, '')
app = eval "Rack::Builder.new {( " + cfgfile + "\n )}.to_app",
TOPLEVEL_BINDING, config
else
require config
app = Object.const_get(::File.basename(config, '.rb').capitalize)
end
return app, options
end
def initialize(&block)
@ins = []
instance_eval(&block) if block_given?

View file

@ -4,31 +4,36 @@ module Rack
# status codes).
class Cascade
NotFound = [404, {}, []]
attr_reader :apps
def initialize(apps, catch=404)
@apps = apps
@catch = [*catch]
@apps = []; @has_app = {}
apps.each { |app| add app }
@catch = {}
[*catch].each { |status| @catch[status] = true }
end
def call(env)
status = headers = body = nil
raise ArgumentError, "empty cascade" if @apps.empty?
@apps.each { |app|
begin
status, headers, body = app.call(env)
break unless @catch.include?(status.to_i)
end
}
[status, headers, body]
result = NotFound
@apps.each do |app|
result = app.call(env)
break unless @catch.include?(result[0].to_i)
end
result
end
def add app
@has_app[app] = true
@apps << app
end
def include? app
@apps.include? app
@has_app.include? app
end
alias_method :<<, :add

View file

@ -19,7 +19,7 @@ module Rack
STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
headers['Content-Length'] ||
headers['Transfer-Encoding']
[status, headers.to_hash, body]
[status, headers, body]
else
dup.chunk(status, headers, body)
end
@ -29,7 +29,7 @@ module Rack
@body = body
headers.delete('Content-Length')
headers['Transfer-Encoding'] = 'chunked'
[status, headers.to_hash, self]
[status, headers, self]
end
def each

View file

@ -2,60 +2,48 @@ module Rack
# Rack::CommonLogger forwards every request to an +app+ given, and
# logs a line in the Apache common log format to the +logger+, or
# rack.errors by default.
class CommonLogger
# Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
# lilith.local - - [07/Aug/2006 23:58:02] "GET / HTTP/1.1" 500 -
# %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n}
def initialize(app, logger=nil)
@app = app
@logger = logger
end
def call(env)
dup._call(env)
began_at = Time.now
status, header, body = @app.call(env)
header = Utils::HeaderHash.new(header)
log(env, status, header, began_at)
[status, header, body]
end
def _call(env)
@env = env
@logger ||= self
@time = Time.now
@status, @header, @body = @app.call(env)
[@status, @header, self]
private
def log(env, status, header, began_at)
now = Time.now
length = extract_content_length(header)
logger = @logger || env['rack.errors']
logger.write FORMAT % [
env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
env["REMOTE_USER"] || "-",
now.strftime("%d/%b/%Y %H:%M:%S"),
env["REQUEST_METHOD"],
env["PATH_INFO"],
env["QUERY_STRING"].empty? ? "" : "?"+env["QUERY_STRING"],
env["HTTP_VERSION"],
status.to_s[0..3],
length,
now - began_at ]
end
def close
@body.close if @body.respond_to? :close
end
# By default, log to rack.errors.
def <<(str)
@env["rack.errors"].write(str)
@env["rack.errors"].flush
end
def each
length = 0
@body.each { |part|
length += part.size
yield part
}
@now = Time.now
# Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
# lilith.local - - [07/Aug/2006 23:58:02] "GET / HTTP/1.1" 500 -
# %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
@logger << %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n} %
[
@env['HTTP_X_FORWARDED_FOR'] || @env["REMOTE_ADDR"] || "-",
@env["REMOTE_USER"] || "-",
@now.strftime("%d/%b/%Y %H:%M:%S"),
@env["REQUEST_METHOD"],
@env["PATH_INFO"],
@env["QUERY_STRING"].empty? ? "" : "?"+@env["QUERY_STRING"],
@env["HTTP_VERSION"],
@status.to_s[0..3],
(length.zero? ? "-" : length.to_s),
@now - @time
]
def extract_content_length(headers)
value = headers['Content-Length'] or return '-'
value.to_s == '0' ? '-' : value
end
end
end

15
vendor/plugins/rack/lib/rack/config.rb vendored Normal file
View file

@ -0,0 +1,15 @@
module Rack
# Rack::Config modifies the environment using the block given during
# initialization.
class Config
def initialize(app, &block)
@app = app
@block = block
end
def call(env)
@block.call(env)
@app.call(env)
end
end
end

View file

@ -17,7 +17,7 @@ module Rack
status, headers, body = @app.call(env)
headers = Utils::HeaderHash.new(headers)
headers['Content-Type'] ||= @content_type
[status, headers.to_hash, body]
[status, headers, body]
end
end
end

View file

@ -71,7 +71,9 @@ table { width:100%%; }
body = "Forbidden\n"
size = Rack::Utils.bytesize(body)
return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]]
return [403, {"Content-Type" => "text/plain",
"Content-Length" => size.to_s,
"X-Cascade" => "pass"}, [body]]
end
def list_directory
@ -123,7 +125,9 @@ table { width:100%%; }
def entity_not_found
body = "Entity not found: #{@path_info}\n"
size = Rack::Utils.bytesize(body)
return [404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]]
return [404, {"Content-Type" => "text/plain",
"Content-Length" => size.to_s,
"X-Cascade" => "pass"}, [body]]
end
def each

23
vendor/plugins/rack/lib/rack/etag.rb vendored Normal file
View file

@ -0,0 +1,23 @@
require 'digest/md5'
module Rack
# Automatically sets the ETag header on all String bodies
class ETag
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
if !headers.has_key?('ETag')
parts = []
body.each { |part| parts << part.to_s }
headers['ETag'] = %("#{Digest::MD5.hexdigest(parts.join(""))}")
[status, headers, parts]
else
[status, headers, body]
end
end
end
end

View file

@ -45,7 +45,8 @@ module Rack
def forbidden
body = "Forbidden\n"
[403, {"Content-Type" => "text/plain",
"Content-Length" => body.size.to_s},
"Content-Length" => body.size.to_s,
"X-Cascade" => "pass"},
[body]]
end
@ -73,7 +74,8 @@ module Rack
def not_found
body = "File not found: #{@path_info}\n"
[404, {"Content-Type" => "text/plain",
"Content-Length" => body.size.to_s},
"Content-Length" => body.size.to_s,
"X-Cascade" => "pass"},
[body]]
end

View file

@ -22,6 +22,25 @@ module Rack
end
end
def self.default(options = {})
# Guess.
if ENV.include?("PHP_FCGI_CHILDREN")
# We already speak FastCGI
options.delete :File
options.delete :Port
Rack::Handler::FastCGI
elsif ENV.include?("REQUEST_METHOD")
Rack::Handler::CGI
else
begin
Rack::Handler::Mongrel
rescue LoadError => e
Rack::Handler::WEBrick
end
end
end
# Transforms server-name constants to their canonical form as filenames,
# then tries to require them but silences the LoadError if not found
#

View file

@ -15,7 +15,7 @@ module Rack
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
env.update({"rack.version" => [1,0],
env.update({"rack.version" => [1,1],
"rack.input" => $stdin,
"rack.errors" => $stderr,

View file

@ -33,10 +33,10 @@ module Rack
env.delete "HTTP_CONTENT_LENGTH"
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
rack_input = RewindableInput.new(request.in)
env.update({"rack.version" => [1,0],
env.update({"rack.version" => [1,1],
"rack.input" => rack_input,
"rack.errors" => request.err,
@ -50,7 +50,6 @@ module Rack
env["QUERY_STRING"] ||= ""
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
env["REQUEST_PATH"] ||= "/"
env.delete "PATH_INFO" if env["PATH_INFO"] == ""
env.delete "CONTENT_TYPE" if env["CONTENT_TYPE"] == ""
env.delete "CONTENT_LENGTH" if env["CONTENT_LENGTH"] == ""

View file

@ -1,5 +1,6 @@
require 'lsapi'
require 'rack/content_length'
require 'rack/rewindable_input'
module Rack
module Handler
@ -19,7 +20,7 @@ module Rack
rack_input = RewindableInput.new($stdin.read.to_s)
env.update(
"rack.version" => [1,0],
"rack.version" => [1,1],
"rack.input" => rack_input,
"rack.errors" => $stderr,
"rack.multithread" => false,
@ -38,6 +39,8 @@ module Rack
ensure
body.close if body.respond_to? :close
end
ensure
rack_input.close
end
def self.send_headers(status, headers)
print "Status: #{status}\r\n"

View file

@ -7,10 +7,14 @@ module Rack
module Handler
class Mongrel < ::Mongrel::HttpHandler
def self.run(app, options={})
server = ::Mongrel::HttpServer.new(options[:Host] || '0.0.0.0',
options[:Port] || 8080)
server = ::Mongrel::HttpServer.new(
options[:Host] || '0.0.0.0',
options[:Port] || 8080,
options[:num_processors] || 950,
options[:throttle] || 0,
options[:timeout] || 60)
# Acts like Rack::URLMap, utilizing Mongrel's own path finding methods.
# Use is similar to #run, replacing the app argument with a hash of
# Use is similar to #run, replacing the app argument with a hash of
# { path=>app, ... } or an instance of Rack::URLMap.
if options[:map]
if app.is_a? Hash
@ -48,7 +52,7 @@ module Rack
rack_input = request.body || StringIO.new('')
rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
env.update({"rack.version" => [1,0],
env.update({"rack.version" => [1,1],
"rack.input" => rack_input,
"rack.errors" => $stderr,
@ -59,7 +63,6 @@ module Rack
"rack.url_scheme" => "http",
})
env["QUERY_STRING"] ||= ""
env.delete "PATH_INFO" if env["PATH_INFO"] == ""
status, headers, body = @app.call(env)

View file

@ -7,14 +7,14 @@ module Rack
module Handler
class SCGI < ::SCGI::Processor
attr_accessor :app
def self.run(app, options=nil)
new(options.merge(:app=>app,
:host=>options[:Host],
:port=>options[:Port],
:socket=>options[:Socket])).listen
end
def initialize(settings = {})
@app = Rack::Chunked.new(Rack::ContentLength.new(settings[:app]))
@log = Object.new
@ -22,7 +22,7 @@ module Rack
def @log.error(*args); end
super(settings)
end
def process_request(request, input_body, socket)
env = {}.replace(request)
env.delete "HTTP_CONTENT_TYPE"
@ -36,7 +36,7 @@ module Rack
rack_input = StringIO.new(input_body)
rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
env.update({"rack.version" => [1,0],
env.update({"rack.version" => [1,1],
"rack.input" => rack_input,
"rack.errors" => $stderr,
"rack.multithread" => true,

View file

@ -27,7 +27,7 @@ module Rack
rack_input = StringIO.new(req.body.to_s)
rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
env.update({"rack.version" => [1,0],
env.update({"rack.version" => [1,1],
"rack.input" => rack_input,
"rack.errors" => $stderr,
@ -41,9 +41,7 @@ module Rack
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
env["QUERY_STRING"] ||= ""
env["REQUEST_PATH"] ||= "/"
if env["PATH_INFO"] == ""
env.delete "PATH_INFO"
else
unless env["PATH_INFO"] == ""
path, n = req.request_uri.path, env["SCRIPT_NAME"].length
env["PATH_INFO"] = path[n, path.length-n]
end

View file

@ -61,7 +61,7 @@ module Rack
## subclassing allowed) that includes CGI-like headers.
## The application is free to modify the environment.
assert("env #{env.inspect} is not a Hash, but #{env.class}") {
env.instance_of? Hash
env.kind_of? Hash
}
##
@ -111,7 +111,7 @@ module Rack
## In addition to this, the Rack environment must include these
## Rack-specific variables:
## <tt>rack.version</tt>:: The Array [1,0], representing this version of Rack.
## <tt>rack.version</tt>:: The Array [1,1], representing this version of Rack.
## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the request URL.
## <tt>rack.input</tt>:: See below, the input stream.
## <tt>rack.errors</tt>:: See below, the error stream.
@ -148,6 +148,35 @@ module Rack
}
end
## <tt>rack.logger</tt>:: A common object interface for logging messages.
## The object must implement:
if logger = env['rack.logger']
## info(message, &block)
assert("logger #{logger.inspect} must respond to info") {
logger.respond_to?(:info)
}
## debug(message, &block)
assert("logger #{logger.inspect} must respond to debug") {
logger.respond_to?(:debug)
}
## warn(message, &block)
assert("logger #{logger.inspect} must respond to warn") {
logger.respond_to?(:warn)
}
## error(message, &block)
assert("logger #{logger.inspect} must respond to error") {
logger.respond_to?(:error)
}
## fatal(message, &block)
assert("logger #{logger.inspect} must respond to fatal") {
logger.respond_to?(:fatal)
}
end
## The server or the application can store their own data in the
## environment, too. The keys must contain at least one dot,
## and should be prefixed uniquely. The prefix <tt>rack.</tt>
@ -175,7 +204,7 @@ module Rack
env.each { |key, value|
next if key.include? "." # Skip extensions
assert("env variable #{key} has non-string value #{value.inspect}") {
value.instance_of? String
value.kind_of? String
}
}
@ -184,7 +213,7 @@ module Rack
## * <tt>rack.version</tt> must be an array of Integers.
assert("rack.version must be an Array, was #{env["rack.version"].class}") {
env["rack.version"].instance_of? Array
env["rack.version"].kind_of? Array
}
## * <tt>rack.url_scheme</tt> must either be +http+ or +https+.
assert("rack.url_scheme unknown: #{env["rack.url_scheme"].inspect}") {
@ -243,7 +272,7 @@ module Rack
assert("rack.input #{input} is not opened in binary mode") {
input.binmode?
} if input.respond_to?(:binmode?)
## The input stream must respond to +gets+, +each+, +read+ and +rewind+.
[:gets, :each, :read, :rewind].each { |method|
assert("rack.input #{input} does not respond to ##{method}") {
@ -269,7 +298,7 @@ module Rack
assert("rack.input#gets called with arguments") { args.size == 0 }
v = @input.gets
assert("rack.input#gets didn't return a String") {
v.nil? or v.instance_of? String
v.nil? or v.kind_of? String
}
v
end
@ -300,18 +329,18 @@ module Rack
args[1].kind_of?(String)
}
end
v = @input.read(*args)
assert("rack.input#read didn't return nil or a String") {
v.nil? or v.instance_of? String
v.nil? or v.kind_of? String
}
if args[0].nil?
assert("rack.input#read(nil) returned nil on EOF") {
!v.nil?
}
end
v
end
@ -320,12 +349,12 @@ module Rack
assert("rack.input#each called with arguments") { args.size == 0 }
@input.each { |line|
assert("rack.input#each didn't yield a String") {
line.instance_of? String
line.kind_of? String
}
yield line
}
end
## * +rewind+ must be called without arguments. It rewinds the input
## stream back to the beginning. It must not raise Errno::ESPIPE:
## that is, it may not be a pipe or a socket. Therefore, handler
@ -373,7 +402,7 @@ module Rack
## * +write+ must be called with a single argument that is a String.
def write(str)
assert("rack.errors#write not called with a String") { str.instance_of? String }
assert("rack.errors#write not called with a String") { str.kind_of? String }
@error.write str
end
@ -407,7 +436,7 @@ module Rack
header.each { |key, value|
## The header keys must be Strings.
assert("header key must be a string, was #{key.class}") {
key.instance_of? String
key.kind_of? String
}
## The header must not contain a +Status+ key,
assert("header must not contain Status") { key.downcase != "status" }
@ -499,7 +528,7 @@ module Rack
@body.each { |part|
## and must only yield String values.
assert("Body yielded non-string value #{part.inspect}") {
part.instance_of? String
part.kind_of? String
}
yield part
}

20
vendor/plugins/rack/lib/rack/logger.rb vendored Normal file
View file

@ -0,0 +1,20 @@
require 'logger'
module Rack
# Sets up rack.logger to write to rack.errors stream
class Logger
def initialize(app, level = ::Logger::INFO)
@app, @level = app, level
end
def call(env)
logger = ::Logger.new(env['rack.errors'])
logger.level = @level
env['rack.logger'] = logger
@app.call(env)
ensure
logger.close
end
end
end

View file

@ -14,7 +14,7 @@ module Rack
# Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream')
def mime_type(ext, fallback='application/octet-stream')
MIME_TYPES.fetch(ext, fallback)
MIME_TYPES.fetch(ext.to_s.downcase, fallback)
end
module_function :mime_type
@ -105,6 +105,7 @@ module Rack
".m3u" => "audio/x-mpegurl",
".m4v" => "video/mp4",
".man" => "text/troff",
".manifest"=> "text/cache-manifest",
".mathml" => "application/mathml+xml",
".mbox" => "application/mbox",
".mdoc" => "text/troff",
@ -126,6 +127,7 @@ module Rack
".ods" => "application/vnd.oasis.opendocument.spreadsheet",
".odt" => "application/vnd.oasis.opendocument.text",
".ogg" => "application/ogg",
".ogv" => "video/ogg",
".p" => "text/x-pascal",
".pas" => "text/x-pascal",
".pbm" => "image/x-portable-bitmap",

View file

@ -40,7 +40,7 @@ module Rack
end
DEFAULT_ENV = {
"rack.version" => [1,0],
"rack.version" => [1,1],
"rack.input" => StringIO.new,
"rack.errors" => StringIO.new,
"rack.multithread" => true,
@ -73,14 +73,17 @@ module Rack
# Return the Rack environment used for a request to +uri+.
def self.env_for(uri="", opts={})
uri = URI(uri)
uri.path = "/#{uri.path}" unless uri.path[0] == ?/
env = DEFAULT_ENV.dup
env["REQUEST_METHOD"] = opts[:method] || "GET"
env["REQUEST_METHOD"] = opts[:method] ? opts[:method].to_s.upcase : "GET"
env["SERVER_NAME"] = uri.host || "example.org"
env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
env["QUERY_STRING"] = uri.query.to_s
env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path
env["rack.url_scheme"] = uri.scheme || "http"
env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
env["SCRIPT_NAME"] = opts[:script_name] || ""
@ -90,7 +93,30 @@ module Rack
env["rack.errors"] = StringIO.new
end
opts[:input] ||= ""
if params = opts[:params]
if env["REQUEST_METHOD"] == "GET"
params = Utils.parse_nested_query(params) if params.is_a?(String)
params.update(Utils.parse_nested_query(env["QUERY_STRING"]))
env["QUERY_STRING"] = Utils.build_nested_query(params)
elsif !opts.has_key?(:input)
opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
if params.is_a?(Hash)
if data = Utils::Multipart.build_multipart(params)
opts[:input] = data
opts["CONTENT_LENGTH"] ||= data.length.to_s
opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Utils::Multipart::MULTIPART_BOUNDARY}"
else
opts[:input] = Utils.build_nested_query(params)
end
else
opts[:input] = params
end
end
end
empty_str = ""
empty_str.force_encoding("ASCII-8BIT") if empty_str.respond_to? :force_encoding
opts[:input] ||= empty_str
if String === opts[:input]
rack_input = StringIO.new(opts[:input])
else
@ -128,7 +154,7 @@ module Rack
@body = ""
body.each { |part| @body << part }
@errors = errors.string
@errors = errors.string if errors.respond_to?(:string)
end
# Status

View file

@ -0,0 +1,18 @@
module Rack
class NullLogger
def initialize(app)
@app = app
end
def call(env)
env['rack.logger'] = self
@app.call(env)
end
def info(progname = nil, &block); end
def debug(progname = nil, &block); end
def warn(progname = nil, &block); end
def error(progname = nil, &block); end
def fatal(progname = nil, &block); end
end
end

View file

@ -1,5 +1,6 @@
# Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com
# All files in this distribution are subject to the terms of the Ruby license.
# Rack::Reloader is subject to the terms of an MIT-style license.
# See COPYING or http://www.opensource.org/licenses/mit-license.php.
require 'pathname'
@ -92,6 +93,8 @@ module Rack
found, stat = safe_stat(path)
return ::File.expand_path(found), stat if found
end
return false, false
end
def safe_stat(file)

View file

@ -32,6 +32,7 @@ module Rack
def content_type; @env['CONTENT_TYPE'] end
def session; @env['rack.session'] ||= {} end
def session_options; @env['rack.session.options'] ||= {} end
def logger; @env['rack.logger'] end
# The media type (type/subtype) portion of the CONTENT_TYPE header
# without any media type parameters. e.g., when CONTENT_TYPE is
@ -63,9 +64,17 @@ module Rack
media_type_params['charset']
end
def host_with_port
if forwarded = @env["HTTP_X_FORWARDED_HOST"]
forwarded.split(/,\s?/).last
else
@env['HTTP_HOST'] || "#{@env['SERVER_NAME'] || @env['SERVER_ADDR']}:#{@env['SERVER_PORT']}"
end
end
def host
# Remove port number.
(@env["HTTP_HOST"] || @env["SERVER_NAME"]).to_s.gsub(/:\d+\z/, '')
host_with_port.to_s.gsub(/:\d+\z/, '')
end
def script_name=(s); @env["SCRIPT_NAME"] = s.to_s end
@ -81,7 +90,6 @@ module Rack
# one of the media types presents in this list will not be eligible
# for form-data / param parsing.
FORM_DATA_MEDIA_TYPES = [
nil,
'application/x-www-form-urlencoded',
'multipart/form-data'
]
@ -92,15 +100,20 @@ module Rack
PARSEABLE_DATA_MEDIA_TYPES = [
'multipart/related',
'multipart/mixed'
]
]
# Determine whether the request body contains form-data by checking
# the request media_type against registered form-data media-types:
# "application/x-www-form-urlencoded" and "multipart/form-data". The
# the request Content-Type for one of the media-types:
# "application/x-www-form-urlencoded" or "multipart/form-data". The
# list of form-data media types can be modified through the
# +FORM_DATA_MEDIA_TYPES+ array.
#
# A request body is also assumed to contain form-data when no
# Content-Type header is provided and the request_method is POST.
def form_data?
FORM_DATA_MEDIA_TYPES.include?(media_type)
type = media_type
meth = env["rack.methodoverride.original_method"] || env['REQUEST_METHOD']
(meth == 'POST' && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
end
# Determine whether the request body contains data by checking
@ -115,8 +128,7 @@ module Rack
@env["rack.request.query_hash"]
else
@env["rack.request.query_string"] = query_string
@env["rack.request.query_hash"] =
Utils.parse_nested_query(query_string)
@env["rack.request.query_hash"] = parse_query(query_string)
end
end
@ -125,19 +137,20 @@ module Rack
# This method support both application/x-www-form-urlencoded and
# multipart/form-data.
def POST
if @env["rack.request.form_input"].eql? @env["rack.input"]
if @env["rack.input"].nil?
raise "Missing rack.input"
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"] =
Utils::Multipart.parse_multipart(env)
unless @env["rack.request.form_hash"] = parse_multipart(env)
form_vars = @env["rack.input"].read
# Fix for Safari Ajax postings that always append \0
form_vars.sub!(/\0\z/, '')
@env["rack.request.form_vars"] = form_vars
@env["rack.request.form_hash"] = Utils.parse_nested_query(form_vars)
@env["rack.request.form_hash"] = parse_query(form_vars)
@env["rack.input"].rewind
end
@ -149,7 +162,7 @@ module Rack
# The union of GET and POST data.
def params
self.put? ? self.GET : self.GET.update(self.POST)
self.GET.update(self.POST)
rescue EOFError => e
self.GET
end
@ -175,6 +188,9 @@ module Rack
end
alias referrer referer
def user_agent
@env['HTTP_USER_AGENT']
end
def cookies
return {} unless @env["HTTP_COOKIE"]
@ -214,11 +230,11 @@ module Rack
url
end
def path
script_name + path_info
end
def fullpath
query_string.empty? ? path : "#{path}?#{query_string}"
end
@ -242,5 +258,14 @@ module Rack
@env['REMOTE_ADDR']
end
end
protected
def parse_query(qs)
Utils.parse_nested_query(qs)
end
def parse_multipart(env)
Utils::Multipart.parse_multipart(env)
end
end
end

View file

@ -19,7 +19,7 @@ module Rack
attr_accessor :length
def initialize(body=[], status=200, header={}, &block)
@status = status
@status = status.to_i
@header = Utils::HeaderHash.new({"Content-Type" => "text/html"}.
merge(header))
@ -54,45 +54,11 @@ module Rack
end
def set_cookie(key, value)
case value
when Hash
domain = "; domain=" + value[:domain] if value[:domain]
path = "; path=" + value[:path] if value[:path]
# According to RFC 2109, we need dashes here.
# N.B.: cgi.rb uses spaces...
expires = "; expires=" + value[:expires].clone.gmtime.
strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
secure = "; secure" if value[:secure]
httponly = "; HttpOnly" if value[:httponly]
value = value[:value]
end
value = [value] unless Array === value
cookie = Utils.escape(key) + "=" +
value.map { |v| Utils.escape v }.join("&") +
"#{domain}#{path}#{expires}#{secure}#{httponly}"
case self["Set-Cookie"]
when Array
self["Set-Cookie"] << cookie
when String
self["Set-Cookie"] = [self["Set-Cookie"], cookie]
when nil
self["Set-Cookie"] = cookie
end
Utils.set_cookie_header!(header, key, value)
end
def delete_cookie(key, value={})
unless Array === self["Set-Cookie"]
self["Set-Cookie"] = [self["Set-Cookie"]].compact
end
self["Set-Cookie"].reject! { |cookie|
cookie =~ /\A#{Utils.escape(key)}=/
}
set_cookie(key,
{:value => '', :path => nil, :domain => nil,
:expires => Time.at(0) }.merge(value))
Utils.delete_cookie_header!(header, key, value)
end
def redirect(target, status=302)
@ -105,9 +71,9 @@ module Rack
if [204, 304].include?(status.to_i)
header.delete "Content-Type"
[status.to_i, header.to_hash, []]
[status.to_i, header, []]
else
[status.to_i, header.to_hash, self]
[status.to_i, header, self]
end
end
alias to_a finish # For *response

View file

@ -16,27 +16,27 @@ module Rack
@rewindable_io = nil
@unlinked = false
end
def gets
make_rewindable unless @rewindable_io
@rewindable_io.gets
end
def read(*args)
make_rewindable unless @rewindable_io
@rewindable_io.read(*args)
end
def each(&block)
make_rewindable unless @rewindable_io
@rewindable_io.each(&block)
end
def rewind
make_rewindable unless @rewindable_io
@rewindable_io.rewind
end
# Closes this RewindableInput object without closing the originally
# wrapped IO oject. Cleans up any temporary resources that this RewindableInput
# has created.
@ -52,9 +52,9 @@ module Rack
@rewindable_io = nil
end
end
private
# Ruby's Tempfile class has a bug. Subclass it and fix it.
class Tempfile < ::Tempfile
def _close
@ -72,11 +72,13 @@ module Rack
# access it because we have the file handle open.
@rewindable_io = Tempfile.new('RackRewindableInput')
@rewindable_io.chmod(0000)
@rewindable_io.set_encoding(Encoding::BINARY) if @rewindable_io.respond_to?(:set_encoding)
@rewindable_io.binmode
if filesystem_has_posix_semantics? && !tempfile_unlink_contains_bug?
@rewindable_io.unlink
@unlinked = true
end
buffer = ""
while @io.read(1024 * 4, buffer)
entire_buffer_written_out = false
@ -90,7 +92,7 @@ module Rack
end
@rewindable_io.rewind
end
def filesystem_has_posix_semantics?
RUBY_PLATFORM !~ /(mswin|mingw|cygwin|java)/
end

27
vendor/plugins/rack/lib/rack/runtime.rb vendored Normal file
View file

@ -0,0 +1,27 @@
module Rack
# Sets an "X-Runtime" response header, indicating the response
# time of the request, in seconds
#
# You can put it right before the application to see the processing
# time, or before all the other middlewares to include time for them,
# too.
class Runtime
def initialize(app, name = nil)
@app = app
@header_name = "X-Runtime"
@header_name << "-#{name}" if name
end
def call(env)
start_time = Time.now
status, headers, body = @app.call(env)
request_time = Time.now - start_time
if !headers.has_key?(@header_name)
headers[@header_name] = "%0.6f" % request_time
end
[status, headers, body]
end
end
end

142
vendor/plugins/rack/lib/rack/sendfile.rb vendored Normal file
View file

@ -0,0 +1,142 @@
require 'rack/file'
module Rack
class File #:nodoc:
alias :to_path :path
end
# = Sendfile
#
# The Sendfile middleware intercepts responses whose body is being
# served from a file and replaces it with a server specific X-Sendfile
# header. The web server is then responsible for writing the file contents
# to the client. This can dramatically reduce the amount of work required
# by the Ruby backend and takes advantage of the web servers optimized file
# delivery code.
#
# In order to take advantage of this middleware, the response body must
# respond to +to_path+ and the request must include an X-Sendfile-Type
# header. Rack::File and other components implement +to_path+ so there's
# rarely anything you need to do in your application. The X-Sendfile-Type
# header is typically set in your web servers configuration. The following
# sections attempt to document
#
# === Nginx
#
# 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.
#
# The following example shows the Nginx configuration required to create
# a private "/files/" area, enable X-Accel-Redirect, and pass the special
# X-Sendfile-Type and X-Accel-Mapping headers to the backend:
#
# location /files/ {
# internal;
# alias /var/www/;
# }
#
# location / {
# proxy_redirect false;
#
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#
# proxy_set_header X-Sendfile-Type X-Accel-Redirect
# proxy_set_header X-Accel-Mapping /files/=/var/www/;
#
# proxy_pass http://127.0.0.1:8080/;
# }
#
# Note that the X-Sendfile-Type header must be set exactly as shown above. The
# X-Accel-Mapping header should specify the name of the private URL pattern,
# followed by an equals sign (=), followed by the location on the file system
# that it maps to. The middleware performs a simple substitution on the
# resulting path.
#
# See Also: http://wiki.codemongers.com/NginxXSendfile
#
# === lighttpd
#
# Lighttpd has supported some variation of the X-Sendfile header for some
# time, although only recent version support X-Sendfile in a reverse proxy
# configuration.
#
# $HTTP["host"] == "example.com" {
# proxy-core.protocol = "http"
# proxy-core.balancer = "round-robin"
# proxy-core.backends = (
# "127.0.0.1:8000",
# "127.0.0.1:8001",
# ...
# )
#
# proxy-core.allow-x-sendfile = "enable"
# proxy-core.rewrite-request = (
# "X-Sendfile-Type" => (".*" => "X-Sendfile")
# )
# }
#
# See Also: http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModProxyCore
#
# === Apache
#
# X-Sendfile is supported under Apache 2.x using a separate module:
#
# http://tn123.ath.cx/mod_xsendfile/
#
# Once the module is compiled and installed, you can enable it using
# XSendFile config directive:
#
# RequestHeader Set X-Sendfile-Type X-Sendfile
# ProxyPassReverse / http://localhost:8001/
# XSendFile on
class Sendfile
F = ::File
def initialize(app, variation=nil)
@app = app
@variation = variation
end
def call(env)
status, headers, body = @app.call(env)
if body.respond_to?(:to_path)
case type = variation(env)
when 'X-Accel-Redirect'
path = F.expand_path(body.to_path)
if url = map_accel_path(env, path)
headers[type] = url
body = []
else
env['rack.errors'] << "X-Accel-Mapping header missing"
end
when 'X-Sendfile', 'X-Lighttpd-Send-File'
path = F.expand_path(body.to_path)
headers[type] = path
body = []
when '', nil
else
env['rack.errors'] << "Unknown x-sendfile variation: '#{variation}'.\n"
end
end
[status, headers, body]
end
private
def variation(env)
@variation ||
env['sendfile.type'] ||
env['HTTP_X_SENDFILE_TYPE']
end
def map_accel_path(env, file)
if mapping = env['HTTP_X_ACCEL_MAPPING']
internal, external = mapping.split('=', 2).map{ |p| p.strip }
file.sub(/^#{internal}/i, external)
end
end
end
end

212
vendor/plugins/rack/lib/rack/server.rb vendored Normal file
View file

@ -0,0 +1,212 @@
require 'optparse'
module Rack
class Server
class Options
def parse!(args)
options = {}
opt_parser = OptionParser.new("", 24, ' ') do |opts|
opts.banner = "Usage: rackup [ruby options] [rack options] [rackup config]"
opts.separator ""
opts.separator "Ruby options:"
lineno = 1
opts.on("-e", "--eval LINE", "evaluate a LINE of code") { |line|
eval line, TOPLEVEL_BINDING, "-e", lineno
lineno += 1
}
opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") {
options[:debug] = true
}
opts.on("-w", "--warn", "turn warnings on for your script") {
options[:warn] = true
}
opts.on("-I", "--include PATH",
"specify $LOAD_PATH (may be used more than once)") { |path|
options[:include] = path.split(":")
}
opts.on("-r", "--require LIBRARY",
"require the library, before executing your script") { |library|
options[:require] = library
}
opts.separator ""
opts.separator "Rack options:"
opts.on("-s", "--server SERVER", "serve using SERVER (webrick/mongrel)") { |s|
options[:server] = s
}
opts.on("-o", "--host HOST", "listen on HOST (default: 0.0.0.0)") { |host|
options[:Host] = host
}
opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port|
options[:Port] = port
}
opts.on("-E", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: development)") { |e|
options[:environment] = e
}
opts.on("-D", "--daemonize", "run daemonized in the background") { |d|
options[:daemonize] = d ? true : false
}
opts.on("-P", "--pid FILE", "file to store PID (default: rack.pid)") { |f|
options[:pid] = ::File.expand_path(f)
}
opts.separator ""
opts.separator "Common options:"
opts.on_tail("-h", "--help", "Show this message") do
puts opts
exit
end
opts.on_tail("--version", "Show version") do
puts "Rack #{Rack.version}"
exit
end
end
opt_parser.parse! args
options[:rack_file] = args.last if args.last
options
end
end
def self.start
new.start
end
attr_accessor :options
def initialize(options = nil)
@options = options
end
def options
@options ||= parse_options(ARGV)
end
def default_options
{
:environment => "development",
:pid => nil,
:Port => 9292,
:Host => "0.0.0.0",
:AccessLog => [],
:rack_file => ::File.expand_path("config.ru")
}
end
def app
@app ||= begin
if !::File.exist? options[:rack_file]
abort "configuration #{options[:rack_file]} not found"
end
app, options = Rack::Builder.parse_file(self.options[:rack_file], opt_parser)
self.options.merge! options
app
end
end
def self.middleware
@middleware ||= begin
m = Hash.new {|h,k| h[k] = []}
m["deployment"].concat [lambda {|server| server.server =~ /CGI/ ? nil : [Rack::CommonLogger, $stderr] }]
m["development"].concat m["deployment"] + [[Rack::ShowExceptions], [Rack::Lint]]
m
end
end
def middleware
self.class.middleware
end
def start
if options[:debug]
$DEBUG = true
require 'pp'
p options[:server]
pp wrapped_app
pp app
end
if options[:warn]
$-w = true
end
if includes = options[:include]
$LOAD_PATH.unshift *includes
end
if library = options[:require]
require library
end
daemonize_app if options[:daemonize]
write_pid if options[:pid]
server.run wrapped_app, options
end
def server
@_server ||= Rack::Handler.get(options[:server]) || Rack::Handler.default
end
private
def parse_options(args)
options = default_options
# Don't evaluate CGI ISINDEX parameters.
# http://hoohoo.ncsa.uiuc.edu/cgi/cl.html
args.clear if ENV.include?("REQUEST_METHOD")
options.merge! opt_parser.parse! args
options
end
def opt_parser
Options.new
end
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)
end
app
end
def wrapped_app
@wrapped_app ||= build_app app
end
def daemonize_app
if RUBY_VERSION < "1.9"
exit if fork
Process.setsid
exit if fork
Dir.chdir "/"
::File.umask 0000
STDIN.reopen "/dev/null"
STDOUT.reopen "/dev/null", "a"
STDERR.reopen "/dev/null", "a"
else
Process.daemon
end
end
def write_pid
::File.open(options[:pid], 'w'){ |f| f.write("#{Process.pid}") }
at_exit { ::File.delete(options[:pid]) if ::File.exist?(options[:pid]) }
end
end
end

View file

@ -107,18 +107,16 @@ module Rack
if not session_id = set_session(env, session_id, session, options)
env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
[status, headers, body]
elsif options[:defer] and not options[:renew]
env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE
[status, headers, body]
else
cookie = Hash.new
cookie[:value] = session_id
cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
response = Rack::Response.new(body, status, headers)
response.set_cookie(@key, cookie.merge(options))
response.to_a
Utils.set_cookie_header!(headers, @key, cookie.merge(options))
end
[status, headers, body]
end
# All thread safety and session retrival proceedures should occur here.

View file

@ -70,16 +70,15 @@ module Rack
if session_data.size > (4096 - @key.size)
env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K. Content dropped.")
[status, headers, body]
else
options = env["rack.session.options"]
cookie = Hash.new
cookie[:value] = session_data
cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
response = Rack::Response.new(body, status, headers)
response.set_cookie(@key, cookie.merge(options))
response.to_a
Utils.set_cookie_header!(headers, @key, cookie.merge(options))
end
[status, headers, body]
end
def generate_hmac(data)

View file

@ -29,9 +29,13 @@ module Rack
super
@mutex = Mutex.new
@pool = MemCache.
new @default_options[:memcache_server], @default_options
raise 'No memcache servers' unless @pool.servers.any?{|s|s.alive?}
mserv = @default_options[:memcache_server]
mopts = @default_options.
reject{|k,v| MemCache::DEFAULT_OPTIONS.include? k }
@pool = MemCache.new mserv, mopts
unless @pool.active? and @pool.servers.any?{|c| c.alive? }
raise 'No memcache servers'
end
end
def generate_sid
@ -41,24 +45,23 @@ module Rack
end
end
def get_session(env, sid)
session = @pool.get(sid) if sid
def get_session(env, session_id)
@mutex.lock if env['rack.multithread']
unless sid and session
env['rack.errors'].puts("Session '#{sid.inspect}' not found, initializing...") if $VERBOSE and not sid.nil?
session = {}
sid = generate_sid
ret = @pool.add sid, session
raise "Session collision on '#{sid.inspect}'" unless /^STORED/ =~ ret
unless session_id and session = @pool.get(session_id)
session_id, session = generate_sid, {}
unless /^STORED/ =~ @pool.add(session_id, session)
raise "Session collision on '#{session_id.inspect}'"
end
end
session.instance_variable_set('@old', {}.merge(session))
return [sid, session]
rescue MemCache::MemCacheError, Errno::ECONNREFUSED # MemCache server cannot be contacted
warn "#{self} is unable to find server."
session.instance_variable_set '@old', @pool.get(session_id, true)
return [session_id, session]
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
# MemCache server cannot be contacted
warn "#{self} is unable to find memcached server."
warn $!.inspect
return [ nil, {} ]
ensure
@mutex.unlock if env['rack.multithread']
@mutex.unlock if @mutex.locked?
end
def set_session(env, session_id, new_session, options)
@ -66,43 +69,50 @@ module Rack
expiry = expiry.nil? ? 0 : expiry + 1
@mutex.lock if env['rack.multithread']
session = @pool.get(session_id) || {}
if options[:renew] or options[:drop]
@pool.delete session_id
return false if options[:drop]
session_id = generate_sid
@pool.add session_id, 0 # so we don't worry about cache miss on #set
@pool.add session_id, {} # so we don't worry about cache miss on #set
end
old_session = new_session.instance_variable_get('@old') || {}
session = merge_sessions session_id, old_session, new_session, session
session = @pool.get(session_id) || {}
old_session = new_session.instance_variable_get '@old'
old_session = old_session ? Marshal.load(old_session) : {}
unless Hash === old_session and Hash === new_session
env['rack.errors'].
puts 'Bad old_session or new_session sessions provided.'
else # merge sessions
# alterations are either update or delete, making as few changes as
# possible to prevent possible issues.
# removed keys
delete = old_session.keys - new_session.keys
if $VERBOSE and not delete.empty?
env['rack.errors'].
puts "//@#{session_id}: delete #{delete*','}"
end
delete.each{|k| session.delete k }
# added or altered keys
update = new_session.keys.
select{|k| new_session[k] != old_session[k] }
if $VERBOSE and not update.empty?
env['rack.errors'].puts "//@#{session_id}: update #{update*','}"
end
update.each{|k| session[k] = new_session[k] }
end
@pool.set session_id, session, expiry
return session_id
rescue MemCache::MemCacheError, Errno::ECONNREFUSED # MemCache server cannot be contacted
warn "#{self} is unable to find server."
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
# MemCache server cannot be contacted
warn "#{self} is unable to find memcached server."
warn $!.inspect
return false
ensure
@mutex.unlock if env['rack.multithread']
end
private
def merge_sessions sid, old, new, cur=nil
cur ||= {}
unless Hash === old and Hash === new
warn 'Bad old or new sessions provided.'
return cur
end
delete = old.keys - new.keys
warn "//@#{sid}: delete #{delete*','}" if $VERBOSE and not delete.empty?
delete.each{|k| cur.delete k }
update = new.keys.select{|k| new[k] != old[k] }
warn "//@#{sid}: update #{update*','}" if $VERBOSE and not update.empty?
update.each{|k| cur[k] = new[k] }
cur
@mutex.unlock if @mutex.locked?
end
end
end

View file

@ -13,7 +13,7 @@ module Rack
# In the context of a multithreaded environment, sessions being
# committed to the pool is done in a merging manner.
#
# The :drop option is available in rack.session.options if you with to
# The :drop option is available in rack.session.options if you wish to
# explicitly remove the session from the session cache.
#
# Example:

View file

@ -28,27 +28,28 @@ module Rack
raise ArgumentError, "paths need to start with /"
end
location = location.chomp('/')
match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
[host, location, app]
}.sort_by { |(h, l, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
[host, location, match, app]
}.sort_by { |(h, l, m, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
end
def call(env)
path = env["PATH_INFO"].to_s.squeeze("/")
path = env["PATH_INFO"].to_s
script_name = env['SCRIPT_NAME']
hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT')
@mapping.each { |host, location, app|
@mapping.each { |host, location, match, app|
next unless (hHost == host || sName == host \
|| (host.nil? && (hHost == sName || hHost == sName+':'+sPort)))
next unless location == path[0, location.size]
next unless path[location.size] == nil || path[location.size] == ?/
next unless path =~ match && rest = $1
next unless rest.empty? || rest[0] == ?/
return app.call(
env.merge(
'SCRIPT_NAME' => (script_name + location),
'PATH_INFO' => path[location.size..-1]))
'PATH_INFO' => rest))
}
[404, {"Content-Type" => "text/plain"}, ["Not Found: #{path}"]]
[404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]]
end
end
end

View file

@ -27,7 +27,7 @@ module Rack
module_function :unescape
DEFAULT_SEP = /[&;] */n
# Stolen from Mongrel, with some small modifications:
# Parses a query string by breaking it up at the '&'
# and ';' characters. You can also use this to parse
@ -38,7 +38,9 @@ module Rack
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
k, v = p.split('=', 2).map { |x| unescape(x) }
if v =~ /^("|')(.*)\1$/
v = $2.gsub('\\'+$1, $1)
end
if cur = params[k]
if cur.class == Array
params[k] << v
@ -67,6 +69,9 @@ module Rack
module_function :parse_nested_query
def normalize_params(params, name, v = nil)
if v and v =~ /^("|')(.*)\1$/
v = $2.gsub('\\'+$1, $1)
end
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
k = $1 || ''
after = $' || ''
@ -109,6 +114,25 @@ module Rack
end
module_function :build_query
def build_nested_query(value, prefix = nil)
case value
when Array
value.map { |v|
build_nested_query(v, "#{prefix}[]")
}.join("&")
when Hash
value.map { |k, v|
build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
}.join("&")
when String
raise ArgumentError, "value must be a Hash" if prefix.nil?
"#{prefix}=#{escape(value)}"
else
prefix
end
end
module_function :build_nested_query
# Escape ampersands, brackets and quotes to their HTML/XML entities.
def escape_html(string)
string.to_s.gsub("&", "&amp;").
@ -149,6 +173,54 @@ module Rack
end
module_function :select_best_encoding
def set_cookie_header!(header, key, value)
case value
when Hash
domain = "; domain=" + value[:domain] if value[:domain]
path = "; path=" + value[:path] if value[:path]
# According to RFC 2109, we need dashes here.
# N.B.: cgi.rb uses spaces...
expires = "; expires=" + value[:expires].clone.gmtime.
strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
secure = "; secure" if value[:secure]
httponly = "; HttpOnly" if value[:httponly]
value = value[:value]
end
value = [value] unless Array === value
cookie = escape(key) + "=" +
value.map { |v| escape v }.join("&") +
"#{domain}#{path}#{expires}#{secure}#{httponly}"
case header["Set-Cookie"]
when Array
header["Set-Cookie"] << cookie
when String
header["Set-Cookie"] = [header["Set-Cookie"], cookie]
when nil
header["Set-Cookie"] = cookie
end
nil
end
module_function :set_cookie_header!
def delete_cookie_header!(header, key, value = {})
unless Array === header["Set-Cookie"]
header["Set-Cookie"] = [header["Set-Cookie"]].compact
end
header["Set-Cookie"].reject! { |cookie|
cookie =~ /\A#{escape(key)}=/
}
set_cookie_header!(header, key,
{:value => '', :path => nil, :domain => nil,
:expires => Time.at(0) }.merge(value))
nil
end
module_function :delete_cookie_header!
# Return the bytesize of String; uses String#length under Ruby 1.8 and
# String#bytesize under 1.9.
if ''.respond_to?(:bytesize)
@ -191,11 +263,22 @@ module Rack
# A case-insensitive Hash that preserves the original case of a
# header when set.
class HeaderHash < Hash
def self.new(hash={})
HeaderHash === hash ? hash : super(hash)
end
def initialize(hash={})
super()
@names = {}
hash.each { |k, v| self[k] = v }
end
def each
super do |k, v|
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
end
end
def to_hash
inject({}) do |hash, (k,v)|
if v.respond_to? :to_ary
@ -208,21 +291,24 @@ module Rack
end
def [](k)
super @names[k.downcase]
super(@names[k] ||= @names[k.downcase])
end
def []=(k, v)
delete k
@names[k.downcase] = k
@names[k] = @names[k.downcase] = k
super k, v
end
def delete(k)
super @names.delete(k.downcase)
canonical = k.downcase
result = super @names.delete(canonical)
@names.delete_if { |name,| name.downcase == canonical }
result
end
def include?(k)
@names.has_key? k.downcase
@names.include?(k) || @names.include?(k.downcase)
end
alias_method :has_key?, :include?
@ -238,13 +324,23 @@ module Rack
hash = dup
hash.merge! other
end
def replace(other)
clear
other.each { |k, v| self[k] = v }
self
end
end
# Every standard HTTP code mapped to the appropriate message.
# Stolen from Mongrel.
# Generated with:
# curl -s http://www.iana.org/assignments/http-status-codes | \
# ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
# puts " #{m[1]} => \x27#{m[2].strip}x27,"'
HTTP_STATUS_CODES = {
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
@ -252,12 +348,15 @@ module Rack
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
226 => 'IM Used',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
306 => 'Reserved',
307 => 'Temporary Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
@ -273,27 +372,76 @@ module Rack
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
426 => 'Upgrade Required',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported'
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
510 => 'Not Extended',
}
# Responses with HTTP status codes that should not have an entity body
STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
SYMBOL_TO_STATUS_CODE = HTTP_STATUS_CODES.inject({}) { |hash, (code, message)|
hash[message.downcase.gsub(/\s|-/, '_').to_sym] = code
hash
}
def status_code(status)
if status.is_a?(Symbol)
SYMBOL_TO_STATUS_CODE[status] || 500
else
status.to_i
end
end
module_function :status_code
# A multipart form data parser, adapted from IOWA.
#
# Usually, Rack::Request#POST takes care of calling this.
module Multipart
class UploadedFile
# The filename, *not* including the path, of the "uploaded" file
attr_reader :original_filename
# The content type of the "uploaded" file
attr_accessor :content_type
def initialize(path, content_type = "text/plain", binary = false)
raise "#{path} file does not exist" unless ::File.exist?(path)
@content_type = content_type
@original_filename = ::File.basename(path)
@tempfile = Tempfile.new(@original_filename)
@tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
@tempfile.binmode if binary
FileUtils.copy_file(path, @tempfile.path)
end
def path
@tempfile.path
end
alias_method :local_path, :path
def method_missing(method_name, *args, &block) #:nodoc:
@tempfile.__send__(method_name, *args, &block)
end
end
EOL = "\r\n"
MULTIPART_BOUNDARY = "AaB03x"
def self.parse_multipart(env)
unless env['CONTENT_TYPE'] =~
@ -378,7 +526,7 @@ module Rack
:name => name, :tempfile => body, :head => head}
elsif !filename && content_type
body.rewind
# Generic multipart cases, not coming from a form
data = {:type => content_type,
:name => name, :tempfile => body, :head => head}
@ -388,7 +536,8 @@ module Rack
Utils.normalize_params(params, name, data) unless data.nil?
break if buf.empty? || content_length == -1
# break if we're at the end of a buffer, but not if it is the end of a field
break if (buf.empty? && $1 != EOL) || content_length == -1
}
input.rewind
@ -396,6 +545,76 @@ module Rack
params
end
end
def self.build_multipart(params, first = true)
if first
unless params.is_a?(Hash)
raise ArgumentError, "value must be a Hash"
end
multipart = false
query = lambda { |value|
case value
when Array
value.each(&query)
when Hash
value.values.each(&query)
when UploadedFile
multipart = true
end
}
params.values.each(&query)
return nil unless multipart
end
flattened_params = Hash.new
params.each do |key, value|
k = first ? key.to_s : "[#{key}]"
case value
when Array
value.map { |v|
build_multipart(v, false).each { |subkey, subvalue|
flattened_params["#{k}[]#{subkey}"] = subvalue
}
}
when Hash
build_multipart(value, false).each { |subkey, subvalue|
flattened_params[k + subkey] = subvalue
}
else
flattened_params[k] = value
end
end
if first
flattened_params.map { |name, file|
if file.respond_to?(:original_filename)
::File.open(file.path, "rb") do |f|
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
<<-EOF
--#{MULTIPART_BOUNDARY}\r
Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
Content-Type: #{file.content_type}\r
Content-Length: #{::File.stat(file.path).size}\r
\r
#{f.read}\r
EOF
end
else
<<-EOF
--#{MULTIPART_BOUNDARY}\r
Content-Disposition: form-data; name="#{name}"\r
\r
#{file}\r
EOF
end
}.join + "--#{MULTIPART_BOUNDARY}--\r"
else
flattened_params
end
end
end
end
end