rack 0.9.1 → 1.0.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.

Files changed (79) hide show
  1. data/COPYING +1 -1
  2. data/RDOX +115 -16
  3. data/README +54 -7
  4. data/Rakefile +61 -85
  5. data/SPEC +50 -17
  6. data/bin/rackup +9 -5
  7. data/example/protectedlobster.ru +1 -1
  8. data/lib/rack.rb +7 -3
  9. data/lib/rack/auth/abstract/handler.rb +13 -4
  10. data/lib/rack/auth/digest/md5.rb +1 -1
  11. data/lib/rack/auth/digest/request.rb +2 -2
  12. data/lib/rack/auth/openid.rb +344 -302
  13. data/lib/rack/builder.rb +1 -5
  14. data/lib/rack/chunked.rb +49 -0
  15. data/lib/rack/conditionalget.rb +4 -0
  16. data/lib/rack/content_length.rb +7 -3
  17. data/lib/rack/content_type.rb +23 -0
  18. data/lib/rack/deflater.rb +83 -74
  19. data/lib/rack/directory.rb +5 -2
  20. data/lib/rack/file.rb +4 -1
  21. data/lib/rack/handler.rb +22 -1
  22. data/lib/rack/handler/cgi.rb +7 -3
  23. data/lib/rack/handler/fastcgi.rb +26 -24
  24. data/lib/rack/handler/lsws.rb +7 -4
  25. data/lib/rack/handler/mongrel.rb +5 -3
  26. data/lib/rack/handler/scgi.rb +5 -3
  27. data/lib/rack/handler/thin.rb +3 -0
  28. data/lib/rack/handler/webrick.rb +11 -5
  29. data/lib/rack/lint.rb +138 -66
  30. data/lib/rack/lock.rb +16 -0
  31. data/lib/rack/mime.rb +4 -4
  32. data/lib/rack/mock.rb +3 -3
  33. data/lib/rack/reloader.rb +88 -46
  34. data/lib/rack/request.rb +46 -10
  35. data/lib/rack/response.rb +15 -3
  36. data/lib/rack/rewindable_input.rb +98 -0
  37. data/lib/rack/session/abstract/id.rb +71 -82
  38. data/lib/rack/session/cookie.rb +2 -0
  39. data/lib/rack/session/memcache.rb +59 -47
  40. data/lib/rack/session/pool.rb +56 -29
  41. data/lib/rack/showexceptions.rb +2 -1
  42. data/lib/rack/showstatus.rb +1 -1
  43. data/lib/rack/urlmap.rb +12 -5
  44. data/lib/rack/utils.rb +115 -65
  45. data/rack.gemspec +54 -0
  46. data/test/multipart/binary +0 -0
  47. data/test/multipart/empty +10 -0
  48. data/test/multipart/ie +6 -0
  49. data/test/multipart/nested +10 -0
  50. data/test/multipart/none +9 -0
  51. data/test/multipart/text +10 -0
  52. data/test/spec_rack_auth_basic.rb +5 -1
  53. data/test/spec_rack_auth_digest.rb +93 -36
  54. data/test/spec_rack_auth_openid.rb +47 -100
  55. data/test/spec_rack_builder.rb +2 -2
  56. data/test/spec_rack_chunked.rb +62 -0
  57. data/test/spec_rack_conditionalget.rb +7 -7
  58. data/test/spec_rack_content_type.rb +30 -0
  59. data/test/spec_rack_deflater.rb +36 -14
  60. data/test/spec_rack_directory.rb +1 -1
  61. data/test/spec_rack_file.rb +11 -0
  62. data/test/spec_rack_handler.rb +21 -2
  63. data/test/spec_rack_lint.rb +163 -44
  64. data/test/spec_rack_lock.rb +38 -0
  65. data/test/spec_rack_mock.rb +6 -1
  66. data/test/spec_rack_request.rb +81 -12
  67. data/test/spec_rack_response.rb +46 -2
  68. data/test/spec_rack_rewindable_input.rb +118 -0
  69. data/test/spec_rack_session_memcache.rb +170 -62
  70. data/test/spec_rack_session_pool.rb +129 -41
  71. data/test/spec_rack_static.rb +2 -2
  72. data/test/spec_rack_thin.rb +3 -2
  73. data/test/spec_rack_urlmap.rb +10 -0
  74. data/test/spec_rack_utils.rb +214 -49
  75. data/test/spec_rack_webrick.rb +7 -0
  76. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  77. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  78. metadata +95 -6
  79. data/AUTHORS +0 -8
