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
@@ -34,11 +34,7 @@ module Rack
34
34
  end
35
35
 
36
36
  def use(middleware, *args, &block)
37
- @ins << if block_given?
38
- lambda { |app| middleware.new(app, *args, &block) }
39
- else
40
- lambda { |app| middleware.new(app, *args) }
41
- end
37
+ @ins << lambda { |app| middleware.new(app, *args, &block) }
42
38
  end
43
39
 
44
40
  def run(app)
@@ -0,0 +1,49 @@
1
+ require 'rack/utils'
2
+
3
+ module Rack
4
+
5
+ # Middleware that applies chunked transfer encoding to response bodies
6
+ # when the response does not include a Content-Length header.
7
+ class Chunked
8
+ include Rack::Utils
9
+
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(env)
15
+ status, headers, body = @app.call(env)
16
+ headers = HeaderHash.new(headers)
17
+
18
+ if env['HTTP_VERSION'] == 'HTTP/1.0' ||
19
+ STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
20
+ headers['Content-Length'] ||
21
+ headers['Transfer-Encoding']
22
+ [status, headers.to_hash, body]
23
+ else
24
+ dup.chunk(status, headers, body)
25
+ end
26
+ end
27
+
28
+ def chunk(status, headers, body)
29
+ @body = body
30
+ headers.delete('Content-Length')
31
+ headers['Transfer-Encoding'] = 'chunked'
32
+ [status, headers.to_hash, self]
33
+ end
34
+
35
+ def each
36
+ term = "\r\n"
37
+ @body.each do |chunk|
38
+ size = bytesize(chunk)
39
+ next if size == 0
40
+ yield [size.to_s(16), term, chunk, term].join
41
+ end
42
+ yield ["0", term, "", term].join
43
+ end
44
+
45
+ def close
46
+ @body.close if @body.respond_to?(:close)
47
+ end
48
+ end
49
+ end
@@ -1,3 +1,5 @@
1
+ require 'rack/utils'
2
+
1
3
  module Rack
2
4
 
3
5
  # Middleware that enables conditional GET using If-None-Match and
@@ -24,6 +26,8 @@ module Rack
24
26
  headers = Utils::HeaderHash.new(headers)
25
27
  if etag_matches?(env, headers) || modified_since?(env, headers)
26
28
  status = 304
29
+ headers.delete('Content-Type')
30
+ headers.delete('Content-Length')
27
31
  body = []
28
32
  end
29
33
  [status, headers, body]
@@ -1,21 +1,25 @@
1
+ require 'rack/utils'
2
+
1
3
  module Rack
2
4
  # Sets the Content-Length header on responses with fixed-length bodies.
3
5
  class ContentLength
6
+ include Rack::Utils
7
+
4
8
  def initialize(app)
5
9
  @app = app
6
10
  end
7
11
 
8
12
  def call(env)
9
13
  status, headers, body = @app.call(env)
10
- headers = Utils::HeaderHash.new(headers)
14
+ headers = HeaderHash.new(headers)
11
15
 
12
- if !Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) &&
16
+ if !STATUS_WITH_NO_ENTITY_BODY.include?(status) &&
13
17
  !headers['Content-Length'] &&
14
18
  !headers['Transfer-Encoding'] &&
15
19
  (body.respond_to?(:to_ary) || body.respond_to?(:to_str))
16
20
 
17
21
  body = [body] if body.respond_to?(:to_str) # rack 0.4 compat
18
- length = body.to_ary.inject(0) { |len, part| len + part.length }
22
+ length = body.to_ary.inject(0) { |len, part| len + bytesize(part) }
19
23
  headers['Content-Length'] = length.to_s
20
24
  end
21
25
 
@@ -0,0 +1,23 @@
1
+ require 'rack/utils'
2
+
3
+ module Rack
4
+
5
+ # Sets the Content-Type header on responses which don't have one.
6
+ #
7
+ # Builder Usage:
8
+ # use Rack::ContentType, "text/plain"
9
+ #
10
+ # When no content type argument is provided, "text/html" is assumed.
11
+ class ContentType
12
+ def initialize(app, content_type = "text/html")
13
+ @app, @content_type = app, content_type
14
+ end
15
+
16
+ def call(env)
17
+ status, headers, body = @app.call(env)
18
+ headers = Utils::HeaderHash.new(headers)
19
+ headers['Content-Type'] ||= @content_type
20
+ [status, headers.to_hash, body]
21
+ end
22
+ end
23
+ end
@@ -1,87 +1,96 @@
1
1
  require "zlib"
