technomancy-rack 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. data/KNOWN-ISSUES +18 -0
  2. data/README +242 -0
  3. data/bin/rackup +183 -0
  4. data/lib/rack.rb +92 -0
  5. data/lib/rack/adapter/camping.rb +22 -0
  6. data/lib/rack/auth/abstract/handler.rb +28 -0
  7. data/lib/rack/auth/abstract/request.rb +37 -0
  8. data/lib/rack/auth/basic.rb +58 -0
  9. data/lib/rack/auth/digest/md5.rb +124 -0
  10. data/lib/rack/auth/digest/nonce.rb +51 -0
  11. data/lib/rack/auth/digest/params.rb +55 -0
  12. data/lib/rack/auth/digest/request.rb +40 -0
  13. data/lib/rack/auth/openid.rb +116 -0
  14. data/lib/rack/builder.rb +56 -0
  15. data/lib/rack/cascade.rb +36 -0
  16. data/lib/rack/commonlogger.rb +56 -0
  17. data/lib/rack/file.rb +112 -0
  18. data/lib/rack/handler/cgi.rb +57 -0
  19. data/lib/rack/handler/fastcgi.rb +83 -0
  20. data/lib/rack/handler/lsws.rb +52 -0
  21. data/lib/rack/handler/mongrel.rb +97 -0
  22. data/lib/rack/handler/scgi.rb +57 -0
  23. data/lib/rack/handler/webrick.rb +57 -0
  24. data/lib/rack/lint.rb +394 -0
  25. data/lib/rack/lobster.rb +65 -0
  26. data/lib/rack/mock.rb +172 -0
  27. data/lib/rack/recursive.rb +57 -0
  28. data/lib/rack/reloader.rb +64 -0
  29. data/lib/rack/request.rb +197 -0
  30. data/lib/rack/response.rb +166 -0
  31. data/lib/rack/session/abstract/id.rb +126 -0
  32. data/lib/rack/session/cookie.rb +71 -0
  33. data/lib/rack/session/memcache.rb +83 -0
  34. data/lib/rack/session/pool.rb +67 -0
  35. data/lib/rack/showexceptions.rb +344 -0
  36. data/lib/rack/showstatus.rb +103 -0
  37. data/lib/rack/static.rb +38 -0
  38. data/lib/rack/urlmap.rb +48 -0
  39. data/lib/rack/utils.rb +256 -0
  40. data/test/spec_rack_auth_basic.rb +69 -0
  41. data/test/spec_rack_auth_digest.rb +169 -0
  42. data/test/spec_rack_builder.rb +50 -0
  43. data/test/spec_rack_camping.rb +47 -0
  44. data/test/spec_rack_cascade.rb +50 -0
  45. data/test/spec_rack_cgi.rb +91 -0
  46. data/test/spec_rack_commonlogger.rb +32 -0
  47. data/test/spec_rack_fastcgi.rb +91 -0
  48. data/test/spec_rack_file.rb +40 -0
  49. data/test/spec_rack_lint.rb +317 -0
  50. data/test/spec_rack_lobster.rb +45 -0
  51. data/test/spec_rack_mock.rb +152 -0
  52. data/test/spec_rack_mongrel.rb +165 -0
  53. data/test/spec_rack_recursive.rb +77 -0
  54. data/test/spec_rack_request.rb +384 -0
  55. data/test/spec_rack_response.rb +167 -0
  56. data/test/spec_rack_session_cookie.rb +49 -0
  57. data/test/spec_rack_session_memcache.rb +100 -0
  58. data/test/spec_rack_session_pool.rb +84 -0
  59. data/test/spec_rack_showexceptions.rb +21 -0
  60. data/test/spec_rack_showstatus.rb +71 -0
  61. data/test/spec_rack_static.rb +37 -0
  62. data/test/spec_rack_urlmap.rb +175 -0
  63. data/test/spec_rack_utils.rb +69 -0
  64. data/test/spec_rack_webrick.rb +106 -0
  65. data/test/testrequest.rb +43 -0
  66. metadata +167 -0