@@ -1,5 +1,6 @@
1
1
  require 'lsapi'
2
- #require 'cgi'
2
+ require 'rack/content_length'
3
+
3
4
  module Rack
4
5
  module Handler
5
6
  class LSWS
@@ -9,12 +10,14 @@ module Rack
9
10
  end
10
11
  end
11
12
  def self.serve(app)
13
+ app = Rack::ContentLength.new(app)
14
+
12
15
  env = ENV.to_hash
13
16
  env.delete "HTTP_CONTENT_LENGTH"
14
17
  env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
15
18
  env.update({"rack.version" => [0,1],
16
- "rack.input" => STDIN,
17
- "rack.errors" => STDERR,
19
+ "rack.input" => StringIO.new($stdin.read.to_s),
20
+ "rack.errors" => $stderr,
18
21
  "rack.multithread" => false,
19
22
  "rack.multiprocess" => true,
20
23
  "rack.run_once" => false,
@@ -34,7 +37,7 @@ module Rack
34
37
  def self.send_headers(status, headers)
35
38
  print "Status: #{status}\r\n"
36
39
  headers.each { |k, vs|
37
- vs.each { |v|
40
+ vs.split("\n").each { |v|
38
41
  print "#{k}: #{v}\r\n"
39
42
  }
40
43
  }
@@ -1,5 +1,7 @@
1
1
  require 'mongrel'
2
2
  require 'stringio'
3
+ require 'rack/content_length'
4
+ require 'rack/chunked'
3
5
 
4
6
  module Rack
5
7
  module Handler
@@ -33,7 +35,7 @@ module Rack
33
35
  end
34
36
 
35
37
  def initialize(app)
36
- @app = app
38
+ @app = Rack::Chunked.new(Rack::ContentLength.new(app))
37
39
  end
38
40
 
39
41
  def process(request, response)
@@ -45,7 +47,7 @@ module Rack
45
47
 
46
48
  env.update({"rack.version" => [0,1],
47
49
  "rack.input" => request.body || StringIO.new(""),
48
- "rack.errors" => STDERR,
50
+ "rack.errors" => $stderr,
49
51
 
50
52
  "rack.multithread" => true,
51
53
  "rack.multiprocess" => false, # ???
@@ -63,7 +65,7 @@ module Rack
63
65
  response.send_status(nil)
64
66
 
65
67
  headers.each { |k, vs|
66
- vs.each { |v|
68
+ vs.split("\n").each { |v|
67
69
  response.header[k] = v
68
70
  }
69
71
  }
@@ -1,5 +1,7 @@
1
1
  require 'scgi'
2
2
  require 'stringio'
3
+ require 'rack/content_length'
4
+ require 'rack/chunked'
3
5
 
4
6
  module Rack
5
7
  module Handler
@@ -14,7 +16,7 @@ module Rack
14
16
  end
15
17
 
16
18
  def initialize(settings = {})
17
- @app = settings[:app]
19
+ @app = Rack::Chunked.new(Rack::ContentLength.new(settings[:app]))
18
20
  @log = Object.new
19
21
  def @log.info(*args); end
20
22
  def @log.error(*args); end
@@ -32,7 +34,7 @@ module Rack
32
34
  env["SCRIPT_NAME"] = ""
33
35
  env.update({"rack.version" => [0,1],
34
36
  "rack.input" => StringIO.new(input_body),
35
- "rack.errors" => STDERR,
37
+ "rack.errors" => $stderr,
36
38
 
37
39
  "rack.multithread" => true,
38
40
  "rack.multiprocess" => true,
@@ -44,7 +46,7 @@ module Rack
44
46
  begin
45
47
  socket.write("Status: #{status}\r\n")
46
48
  headers.each do |k, vs|
47
- vs.each {|v| socket.write("#{k}: #{v}\r\n")}
49
+ vs.split("\n").each { |v| socket.write("#{k}: #{v}\r\n")}
48
50
  end
49
51
  socket.write("\r\n")
50
52
  body.each {|s| socket.write(s)}
@@ -1,9 +1,12 @@
1
1
  require "thin"
2
+ require "rack/content_length"
3
+ require "rack/chunked"
2
4
 
3
5
  module Rack
4
6
  module Handler
5
7
  class Thin
6
8
  def self.run(app, options={})
9
+ app = Rack::Chunked.new(Rack::ContentLength.new(app))
7
10
  server = ::Thin::Server.new(options[:Host] || '0.0.0.0',
8
11
  options[:Port] || 8080,
9
12
  app)
@@ -1,5 +1,6 @@
1
1
  require 'webrick'
2
2
  require 'stringio'
3
+ require 'rack/content_length'
3
4
 
4
5
  module Rack
5
6
  module Handler
@@ -14,7 +15,7 @@ module Rack
14
15
 
15
16
  def initialize(server, app)
16
17
  super server
17
- @app = app
18
+ @app = Rack::ContentLength.new(app)
18
19
  end
19
20
 
20
21
  def service(req, res)
@@ -23,7 +24,7 @@ module Rack
23
24
 
24
25
  env.update({"rack.version" => [0,1],
25
26
  "rack.input" => StringIO.new(req.body.to_s),
26
- "rack.errors" => STDERR,
27
+ "rack.errors" => $stderr,
27
28
 
28
29
  "rack.multithread" => true,
29
30
  "rack.multiprocess" => false,
@@ -35,16 +36,21 @@ module Rack
35
36
  env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
36
37
  env["QUERY_STRING"] ||= ""
37
38
  env["REQUEST_PATH"] ||= "/"
38
- env.delete "PATH_INFO" if env["PATH_INFO"] == ""
39
+ if env["PATH_INFO"] == ""
40
+ env.delete "PATH_INFO"
41
+ else
42
+ path, n = req.request_uri.path, env["SCRIPT_NAME"].length
43
+ env["PATH_INFO"] = path[n, path.length-n]
44
+ end
39
45
 
40
46
  status, headers, body = @app.call(env)
41
47
  begin
42
48
  res.status = status.to_i
43
49
  headers.each { |k, vs|
44
50
  if k.downcase == "set-cookie"
45
- res.cookies.concat vs.to_a
51
+ res.cookies.concat vs.split("\n")
46
52
  else
47
- vs.each { |v|
53
+ vs.split("\n").each { |v|
48
54
  res[k] = v
49
55
  }
50
56
  end
@@ -1,3 +1,5 @@
1
+ require 'rack/utils'
2
+
1
3
  module Rack
2
4
  # Rack::Lint validates your application and the requests and
3
5
  # responses according to the Rack spec.
@@ -86,7 +88,9 @@ module Rack
86
88
  ## within the application. This may be an
87
89
  ## empty string, if the request URL targets
88
90
  ## the application root and does not have a
89
- ## trailing slash.
91
+ ## trailing slash. This value may be
92
+ ## percent-encoded when I originating from
93
+ ## a URL.
90
94
 
91
95
  ## <tt>QUERY_STRING</tt>:: The portion of the request URL that
92
96
  ## follows the <tt>?</tt>, if any. May be
@@ -107,19 +111,48 @@ module Rack
107
111
  ## In addition to this, the Rack environment must include these
108
112
  ## Rack-specific variables:
109
113
 
110
- ## <tt>rack.version</tt>:: The Array [0,1], representing this version of Rack.
114
+ ## <tt>rack.version</tt>:: The Array [1,0], representing this version of Rack.
111
115
  ## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the request URL.
112
116
  ## <tt>rack.input</tt>:: See below, the input stream.
113
117
  ## <tt>rack.errors</tt>:: See below, the error stream.
114
118
  ## <tt>rack.multithread</tt>:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise.
115
119
  ## <tt>rack.multiprocess</tt>:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise.
116
120
  ## <tt>rack.run_once</tt>:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar).
121
+ ##
122
+
123
+ ## Additional environment specifications have approved to
124
+ ## standardized middleware APIs. None of these are required to
125
+ ## be implemented by the server.
126
+
127
+ ## <tt>rack.session</tt>:: A hash like interface for storing request session data.
128
+ ## The store must implement:
129
+ if session = env['rack.session']
130
+ ## store(key, value) (aliased as []=);
131
+ assert("session #{session.inspect} must respond to store and []=") {
132
+ session.respond_to?(:store) && session.respond_to?(:[]=)
133
+ }
134
+
135
+ ## fetch(key, default = nil) (aliased as []);
136
+ assert("session #{session.inspect} must respond to fetch and []") {
137
+ session.respond_to?(:fetch) && session.respond_to?(:[])
138
+ }
139
+
140
+ ## delete(key);
141
+ assert("session #{session.inspect} must respond to delete") {
142
+ session.respond_to?(:delete)
143
+ }
144
+
145
+ ## clear;
146
+ assert("session #{session.inspect} must respond to clear") {
147
+ session.respond_to?(:clear)
148
+ }
149
+ end
117
150
 
118
151
  ## The server or the application can store their own data in the
119
152
  ## environment, too. The keys must contain at least one dot,
120
153
  ## and should be prefixed uniquely. The prefix <tt>rack.</tt>
121
- ## is reserved for use with the Rack core distribution and must
122
- ## not be used otherwise.
154
+ ## is reserved for use with the Rack core distribution and other
155
+ ## accepted specifications and must not be used otherwise.
123
156
  ##
124
157
 
125
158
  %w[REQUEST_METHOD SERVER_NAME SERVER_PORT
@@ -198,9 +231,12 @@ module Rack
198
231
  end
199
232
 
200
233
  ## === The Input Stream
234
+ ##
235
+ ## The input stream is an IO-like object which contains the raw HTTP
236
+ ## POST data. If it is a file then it must be opened in binary mode.
201
237
  def check_input(input)
202
- ## The input stream must respond to +gets+, +each+ and +read+.
203
- [:gets, :each, :read].each { |method|
238
+ ## The input stream must respond to +gets+, +each+, +read+ and +rewind+.
239
+ [:gets, :each, :read, :rewind].each { |method|
204
240
  assert("rack.input #{input} does not respond to ##{method}") {
205
241
  input.respond_to? method
206
242
  }
@@ -218,10 +254,6 @@ module Rack
218
254
  @input.size
219
255
  end
220
256
 
221
- def rewind
222
- @input.rewind
223
- end
224
-
225
257
  ## * +gets+ must be called without arguments and return a string,
226
258
  ## or +nil+ on EOF.
227
259
  def gets(*args)
@@ -233,21 +265,44 @@ module Rack
233
265
  v
234
266
  end
235
267
 
236
- ## * +read+ must be called without or with one integer argument
237
- ## and return a string, or +nil+ on EOF.
268
+ ## * +read+ behaves like IO#read. Its signature is <tt>read([length, [buffer]])</tt>.
269
+ ## If given, +length+ must be an non-negative Integer (>= 0) or +nil+, and +buffer+ must
270
+ ## be a String and may not be nil. If +length+ is given and not nil, then this method
271
+ ## reads at most +length+ bytes from the input stream. If +length+ is not given or nil,
272
+ ## then this method reads all data until EOF.
273
+ ## When EOF is reached, this method returns nil if +length+ is given and not nil, or ""
274
+ ## if +length+ is not given or is nil.
275
+ ## If +buffer+ is given, then the read data will be placed into +buffer+ instead of a
276
+ ## newly created String object.
238
277
  def read(*args)
239
278
  assert("rack.input#read called with too many arguments") {
240
- args.size <= 1
279
+ args.size <= 2
241
280
  }
242
- if args.size == 1
243
- assert("rack.input#read called with non-integer argument") {
244
- args.first.kind_of? Integer
281
+ if args.size >= 1
282
+ assert("rack.input#read called with non-integer and non-nil length") {
283
+ args.first.kind_of?(Integer) || args.first.nil?
284
+ }
285
+ assert("rack.input#read called with a negative length") {
286
+ args.first.nil? || args.first >= 0
245
287
  }
246
288
  end
289
+ if args.size >= 2
290
+ assert("rack.input#read called with non-String buffer") {
291
+ args[1].kind_of?(String)
292
+ }
293
+ end
294
+
247
295
  v = @input.read(*args)
248
- assert("rack.input#read didn't return a String") {
296
+
297
+ assert("rack.input#read didn't return nil or a String") {
249
298
  v.nil? or v.instance_of? String
250
299
  }
300
+ if args[0].nil?
301
+ assert("rack.input#read(nil) returned nil on EOF") {
302
+ !v.nil?
303
+ }
304
+ end
305
+
251
306
  v
252
307
  end
253
308
 
@@ -261,6 +316,23 @@ module Rack
261
316
  yield line
262
317
  }
263
318
  end
319
+
320
+ ## * +rewind+ must be called without arguments. It rewinds the input
321
+ ## stream back to the beginning. It must not raise Errno::ESPIPE:
322
+ ## that is, it may not be a pipe or a socket. Therefore, handler
323
+ ## developers must buffer the input data into some rewindable object
324
+ ## if the underlying input stream is not rewindable.
325
+ def rewind(*args)
326
+ assert("rack.input#rewind called with arguments") { args.size == 0 }
327
+ assert("rack.input#rewind raised Errno::ESPIPE") {
328
+ begin
329
+ @input.rewind
330
+ true
331
+ rescue Errno::ESPIPE
332
+ false
333
+ end
334
+ }
335
+ end
264
336
 
265
337
  ## * +close+ must never be called on the input stream.
266
338
  def close(*args)
@@ -312,13 +384,14 @@ module Rack
312
384
 
313
385
  ## === The Status
314
386
  def check_status(status)
315
- ## The status, if parsed as integer (+to_i+), must be greater than or equal to 100.
387
+ ## This is an HTTP status. When parsed as integer (+to_i+), it must be
388
+ ## greater than or equal to 100.
316
389
  assert("Status must be >=100 seen as integer") { status.to_i >= 100 }
317
390
  end
318
391
 
319
392
  ## === The Headers
320
393
  def check_headers(header)
321
- ## The header must respond to each, and yield values of key and value.
394
+ ## The header must respond to +each+, and yield values of key and value.
322
395
  assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
323
396
  header.respond_to? :each
324
397
  }
@@ -336,16 +409,14 @@ module Rack
336
409
  ## but only contain keys that consist of
337
410
  ## letters, digits, <tt>_</tt> or <tt>-</tt> and start with a letter.
338
411
  assert("invalid header name: #{key}") { key =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/ }
339
- ##
340
- ## The values of the header must respond to #each.
341
- assert("header values must respond to #each, but the value of " +
342
- "'#{key}' doesn't (is #{value.class})") { value.respond_to? :each }
343
- value.each { |item|
344
- ## The values passed on #each must be Strings
345
- assert("header values must consist of Strings, but '#{key}' also contains a #{item.class}") {
346
- item.instance_of?(String)
347
- }
348
- ## and not contain characters below 037.
412
+
413
+ ## The values of the header must be Strings,
414
+ assert("a header value must be a String, but the value of " +
415
+ "'#{key}' is a #{value.class}") { value.kind_of? String }
416
+ ## consisting of lines (for multiple header values, e.g. multiple
417
+ ## <tt>Set-Cookie</tt> values) seperated by "\n".
418
+ value.split("\n").each { |item|
419
+ ## The lines must not contain characters below 037.
349
420
  assert("invalid header value #{key}: #{item.inspect}") {
350
421
  item !~ /[\000-\037]/
351
422
  }
@@ -373,65 +444,49 @@ module Rack
373
444
 
374
445
  ## === The Content-Length
375
446
  def check_content_length(status, headers, env)
376
- chunked_response = false
377
- headers.each { |key, value|
378
- if key.downcase == 'transfer-encoding'
379
- chunked_response = value.downcase != 'identity'
380
- end
381
- }
382
-
383
447
  headers.each { |key, value|
384
448
  if key.downcase == 'content-length'
385
- ## There must be a <tt>Content-Length</tt>, except when the
386
- ## +Status+ is 1xx, 204 or 304, in which case there must be none
387
- ## given.
449
+ ## There must not be a <tt>Content-Length</tt> header when the
450
+ ## +Status+ is 1xx, 204 or 304.
388
451
  assert("Content-Length header found in #{status} response, not allowed") {
389
452
  not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
390
453
  }
391
454
 
392
- assert('Content-Length header should not be used if body is chunked') {
393
- not chunked_response
394
- }
395
-
396
455
  bytes = 0
397
456
  string_body = true
398
457
 
399
- @body.each { |part|
400
- unless part.kind_of?(String)
401
- string_body = false
402
- break
403
- end
404
-
405
- bytes += (part.respond_to?(:bytesize) ? part.bytesize : part.size)
406
- }
458
+ if @body.respond_to?(:to_ary)
459
+ @body.each { |part|
460
+ unless part.kind_of?(String)
461
+ string_body = false
462
+ break
463
+ end
407
464
 
408
- if env["REQUEST_METHOD"] == "HEAD"
409
- assert("Response body was given for HEAD request, but should be empty") {
410
- bytes == 0
465
+ bytes += Rack::Utils.bytesize(part)
411
466
  }
412
- else
413
- if string_body
414
- assert("Content-Length header was #{value}, but should be #{bytes}") {
415
- value == bytes.to_s
467
+
468
+ if env["REQUEST_METHOD"] == "HEAD"
469
+ assert("Response body was given for HEAD request, but should be empty") {
470
+ bytes == 0
416
471
  }
472
+ else
473
+ if string_body
474
+ assert("Content-Length header was #{value}, but should be #{bytes}") {
475
+ value == bytes.to_s
476
+ }
477
+ end
417
478
  end
418
479
  end
419
480
 
420
481
  return
421
482
  end
422
483
  }
423
-
424
- if [ String, Array ].include?(@body.class) && !chunked_response
425
- assert('No Content-Length header found') {
426
- Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
427
- }
428
- end
429
484
  end
430
485
 
431
486
  ## === The Body
432
487
  def each
433
488
  @closed = false
434
- ## The Body must respond to #each
489
+ ## The Body must respond to +each+
435
490
  @body.each { |part|
436
491
  ## and must only yield String values.
437
492
  assert("Body yielded non-string value #{part.inspect}") {
@@ -440,9 +495,26 @@ module Rack
440
495
  yield part
441
496
  }
442
497
  ##
443
- ## If the Body responds to #close, it will be called after iteration.
498
+ ## The Body itself should not be an instance of String, as this will
499
+ ## break in Ruby 1.9.
500
+ ##
501
+ ## If the Body responds to +close+, it will be called after iteration.
444
502
  # XXX howto: assert("Body has not been closed") { @closed }
445
503
 
504
+
505
+ ##
506
+ ## If the Body responds to +to_path+, it must return a String
507
+ ## identifying the location of a file whose contents are identical
508
+ ## to that produced by calling +each+; this may be used by the
509
+ ## server as an alternative, possibly more efficient way to
510
+ ## transport the response.
511
+
512
+ if @body.respond_to?(:to_path)
513
+ assert("The file identified by body.to_path does not exist") {
514
+ ::File.exist? @body.to_path
515
+ }
516
+ end
517
+
446
518
  ##
447
519
  ## The Body commonly is an Array of Strings, the application
448
520
  ## instance itself, or a File-like object.