2
2
  require "stringio"
3
3
  require "time" # for Time.httpdate
4
+ require 'rack/utils'
4
5
 
5
6
  module Rack
6
-
7
- class Deflater
8
- def initialize(app)
9
- @app = app
10
- end
11
-
12
- def call(env)
13
- status, headers, body = @app.call(env)
14
- headers = Utils::HeaderHash.new(headers)
15
-
16
- # Skip compressing empty entity body responses and responses with
17
- # no-transform set.
18
- if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
19
- headers['Cache-Control'].to_s =~ /\bno-transform\b/
20
- return [status, headers, body]
7
+ class Deflater
8
+ def initialize(app)
9
+ @app = app
21
10
  end
22
11
 
23
- request = Request.new(env)
24
-
25
- encoding = Utils.select_best_encoding(%w(gzip deflate identity),
26
- request.accept_encoding)
27
-
28
- # Set the Vary HTTP header.
29
- vary = headers["Vary"].to_s.split(",").map { |v| v.strip }
30
- unless vary.include?("*") || vary.include?("Accept-Encoding")
31
- headers["Vary"] = vary.push("Accept-Encoding").join(",")
12
+ def call(env)
13
+ status, headers, body = @app.call(env)
14
+ headers = Utils::HeaderHash.new(headers)
15
+
16
+ # Skip compressing empty entity body responses and responses with
17
+ # no-transform set.
18
+ if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
19
+ headers['Cache-Control'].to_s =~ /\bno-transform\b/
20
+ return [status, headers, body]
21
+ end
22
+
23
+ request = Request.new(env)
24
+
25
+ encoding = Utils.select_best_encoding(%w(gzip deflate identity),
26
+ request.accept_encoding)
27
+
28
+ # Set the Vary HTTP header.
29
+ vary = headers["Vary"].to_s.split(",").map { |v| v.strip }
30
+ unless vary.include?("*") || vary.include?("Accept-Encoding")
31
+ headers["Vary"] = vary.push("Accept-Encoding").join(",")
32
+ end
33
+
34
+ case encoding
35
+ when "gzip"
36
+ headers['Content-Encoding'] = "gzip"
37
+ headers.delete('Content-Length')
38
+ mtime = headers.key?("Last-Modified") ?
39
+ Time.httpdate(headers["Last-Modified"]) : Time.now
40
+ [status, headers, GzipStream.new(body, mtime)]
41
+ when "deflate"
42
+ headers['Content-Encoding'] = "deflate"
43
+ headers.delete('Content-Length')
44
+ [status, headers, DeflateStream.new(body)]
45
+ when "identity"
46
+ [status, headers, body]
47
+ when nil
48
+ message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
49
+ [406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, [message]]
50
+ end
32
51
  end
33
52
 
34
- case encoding
35
- when "gzip"
36
- mtime = if headers.key?("Last-Modified")
37
- Time.httpdate(headers["Last-Modified"])
38
- else
39
- Time.now
40
- end
41
- [status,
42
- headers.merge("Content-Encoding" => "gzip"),
43
- self.class.gzip(body, mtime)]
44
- when "deflate"
45
- [status,
46
- headers.merge("Content-Encoding" => "deflate"),
47
- self.class.deflate(body)]
48
- when "identity"
49
- [status, headers, body]
50
- when nil
51
- message = ["An acceptable encoding for the requested resource #{request.fullpath} could not be found."]
52
- [406, {"Content-Type" => "text/plain"}, message]
53
+ class GzipStream
54
+ def initialize(body, mtime)
55
+ @body = body
56
+ @mtime = mtime
57
+ end
58
+
59
+ def each(&block)
60
+ @writer = block
61
+ gzip =::Zlib::GzipWriter.new(self)
62
+ gzip.mtime = @mtime
63
+ @body.each { |part| gzip << part }
64
+ @body.close if @body.respond_to?(:close)
65
+ gzip.close
66
+ @writer = nil
67
+ end
68
+
69
+ def write(data)
70
+ @writer.call(data)
71
+ end
53
72
  end
54
- end
55
-
56
- def self.gzip(body, mtime)
57
- io = StringIO.new
58
- gzip = Zlib::GzipWriter.new(io)
59
- gzip.mtime = mtime
60
-
61
- # TODO: Add streaming
62
- body.each { |part| gzip << part }
63
73
 