@@ -0,0 +1,48 @@
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
+ unless location[0] == ?/
24
+ raise ArgumentError, "paths need to start with /"
25
+ end
26
+ location = location.chomp('/')
27
+
28
+ [host, location, app]
29
+ }.sort_by { |(h, l, a)| -l.size } # Longest path first
30
+ end
31
+
32
+ def call(env)
33
+ path = env["PATH_INFO"].to_s.squeeze("/")
34
+ hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT')
35
+ @mapping.each { |host, location, app|
36
+ next unless (hHost == host || sName == host \
37
+ || (host.nil? && (hHost == sName || hHost == sName+':'+sPort)))
38
+ next unless location == path[0, location.size]
39
+ next unless path[location.size] == nil || path[location.size] == ?/
40
+ env["SCRIPT_NAME"] += location
41
+ env["PATH_INFO"] = path[location.size..-1]
42
+ return app.call(env)
43
+ }
44
+ [404, {"Content-Type" => "text/plain"}, ["Not Found: #{path}"]]
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,256 @@
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, with some small modifications:
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
+
35
+ (qs || '').split(/[#{d}] */n).each do |p|
36
+ k, v = unescape(p).split('=', 2)
37
+
38
+ if cur = params[k]
39
+ if cur.class == Array
40
+ params[k] << v
41
+ else
42
+ params[k] = [cur, v]
43
+ end
44
+ else
45
+ params[k] = v
46
+ end
47
+ end
48
+
49
+ return params
50
+ end
51
+ module_function :parse_query
52
+
53
+ def build_query(params)
54
+ params.map { |k, v|
55
+ if v.class == Array
56
+ build_query(v.map { |x| [k, x] })
57
+ else
58
+ escape(k) + "=" + escape(v)
59
+ end
60
+ }.join("&")
61
+ end
62
+ module_function :build_query
63
+
64
+ # Escape ampersands, brackets and quotes to their HTML/XML entities.
65
+ def escape_html(string)
66
+ string.to_s.gsub("&", "&amp;").
67
+ gsub("<", "&lt;").
68
+ gsub(">", "&gt;").
69
+ gsub("'", "&#39;").
70
+ gsub('"', "&quot;")
71
+ end
72
+ module_function :escape_html
73
+
74
+ class Context < Proc
75
+ attr_reader :for, :app
76
+ def initialize app_f=nil, app_r=nil
77
+ @for, @app = app_f, app_r
78
+ end
79
+ alias_method :old_inspect, :inspect
80
+ def inspect
81
+ "#{old_inspect} ==> #{@for.inspect} ==> #{@app.inspect}"
82
+ end
83
+ def pretty_print pp
84
+ pp.text old_inspect
85
+ pp.nest 1 do
86
+ pp.breakable
87
+ pp.text '=for> '
88
+ pp.pp @for
89
+ pp.breakable
90
+ pp.text '=app> '
91
+ pp.pp @app
92
+ end
93
+ end
94
+ end
95
+
96
+ # A case-normalizing Hash, adjusting on [] and []=.
97
+ class HeaderHash < Hash
98
+ def initialize(hash={})
99
+ hash.each { |k, v| self[k] = v }
100
+ end
101
+
102
+ def to_hash
103
+ {}.replace(self)
104
+ end
105
+
106
+ def [](k)
107
+ super capitalize(k)
108
+ end
109
+
110
+ def []=(k, v)
111
+ super capitalize(k), v
112
+ end
113
+
114
+ def capitalize(k)
115
+ k.to_s.downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }
116
+ end
117
+ end
118
+
119
+ # Every standard HTTP code mapped to the appropriate message.
120
+ # Stolen from Mongrel.
121
+ HTTP_STATUS_CODES = {
122
+ 100 => 'Continue',
123
+ 101 => 'Switching Protocols',
124
+ 200 => 'OK',
125
+ 201 => 'Created',
126
+ 202 => 'Accepted',
127
+ 203 => 'Non-Authoritative Information',
128
+ 204 => 'No Content',
129
+ 205 => 'Reset Content',
130
+ 206 => 'Partial Content',
131
+ 300 => 'Multiple Choices',
132
+ 301 => 'Moved Permanently',
133
+ 302 => 'Moved Temporarily',
134
+ 303 => 'See Other',
135
+ 304 => 'Not Modified',
136
+ 305 => 'Use Proxy',
137
+ 400 => 'Bad Request',
138
+ 401 => 'Unauthorized',
139
+ 402 => 'Payment Required',
140
+ 403 => 'Forbidden',
141
+ 404 => 'Not Found',
142
+ 405 => 'Method Not Allowed',
143
+ 406 => 'Not Acceptable',
144
+ 407 => 'Proxy Authentication Required',
145
+ 408 => 'Request Time-out',
146
+ 409 => 'Conflict',
147
+ 410 => 'Gone',
148
+ 411 => 'Length Required',
149
+ 412 => 'Precondition Failed',
150
+ 413 => 'Request Entity Too Large',
151
+ 414 => 'Request-URI Too Large',
152
+ 415 => 'Unsupported Media Type',
153
+ 500 => 'Internal Server Error',
154
+ 501 => 'Not Implemented',
155
+ 502 => 'Bad Gateway',
156
+ 503 => 'Service Unavailable',
157
+ 504 => 'Gateway Time-out',
158
+ 505 => 'HTTP Version not supported'
159
+ }
160
+
161
+ # A multipart form data parser, adapted from IOWA.
162
+ #
163
+ # Usually, Rack::Request#POST takes care of calling this.
164
+
165
+ module Multipart
166
+ EOL = "\r\n"
167
+
168
+ def self.parse_multipart(env)
169
+ unless env['CONTENT_TYPE'] =~
170
+ %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n
171
+ nil
172
+ else
173
+ boundary = "--#{$1}"
174
+
175
+ params = {}
176
+ buf = ""
177
+ content_length = env['CONTENT_LENGTH'].to_i
178
+ input = env['rack.input']
179
+
180
+ boundary_size = boundary.size + EOL.size
181
+ bufsize = 16384
182
+
183
+ content_length -= boundary_size
184
+
185
+ status = input.read(boundary_size)
186
+ raise EOFError, "bad content body" unless status == boundary + EOL
187
+
188
+ rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/
189
+
190
+ loop {
191
+ head = nil
192
+ body = ''
193
+ filename = content_type = name = nil
194
+
195
+ until head && buf =~ rx
196
+ if !head && i = buf.index("\r\n\r\n")
197
+ head = buf.slice!(0, i+2) # First \r\n
198
+ buf.slice!(0, 2) # Second \r\n
199
+
200
+ filename = head[/Content-Disposition:.* filename="?([^\";]*)"?/ni, 1]
201
+ content_type = head[/Content-Type: (.*)\r\n/ni, 1]
202
+ name = head[/Content-Disposition:.* name="?([^\";]*)"?/ni, 1]
203
+
204
+ if filename
205
+ body = Tempfile.new("RackMultipart")
206
+ body.binmode
207
+ end
208
+
209
+ next
210
+ end
211
+
212
+ # Save the read body part.
213
+ if head && (boundary_size+4 < buf.size)
214
+ body << buf.slice!(0, buf.size - (boundary_size+4))
215
+ end
216
+
217
+ c = input.read(bufsize < content_length ? bufsize : content_length)
218
+ raise EOFError, "bad content body" if c.nil? || c.empty?
219
+ buf << c
220
+ content_length -= c.size
221
+ end
222
+
223
+ # Save the rest.
224
+ if i = buf.index(rx)
225
+ body << buf.slice!(0, i)
226
+ buf.slice!(0, boundary_size+2)
227
+
228
+ content_length = -1 if $1 == "--"
229
+ end
230
+
231
+ if filename
232
+ body.rewind
233
+ data = {:filename => filename, :type => content_type,
234
+ :name => name, :tempfile => body, :head => head}
235
+ else
236
+ data = body
237
+ end
238
+
239
+ if name
240
+ if name =~ /\[\]\z/
241
+ params[name] ||= []
242
+ params[name] << data
243
+ else
244
+ params[name] = data
245
+ end
246
+ end
247
+
248
+ break if buf.empty? || content_length == -1
249
+ }
250
+
251
+ params
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,69 @@
1
+ require 'test/spec'
2
+
3
+ require 'rack/auth/basic'
4
+ require 'rack/mock'
5
+
6
+ context 'Rack::Auth::Basic' do
7
+
8
+ def realm
9
+ 'WallysWorld'
10
+ end
11
+
12
+ def unprotected_app
13
+ lambda { |env| [ 200, {'Content-Type' => 'text/plain'}, ["Hi #{env['REMOTE_USER']}"] ] }
14
+ end
15
+
16
+ def protected_app
17
+ app = Rack::Auth::Basic.new(unprotected_app) { |username, password| 'Boss' == username }
18
+ app.realm = realm
19
+ app
20
+ end
21
+
22
+ setup do
23
+ @request = Rack::MockRequest.new(protected_app)
24
+ end
25
+
26
+ def request_with_basic_auth(username, password, &block)
27
+ request 'HTTP_AUTHORIZATION' => 'Basic ' + ["#{username}:#{password}"].pack("m*"), &block
28
+ end
29
+
30
+ def request(headers = {})
31
+ yield @request.get('/', headers)
32
+ end
33
+
34
+ def assert_basic_auth_challenge(response)
35
+ response.should.be.a.client_error
36
+ response.status.should.equal 401
37
+ response.should.include 'WWW-Authenticate'
38
+ response.headers['WWW-Authenticate'].should =~ /Basic realm="/
39
+ response.body.should.be.empty
40
+ end
41
+
42
+ specify 'should challenge correctly when no credentials are specified' do
43
+ request do |response|
44
+ assert_basic_auth_challenge response
45
+ end
46
+ end
47
+
48
+ specify 'should rechallenge if incorrect credentials are specified' do
49
+ request_with_basic_auth 'joe', 'password' do |response|
50
+ assert_basic_auth_challenge response
51
+ end
52
+ end
53
+
54
+ specify 'should return application output if correct credentials are specified' do
55
+ request_with_basic_auth 'Boss', 'password' do |response|
56
+ response.status.should.equal 200
57
+ response.body.to_s.should.equal 'Hi Boss'
58
+ end
59
+ end
60
+
61
+ specify 'should return 400 Bad Request if different auth scheme used' do
62
+ request 'HTTP_AUTHORIZATION' => 'Digest params' do |response|
63
+ response.should.be.a.client_error
64
+ response.status.should.equal 400
65
+ response.should.not.include 'WWW-Authenticate'
66
+ end
67
+ end
68
+
69
+ end
@@ -0,0 +1,169 @@
1
+ require 'test/spec'
2
+
3
+ require 'rack/auth/digest/md5'
4
+ require 'rack/mock'
5
+
6
+ context 'Rack::Auth::Digest::MD5' do
7
+
8
+ def realm
9
+ 'WallysWorld'
10
+ end
11
+
12
+ def unprotected_app
13
+ lambda do |env|
14
+ [ 200, {'Content-Type' => 'text/plain'}, ["Hi #{env['REMOTE_USER']}"] ]
15
+ end
16
+ end
17
+
18
+ def protected_app
19
+ app = Rack::Auth::Digest::MD5.new(unprotected_app) do |username|
20
+ { 'Alice' => 'correct-password' }[username]
21
+ end
22
+ app.realm = realm
23
+ app.opaque = 'this-should-be-secret'
24
+ app
25
+ end
26
+
27
+ def protected_app_with_hashed_passwords
28
+ app = Rack::Auth::Digest::MD5.new(unprotected_app) do |username|
29
+ username == 'Alice' ? Digest::MD5.hexdigest("Alice:#{realm}:correct-password") : nil
30
+ end
31
+ app.realm = realm
32
+ app.opaque = 'this-should-be-secret'
33
+ app.passwords_hashed = true
34
+ app
35
+ end
36
+
37
+ setup do
38
+ @request = Rack::MockRequest.new(protected_app)
39
+ end
40
+
41
+ def request(path, headers = {}, &block)
42
+ response = @request.get(path, headers)
43
+ block.call(response) if block
44
+ return response
45
+ end
46
+
47
+ class MockDigestRequest
48
+ def initialize(params)
49
+ @params = params
50
+ end
51
+ def method_missing(sym)
52
+ if @params.has_key? k = sym.to_s
53
+ return @params[k]
54
+ end
55
+ super
56
+ end
57
+ def method
58
+ 'GET'
59
+ end
60
+ def response(password)
61
+ Rack::Auth::Digest::MD5.new(nil).send :digest, self, password
62
+ end
63
+ end
64
+
65
+ def request_with_digest_auth(path, username, password, options = {}, &block)
66
+ response = request('/')
67
+
68
+ return response unless response.status == 401
69
+
70
+ if wait = options.delete(:wait)
71
+ sleep wait
72
+ end
73
+
74
+ challenge = response['WWW-Authenticate'].split(' ', 2).last
75
+
76
+ params = Rack::Auth::Digest::Params.parse(challenge)
77
+
78
+ params['username'] = username
79
+ params['nc'] = '00000001'
80
+ params['cnonce'] = 'nonsensenonce'
81
+ params['uri'] = path
82
+
83
+ params.update options
84
+
85
+ params['response'] = MockDigestRequest.new(params).response(password)
86
+
87
+ request(path, { 'HTTP_AUTHORIZATION' => "Digest #{params}" }, &block)
88
+ end
89
+
90
+ def assert_digest_auth_challenge(response)
91
+ response.should.be.a.client_error
92
+ response.status.should.equal 401
93
+ response.should.include 'WWW-Authenticate'
94
+ response.headers['WWW-Authenticate'].should =~ /^Digest /
95
+ response.body.should.be.empty
96
+ end
97
+
98
+ def assert_bad_request(response)
99
+ response.should.be.a.client_error
100
+ response.status.should.equal 400
101
+ response.should.not.include 'WWW-Authenticate'
102
+ end
103
+
104
+ specify 'should challenge when no credentials are specified' do
105
+ request '/' do |response|
106
+ assert_digest_auth_challenge response
107
+ end
108
+ end
109
+
110
+ specify 'should return application output if correct credentials given' do
111
+ request_with_digest_auth '/', 'Alice', 'correct-password' do |response|
112
+ response.status.should.equal 200
113
+ response.body.to_s.should.equal 'Hi Alice'
114
+ end
115
+ end
116
+
117
+ specify 'should return application output if correct credentials given (hashed passwords)' do
118
+ @request = Rack::MockRequest.new(protected_app_with_hashed_passwords)
119
+
120
+ request_with_digest_auth '/', 'Alice', 'correct-password' do |response|
121
+ response.status.should.equal 200
122
+ response.body.to_s.should.equal 'Hi Alice'
123
+ end
124
+ end
125
+
126
+ specify 'should rechallenge if incorrect username given' do
127
+ request_with_digest_auth '/', 'Bob', 'correct-password' do |response|
128
+ assert_digest_auth_challenge response
129
+ end
130
+ end
131
+
132
+ specify 'should rechallenge if incorrect password given' do
133
+ request_with_digest_auth '/', 'Alice', 'wrong-password' do |response|
134
+ assert_digest_auth_challenge response
135
+ end
136
+ end
137
+
138
+ specify 'should rechallenge with stale parameter if nonce is stale' do
139
+ begin
140
+ Rack::Auth::Digest::Nonce.time_limit = 1
141
+
142
+ request_with_digest_auth '/', 'Alice', 'correct-password', :wait => 2 do |response|
143
+ assert_digest_auth_challenge response
144
+ response.headers['WWW-Authenticate'].should =~ /\bstale=true\b/
145
+ end
146
+ ensure
147
+ Rack::Auth::Digest::Nonce.time_limit = nil
148
+ end
149
+ end
150
+
151
+ specify 'should return 400 Bad Request if incorrect qop given' do
152
+ request_with_digest_auth '/', 'Alice', 'correct-password', 'qop' => 'auth-int' do |response|
153
+ assert_bad_request response
154
+ end
155
+ end
156
+
157
+ specify 'should return 400 Bad Request if incorrect uri given' do
158
+ request_with_digest_auth '/', 'Alice', 'correct-password', 'uri' => '/foo' do |response|
159
+ assert_bad_request response
160
+ end
161
+ end
162
+
163
+ specify 'should return 400 Bad Request if different auth scheme used' do
164
+ request '/', 'HTTP_AUTHORIZATION' => 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' do |response|
165
+ assert_bad_request response
166
+ end
167
+ end
168
+
169
+ end