rack 0.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/AUTHORS +3 -0
- data/COPYING +18 -0
- data/KNOWN-ISSUES +18 -0
- data/RDOX +144 -0
- data/README +154 -0
- data/Rakefile +174 -0
- data/SPEC +132 -0
- data/bin/rackup +148 -0
- data/contrib/rack_logo.svg +111 -0
- data/example/lobster.ru +4 -0
- data/lib/rack.rb +67 -0
- data/lib/rack/adapter/camping.rb +16 -0
- data/lib/rack/adapter/rails.rb +65 -0
- data/lib/rack/builder.rb +52 -0
- data/lib/rack/cascade.rb +26 -0
- data/lib/rack/commonlogger.rb +56 -0
- data/lib/rack/file.rb +108 -0
- data/lib/rack/handler/cgi.rb +57 -0
- data/lib/rack/handler/fastcgi.rb +81 -0
- data/lib/rack/handler/mongrel.rb +57 -0
- data/lib/rack/handler/webrick.rb +56 -0
- data/lib/rack/lint.rb +394 -0
- data/lib/rack/lobster.rb +65 -0
- data/lib/rack/mock.rb +183 -0
- data/lib/rack/recursive.rb +57 -0
- data/lib/rack/reloader.rb +64 -0
- data/lib/rack/request.rb +112 -0
- data/lib/rack/response.rb +114 -0
- data/lib/rack/showexceptions.rb +344 -0
- data/lib/rack/urlmap.rb +50 -0
- data/lib/rack/utils.rb +176 -0
- data/test/cgi/lighttpd.conf +20 -0
- data/test/cgi/test +9 -0
- data/test/cgi/test.fcgi +9 -0
- data/test/cgi/test.ru +7 -0
- data/test/spec_rack_camping.rb +44 -0
- data/test/spec_rack_cascade.rb +35 -0
- data/test/spec_rack_cgi.rb +82 -0
- data/test/spec_rack_commonlogger.rb +32 -0
- data/test/spec_rack_fastcgi.rb +82 -0
- data/test/spec_rack_file.rb +32 -0
- data/test/spec_rack_lint.rb +317 -0
- data/test/spec_rack_lobster.rb +45 -0
- data/test/spec_rack_mock.rb +150 -0
- data/test/spec_rack_mongrel.rb +87 -0
- data/test/spec_rack_recursive.rb +77 -0
- data/test/spec_rack_request.rb +219 -0
- data/test/spec_rack_response.rb +110 -0
- data/test/spec_rack_showexceptions.rb +21 -0
- data/test/spec_rack_urlmap.rb +140 -0
- data/test/spec_rack_utils.rb +57 -0
- data/test/spec_rack_webrick.rb +89 -0
- data/test/testrequest.rb +43 -0
- metadata +117 -0
data/lib/rack/urlmap.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::URLMap takes a hash mapping urls or paths to apps, and
|
3
|
+
# dispatches accordingly. Support for HTTP/1.1 host names exists if
|
4
|
+
# the URLs start with <tt>http://</tt> or <tt>https://</tt>.
|
5
|
+
#
|
6
|
+
# URLMap modifies the SCRIPT_NAME and PATH_INFO such that the part
|
7
|
+
# relevant for dispatch is in the SCRIPT_NAME, and the rest in the
|
8
|
+
# PATH_INFO. This should be taken care of when you need to
|
9
|
+
# reconstruct the URL in order to create links.
|
10
|
+
#
|
11
|
+
# URLMap dispatches in such a way that the longest paths are tried
|
12
|
+
# first, since they are most specific.
|
13
|
+
|
14
|
+
class URLMap
|
15
|
+
def initialize(map)
|
16
|
+
@mapping = map.map { |location, app|
|
17
|
+
if location =~ %r{\Ahttps?://(.*?)(/.*)}
|
18
|
+
host, location = $1, $2
|
19
|
+
else
|
20
|
+
host = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
location = "" if location == "/"
|
24
|
+
|
25
|
+
[host, location, app]
|
26
|
+
}.sort_by { |(h, l, a)| -l.size } # Longest path first
|
27
|
+
end
|
28
|
+
|
29
|
+
def call(env)
|
30
|
+
path = env["PATH_INFO"].to_s.squeeze("/")
|
31
|
+
@mapping.each { |host, location, app|
|
32
|
+
if (env["HTTP_HOST"] == host ||
|
33
|
+
env["SERVER_NAME"] == host ||
|
34
|
+
(host == nil && (env["HTTP_HOST"] == env["SERVER_NAME"] ||
|
35
|
+
env["HTTP_HOST"] ==
|
36
|
+
"#{env["SERVER_NAME"]}:#{env["SERVER_PORT"]}"))) &&
|
37
|
+
location == path[0, location.size] && (path[location.size] == nil ||
|
38
|
+
path[location.size] == ?/)
|
39
|
+
env["SCRIPT_NAME"] = location.dup
|
40
|
+
env["PATH_INFO"] = path[location.size..-1]
|
41
|
+
env["PATH_INFO"].gsub!(/\/\z/, '')
|
42
|
+
env["PATH_INFO"] = "/" if env["PATH_INFO"].empty?
|
43
|
+
return app.call(env)
|
44
|
+
end
|
45
|
+
}
|
46
|
+
[404, {"Content-Type" => "text/plain"}, ["Not Found: #{path}"]]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
data/lib/rack/utils.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
# Rack::Utils contains a grab-bag of useful methods for writing web
|
5
|
+
# applications adopted from all kinds of Ruby libraries.
|
6
|
+
|
7
|
+
module Utils
|
8
|
+
# Performs URI escaping so that you can construct proper
|
9
|
+
# query strings faster. Use this rather than the cgi.rb
|
10
|
+
# version since it's faster. (Stolen from Camping).
|
11
|
+
def escape(s)
|
12
|
+
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
13
|
+
'%'+$1.unpack('H2'*$1.size).join('%').upcase
|
14
|
+
}.tr(' ', '+')
|
15
|
+
end
|
16
|
+
module_function :escape
|
17
|
+
|
18
|
+
# Unescapes a URI escaped string. (Stolen from Camping).
|
19
|
+
def unescape(s)
|
20
|
+
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
|
21
|
+
[$1.delete('%')].pack('H*')
|
22
|
+
}
|
23
|
+
end
|
24
|
+
module_function :unescape
|
25
|
+
|
26
|
+
# Stolen from Mongrel:
|
27
|
+
# Parses a query string by breaking it up at the '&'
|
28
|
+
# and ';' characters. You can also use this to parse
|
29
|
+
# cookies by changing the characters used in the second
|
30
|
+
# parameter (which defaults to '&;'.
|
31
|
+
|
32
|
+
def parse_query(qs, d = '&;')
|
33
|
+
params = {}
|
34
|
+
(qs||'').split(/[#{d}] */n).inject(params) { |h,p|
|
35
|
+
k, v=unescape(p).split('=',2)
|
36
|
+
if cur = params[k]
|
37
|
+
if cur.class == Array
|
38
|
+
params[k] << v
|
39
|
+
else
|
40
|
+
params[k] = [cur, v]
|
41
|
+
end
|
42
|
+
else
|
43
|
+
params[k] = v
|
44
|
+
end
|
45
|
+
}
|
46
|
+
|
47
|
+
return params
|
48
|
+
end
|
49
|
+
module_function :parse_query
|
50
|
+
|
51
|
+
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
52
|
+
def escape_html(string)
|
53
|
+
string.to_s.gsub("&", "&").
|
54
|
+
gsub("<", "<").
|
55
|
+
gsub(">", ">").
|
56
|
+
gsub("'", "'").
|
57
|
+
gsub('"', """)
|
58
|
+
end
|
59
|
+
module_function :escape_html
|
60
|
+
|
61
|
+
# A case-normalizing Hash, adjusting on [] and []=.
|
62
|
+
class HeaderHash < Hash
|
63
|
+
def initialize(hash={})
|
64
|
+
hash.each { |k, v| self[k] = v }
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_hash
|
68
|
+
{}.replace(self)
|
69
|
+
end
|
70
|
+
|
71
|
+
def [](k)
|
72
|
+
super capitalize(k)
|
73
|
+
end
|
74
|
+
|
75
|
+
def []=(k, v)
|
76
|
+
super capitalize(k), v
|
77
|
+
end
|
78
|
+
|
79
|
+
def capitalize(k)
|
80
|
+
k.to_s.downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# A multipart form data parser, adapted from IOWA.
|
85
|
+
#
|
86
|
+
# Usually, Rack::Request#POST takes care of calling this.
|
87
|
+
|
88
|
+
module Multipart
|
89
|
+
EOL = "\r\n"
|
90
|
+
|
91
|
+
def self.parse_multipart(env)
|
92
|
+
unless env['CONTENT_TYPE'] =~
|
93
|
+
%r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n
|
94
|
+
nil
|
95
|
+
else
|
96
|
+
boundary = "--#{$1}"
|
97
|
+
|
98
|
+
params = {}
|
99
|
+
buf = ""
|
100
|
+
content_length = env['CONTENT_LENGTH'].to_i
|
101
|
+
input = env['rack.input']
|
102
|
+
|
103
|
+
boundary_size = boundary.size + EOL.size
|
104
|
+
bufsize = 16384
|
105
|
+
|
106
|
+
content_length -= boundary_size
|
107
|
+
|
108
|
+
status = input.read(boundary_size)
|
109
|
+
raise EOFError, "bad content body" unless status == boundary + EOL
|
110
|
+
|
111
|
+
rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/
|
112
|
+
|
113
|
+
loop {
|
114
|
+
head = nil
|
115
|
+
body = ''
|
116
|
+
filename = content_type = name = nil
|
117
|
+
|
118
|
+
until head && buf =~ rx
|
119
|
+
if !head && i = buf.index("\r\n\r\n")
|
120
|
+
head = buf.slice!(0, i+2) # First \r\n
|
121
|
+
buf.slice!(0, 2) # Second \r\n
|
122
|
+
|
123
|
+
filename = head[/Content-Disposition:.* filename="?([^\";]*)"?/ni, 1]
|
124
|
+
content_type = head[/Content-Type: (.*)\r\n/ni, 1]
|
125
|
+
name = head[/Content-Disposition:.* name="?([^\";]*)"?/ni, 1]
|
126
|
+
|
127
|
+
body = Tempfile.new("RackMultipart") if filename
|
128
|
+
|
129
|
+
next
|
130
|
+
end
|
131
|
+
|
132
|
+
# Save the read body part.
|
133
|
+
if head && (boundary_size+4 < buf.size)
|
134
|
+
body << buf.slice!(0, buf.size - (boundary_size+4))
|
135
|
+
end
|
136
|
+
|
137
|
+
c = input.read(bufsize < content_length ? bufsize : content_length)
|
138
|
+
raise EOFError, "bad content body" if c.nil? || c.empty?
|
139
|
+
buf << c
|
140
|
+
content_length -= c.size
|
141
|
+
end
|
142
|
+
|
143
|
+
# Save the rest.
|
144
|
+
if i = buf.index(rx)
|
145
|
+
body << buf.slice!(0, i)
|
146
|
+
buf.slice!(0, boundary_size+2)
|
147
|
+
|
148
|
+
content_length = -1 if $1 == "--"
|
149
|
+
end
|
150
|
+
|
151
|
+
if filename
|
152
|
+
body.rewind
|
153
|
+
data = {:filename => filename, :type => content_type,
|
154
|
+
:name => name, :tempfile => body, :head => head}
|
155
|
+
else
|
156
|
+
data = body
|
157
|
+
end
|
158
|
+
|
159
|
+
if name
|
160
|
+
if name =~ /\[\]\z/
|
161
|
+
params[name] ||= []
|
162
|
+
params[name] << data
|
163
|
+
else
|
164
|
+
params[name] = data
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
break if buf.empty? || content_length == -1
|
169
|
+
}
|
170
|
+
|
171
|
+
params
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
server.modules = ("mod_fastcgi", "mod_cgi")
|
2
|
+
server.document-root = "."
|
3
|
+
server.errorlog = "lighttpd.errors"
|
4
|
+
server.port = 9203
|
5
|
+
|
6
|
+
server.event-handler = "freebsd-kqueue"
|
7
|
+
|
8
|
+
cgi.assign = ("/test" => "/usr/local/bin/ruby",
|
9
|
+
# ".ru" => ""
|
10
|
+
)
|
11
|
+
|
12
|
+
fastcgi.server = ("test.fcgi" => ("localhost" =>
|
13
|
+
("min-procs" => 1,
|
14
|
+
"socket" => "/tmp/rack-test-fcgi",
|
15
|
+
"bin-path" => "test.fcgi")),
|
16
|
+
"test.ru" => ("localhost" =>
|
17
|
+
("min-procs" => 1,
|
18
|
+
"socket" => "/tmp/rack-test-ru-fcgi",
|
19
|
+
"bin-path" => "test.ru")),
|
20
|
+
)
|
data/test/cgi/test
ADDED
data/test/cgi/test.fcgi
ADDED
data/test/cgi/test.ru
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'test/spec'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
require 'rack/mock'
|
5
|
+
|
6
|
+
$-w, w = nil, $-w # yuck
|
7
|
+
require 'camping'
|
8
|
+
require 'rack/adapter/camping'
|
9
|
+
|
10
|
+
Camping.goes :CampApp
|
11
|
+
module CampApp
|
12
|
+
module Controllers
|
13
|
+
class HW < R('/')
|
14
|
+
def get
|
15
|
+
"Camping works!"
|
16
|
+
end
|
17
|
+
|
18
|
+
def post
|
19
|
+
"Data: #{input.foo}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
$-w = w
|
25
|
+
|
26
|
+
context "Rack::Adapter::Camping" do
|
27
|
+
specify "works with GET" do
|
28
|
+
res = Rack::MockRequest.new(Rack::Adapter::Camping.new(CampApp)).
|
29
|
+
get("/")
|
30
|
+
|
31
|
+
res.should.be.ok
|
32
|
+
res["Content-Type"].should.equal "text/html"
|
33
|
+
|
34
|
+
res.body.should.equal "Camping works!"
|
35
|
+
end
|
36
|
+
|
37
|
+
specify "works with POST" do
|
38
|
+
res = Rack::MockRequest.new(Rack::Adapter::Camping.new(CampApp)).
|
39
|
+
post("/", :input => "foo=bar")
|
40
|
+
|
41
|
+
res.should.be.ok
|
42
|
+
res.body.should.equal "Data: bar"
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'test/spec'
|
2
|
+
|
3
|
+
require 'rack/cascade'
|
4
|
+
require 'rack/mock'
|
5
|
+
|
6
|
+
require 'rack/urlmap'
|
7
|
+
require 'rack/file'
|
8
|
+
|
9
|
+
context "Rack::Cascade" do
|
10
|
+
docroot = File.expand_path(File.dirname(__FILE__))
|
11
|
+
app1 = Rack::File.new(docroot)
|
12
|
+
|
13
|
+
app2 = Rack::URLMap.new("/crash" => lambda { |env| raise "boom" })
|
14
|
+
|
15
|
+
app3 = Rack::URLMap.new("/foo" => lambda { |env|
|
16
|
+
[200, { "Content-Type" => "text/plain"}, [""]]})
|
17
|
+
|
18
|
+
specify "should dispatch onward on 404 by default" do
|
19
|
+
cascade = Rack::Cascade.new([app1, app2, app3])
|
20
|
+
Rack::MockRequest.new(cascade).get("/cgi/test").should.be.ok
|
21
|
+
Rack::MockRequest.new(cascade).get("/foo").should.be.ok
|
22
|
+
Rack::MockRequest.new(cascade).get("/toobad").should.be.not_found
|
23
|
+
Rack::MockRequest.new(cascade).get("/cgi/../bla").should.be.forbidden
|
24
|
+
end
|
25
|
+
|
26
|
+
specify "should dispatch onward on whatever is passed" do
|
27
|
+
cascade = Rack::Cascade.new([app1, app2, app3], [404, 403])
|
28
|
+
Rack::MockRequest.new(cascade).get("/cgi/../bla").should.be.not_found
|
29
|
+
end
|
30
|
+
|
31
|
+
specify "should fail if empty" do
|
32
|
+
lambda { Rack::MockRequest.new(Rack::Cascade.new([])).get("/") }.
|
33
|
+
should.raise(ArgumentError)
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'test/spec'
|
2
|
+
require 'testrequest'
|
3
|
+
|
4
|
+
pid = fork {
|
5
|
+
exec "cd #{File.join(File.dirname(__FILE__), 'cgi')} && lighttpd -D -f lighttpd.conf"
|
6
|
+
}
|
7
|
+
|
8
|
+
at_exit {
|
9
|
+
Process.kill 15, pid
|
10
|
+
}
|
11
|
+
|
12
|
+
context "Rack::Handler::CGI" do
|
13
|
+
include TestRequest::Helpers
|
14
|
+
|
15
|
+
setup do
|
16
|
+
@host = '0.0.0.0'
|
17
|
+
@port = 9203
|
18
|
+
end
|
19
|
+
|
20
|
+
specify "should respond" do
|
21
|
+
lambda {
|
22
|
+
GET("/test")
|
23
|
+
}.should.not.raise
|
24
|
+
end
|
25
|
+
|
26
|
+
specify "should be a lighttpd" do
|
27
|
+
GET("/test")
|
28
|
+
status.should.be 200
|
29
|
+
response["SERVER_SOFTWARE"].should =~ /lighttpd/
|
30
|
+
response["HTTP_VERSION"].should.equal "HTTP/1.1"
|
31
|
+
response["SERVER_PROTOCOL"].should.equal "HTTP/1.1"
|
32
|
+
response["SERVER_PORT"].should.equal "9203"
|
33
|
+
response["SERVER_NAME"].should =~ "0.0.0.0"
|
34
|
+
end
|
35
|
+
|
36
|
+
specify "should have rack headers" do
|
37
|
+
GET("/test")
|
38
|
+
response["rack.version"].should.equal [0,1]
|
39
|
+
response["rack.multithread"].should.be false
|
40
|
+
response["rack.multiprocess"].should.be true
|
41
|
+
response["rack.run_once"].should.be true
|
42
|
+
end
|
43
|
+
|
44
|
+
specify "should have CGI headers on GET" do
|
45
|
+
GET("/test")
|
46
|
+
response["REQUEST_METHOD"].should.equal "GET"
|
47
|
+
response["SCRIPT_NAME"].should.equal "/test"
|
48
|
+
response["REQUEST_PATH"].should.equal "/"
|
49
|
+
response["PATH_INFO"].should.be.nil
|
50
|
+
response["QUERY_STRING"].should.equal ""
|
51
|
+
response["test.postdata"].should.equal ""
|
52
|
+
|
53
|
+
GET("/test/foo?quux=1")
|
54
|
+
response["REQUEST_METHOD"].should.equal "GET"
|
55
|
+
response["SCRIPT_NAME"].should.equal "/test"
|
56
|
+
response["REQUEST_PATH"].should.equal "/"
|
57
|
+
response["PATH_INFO"].should.equal "/foo"
|
58
|
+
response["QUERY_STRING"].should.equal "quux=1"
|
59
|
+
end
|
60
|
+
|
61
|
+
specify "should have CGI headers on POST" do
|
62
|
+
POST("/test", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
|
63
|
+
status.should.equal 200
|
64
|
+
response["REQUEST_METHOD"].should.equal "POST"
|
65
|
+
response["SCRIPT_NAME"].should.equal "/test"
|
66
|
+
response["REQUEST_PATH"].should.equal "/"
|
67
|
+
response["QUERY_STRING"].should.equal ""
|
68
|
+
response["HTTP_X_TEST_HEADER"].should.equal "42"
|
69
|
+
response["test.postdata"].should.equal "rack-form-data=23"
|
70
|
+
end
|
71
|
+
|
72
|
+
specify "should support HTTP auth" do
|
73
|
+
GET("/test", {:user => "ruth", :passwd => "secret"})
|
74
|
+
response["HTTP_AUTHORIZATION"].should.equal "Basic cnV0aDpzZWNyZXQ="
|
75
|
+
end
|
76
|
+
|
77
|
+
specify "should set status" do
|
78
|
+
GET("/test?secret")
|
79
|
+
status.should.equal 403
|
80
|
+
response["rack.url_scheme"].should.equal "http"
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'test/spec'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
require 'rack/commonlogger'
|
5
|
+
require 'rack/lobster'
|
6
|
+
require 'rack/mock'
|
7
|
+
|
8
|
+
context "Rack::CommonLogger" do
|
9
|
+
app = lambda { |env|
|
10
|
+
[200,
|
11
|
+
{"Content-Type" => "text/html"},
|
12
|
+
["foo"]]}
|
13
|
+
|
14
|
+
specify "should log to rack.errors by default" do
|
15
|
+
log = StringIO.new
|
16
|
+
res = Rack::MockRequest.new(Rack::CommonLogger.new(app)).get("/")
|
17
|
+
|
18
|
+
res.errors.should.not.be.empty
|
19
|
+
res.errors.should =~ /GET /
|
20
|
+
res.errors.should =~ / 200 / # status
|
21
|
+
res.errors.should =~ / 3 / # length
|
22
|
+
end
|
23
|
+
|
24
|
+
specify "should log to anything with <<" do
|
25
|
+
log = ""
|
26
|
+
res = Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/")
|
27
|
+
|
28
|
+
log.should =~ /GET /
|
29
|
+
log.should =~ / 200 / # status
|
30
|
+
log.should =~ / 3 / # length
|
31
|
+
end
|
32
|
+
end
|