64
- gzip.close
65
- return io.string
66
- end
67
-
68
- DEFLATE_ARGS = [
69
- Zlib::DEFAULT_COMPRESSION,
70
- # drop the zlib header which causes both Safari and IE to choke
71
- -Zlib::MAX_WBITS,
72
- Zlib::DEF_MEM_LEVEL,
73
- Zlib::DEFAULT_STRATEGY
74
- ]
75
-
76
- # Loosely based on Mongrel's Deflate handler
77
- def self.deflate(body)
78
- deflater = Zlib::Deflate.new(*DEFLATE_ARGS)
79
-
80
- # TODO: Add streaming
81
- body.each { |part| deflater << part }
82
-
83
- return deflater.finish
74
+ class DeflateStream
75
+ DEFLATE_ARGS = [
76
+ Zlib::DEFAULT_COMPRESSION,
77
+ # drop the zlib header which causes both Safari and IE to choke
78
+ -Zlib::MAX_WBITS,
79
+ Zlib::DEF_MEM_LEVEL,
80
+ Zlib::DEFAULT_STRATEGY
81
+ ]
82
+
83
+ def initialize(body)
84
+ @body = body
85
+ end
86
+
87
+ def each
88
+ deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS)
89
+ @body.each { |part| yield deflater.deflate(part) }
90
+ @body.close if @body.respond_to?(:close)
91
+ yield deflater.finish
92
+ nil
93
+ end
94
+ end
84
95
  end
85
96
  end
86
-
87
- end
@@ -1,4 +1,5 @@
1
1
  require 'time'
2
+ require 'rack/utils'
2
3
  require 'rack/mime'
3
4
 
4
5
  module Rack
@@ -69,7 +70,7 @@ table { width:100%%; }
69
70
  return unless @path_info.include? ".."
70
71
 
71
72
  body = "Forbidden\n"
72
- size = body.respond_to?(:bytesize) ? body.bytesize : body.size
73
+ size = Rack::Utils.bytesize(body)
73
74
  return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]]
74
75
  end
75
76
 
@@ -88,6 +89,8 @@ table { width:100%%; }
88
89
  type = stat.directory? ? 'directory' : Mime.mime_type(ext)
89
90
  size = stat.directory? ? '-' : filesize_format(size)
90
91
  mtime = stat.mtime.httpdate
92
+ url << '/' if stat.directory?
93
+ basename << '/' if stat.directory?
91
94
 
92
95
  @files << [ url, basename, size, type, mtime ]
93
96
  end
@@ -119,7 +122,7 @@ table { width:100%%; }
119
122
 
120
123
  def entity_not_found
121
124
  body = "Entity not found: #{@path_info}\n"
122
- size = body.respond_to?(:bytesize) ? body.bytesize : body.size
125
+ size = Rack::Utils.bytesize(body)
123
126
  return [404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]]
124
127
  end
125
128
 
@@ -1,4 +1,5 @@
1
1
  require 'time'
2
+ require 'rack/utils'
2
3
  require 'rack/mime'
3
4
 
4
5
  module Rack
@@ -12,6 +13,8 @@ module Rack
12
13
  attr_accessor :root
13
14
  attr_accessor :path
14
15
 
16
+ alias :to_path :path
17
+
15
18
  def initialize(root)
16
19
  @root = root
17
20
  end
@@ -57,7 +60,7 @@ module Rack
57
60
  body = self
58
61
  else
59
62
  body = [F.read(@path)]
60
- size = body.first.size
63
+ size = Utils.bytesize(body.first)
61
64
  end
62
65
 
