rack 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rack might be problematic. Click here for more details.
- data/COPYING +1 -1
- data/KNOWN-ISSUES +3 -0
- data/RDOX +0 -428
- data/README +61 -26
- data/SPEC +8 -1
- data/bin/rackup +2 -174
- data/lib/rack.rb +10 -8
- data/lib/rack/builder.rb +17 -0
- data/lib/rack/cascade.rb +17 -12
- data/lib/rack/chunked.rb +2 -2
- data/lib/rack/commonlogger.rb +31 -43
- data/lib/rack/config.rb +15 -0
- data/lib/rack/content_type.rb +1 -1
- data/lib/rack/directory.rb +6 -2
- data/lib/rack/etag.rb +23 -0
- data/lib/rack/file.rb +4 -2
- data/lib/rack/handler.rb +19 -0
- data/lib/rack/handler/cgi.rb +1 -1
- data/lib/rack/handler/fastcgi.rb +2 -3
- data/lib/rack/handler/lsws.rb +4 -1
- data/lib/rack/handler/mongrel.rb +8 -5
- data/lib/rack/handler/scgi.rb +4 -4
- data/lib/rack/handler/webrick.rb +2 -4
- data/lib/rack/lint.rb +44 -15
- data/lib/rack/logger.rb +20 -0
- data/lib/rack/mime.rb +3 -1
- data/lib/rack/mock.rb +30 -4
- data/lib/rack/nulllogger.rb +18 -0
- data/lib/rack/reloader.rb +4 -1
- data/lib/rack/request.rb +40 -15
- data/lib/rack/response.rb +5 -39
- data/lib/rack/runtime.rb +27 -0
- data/lib/rack/sendfile.rb +142 -0
- data/lib/rack/server.rb +212 -0
- data/lib/rack/session/abstract/id.rb +3 -5
- data/lib/rack/session/cookie.rb +3 -4
- data/lib/rack/session/memcache.rb +53 -43
- data/lib/rack/session/pool.rb +1 -1
- data/lib/rack/urlmap.rb +9 -8
- data/lib/rack/utils.rb +230 -11
- data/rack.gemspec +33 -49
- data/test/spec_rack_cascade.rb +3 -5
- data/test/spec_rack_cgi.rb +3 -3
- data/test/spec_rack_commonlogger.rb +39 -10
- data/test/spec_rack_config.rb +24 -0
- data/test/spec_rack_directory.rb +1 -1
- data/test/spec_rack_etag.rb +17 -0
- data/test/spec_rack_fastcgi.rb +2 -2
- data/test/spec_rack_file.rb +1 -1
- data/test/spec_rack_lint.rb +26 -19
- data/test/spec_rack_logger.rb +21 -0
- data/test/spec_rack_mock.rb +87 -1
- data/test/spec_rack_mongrel.rb +4 -4
- data/test/spec_rack_nulllogger.rb +13 -0
- data/test/spec_rack_request.rb +47 -6
- data/test/spec_rack_response.rb +3 -0
- data/test/spec_rack_runtime.rb +35 -0
- data/test/spec_rack_sendfile.rb +86 -0
- data/test/spec_rack_session_cookie.rb +1 -10
- data/test/spec_rack_session_memcache.rb +53 -20
- data/test/spec_rack_urlmap.rb +30 -0
- data/test/spec_rack_utils.rb +171 -6
- data/test/spec_rack_webrick.rb +4 -4
- data/test/spec_rackup.rb +154 -0
- metadata +37 -79
- data/Rakefile +0 -164
- data/lib/rack/auth/openid.rb +0 -480
- data/test/cgi/lighttpd.conf +0 -20
- data/test/cgi/test +0 -9
- data/test/cgi/test.fcgi +0 -8
- data/test/cgi/test.ru +0 -7
- data/test/multipart/binary +0 -0
- data/test/multipart/empty +0 -10
- data/test/multipart/ie +0 -6
- data/test/multipart/nested +0 -10
- data/test/multipart/none +0 -9
- data/test/multipart/semicolon +0 -6
- data/test/multipart/text +0 -10
- data/test/spec_rack_auth_openid.rb +0 -84
- data/test/testrequest.rb +0 -57
- data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
- data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
data/lib/rack/server.rb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Server
|
5
|
+
class Options
|
6
|
+
def parse!(args)
|
7
|
+
options = {}
|
8
|
+
opt_parser = OptionParser.new("", 24, ' ') do |opts|
|
9
|
+
opts.banner = "Usage: rackup [ruby options] [rack options] [rackup config]"
|
10
|
+
|
11
|
+
opts.separator ""
|
12
|
+
opts.separator "Ruby options:"
|
13
|
+
|
14
|
+
lineno = 1
|
15
|
+
opts.on("-e", "--eval LINE", "evaluate a LINE of code") { |line|
|
16
|
+
eval line, TOPLEVEL_BINDING, "-e", lineno
|
17
|
+
lineno += 1
|
18
|
+
}
|
19
|
+
|
20
|
+
opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") {
|
21
|
+
options[:debug] = true
|
22
|
+
}
|
23
|
+
opts.on("-w", "--warn", "turn warnings on for your script") {
|
24
|
+
options[:warn] = true
|
25
|
+
}
|
26
|
+
|
27
|
+
opts.on("-I", "--include PATH",
|
28
|
+
"specify $LOAD_PATH (may be used more than once)") { |path|
|
29
|
+
options[:include] = path.split(":")
|
30
|
+
}
|
31
|
+
|
32
|
+
opts.on("-r", "--require LIBRARY",
|
33
|
+
"require the library, before executing your script") { |library|
|
34
|
+
options[:require] = library
|
35
|
+
}
|
36
|
+
|
37
|
+
opts.separator ""
|
38
|
+
opts.separator "Rack options:"
|
39
|
+
opts.on("-s", "--server SERVER", "serve using SERVER (webrick/mongrel)") { |s|
|
40
|
+
options[:server] = s
|
41
|
+
}
|
42
|
+
|
43
|
+
opts.on("-o", "--host HOST", "listen on HOST (default: 0.0.0.0)") { |host|
|
44
|
+
options[:Host] = host
|
45
|
+
}
|
46
|
+
|
47
|
+
opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port|
|
48
|
+
options[:Port] = port
|
49
|
+
}
|
50
|
+
|
51
|
+
opts.on("-E", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: development)") { |e|
|
52
|
+
options[:environment] = e
|
53
|
+
}
|
54
|
+
|
55
|
+
opts.on("-D", "--daemonize", "run daemonized in the background") { |d|
|
56
|
+
options[:daemonize] = d ? true : false
|
57
|
+
}
|
58
|
+
|
59
|
+
opts.on("-P", "--pid FILE", "file to store PID (default: rack.pid)") { |f|
|
60
|
+
options[:pid] = f
|
61
|
+
}
|
62
|
+
|
63
|
+
opts.separator ""
|
64
|
+
opts.separator "Common options:"
|
65
|
+
|
66
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
67
|
+
puts opts
|
68
|
+
exit
|
69
|
+
end
|
70
|
+
|
71
|
+
opts.on_tail("--version", "Show version") do
|
72
|
+
puts "Rack #{Rack.version}"
|
73
|
+
exit
|
74
|
+
end
|
75
|
+
end
|
76
|
+
opt_parser.parse! args
|
77
|
+
options[:config] = args.last if args.last
|
78
|
+
options
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.start
|
83
|
+
new.start
|
84
|
+
end
|
85
|
+
|
86
|
+
attr_accessor :options
|
87
|
+
|
88
|
+
def initialize(options = nil)
|
89
|
+
@options = options
|
90
|
+
end
|
91
|
+
|
92
|
+
def options
|
93
|
+
@options ||= parse_options(ARGV)
|
94
|
+
end
|
95
|
+
|
96
|
+
def default_options
|
97
|
+
{
|
98
|
+
:environment => "development",
|
99
|
+
:pid => nil,
|
100
|
+
:Port => 9292,
|
101
|
+
:Host => "0.0.0.0",
|
102
|
+
:AccessLog => [],
|
103
|
+
:config => "config.ru"
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
def app
|
108
|
+
@app ||= begin
|
109
|
+
if !::File.exist? options[:config]
|
110
|
+
abort "configuration #{options[:config]} not found"
|
111
|
+
end
|
112
|
+
|
113
|
+
app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
|
114
|
+
self.options.merge! options
|
115
|
+
app
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.middleware
|
120
|
+
@middleware ||= begin
|
121
|
+
m = Hash.new {|h,k| h[k] = []}
|
122
|
+
m["deployment"].concat [lambda {|server| server.server =~ /CGI/ ? nil : [Rack::CommonLogger, $stderr] }]
|
123
|
+
m["development"].concat m["deployment"] + [[Rack::ShowExceptions], [Rack::Lint]]
|
124
|
+
m
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def middleware
|
129
|
+
self.class.middleware
|
130
|
+
end
|
131
|
+
|
132
|
+
def start
|
133
|
+
if options[:debug]
|
134
|
+
$DEBUG = true
|
135
|
+
require 'pp'
|
136
|
+
p options[:server]
|
137
|
+
pp wrapped_app
|
138
|
+
pp app
|
139
|
+
end
|
140
|
+
|
141
|
+
if options[:warn]
|
142
|
+
$-w = true
|
143
|
+
end
|
144
|
+
|
145
|
+
if includes = options[:include]
|
146
|
+
$LOAD_PATH.unshift *includes
|
147
|
+
end
|
148
|
+
|
149
|
+
if library = options[:require]
|
150
|
+
require library
|
151
|
+
end
|
152
|
+
|
153
|
+
daemonize_app if options[:daemonize]
|
154
|
+
write_pid if options[:pid]
|
155
|
+
server.run wrapped_app, options
|
156
|
+
end
|
157
|
+
|
158
|
+
def server
|
159
|
+
@_server ||= Rack::Handler.get(options[:server]) || Rack::Handler.default
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
def parse_options(args)
|
164
|
+
options = default_options
|
165
|
+
|
166
|
+
# Don't evaluate CGI ISINDEX parameters.
|
167
|
+
# http://hoohoo.ncsa.uiuc.edu/cgi/cl.html
|
168
|
+
args.clear if ENV.include?("REQUEST_METHOD")
|
169
|
+
|
170
|
+
options.merge! opt_parser.parse! args
|
171
|
+
options
|
172
|
+
end
|
173
|
+
|
174
|
+
def opt_parser
|
175
|
+
Options.new
|
176
|
+
end
|
177
|
+
|
178
|
+
def build_app(app)
|
179
|
+
middleware[options[:environment]].reverse_each do |middleware|
|
180
|
+
middleware = middleware.call(self) if middleware.respond_to?(:call)
|
181
|
+
next unless middleware
|
182
|
+
klass = middleware.shift
|
183
|
+
app = klass.new(app, *middleware)
|
184
|
+
end
|
185
|
+
app
|
186
|
+
end
|
187
|
+
|
188
|
+
def wrapped_app
|
189
|
+
@wrapped_app ||= build_app app
|
190
|
+
end
|
191
|
+
|
192
|
+
def daemonize_app
|
193
|
+
if RUBY_VERSION < "1.9"
|
194
|
+
exit if fork
|
195
|
+
Process.setsid
|
196
|
+
exit if fork
|
197
|
+
Dir.chdir "/"
|
198
|
+
::File.umask 0000
|
199
|
+
STDIN.reopen "/dev/null"
|
200
|
+
STDOUT.reopen "/dev/null", "a"
|
201
|
+
STDERR.reopen "/dev/null", "a"
|
202
|
+
else
|
203
|
+
Process.daemon
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def write_pid
|
208
|
+
::File.open(options[:pid], 'w'){ |f| f.write("#{Process.pid}") }
|
209
|
+
at_exit { ::File.delete(options[:pid]) if ::File.exist?(options[:pid]) }
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -107,18 +107,16 @@ module Rack
|
|
107
107
|
|
108
108
|
if not session_id = set_session(env, session_id, session, options)
|
109
109
|
env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
|
110
|
-
[status, headers, body]
|
111
110
|
elsif options[:defer] and not options[:renew]
|
112
111
|
env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE
|
113
|
-
[status, headers, body]
|
114
112
|
else
|
115
113
|
cookie = Hash.new
|
116
114
|
cookie[:value] = session_id
|
117
115
|
cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
|
118
|
-
|
119
|
-
response.set_cookie(@key, cookie.merge(options))
|
120
|
-
response.to_a
|
116
|
+
Utils.set_cookie_header!(headers, @key, cookie.merge(options))
|
121
117
|
end
|
118
|
+
|
119
|
+
[status, headers, body]
|
122
120
|
end
|
123
121
|
|
124
122
|
# All thread safety and session retrival proceedures should occur here.
|
data/lib/rack/session/cookie.rb
CHANGED
@@ -70,16 +70,15 @@ module Rack
|
|
70
70
|
|
71
71
|
if session_data.size > (4096 - @key.size)
|
72
72
|
env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K. Content dropped.")
|
73
|
-
[status, headers, body]
|
74
73
|
else
|
75
74
|
options = env["rack.session.options"]
|
76
75
|
cookie = Hash.new
|
77
76
|
cookie[:value] = session_data
|
78
77
|
cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
|
79
|
-
|
80
|
-
response.set_cookie(@key, cookie.merge(options))
|
81
|
-
response.to_a
|
78
|
+
Utils.set_cookie_header!(headers, @key, cookie.merge(options))
|
82
79
|
end
|
80
|
+
|
81
|
+
[status, headers, body]
|
83
82
|
end
|
84
83
|
|
85
84
|
def generate_hmac(data)
|
@@ -29,9 +29,13 @@ module Rack
|
|
29
29
|
super
|
30
30
|
|
31
31
|
@mutex = Mutex.new
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
mserv = @default_options[:memcache_server]
|
33
|
+
mopts = @default_options.
|
34
|
+
reject{|k,v| MemCache::DEFAULT_OPTIONS.include? k }
|
35
|
+
@pool = MemCache.new mserv, mopts
|
36
|
+
unless @pool.active? and @pool.servers.any?{|c| c.alive? }
|
37
|
+
raise 'No memcache servers'
|
38
|
+
end
|
35
39
|
end
|
36
40
|
|
37
41
|
def generate_sid
|
@@ -41,24 +45,23 @@ module Rack
|
|
41
45
|
end
|
42
46
|
end
|
43
47
|
|
44
|
-
def get_session(env,
|
45
|
-
session = @pool.get(sid) if sid
|
48
|
+
def get_session(env, session_id)
|
46
49
|
@mutex.lock if env['rack.multithread']
|
47
|
-
unless
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
raise "Session collision on '#{sid.inspect}'" unless /^STORED/ =~ ret
|
50
|
+
unless session_id and session = @pool.get(session_id)
|
51
|
+
session_id, session = generate_sid, {}
|
52
|
+
unless /^STORED/ =~ @pool.add(session_id, session)
|
53
|
+
raise "Session collision on '#{session_id.inspect}'"
|
54
|
+
end
|
53
55
|
end
|
54
|
-
session.instance_variable_set
|
55
|
-
return [
|
56
|
-
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
|
57
|
-
|
56
|
+
session.instance_variable_set '@old', @pool.get(session_id, true)
|
57
|
+
return [session_id, session]
|
58
|
+
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
|
59
|
+
# MemCache server cannot be contacted
|
60
|
+
warn "#{self} is unable to find memcached server."
|
58
61
|
warn $!.inspect
|
59
62
|
return [ nil, {} ]
|
60
63
|
ensure
|
61
|
-
@mutex.unlock if
|
64
|
+
@mutex.unlock if @mutex.locked?
|
62
65
|
end
|
63
66
|
|
64
67
|
def set_session(env, session_id, new_session, options)
|
@@ -66,43 +69,50 @@ module Rack
|
|
66
69
|
expiry = expiry.nil? ? 0 : expiry + 1
|
67
70
|
|
68
71
|
@mutex.lock if env['rack.multithread']
|
69
|
-
session = @pool.get(session_id) || {}
|
70
72
|
if options[:renew] or options[:drop]
|
71
73
|
@pool.delete session_id
|
72
74
|
return false if options[:drop]
|
73
75
|
session_id = generate_sid
|
74
|
-
@pool.add session_id,
|
76
|
+
@pool.add session_id, {} # so we don't worry about cache miss on #set
|
75
77
|
end
|
76
|
-
old_session = new_session.instance_variable_get('@old') || {}
|
77
|
-
session = merge_sessions session_id, old_session, new_session, session
|
78
|
-
@pool.set session_id, session, expiry
|
79
|
-
return session_id
|
80
|
-
rescue MemCache::MemCacheError, Errno::ECONNREFUSED # MemCache server cannot be contacted
|
81
|
-
warn "#{self} is unable to find server."
|
82
|
-
warn $!.inspect
|
83
|
-
return false
|
84
|
-
ensure
|
85
|
-
@mutex.unlock if env['rack.multithread']
|
86
|
-
end
|
87
78
|
|
88
|
-
|
79
|
+
session = @pool.get(session_id) || {}
|
80
|
+
old_session = new_session.instance_variable_get '@old'
|
81
|
+
old_session = old_session ? Marshal.load(old_session) : {}
|
89
82
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
83
|
+
unless Hash === old_session and Hash === new_session
|
84
|
+
env['rack.errors'].
|
85
|
+
puts 'Bad old_session or new_session sessions provided.'
|
86
|
+
else # merge sessions
|
87
|
+
# alterations are either update or delete, making as few changes as
|
88
|
+
# possible to prevent possible issues.
|
96
89
|
|
97
|
-
|
98
|
-
|
99
|
-
|
90
|
+
# removed keys
|
91
|
+
delete = old_session.keys - new_session.keys
|
92
|
+
if $VERBOSE and not delete.empty?
|
93
|
+
env['rack.errors'].
|
94
|
+
puts "//@#{session_id}: delete #{delete*','}"
|
95
|
+
end
|
96
|
+
delete.each{|k| session.delete k }
|
100
97
|
|
101
|
-
|
102
|
-
|
103
|
-
|
98
|
+
# added or altered keys
|
99
|
+
update = new_session.keys.
|
100
|
+
select{|k| new_session[k] != old_session[k] }
|
101
|
+
if $VERBOSE and not update.empty?
|
102
|
+
env['rack.errors'].puts "//@#{session_id}: update #{update*','}"
|
103
|
+
end
|
104
|
+
update.each{|k| session[k] = new_session[k] }
|
105
|
+
end
|
104
106
|
|
105
|
-
|
107
|
+
@pool.set session_id, session, expiry
|
108
|
+
return session_id
|
109
|
+
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
|
110
|
+
# MemCache server cannot be contacted
|
111
|
+
warn "#{self} is unable to find memcached server."
|
112
|
+
warn $!.inspect
|
113
|
+
return false
|
114
|
+
ensure
|
115
|
+
@mutex.unlock if @mutex.locked?
|
106
116
|
end
|
107
117
|
end
|
108
118
|
end
|
data/lib/rack/session/pool.rb
CHANGED
@@ -13,7 +13,7 @@ module Rack
|
|
13
13
|
# In the context of a multithreaded environment, sessions being
|
14
14
|
# committed to the pool is done in a merging manner.
|
15
15
|
#
|
16
|
-
# The :drop option is available in rack.session.options if you
|
16
|
+
# The :drop option is available in rack.session.options if you wish to
|
17
17
|
# explicitly remove the session from the session cache.
|
18
18
|
#
|
19
19
|
# Example:
|
data/lib/rack/urlmap.rb
CHANGED
@@ -28,27 +28,28 @@ module Rack
|
|
28
28
|
raise ArgumentError, "paths need to start with /"
|
29
29
|
end
|
30
30
|
location = location.chomp('/')
|
31
|
+
match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
|
31
32
|
|
32
|
-
[host, location, app]
|
33
|
-
}.sort_by { |(h, l, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
|
33
|
+
[host, location, match, app]
|
34
|
+
}.sort_by { |(h, l, m, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
|
34
35
|
end
|
35
36
|
|
36
37
|
def call(env)
|
37
|
-
path = env["PATH_INFO"].to_s
|
38
|
+
path = env["PATH_INFO"].to_s
|
38
39
|
script_name = env['SCRIPT_NAME']
|
39
40
|
hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT')
|
40
|
-
@mapping.each { |host, location, app|
|
41
|
+
@mapping.each { |host, location, match, app|
|
41
42
|
next unless (hHost == host || sName == host \
|
42
43
|
|| (host.nil? && (hHost == sName || hHost == sName+':'+sPort)))
|
43
|
-
next unless
|
44
|
-
next unless
|
44
|
+
next unless path =~ match && rest = $1
|
45
|
+
next unless rest.empty? || rest[0] == ?/
|
45
46
|
|
46
47
|
return app.call(
|
47
48
|
env.merge(
|
48
49
|
'SCRIPT_NAME' => (script_name + location),
|
49
|
-
'PATH_INFO' =>
|
50
|
+
'PATH_INFO' => rest))
|
50
51
|
}
|
51
|
-
[404, {"Content-Type" => "text/plain"}, ["Not Found: #{path}"]]
|
52
|
+
[404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]]
|
52
53
|
end
|
53
54
|
end
|
54
55
|
end
|
data/lib/rack/utils.rb
CHANGED
@@ -27,7 +27,7 @@ module Rack
|
|
27
27
|
module_function :unescape
|
28
28
|
|
29
29
|
DEFAULT_SEP = /[&;] */n
|
30
|
-
|
30
|
+
|
31
31
|
# Stolen from Mongrel, with some small modifications:
|
32
32
|
# Parses a query string by breaking it up at the '&'
|
33
33
|
# and ';' characters. You can also use this to parse
|
@@ -38,7 +38,9 @@ module Rack
|
|
38
38
|
|
39
39
|
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
|
40
40
|
k, v = p.split('=', 2).map { |x| unescape(x) }
|
41
|
-
|
41
|
+
if v =~ /^("|')(.*)\1$/
|
42
|
+
v = $2.gsub('\\'+$1, $1)
|
43
|
+
end
|
42
44
|
if cur = params[k]
|
43
45
|
if cur.class == Array
|
44
46
|
params[k] << v
|
@@ -67,6 +69,9 @@ module Rack
|
|
67
69
|
module_function :parse_nested_query
|
68
70
|
|
69
71
|
def normalize_params(params, name, v = nil)
|
72
|
+
if v and v =~ /^("|')(.*)\1$/
|
73
|
+
v = $2.gsub('\\'+$1, $1)
|
74
|
+
end
|
70
75
|
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
|
71
76
|
k = $1 || ''
|
72
77
|
after = $' || ''
|
@@ -109,6 +114,25 @@ module Rack
|
|
109
114
|
end
|
110
115
|
module_function :build_query
|
111
116
|
|
117
|
+
def build_nested_query(value, prefix = nil)
|
118
|
+
case value
|
119
|
+
when Array
|
120
|
+
value.map { |v|
|
121
|
+
build_nested_query(v, "#{prefix}[]")
|
122
|
+
}.join("&")
|
123
|
+
when Hash
|
124
|
+
value.map { |k, v|
|
125
|
+
build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
|
126
|
+
}.join("&")
|
127
|
+
when String
|
128
|
+
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
129
|
+
"#{prefix}=#{escape(value)}"
|
130
|
+
else
|
131
|
+
prefix
|
132
|
+
end
|
133
|
+
end
|
134
|
+
module_function :build_nested_query
|
135
|
+
|
112
136
|
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
113
137
|
def escape_html(string)
|
114
138
|
string.to_s.gsub("&", "&").
|
@@ -149,6 +173,54 @@ module Rack
|
|
149
173
|
end
|
150
174
|
module_function :select_best_encoding
|
151
175
|
|
176
|
+
def set_cookie_header!(header, key, value)
|
177
|
+
case value
|
178
|
+
when Hash
|
179
|
+
domain = "; domain=" + value[:domain] if value[:domain]
|
180
|
+
path = "; path=" + value[:path] if value[:path]
|
181
|
+
# According to RFC 2109, we need dashes here.
|
182
|
+
# N.B.: cgi.rb uses spaces...
|
183
|
+
expires = "; expires=" + value[:expires].clone.gmtime.
|
184
|
+
strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
|
185
|
+
secure = "; secure" if value[:secure]
|
186
|
+
httponly = "; HttpOnly" if value[:httponly]
|
187
|
+
value = value[:value]
|
188
|
+
end
|
189
|
+
value = [value] unless Array === value
|
190
|
+
cookie = escape(key) + "=" +
|
191
|
+
value.map { |v| escape v }.join("&") +
|
192
|
+
"#{domain}#{path}#{expires}#{secure}#{httponly}"
|
193
|
+
|
194
|
+
case header["Set-Cookie"]
|
195
|
+
when Array
|
196
|
+
header["Set-Cookie"] << cookie
|
197
|
+
when String
|
198
|
+
header["Set-Cookie"] = [header["Set-Cookie"], cookie]
|
199
|
+
when nil
|
200
|
+
header["Set-Cookie"] = cookie
|
201
|
+
end
|
202
|
+
|
203
|
+
nil
|
204
|
+
end
|
205
|
+
module_function :set_cookie_header!
|
206
|
+
|
207
|
+
def delete_cookie_header!(header, key, value = {})
|
208
|
+
unless Array === header["Set-Cookie"]
|
209
|
+
header["Set-Cookie"] = [header["Set-Cookie"]].compact
|
210
|
+
end
|
211
|
+
|
212
|
+
header["Set-Cookie"].reject! { |cookie|
|
213
|
+
cookie =~ /\A#{escape(key)}=/
|
214
|
+
}
|
215
|
+
|
216
|
+
set_cookie_header!(header, key,
|
217
|
+
{:value => '', :path => nil, :domain => nil,
|
218
|
+
:expires => Time.at(0) }.merge(value))
|
219
|
+
|
220
|
+
nil
|
221
|
+
end
|
222
|
+
module_function :delete_cookie_header!
|
223
|
+
|
152
224
|
# Return the bytesize of String; uses String#length under Ruby 1.8 and
|
153
225
|
# String#bytesize under 1.9.
|
154
226
|
if ''.respond_to?(:bytesize)
|
@@ -191,11 +263,22 @@ module Rack
|
|
191
263
|
# A case-insensitive Hash that preserves the original case of a
|
192
264
|
# header when set.
|
193
265
|
class HeaderHash < Hash
|
266
|
+
def self.new(hash={})
|
267
|
+
HeaderHash === hash ? hash : super(hash)
|
268
|
+
end
|
269
|
+
|
194
270
|
def initialize(hash={})
|
271
|
+
super()
|
195
272
|
@names = {}
|
196
273
|
hash.each { |k, v| self[k] = v }
|
197
274
|
end
|
198
275
|
|
276
|
+
def each
|
277
|
+
super do |k, v|
|
278
|
+
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
199
282
|
def to_hash
|
200
283
|
inject({}) do |hash, (k,v)|
|
201
284
|
if v.respond_to? :to_ary
|
@@ -208,21 +291,24 @@ module Rack
|
|
208
291
|
end
|
209
292
|
|
210
293
|
def [](k)
|
211
|
-
super @names[k.downcase]
|
294
|
+
super(@names[k] ||= @names[k.downcase])
|
212
295
|
end
|
213
296
|
|
214
297
|
def []=(k, v)
|
215
298
|
delete k
|
216
|
-
@names[k.downcase] = k
|
299
|
+
@names[k] = @names[k.downcase] = k
|
217
300
|
super k, v
|
218
301
|
end
|
219
302
|
|
220
303
|
def delete(k)
|
221
|
-
|
304
|
+
canonical = k.downcase
|
305
|
+
result = super @names.delete(canonical)
|
306
|
+
@names.delete_if { |name,| name.downcase == canonical }
|
307
|
+
result
|
222
308
|
end
|
223
309
|
|
224
310
|
def include?(k)
|
225
|
-
@names.
|
311
|
+
@names.include?(k) || @names.include?(k.downcase)
|
226
312
|
end
|
227
313
|
|
228
314
|
alias_method :has_key?, :include?
|
@@ -238,13 +324,23 @@ module Rack
|
|
238
324
|
hash = dup
|
239
325
|
hash.merge! other
|
240
326
|
end
|
327
|
+
|
328
|
+
def replace(other)
|
329
|
+
clear
|
330
|
+
other.each { |k, v| self[k] = v }
|
331
|
+
self
|
332
|
+
end
|
241
333
|
end
|
242
334
|
|
243
335
|
# Every standard HTTP code mapped to the appropriate message.
|
244
|
-
#
|
336
|
+
# Generated with:
|
337
|
+
# curl -s http://www.iana.org/assignments/http-status-codes | \
|
338
|
+
# ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
|
339
|
+
# puts " #{m[1]} => \x27#{m[2].strip}x27,"'
|
245
340
|
HTTP_STATUS_CODES = {
|
246
341
|
100 => 'Continue',
|
247
342
|
101 => 'Switching Protocols',
|
343
|
+
102 => 'Processing',
|
248
344
|
200 => 'OK',
|
249
345
|
201 => 'Created',
|
250
346
|
202 => 'Accepted',
|
@@ -252,12 +348,15 @@ module Rack
|
|
252
348
|
204 => 'No Content',
|
253
349
|
205 => 'Reset Content',
|
254
350
|
206 => 'Partial Content',
|
351
|
+
207 => 'Multi-Status',
|
352
|
+
226 => 'IM Used',
|
255
353
|
300 => 'Multiple Choices',
|
256
354
|
301 => 'Moved Permanently',
|
257
355
|
302 => 'Found',
|
258
356
|
303 => 'See Other',
|
259
357
|
304 => 'Not Modified',
|
260
358
|
305 => 'Use Proxy',
|
359
|
+
306 => 'Reserved',
|
261
360
|
307 => 'Temporary Redirect',
|
262
361
|
400 => 'Bad Request',
|
263
362
|
401 => 'Unauthorized',
|
@@ -273,27 +372,76 @@ module Rack
|
|
273
372
|
411 => 'Length Required',
|
274
373
|
412 => 'Precondition Failed',
|
275
374
|
413 => 'Request Entity Too Large',
|
276
|
-
414 => 'Request-URI Too
|
375
|
+
414 => 'Request-URI Too Long',
|
277
376
|
415 => 'Unsupported Media Type',
|
278
377
|
416 => 'Requested Range Not Satisfiable',
|
279
378
|
417 => 'Expectation Failed',
|
379
|
+
422 => 'Unprocessable Entity',
|
380
|
+
423 => 'Locked',
|
381
|
+
424 => 'Failed Dependency',
|
382
|
+
426 => 'Upgrade Required',
|
280
383
|
500 => 'Internal Server Error',
|
281
384
|
501 => 'Not Implemented',
|
282
385
|
502 => 'Bad Gateway',
|
283
386
|
503 => 'Service Unavailable',
|
284
387
|
504 => 'Gateway Timeout',
|
285
|
-
505 => 'HTTP Version Not Supported'
|
388
|
+
505 => 'HTTP Version Not Supported',
|
389
|
+
506 => 'Variant Also Negotiates',
|
390
|
+
507 => 'Insufficient Storage',
|
391
|
+
510 => 'Not Extended',
|
286
392
|
}
|
287
393
|
|
288
394
|
# Responses with HTTP status codes that should not have an entity body
|
289
395
|
STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
|
290
396
|
|
397
|
+
SYMBOL_TO_STATUS_CODE = HTTP_STATUS_CODES.inject({}) { |hash, (code, message)|
|
398
|
+
hash[message.downcase.gsub(/\s|-/, '_').to_sym] = code
|
399
|
+
hash
|
400
|
+
}
|
401
|
+
|
402
|
+
def status_code(status)
|
403
|
+
if status.is_a?(Symbol)
|
404
|
+
SYMBOL_TO_STATUS_CODE[status] || 500
|
405
|
+
else
|
406
|
+
status.to_i
|
407
|
+
end
|
408
|
+
end
|
409
|
+
module_function :status_code
|
410
|
+
|
291
411
|
# A multipart form data parser, adapted from IOWA.
|
292
412
|
#
|
293
413
|
# Usually, Rack::Request#POST takes care of calling this.
|
294
414
|
|
295
415
|
module Multipart
|
416
|
+
class UploadedFile
|
417
|
+
# The filename, *not* including the path, of the "uploaded" file
|
418
|
+
attr_reader :original_filename
|
419
|
+
|
420
|
+
# The content type of the "uploaded" file
|
421
|
+
attr_accessor :content_type
|
422
|
+
|
423
|
+
def initialize(path, content_type = "text/plain", binary = false)
|
424
|
+
raise "#{path} file does not exist" unless ::File.exist?(path)
|
425
|
+
@content_type = content_type
|
426
|
+
@original_filename = ::File.basename(path)
|
427
|
+
@tempfile = Tempfile.new(@original_filename)
|
428
|
+
@tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
|
429
|
+
@tempfile.binmode if binary
|
430
|
+
FileUtils.copy_file(path, @tempfile.path)
|
431
|
+
end
|
432
|
+
|
433
|
+
def path
|
434
|
+
@tempfile.path
|
435
|
+
end
|
436
|
+
alias_method :local_path, :path
|
437
|
+
|
438
|
+
def method_missing(method_name, *args, &block) #:nodoc:
|
439
|
+
@tempfile.__send__(method_name, *args, &block)
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
296
443
|
EOL = "\r\n"
|
444
|
+
MULTIPART_BOUNDARY = "AaB03x"
|
297
445
|
|
298
446
|
def self.parse_multipart(env)
|
299
447
|
unless env['CONTENT_TYPE'] =~
|
@@ -378,7 +526,7 @@ module Rack
|
|
378
526
|
:name => name, :tempfile => body, :head => head}
|
379
527
|
elsif !filename && content_type
|
380
528
|
body.rewind
|
381
|
-
|
529
|
+
|
382
530
|
# Generic multipart cases, not coming from a form
|
383
531
|
data = {:type => content_type,
|
384
532
|
:name => name, :tempfile => body, :head => head}
|
@@ -388,7 +536,8 @@ module Rack
|
|
388
536
|
|
389
537
|
Utils.normalize_params(params, name, data) unless data.nil?
|
390
538
|
|
391
|
-
break
|
539
|
+
# break if we're at the end of a buffer, but not if it is the end of a field
|
540
|
+
break if (buf.empty? && $1 != EOL) || content_length == -1
|
392
541
|
}
|
393
542
|
|
394
543
|
input.rewind
|
@@ -396,6 +545,76 @@ module Rack
|
|
396
545
|
params
|
397
546
|
end
|
398
547
|
end
|
548
|
+
|
549
|
+
def self.build_multipart(params, first = true)
|
550
|
+
if first
|
551
|
+
unless params.is_a?(Hash)
|
552
|
+
raise ArgumentError, "value must be a Hash"
|
553
|
+
end
|
554
|
+
|
555
|
+
multipart = false
|
556
|
+
query = lambda { |value|
|
557
|
+
case value
|
558
|
+
when Array
|
559
|
+
value.each(&query)
|
560
|
+
when Hash
|
561
|
+
value.values.each(&query)
|
562
|
+
when UploadedFile
|
563
|
+
multipart = true
|
564
|
+
end
|
565
|
+
}
|
566
|
+
params.values.each(&query)
|
567
|
+
return nil unless multipart
|
568
|
+
end
|
569
|
+
|
570
|
+
flattened_params = Hash.new
|
571
|
+
|
572
|
+
params.each do |key, value|
|
573
|
+
k = first ? key.to_s : "[#{key}]"
|
574
|
+
|
575
|
+
case value
|
576
|
+
when Array
|
577
|
+
value.map { |v|
|
578
|
+
build_multipart(v, false).each { |subkey, subvalue|
|
579
|
+
flattened_params["#{k}[]#{subkey}"] = subvalue
|
580
|
+
}
|
581
|
+
}
|
582
|
+
when Hash
|
583
|
+
build_multipart(value, false).each { |subkey, subvalue|
|
584
|
+
flattened_params[k + subkey] = subvalue
|
585
|
+
}
|
586
|
+
else
|
587
|
+
flattened_params[k] = value
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
if first
|
592
|
+
flattened_params.map { |name, file|
|
593
|
+
if file.respond_to?(:original_filename)
|
594
|
+
::File.open(file.path, "rb") do |f|
|
595
|
+
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
|
596
|
+
<<-EOF
|
597
|
+
--#{MULTIPART_BOUNDARY}\r
|
598
|
+
Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
|
599
|
+
Content-Type: #{file.content_type}\r
|
600
|
+
Content-Length: #{::File.stat(file.path).size}\r
|
601
|
+
\r
|
602
|
+
#{f.read}\r
|
603
|
+
EOF
|
604
|
+
end
|
605
|
+
else
|
606
|
+
<<-EOF
|
607
|
+
--#{MULTIPART_BOUNDARY}\r
|
608
|
+
Content-Disposition: form-data; name="#{name}"\r
|
609
|
+
\r
|
610
|
+
#{file}\r
|
611
|
+
EOF
|
612
|
+
end
|
613
|
+
}.join + "--#{MULTIPART_BOUNDARY}--\r"
|
614
|
+
else
|
615
|
+
flattened_params
|
616
|
+
end
|
617
|
+
end
|
399
618
|
end
|
400
619
|
end
|
401
620
|
end
|