rack 0.2.0 → 0.3.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/AUTHORS +3 -1
- data/RDOX +19 -1
- data/README +39 -6
- data/Rakefile +1 -1
- data/SPEC +2 -2
- data/bin/rackup +4 -0
- data/example/protectedlobster.ru +8 -0
- data/lib/rack.rb +12 -6
- data/lib/rack/auth/abstract/request.rb +0 -2
- data/lib/rack/auth/basic.rb +1 -1
- data/lib/rack/auth/digest/nonce.rb +2 -3
- data/lib/rack/auth/openid.rb +111 -0
- data/lib/rack/builder.rb +8 -4
- data/lib/rack/file.rb +3 -1
- data/lib/rack/handler/fastcgi.rb +3 -1
- data/lib/rack/handler/lsws.rb +52 -0
- data/lib/rack/handler/scgi.rb +57 -0
- data/lib/rack/lint.rb +5 -5
- data/lib/rack/request.rb +9 -2
- data/lib/rack/response.rb +9 -3
- data/lib/rack/session/cookie.rb +2 -4
- data/lib/rack/session/pool.rb +82 -0
- data/lib/rack/showexceptions.rb +1 -1
- data/lib/rack/urlmap.rb +6 -8
- data/lib/rack/utils.rb +21 -0
- data/test/cgi/lighttpd.conf +1 -1
- data/test/cgi/test.ru +1 -1
- data/test/spec_rack_auth_basic.rb +4 -3
- data/test/spec_rack_auth_digest.rb +4 -2
- data/test/spec_rack_builder.rb +50 -0
- data/test/spec_rack_cgi.rb +14 -8
- data/test/spec_rack_fastcgi.rb +14 -8
- data/test/spec_rack_file.rb +8 -0
- data/test/spec_rack_lint.rb +3 -3
- data/test/spec_rack_request.rb +32 -0
- data/test/spec_rack_response.rb +10 -2
- data/test/spec_rack_session_pool.rb +37 -0
- data/test/spec_rack_urlmap.rb +18 -12
- metadata +12 -3
data/lib/rack/builder.rb
CHANGED
@@ -22,8 +22,12 @@ module Rack
|
|
22
22
|
instance_eval(&block)
|
23
23
|
end
|
24
24
|
|
25
|
-
def use(middleware, *args)
|
26
|
-
@ins <<
|
25
|
+
def use(middleware, *args, &block)
|
26
|
+
@ins << if block_given?
|
27
|
+
lambda { |app| middleware.new(app, *args, &block) }
|
28
|
+
else
|
29
|
+
lambda { |app| middleware.new(app, *args) }
|
30
|
+
end
|
27
31
|
end
|
28
32
|
|
29
33
|
def run(app)
|
@@ -41,8 +45,8 @@ module Rack
|
|
41
45
|
|
42
46
|
def to_app
|
43
47
|
@ins[-1] = Rack::URLMap.new(@ins.last) if Hash === @ins.last
|
44
|
-
inner_app = @ins.
|
45
|
-
@ins.reverse.inject(inner_app) { |a, e| e.call(a) }
|
48
|
+
inner_app = @ins.last
|
49
|
+
@ins[0...-1].reverse.inject(inner_app) { |a, e| e.call(a) }
|
46
50
|
end
|
47
51
|
|
48
52
|
def call(env)
|
data/lib/rack/file.rb
CHANGED
@@ -24,11 +24,12 @@ module Rack
|
|
24
24
|
return [403, {"Content-Type" => "text/plain"}, ["Forbidden\n"]]
|
25
25
|
end
|
26
26
|
|
27
|
-
@path = F.join(@root, env["PATH_INFO"])
|
27
|
+
@path = F.join(@root, Utils.unescape(env["PATH_INFO"]))
|
28
28
|
ext = F.extname(@path)[1..-1]
|
29
29
|
|
30
30
|
if F.file?(@path) && F.readable?(@path)
|
31
31
|
[200, {
|
32
|
+
"Last-Modified" => F.mtime(@path).rfc822,
|
32
33
|
"Content-Type" => MIME_TYPES[ext] || "text/plain",
|
33
34
|
"Content-Length" => F.size(@path).to_s
|
34
35
|
}, self]
|
@@ -72,6 +73,7 @@ module Rack
|
|
72
73
|
"jpe" => "image/jpeg",
|
73
74
|
"jpeg" => "image/jpeg",
|
74
75
|
"jpg" => "image/jpeg",
|
76
|
+
"js" => "text/javascript",
|
75
77
|
"lha" => "application/octet-stream",
|
76
78
|
"lzh" => "application/octet-stream",
|
77
79
|
"mov" => "video/quicktime",
|
data/lib/rack/handler/fastcgi.rb
CHANGED
@@ -3,7 +3,9 @@ require 'fcgi'
|
|
3
3
|
module Rack
|
4
4
|
module Handler
|
5
5
|
class FastCGI
|
6
|
-
def self.run(app, options=
|
6
|
+
def self.run(app, options={})
|
7
|
+
file = options[:File] and STDIN.reopen(UNIXServer.new(file))
|
8
|
+
port = options[:Port] and STDIN.reopen(TCPServer.new(port))
|
7
9
|
FCGI.each { |request|
|
8
10
|
serve request, app
|
9
11
|
}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'lsapi'
|
2
|
+
#require 'cgi'
|
3
|
+
module Rack
|
4
|
+
module Handler
|
5
|
+
class LSWS
|
6
|
+
def self.run(app, options=nil)
|
7
|
+
while LSAPI.accept != nil
|
8
|
+
serve app
|
9
|
+
end
|
10
|
+
end
|
11
|
+
def self.serve(app)
|
12
|
+
env = ENV.to_hash
|
13
|
+
env.delete "HTTP_CONTENT_LENGTH"
|
14
|
+
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
|
15
|
+
env.update({"rack.version" => [0,1],
|
16
|
+
"rack.input" => STDIN,
|
17
|
+
"rack.errors" => STDERR,
|
18
|
+
"rack.multithread" => false,
|
19
|
+
"rack.multiprocess" => true,
|
20
|
+
"rack.run_once" => false,
|
21
|
+
"rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
|
22
|
+
})
|
23
|
+
env["QUERY_STRING"] ||= ""
|
24
|
+
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
|
25
|
+
env["REQUEST_PATH"] ||= "/"
|
26
|
+
status, headers, body = app.call(env)
|
27
|
+
begin
|
28
|
+
send_headers status, headers
|
29
|
+
send_body body
|
30
|
+
ensure
|
31
|
+
body.close if body.respond_to? :close
|
32
|
+
end
|
33
|
+
end
|
34
|
+
def self.send_headers(status, headers)
|
35
|
+
print "Status: #{status}\r\n"
|
36
|
+
headers.each { |k, vs|
|
37
|
+
vs.each { |v|
|
38
|
+
print "#{k}: #{v}\r\n"
|
39
|
+
}
|
40
|
+
}
|
41
|
+
print "\r\n"
|
42
|
+
STDOUT.flush
|
43
|
+
end
|
44
|
+
def self.send_body(body)
|
45
|
+
body.each { |part|
|
46
|
+
print part
|
47
|
+
STDOUT.flush
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'scgi'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
module Handler
|
6
|
+
class SCGI < ::SCGI::Processor
|
7
|
+
attr_accessor :app
|
8
|
+
|
9
|
+
def self.run(app, options=nil)
|
10
|
+
new(options.merge(:app=>app,
|
11
|
+
:host=>options[:Host],
|
12
|
+
:port=>options[:Port],
|
13
|
+
:socket=>options[:Socket])).listen
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(settings = {})
|
17
|
+
@app = settings[:app]
|
18
|
+
@log = Object.new
|
19
|
+
def @log.info(*args); end
|
20
|
+
def @log.error(*args); end
|
21
|
+
super(settings)
|
22
|
+
end
|
23
|
+
|
24
|
+
def process_request(request, input_body, socket)
|
25
|
+
env = {}.replace(request)
|
26
|
+
env.delete "HTTP_CONTENT_TYPE"
|
27
|
+
env.delete "HTTP_CONTENT_LENGTH"
|
28
|
+
env["REQUEST_PATH"], env["QUERY_STRING"] = env["REQUEST_URI"].split('?', 2)
|
29
|
+
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
|
30
|
+
env["PATH_INFO"] = env["REQUEST_PATH"]
|
31
|
+
env["QUERY_STRING"] ||= ""
|
32
|
+
env["SCRIPT_NAME"] = ""
|
33
|
+
env.update({"rack.version" => [0,1],
|
34
|
+
"rack.input" => StringIO.new(input_body),
|
35
|
+
"rack.errors" => STDERR,
|
36
|
+
|
37
|
+
"rack.multithread" => true,
|
38
|
+
"rack.multiprocess" => true,
|
39
|
+
"rack.run_once" => false,
|
40
|
+
|
41
|
+
"rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
|
42
|
+
})
|
43
|
+
status, headers, body = app.call(env)
|
44
|
+
begin
|
45
|
+
socket.write("Status: #{status}\r\n")
|
46
|
+
headers.each do |k, vs|
|
47
|
+
vs.each {|v| socket.write("#{k}: #{v}\r\n")}
|
48
|
+
end
|
49
|
+
socket.write("\r\n")
|
50
|
+
body.each {|s| socket.write(s)}
|
51
|
+
ensure
|
52
|
+
body.close if body.respond_to? :close
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/rack/lint.rb
CHANGED
@@ -158,7 +158,7 @@ module Rack
|
|
158
158
|
## * There must be a valid error stream in <tt>rack.errors</tt>.
|
159
159
|
check_error env["rack.errors"]
|
160
160
|
|
161
|
-
## * The <tt>REQUEST_METHOD</tt> must be one of +GET+, +POST+,
|
161
|
+
## * The <tt>REQUEST_METHOD</tt> must be one of +GET+, +POST+, +PUT+,
|
162
162
|
## +DELETE+, +HEAD+, +OPTIONS+, +TRACE+.
|
163
163
|
assert("REQUEST_METHOD unknown: #{env["REQUEST_METHOD"]}") {
|
164
164
|
%w[GET POST PUT DELETE
|
@@ -226,11 +226,11 @@ module Rack
|
|
226
226
|
## and return a string, or +nil+ on EOF.
|
227
227
|
def read(*args)
|
228
228
|
assert("rack.input#read called with too many arguments") {
|
229
|
-
args.size
|
229
|
+
args.size <= 1
|
230
230
|
}
|
231
231
|
if args.size == 1
|
232
232
|
assert("rack.input#read called with non-integer argument") {
|
233
|
-
args.first.
|
233
|
+
args.first.kind_of? Integer
|
234
234
|
}
|
235
235
|
end
|
236
236
|
v = @input.read(*args)
|
@@ -343,11 +343,11 @@ module Rack
|
|
343
343
|
def check_content_type(status, headers)
|
344
344
|
headers.each { |key, value|
|
345
345
|
## There must be a <tt>Content-Type</tt>, except when the
|
346
|
-
## +Status+ is
|
346
|
+
## +Status+ is 204 or 304, in which case there must be none
|
347
347
|
## given.
|
348
348
|
if key.downcase == "content-type"
|
349
349
|
assert("Content-Type header found in #{status} response, not allowed"){
|
350
|
-
not [
|
350
|
+
not [204, 304].include? status.to_i
|
351
351
|
}
|
352
352
|
return
|
353
353
|
end
|
data/lib/rack/request.rb
CHANGED
@@ -101,9 +101,16 @@ module Rack
|
|
101
101
|
@env["rack.request.cookie_hash"]
|
102
102
|
else
|
103
103
|
@env["rack.request.cookie_string"] = @env["HTTP_COOKIE"]
|
104
|
-
#
|
104
|
+
# According to RFC 2109:
|
105
|
+
# If multiple cookies satisfy the criteria above, they are ordered in
|
106
|
+
# the Cookie header such that those with more specific Path attributes
|
107
|
+
# precede those with less specific. Ordering with respect to other
|
108
|
+
# attributes (e.g., Domain) is unspecified.
|
105
109
|
@env["rack.request.cookie_hash"] =
|
106
|
-
Utils.parse_query(@env["rack.request.cookie_string"], ';,')
|
110
|
+
Utils.parse_query(@env["rack.request.cookie_string"], ';,').inject({}) {|h,(k,v)|
|
111
|
+
h[k] = Array === v ? v.first : v
|
112
|
+
h
|
113
|
+
}
|
107
114
|
end
|
108
115
|
end
|
109
116
|
|
data/lib/rack/response.rb
CHANGED
@@ -55,8 +55,10 @@ module Rack
|
|
55
55
|
when Hash
|
56
56
|
domain = "; domain=" + value[:domain] if value[:domain]
|
57
57
|
path = "; path=" + value[:path] if value[:path]
|
58
|
+
# According to RFC 2109, we need dashes here.
|
59
|
+
# N.B.: cgi.rb uses spaces...
|
58
60
|
expires = "; expires=" + value[:expires].clone.gmtime.
|
59
|
-
strftime("%a, %d
|
61
|
+
strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
|
60
62
|
value = value[:value]
|
61
63
|
end
|
62
64
|
value = [value] unless Array === value
|
@@ -76,7 +78,7 @@ module Rack
|
|
76
78
|
|
77
79
|
def delete_cookie(key, value={})
|
78
80
|
unless Array === self["Set-Cookie"]
|
79
|
-
self["Set-Cookie"] = [self["Set-Cookie"]]
|
81
|
+
self["Set-Cookie"] = [self["Set-Cookie"]].compact
|
80
82
|
end
|
81
83
|
|
82
84
|
self["Set-Cookie"].reject! { |cookie|
|
@@ -92,7 +94,7 @@ module Rack
|
|
92
94
|
def finish(&block)
|
93
95
|
@block = block
|
94
96
|
|
95
|
-
if [
|
97
|
+
if [204, 304].include?(status.to_i)
|
96
98
|
header.delete "Content-Type"
|
97
99
|
[status.to_i, header.to_hash, []]
|
98
100
|
else
|
@@ -112,6 +114,10 @@ module Rack
|
|
112
114
|
str
|
113
115
|
end
|
114
116
|
|
117
|
+
def close
|
118
|
+
body.close if body.respond_to?(:close)
|
119
|
+
end
|
120
|
+
|
115
121
|
def empty?
|
116
122
|
@block == nil && @body.empty?
|
117
123
|
end
|
data/lib/rack/session/cookie.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'base64'
|
2
|
-
|
3
1
|
module Rack
|
4
2
|
|
5
3
|
module Session
|
@@ -40,7 +38,7 @@ module Rack
|
|
40
38
|
session_data = request.cookies[@key]
|
41
39
|
|
42
40
|
begin
|
43
|
-
session_data =
|
41
|
+
session_data = session_data.unpack("m*").first
|
44
42
|
session_data = Marshal.load(session_data)
|
45
43
|
env["rack.session"] = session_data
|
46
44
|
rescue
|
@@ -52,7 +50,7 @@ module Rack
|
|
52
50
|
|
53
51
|
def commit_session(env, status, headers, body)
|
54
52
|
session_data = Marshal.dump(env["rack.session"])
|
55
|
-
session_data =
|
53
|
+
session_data = [session_data].pack("m*")
|
56
54
|
|
57
55
|
if session_data.size > (4096 - @key.size)
|
58
56
|
env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K. Content dropped.")
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Session
|
5
|
+
# Rack::Session::Pool provides simple cookie based session management.
|
6
|
+
# Session data is stored in a hash held by @pool. The corresponding
|
7
|
+
# session key sent to the client.
|
8
|
+
# The pool is unmonitored and unregulated, which means that over
|
9
|
+
# prolonged use the session pool will be very large.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# use Rack::Session::Pool, :key => 'rack.session',
|
14
|
+
# :domain => 'foo.com',
|
15
|
+
# :path => '/',
|
16
|
+
# :expire_after => 2592000
|
17
|
+
#
|
18
|
+
# All parameters are optional.
|
19
|
+
|
20
|
+
class Pool
|
21
|
+
attr_reader :pool, :key
|
22
|
+
|
23
|
+
def initialize(app, options={})
|
24
|
+
@app = app
|
25
|
+
@key = options[:key] || "rack.session"
|
26
|
+
@default_options = {:domain => nil,
|
27
|
+
:path => "/",
|
28
|
+
:expire_after => nil}.merge(options)
|
29
|
+
@pool = Hash.new
|
30
|
+
@default_context = context app, &nil
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def call(env)
|
35
|
+
@default_context.call(env)
|
36
|
+
end
|
37
|
+
|
38
|
+
def context(app, &block)
|
39
|
+
Rack::Utils::Context.new self, app do |env|
|
40
|
+
load_session env
|
41
|
+
block[env] if block
|
42
|
+
response = app.call(env)
|
43
|
+
commit_session env, response
|
44
|
+
response
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def load_session(env)
|
51
|
+
sess_id = env.fetch('HTTP_COOKIE','')[/#{@key}=([^,;]+)/,1]
|
52
|
+
begin
|
53
|
+
sess_id = Array.new(8){rand(16).to_s(16)}*''
|
54
|
+
end while @pool.key? sess_id if sess_id.nil? or !@pool.key? sess_id
|
55
|
+
|
56
|
+
session = @pool.fetch sess_id, {}
|
57
|
+
session.instance_variable_set '@dat', [sess_id, Time.now]
|
58
|
+
|
59
|
+
@pool.store sess_id, env['rack.session'] = session
|
60
|
+
env["rack.session.options"] = @default_options.dup
|
61
|
+
end
|
62
|
+
|
63
|
+
def commit_session(env, response)
|
64
|
+
session = env['rack.session']
|
65
|
+
options = env['rack.session.options']
|
66
|
+
sdat = session.instance_variable_get '@dat'
|
67
|
+
|
68
|
+
cookie = Utils.escape(@key)+'='+Utils.escape(sdat[0])
|
69
|
+
cookie<< "; domain=#{options[:domain]}" if options[:domain]
|
70
|
+
cookie<< "; path=#{options[:path]}" if options[:path]
|
71
|
+
cookie<< "; expires=#{sdat[1]+options[:expires_after]}" if options[:expires_after]
|
72
|
+
|
73
|
+
case a = (h = response[1])['Set-Cookie']
|
74
|
+
when Array then a << cookie
|
75
|
+
when String then h['Set-Cookie'] = [a, cookie]
|
76
|
+
when nil then h['Set-Cookie'] = cookie
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/rack/showexceptions.rb
CHANGED
@@ -217,7 +217,7 @@ TEMPLATE = <<'HTML'
|
|
217
217
|
<% if frame.pre_context %>
|
218
218
|
<ol start="<%=h frame.pre_context_lineno+1 %>" class="pre-context" id="pre<%=h frame.object_id %>">
|
219
219
|
<% frame.pre_context.each { |line| %>
|
220
|
-
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>"><%=h line %></li>
|
220
|
+
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
|
221
221
|
<% } %>
|
222
222
|
</ol>
|
223
223
|
<% end %>
|
data/lib/rack/urlmap.rb
CHANGED
@@ -28,15 +28,13 @@ module Rack
|
|
28
28
|
|
29
29
|
def call(env)
|
30
30
|
path = env["PATH_INFO"].to_s.squeeze("/")
|
31
|
+
hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT')
|
31
32
|
@mapping.each { |host, location, app|
|
32
|
-
if (
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
location == path[0, location.size] && (path[location.size] == nil ||
|
38
|
-
path[location.size] == ?/)
|
39
|
-
env["SCRIPT_NAME"] = location.dup
|
33
|
+
if (hHost == host || sName == host \
|
34
|
+
|| (host.nil? && (hHost == sName || hHost == sName+':'+sPort))) \
|
35
|
+
and location == path[0, location.size] \
|
36
|
+
and (path[location.size] == nil || path[location.size] == ?/)
|
37
|
+
env["SCRIPT_NAME"] += location.dup
|
40
38
|
env["PATH_INFO"] = path[location.size..-1]
|
41
39
|
env["PATH_INFO"].gsub!(/\/\z/, '')
|
42
40
|
env["PATH_INFO"] = "/" if env["PATH_INFO"].empty?
|
data/lib/rack/utils.rb
CHANGED
@@ -58,6 +58,27 @@ module Rack
|
|
58
58
|
end
|
59
59
|
module_function :escape_html
|
60
60
|
|
61
|
+
class Context < Proc
|
62
|
+
def initialize app_f=nil, app_r=nil
|
63
|
+
@for, @app = app_f, app_r
|
64
|
+
end
|
65
|
+
alias_method :old_inspect, :inspect
|
66
|
+
def inspect
|
67
|
+
"#{old_inspect} ==> #{@for.inspect} ==> #{@app.inspect}"
|
68
|
+
end
|
69
|
+
def pretty_print pp
|
70
|
+
pp.text old_inspect
|
71
|
+
pp.nest 1 do
|
72
|
+
pp.breakable
|
73
|
+
pp.text '=for> '
|
74
|
+
pp.pp @for
|
75
|
+
pp.breakable
|
76
|
+
pp.text '=app> '
|
77
|
+
pp.pp @app
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
61
82
|
# A case-normalizing Hash, adjusting on [] and []=.
|
62
83
|
class HeaderHash < Hash
|
63
84
|
def initialize(hash={})
|
data/test/cgi/lighttpd.conf
CHANGED
data/test/cgi/test.ru
CHANGED