63
66
  [200, {
@@ -10,16 +10,37 @@ module Rack
10
10
  module Handler
11
11
  def self.get(server)
12
12
  return unless server
13
+ server = server.to_s
13
14
 
14
15
  if klass = @handlers[server]
15
16
  obj = Object
16
17
  klass.split("::").each { |x| obj = obj.const_get(x) }
17
18
  obj
18
19
  else
19
- Rack::Handler.const_get(server.capitalize)
20
+ try_require('rack/handler', server)
21
+ const_get(server)
20
22
  end
21
23
  end
22
24
 
25
+ # Transforms server-name constants to their canonical form as filenames,
26
+ # then tries to require them but silences the LoadError if not found
27
+ #
28
+ # Naming convention:
29
+ #
30
+ # Foo # => 'foo'
31
+ # FooBar # => 'foo_bar.rb'
32
+ # FooBAR # => 'foobar.rb'
33
+ # FOObar # => 'foobar.rb'
34
+ # FOOBAR # => 'foobar.rb'
35
+ # FooBarBaz # => 'foo_bar_baz.rb'
36
+ def self.try_require(prefix, const_name)
37
+ file = const_name.gsub(/^[A-Z]+/) { |pre| pre.downcase }.
38
+ gsub(/[A-Z]+[^A-Z]/, '_\&').downcase
39
+
40
+ require(::File.join(prefix, file))
41
+ rescue LoadError
42
+ end
43
+
23
44
  def self.register(server, klass)
24
45
  @handlers ||= {}
25
46
  @handlers[server] = klass
@@ -1,3 +1,5 @@
1
+ require 'rack/content_length'
2
+
1
3
  module Rack
2
4
  module Handler
3
5
  class CGI
@@ -6,14 +8,16 @@ module Rack
6
8
  end
7
9
 
8
10
  def self.serve(app)
11
+ app = ContentLength.new(app)
12
+
9
13
  env = ENV.to_hash
10
14
  env.delete "HTTP_CONTENT_LENGTH"
11
15
 
12
16
  env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
13
17
 
14
18
  env.update({"rack.version" => [0,1],
15
- "rack.input" => STDIN,
16
- "rack.errors" => STDERR,
19
+ "rack.input" => $stdin,
20
+ "rack.errors" => $stderr,
17
21
 
18
22
  "rack.multithread" => false,
19
23
  "rack.multiprocess" => true,
@@ -38,7 +42,7 @@ module Rack
38
42
  def self.send_headers(status, headers)
39
43
  STDOUT.print "Status: #{status}\r\n"
40
44
  headers.each { |k, vs|
41
- vs.each { |v|
45
+ vs.split("\n").each { |v|
42
46
  STDOUT.print "#{k}: #{v}\r\n"
43
47
  }
44
48
  }
@@ -1,5 +1,17 @@
1
1
  require 'fcgi'
2
2
  require 'socket'
3
+ require 'rack/content_length'
4
+ require 'rack/rewindable_input'
5
+
6
+ class FCGI::Stream
7
+ alias _rack_read_without_buffer read
8
+
9
+ def read(n, buffer=nil)
10
+ buf = _rack_read_without_buffer n
11
+ buffer.replace(buf.to_s) if buffer
12
+ buf
13
+ end
14
+ end
3
15
 
4
16
  module Rack
5
17
  module Handler
@@ -12,32 +24,18 @@ module Rack
12
24
  }
13
25
  end
14
26
 
15
- module ProperStream # :nodoc:
16
- def each # This is missing by default.
17
- while line = gets
18
- yield line
19
- end
20
- end
21
-
22
- def read(*args)
23
- if args.empty?
24
- super || "" # Empty string on EOF.
25
- else
26
- super
27
- end
28
- end
29
- end
30
-
31
27
  def self.serve(request, app)
28
+ app = Rack::ContentLength.new(app)
29
+
32
30
  env = request.env
33
31
  env.delete "HTTP_CONTENT_LENGTH"
34
32
 
35
- request.in.extend ProperStream
36
-
37
33
  env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
34
+
35
+ rack_input = RewindableInput.new(request.in)
38
36
 
39
37
  env.update({"rack.version" => [0,1],
40
- "rack.input" => request.in,
38
+ "rack.input" => rack_input,
41
39
  "rack.errors" => request.err,
42
40
 
43
41
  "rack.multithread" => false,
@@ -54,12 +52,16 @@ module Rack
54
52
  env.delete "CONTENT_TYPE" if env["CONTENT_TYPE"] == ""
55
53
  env.delete "CONTENT_LENGTH" if env["CONTENT_LENGTH"] == ""
56
54
 
57
- status, headers, body = app.call(env)
58
55
  begin
59
- send_headers request.out, status, headers
60
- send_body request.out, body
56
+ status, headers, body = app.call(env)
57
+ begin
58
+ send_headers request.out, status, headers
59
+ send_body request.out, body
60
+ ensure
61
+ body.close if body.respond_to? :close
62
+ end
61
63
  ensure
62
- body.close if body.respond_to? :close
64
+ rack_input.close
63
65
  request.finish
64
66
  end
65
67
  end
@@ -67,7 +69,7 @@ module Rack
67
69
  def self.send_headers(out, status, headers)
68
70
  out.print "Status: #{status}\r\n"
69
71
  headers.each { |k, vs|
70
- vs.each { |v|
72
+ vs.split("\n").each { |v|
71
73
  out.print "#{k}: #{v}\r\n"
72
74
  }
73
75
  }