rack 0.4.0 → 0.9.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/RDOX +61 -3
- data/README +52 -37
- data/Rakefile +9 -0
- data/SPEC +6 -3
- data/bin/rackup +0 -0
- data/lib/rack.rb +7 -2
- data/lib/rack/adapter/camping.rb +1 -1
- data/lib/rack/auth/openid.rb +4 -3
- data/lib/rack/builder.rb +12 -1
- data/lib/rack/conditionalget.rb +43 -0
- data/lib/rack/content_length.rb +25 -0
- data/lib/rack/deflater.rb +29 -5
- data/lib/rack/directory.rb +82 -91
- data/lib/rack/file.rb +45 -76
- data/lib/rack/handler.rb +4 -0
- data/lib/rack/handler/evented_mongrel.rb +1 -1
- data/lib/rack/handler/fastcgi.rb +2 -0
- data/lib/rack/handler/mongrel.rb +6 -2
- data/lib/rack/handler/swiftiplied_mongrel.rb +8 -0
- data/lib/rack/handler/thin.rb +15 -0
- data/lib/rack/handler/webrick.rb +8 -4
- data/lib/rack/head.rb +19 -0
- data/lib/rack/lint.rb +74 -10
- data/lib/rack/lobster.rb +13 -13
- data/lib/rack/methodoverride.rb +27 -0
- data/lib/rack/mime.rb +204 -0
- data/lib/rack/request.rb +10 -1
- data/lib/rack/response.rb +7 -2
- data/lib/rack/session/abstract/id.rb +14 -1
- data/lib/rack/session/cookie.rb +19 -1
- data/lib/rack/session/memcache.rb +1 -1
- data/lib/rack/session/pool.rb +1 -1
- data/lib/rack/showexceptions.rb +5 -1
- data/lib/rack/showstatus.rb +3 -2
- data/lib/rack/urlmap.rb +1 -1
- data/lib/rack/utils.rb +42 -13
- data/test/cgi/lighttpd.conf +1 -1
- data/test/cgi/test +0 -0
- data/test/cgi/test.fcgi +0 -0
- data/test/cgi/test.ru +0 -0
- data/test/spec_rack_builder.rb +34 -0
- data/test/spec_rack_conditionalget.rb +41 -0
- data/test/spec_rack_content_length.rb +43 -0
- data/test/spec_rack_deflater.rb +49 -14
- data/test/spec_rack_file.rb +7 -0
- data/test/spec_rack_handler.rb +3 -3
- data/test/spec_rack_head.rb +30 -0
- data/test/spec_rack_lint.rb +79 -2
- data/test/spec_rack_methodoverride.rb +60 -0
- data/test/spec_rack_mock.rb +1 -1
- data/test/spec_rack_mongrel.rb +20 -1
- data/test/spec_rack_request.rb +46 -1
- data/test/spec_rack_response.rb +10 -3
- data/test/spec_rack_session_cookie.rb +33 -0
- data/test/spec_rack_thin.rb +90 -0
- data/test/spec_rack_utils.rb +20 -18
- data/test/spec_rack_webrick.rb +17 -0
- data/test/testrequest.rb +12 -0
- metadata +91 -5
data/lib/rack/request.rb
CHANGED
@@ -113,6 +113,7 @@ module Rack
|
|
113
113
|
Utils::Multipart.parse_multipart(env)
|
114
114
|
@env["rack.request.form_vars"] = @env["rack.input"].read
|
115
115
|
@env["rack.request.form_hash"] = Utils.parse_query(@env["rack.request.form_vars"])
|
116
|
+
@env["rack.input"].rewind if @env["rack.input"].respond_to?(:rewind)
|
116
117
|
end
|
117
118
|
@env["rack.request.form_hash"]
|
118
119
|
else
|
@@ -122,7 +123,7 @@ module Rack
|
|
122
123
|
|
123
124
|
# The union of GET and POST data.
|
124
125
|
def params
|
125
|
-
self.GET.update(self.POST)
|
126
|
+
self.put? ? self.GET : self.GET.update(self.POST)
|
126
127
|
rescue EOFError => e
|
127
128
|
self.GET
|
128
129
|
end
|
@@ -205,5 +206,13 @@ module Rack
|
|
205
206
|
end
|
206
207
|
end
|
207
208
|
end
|
209
|
+
|
210
|
+
def ip
|
211
|
+
if addr = @env['HTTP_X_FORWARDED_FOR']
|
212
|
+
addr.split(',').last.strip
|
213
|
+
else
|
214
|
+
@env['REMOTE_ADDR']
|
215
|
+
end
|
216
|
+
end
|
208
217
|
end
|
209
218
|
end
|
data/lib/rack/response.rb
CHANGED
@@ -23,6 +23,7 @@ module Rack
|
|
23
23
|
|
24
24
|
@writer = lambda { |x| @body << x }
|
25
25
|
@block = nil
|
26
|
+
@length = 0
|
26
27
|
|
27
28
|
@body = []
|
28
29
|
|
@@ -59,12 +60,13 @@ module Rack
|
|
59
60
|
# N.B.: cgi.rb uses spaces...
|
60
61
|
expires = "; expires=" + value[:expires].clone.gmtime.
|
61
62
|
strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
|
63
|
+
secure = "; secure" if value[:secure]
|
62
64
|
value = value[:value]
|
63
65
|
end
|
64
66
|
value = [value] unless Array === value
|
65
67
|
cookie = Utils.escape(key) + "=" +
|
66
68
|
value.map { |v| Utils.escape v }.join("&") +
|
67
|
-
"#{domain}#{path}#{expires}"
|
69
|
+
"#{domain}#{path}#{expires}#{secure}"
|
68
70
|
|
69
71
|
case self["Set-Cookie"]
|
70
72
|
when Array
|
@@ -98,6 +100,7 @@ module Rack
|
|
98
100
|
header.delete "Content-Type"
|
99
101
|
[status.to_i, header.to_hash, []]
|
100
102
|
else
|
103
|
+
header["Content-Length"] ||= @length.to_s
|
101
104
|
[status.to_i, header.to_hash, self]
|
102
105
|
end
|
103
106
|
end
|
@@ -110,7 +113,9 @@ module Rack
|
|
110
113
|
end
|
111
114
|
|
112
115
|
def write(str)
|
113
|
-
|
116
|
+
s = str.to_s
|
117
|
+
@length += s.size
|
118
|
+
@writer.call s
|
114
119
|
str
|
115
120
|
end
|
116
121
|
|
@@ -26,7 +26,10 @@ module Rack
|
|
26
26
|
:key => 'rack.session',
|
27
27
|
:path => '/',
|
28
28
|
:domain => nil,
|
29
|
-
:expire_after => nil
|
29
|
+
:expire_after => nil,
|
30
|
+
:secure => false,
|
31
|
+
:httponly => true,
|
32
|
+
:sidbits => 128
|
30
33
|
}
|
31
34
|
|
32
35
|
def initialize(app, options={})
|
@@ -50,6 +53,14 @@ module Rack
|
|
50
53
|
|
51
54
|
private
|
52
55
|
|
56
|
+
# Generate a new session id using Ruby #rand. The size of the
|
57
|
+
# session id is controlled by the :sidbits option.
|
58
|
+
# Monkey patch this to use custom methods for session id generation.
|
59
|
+
def generate_sid
|
60
|
+
"%0#{@default_options[:sidbits] / 4}x" %
|
61
|
+
rand(2**@default_options[:sidbits] - 1)
|
62
|
+
end
|
63
|
+
|
53
64
|
# Extracts the session id from provided cookies and passes it and the
|
54
65
|
# environment to #get_session. It then sets the resulting session into
|
55
66
|
# 'rack.session', and places options and session metadata into
|
@@ -110,6 +121,8 @@ module Rack
|
|
110
121
|
expiry = time + options[:expire_after]
|
111
122
|
cookie<< "; expires=#{expiry.httpdate}"
|
112
123
|
end
|
124
|
+
cookie<< "; Secure" if options[:secure]
|
125
|
+
cookie<< "; HttpOnly" if options[:httponly]
|
113
126
|
|
114
127
|
case a = (h = response[1])['Set-Cookie']
|
115
128
|
when Array then a << cookie
|
data/lib/rack/session/cookie.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
|
3
5
|
module Session
|
@@ -5,13 +7,15 @@ module Rack
|
|
5
7
|
# Rack::Session::Cookie provides simple cookie based session management.
|
6
8
|
# The session is a Ruby Hash stored as base64 encoded marshalled data
|
7
9
|
# set to :key (default: rack.session).
|
10
|
+
# When the secret key is set, cookie data is checked for data integrity.
|
8
11
|
#
|
9
12
|
# Example:
|
10
13
|
#
|
11
14
|
# use Rack::Session::Cookie, :key => 'rack.session',
|
12
15
|
# :domain => 'foo.com',
|
13
16
|
# :path => '/',
|
14
|
-
# :expire_after => 2592000
|
17
|
+
# :expire_after => 2592000,
|
18
|
+
# :secret => 'change_me'
|
15
19
|
#
|
16
20
|
# All parameters are optional.
|
17
21
|
|
@@ -20,6 +24,7 @@ module Rack
|
|
20
24
|
def initialize(app, options={})
|
21
25
|
@app = app
|
22
26
|
@key = options[:key] || "rack.session"
|
27
|
+
@secret = options[:secret]
|
23
28
|
@default_options = {:domain => nil,
|
24
29
|
:path => "/",
|
25
30
|
:expire_after => nil}.merge(options)
|
@@ -37,6 +42,11 @@ module Rack
|
|
37
42
|
request = Rack::Request.new(env)
|
38
43
|
session_data = request.cookies[@key]
|
39
44
|
|
45
|
+
if @secret && session_data
|
46
|
+
session_data, digest = session_data.split("--")
|
47
|
+
session_data = nil unless digest == generate_hmac(session_data)
|
48
|
+
end
|
49
|
+
|
40
50
|
begin
|
41
51
|
session_data = session_data.unpack("m*").first
|
42
52
|
session_data = Marshal.load(session_data)
|
@@ -52,6 +62,10 @@ module Rack
|
|
52
62
|
session_data = Marshal.dump(env["rack.session"])
|
53
63
|
session_data = [session_data].pack("m*")
|
54
64
|
|
65
|
+
if @secret
|
66
|
+
session_data = "#{session_data}--#{generate_hmac(session_data)}"
|
67
|
+
end
|
68
|
+
|
55
69
|
if session_data.size > (4096 - @key.size)
|
56
70
|
env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K. Content dropped.")
|
57
71
|
[status, headers, body]
|
@@ -66,6 +80,10 @@ module Rack
|
|
66
80
|
end
|
67
81
|
end
|
68
82
|
|
83
|
+
def generate_hmac(data)
|
84
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, @secret, data)
|
85
|
+
end
|
86
|
+
|
69
87
|
end
|
70
88
|
end
|
71
89
|
end
|
data/lib/rack/session/pool.rb
CHANGED
@@ -40,7 +40,7 @@ module Rack
|
|
40
40
|
unless sess = @pool[sid] and ((expires = sess[:expire_at]).nil? or expires > Time.now)
|
41
41
|
@pool.delete_if{|k,v| expiry = v[:expire_at] and expiry < Time.now }
|
42
42
|
begin
|
43
|
-
sid =
|
43
|
+
sid = generate_sid
|
44
44
|
end while @pool.has_key?(sid)
|
45
45
|
end
|
46
46
|
@pool[sid] ||= {}
|
data/lib/rack/showexceptions.rb
CHANGED
@@ -22,7 +22,11 @@ module Rack
|
|
22
22
|
def call(env)
|
23
23
|
@app.call(env)
|
24
24
|
rescue StandardError, LoadError, SyntaxError => e
|
25
|
-
|
25
|
+
backtrace = pretty(env, e)
|
26
|
+
[500,
|
27
|
+
{"Content-Type" => "text/html",
|
28
|
+
"Content-Length" => backtrace.join.size.to_s},
|
29
|
+
backtrace]
|
26
30
|
end
|
27
31
|
|
28
32
|
def pretty(env, exception)
|
data/lib/rack/showstatus.rb
CHANGED
@@ -18,10 +18,11 @@ module Rack
|
|
18
18
|
|
19
19
|
def call(env)
|
20
20
|
status, headers, body = @app.call(env)
|
21
|
+
headers = Utils::HeaderHash.new(headers)
|
22
|
+
empty = headers['Content-Length'].to_i <= 0
|
21
23
|
|
22
24
|
# client or server error, or explicit message
|
23
|
-
if status.to_i >= 400 &&
|
24
|
-
(body.empty? rescue false) || env["rack.showstatus.detail"]
|
25
|
+
if (status.to_i >= 400 && empty) || env["rack.showstatus.detail"]
|
25
26
|
req = Rack::Request.new(env)
|
26
27
|
message = Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s
|
27
28
|
detail = env["rack.showstatus.detail"] || message
|
data/lib/rack/urlmap.rb
CHANGED
data/lib/rack/utils.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'set'
|
1
2
|
require 'tempfile'
|
2
3
|
|
3
4
|
module Rack
|
@@ -31,10 +32,10 @@ module Rack
|
|
31
32
|
|
32
33
|
def parse_query(qs, d = '&;')
|
33
34
|
params = {}
|
34
|
-
|
35
|
+
|
35
36
|
(qs || '').split(/[#{d}] */n).each do |p|
|
36
37
|
k, v = unescape(p).split('=', 2)
|
37
|
-
|
38
|
+
|
38
39
|
if cur = params[k]
|
39
40
|
if cur.class == Array
|
40
41
|
params[k] << v
|
@@ -45,11 +46,11 @@ module Rack
|
|
45
46
|
params[k] = v
|
46
47
|
end
|
47
48
|
end
|
48
|
-
|
49
|
+
|
49
50
|
return params
|
50
51
|
end
|
51
52
|
module_function :parse_query
|
52
|
-
|
53
|
+
|
53
54
|
def build_query(params)
|
54
55
|
params.map { |k, v|
|
55
56
|
if v.class == Array
|
@@ -155,9 +156,11 @@ module Rack
|
|
155
156
|
end
|
156
157
|
end
|
157
158
|
|
158
|
-
# A case-
|
159
|
+
# A case-insensitive Hash that preserves the original case of a
|
160
|
+
# header when set.
|
159
161
|
class HeaderHash < Hash
|
160
162
|
def initialize(hash={})
|
163
|
+
@names = {}
|
161
164
|
hash.each { |k, v| self[k] = v }
|
162
165
|
end
|
163
166
|
|
@@ -166,15 +169,35 @@ module Rack
|
|
166
169
|
end
|
167
170
|
|
168
171
|
def [](k)
|
169
|
-
super
|
172
|
+
super @names[k.downcase]
|
170
173
|
end
|
171
174
|
|
172
175
|
def []=(k, v)
|
173
|
-
|
176
|
+
delete k
|
177
|
+
@names[k.downcase] = k
|
178
|
+
super k, v
|
179
|
+
end
|
180
|
+
|
181
|
+
def delete(k)
|
182
|
+
super @names.delete(k.downcase)
|
174
183
|
end
|
175
184
|
|
176
|
-
def
|
177
|
-
k.
|
185
|
+
def include?(k)
|
186
|
+
@names.has_key? k.downcase
|
187
|
+
end
|
188
|
+
|
189
|
+
alias_method :has_key?, :include?
|
190
|
+
alias_method :member?, :include?
|
191
|
+
alias_method :key?, :include?
|
192
|
+
|
193
|
+
def merge!(other)
|
194
|
+
other.each { |k, v| self[k] = v }
|
195
|
+
self
|
196
|
+
end
|
197
|
+
|
198
|
+
def merge(other)
|
199
|
+
hash = dup
|
200
|
+
hash.merge! other
|
178
201
|
end
|
179
202
|
end
|
180
203
|
|
@@ -192,10 +215,11 @@ module Rack
|
|
192
215
|
206 => 'Partial Content',
|
193
216
|
300 => 'Multiple Choices',
|
194
217
|
301 => 'Moved Permanently',
|
195
|
-
302 => '
|
218
|
+
302 => 'Found',
|
196
219
|
303 => 'See Other',
|
197
220
|
304 => 'Not Modified',
|
198
221
|
305 => 'Use Proxy',
|
222
|
+
307 => 'Temporary Redirect',
|
199
223
|
400 => 'Bad Request',
|
200
224
|
401 => 'Unauthorized',
|
201
225
|
402 => 'Payment Required',
|
@@ -204,7 +228,7 @@ module Rack
|
|
204
228
|
405 => 'Method Not Allowed',
|
205
229
|
406 => 'Not Acceptable',
|
206
230
|
407 => 'Proxy Authentication Required',
|
207
|
-
408 => 'Request
|
231
|
+
408 => 'Request Timeout',
|
208
232
|
409 => 'Conflict',
|
209
233
|
410 => 'Gone',
|
210
234
|
411 => 'Length Required',
|
@@ -212,14 +236,19 @@ module Rack
|
|
212
236
|
413 => 'Request Entity Too Large',
|
213
237
|
414 => 'Request-URI Too Large',
|
214
238
|
415 => 'Unsupported Media Type',
|
239
|
+
416 => 'Requested Range Not Satisfiable',
|
240
|
+
417 => 'Expectation Failed',
|
215
241
|
500 => 'Internal Server Error',
|
216
242
|
501 => 'Not Implemented',
|
217
243
|
502 => 'Bad Gateway',
|
218
244
|
503 => 'Service Unavailable',
|
219
|
-
504 => 'Gateway
|
220
|
-
505 => 'HTTP Version
|
245
|
+
504 => 'Gateway Timeout',
|
246
|
+
505 => 'HTTP Version Not Supported'
|
221
247
|
}
|
222
248
|
|
249
|
+
# Responses with HTTP status codes that should not have an entity body
|
250
|
+
STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
|
251
|
+
|
223
252
|
# A multipart form data parser, adapted from IOWA.
|
224
253
|
#
|
225
254
|
# Usually, Rack::Request#POST takes care of calling this.
|
data/test/cgi/lighttpd.conf
CHANGED
data/test/cgi/test
CHANGED
File without changes
|
data/test/cgi/test.fcgi
CHANGED
File without changes
|
data/test/cgi/test.ru
CHANGED
File without changes
|
data/test/spec_rack_builder.rb
CHANGED
@@ -2,6 +2,8 @@ require 'test/spec'
|
|
2
2
|
|
3
3
|
require 'rack/builder'
|
4
4
|
require 'rack/mock'
|
5
|
+
require 'rack/showexceptions'
|
6
|
+
require 'rack/auth/basic'
|
5
7
|
|
6
8
|
context "Rack::Builder" do
|
7
9
|
specify "chains apps by default" do
|
@@ -47,4 +49,36 @@ context "Rack::Builder" do
|
|
47
49
|
response.body.to_s.should.equal 'Hi Boss'
|
48
50
|
end
|
49
51
|
|
52
|
+
specify "has explicit #to_app" do
|
53
|
+
app = Rack::Builder.app do
|
54
|
+
use Rack::ShowExceptions
|
55
|
+
run lambda { |env| raise "bzzzt" }
|
56
|
+
end
|
57
|
+
|
58
|
+
Rack::MockRequest.new(app).get("/").should.be.server_error
|
59
|
+
Rack::MockRequest.new(app).get("/").should.be.server_error
|
60
|
+
Rack::MockRequest.new(app).get("/").should.be.server_error
|
61
|
+
end
|
62
|
+
|
63
|
+
specify "apps are initialized once" do
|
64
|
+
app = Rack::Builder.new do
|
65
|
+
class AppClass
|
66
|
+
def initialize
|
67
|
+
@called = 0
|
68
|
+
end
|
69
|
+
def call(env)
|
70
|
+
raise "bzzzt" if @called > 0
|
71
|
+
@called += 1
|
72
|
+
[200, {'Content-Type' => 'text/plain'}, 'OK']
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
use Rack::ShowExceptions
|
77
|
+
run AppClass.new
|
78
|
+
end
|
79
|
+
|
80
|
+
Rack::MockRequest.new(app).get("/").status.should.equal 200
|
81
|
+
Rack::MockRequest.new(app).get("/").should.be.server_error
|
82
|
+
end
|
83
|
+
|
50
84
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'test/spec'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
require 'rack/mock'
|
5
|
+
require 'rack/conditionalget'
|
6
|
+
|
7
|
+
context "Rack::ConditionalGet" do
|
8
|
+
specify "should set a 304 status and truncate body when If-Modified-Since hits" do
|
9
|
+
timestamp = Time.now.httpdate
|
10
|
+
app = Rack::ConditionalGet.new(lambda { |env|
|
11
|
+
[200, {'Last-Modified'=>timestamp}, 'TEST'] })
|
12
|
+
|
13
|
+
response = Rack::MockRequest.new(app).
|
14
|
+
get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp)
|
15
|
+
|
16
|
+
response.status.should.be == 304
|
17
|
+
response.body.should.be.empty
|
18
|
+
end
|
19
|
+
|
20
|
+
specify "should set a 304 status and truncate body when If-None-Match hits" do
|
21
|
+
app = Rack::ConditionalGet.new(lambda { |env|
|
22
|
+
[200, {'Etag'=>'1234'}, 'TEST'] })
|
23
|
+
|
24
|
+
response = Rack::MockRequest.new(app).
|
25
|
+
get("/", 'HTTP_IF_NONE_MATCH' => '1234')
|
26
|
+
|
27
|
+
response.status.should.be == 304
|
28
|
+
response.body.should.be.empty
|
29
|
+
end
|
30
|
+
|
31
|
+
specify "should not affect non-GET/HEAD requests" do
|
32
|
+
app = Rack::ConditionalGet.new(lambda { |env|
|
33
|
+
[200, {'Etag'=>'1234'}, 'TEST'] })
|
34
|
+
|
35
|
+
response = Rack::MockRequest.new(app).
|
36
|
+
post("/", 'HTTP_IF_NONE_MATCH' => '1234')
|
37
|
+
|
38
|
+
response.status.should.be == 200
|
39
|
+
response.body.should.be == 'TEST'
|
40
|
+
end
|
41
|
+
end
|