rack 1.2.8 → 1.3.0.beta

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 (89) hide show
  1. data/README +9 -177
  2. data/Rakefile +2 -1
  3. data/SPEC +2 -2
  4. data/lib/rack.rb +2 -13
  5. data/lib/rack/auth/abstract/request.rb +7 -5
  6. data/lib/rack/auth/digest/md5.rb +6 -2
  7. data/lib/rack/auth/digest/params.rb +5 -7
  8. data/lib/rack/auth/digest/request.rb +1 -1
  9. data/lib/rack/backports/uri/common.rb +64 -0
  10. data/lib/rack/builder.rb +60 -3
  11. data/lib/rack/chunked.rb +29 -22
  12. data/lib/rack/conditionalget.rb +35 -16
  13. data/lib/rack/content_length.rb +3 -3
  14. data/lib/rack/deflater.rb +5 -2
  15. data/lib/rack/etag.rb +38 -10
  16. data/lib/rack/file.rb +76 -43
  17. data/lib/rack/handler.rb +13 -7
  18. data/lib/rack/handler/cgi.rb +0 -2
  19. data/lib/rack/handler/fastcgi.rb +13 -4
  20. data/lib/rack/handler/lsws.rb +0 -2
  21. data/lib/rack/handler/mongrel.rb +12 -2
  22. data/lib/rack/handler/scgi.rb +9 -1
  23. data/lib/rack/handler/thin.rb +7 -1
  24. data/lib/rack/handler/webrick.rb +12 -5
  25. data/lib/rack/lint.rb +2 -2
  26. data/lib/rack/lock.rb +29 -3
  27. data/lib/rack/methodoverride.rb +1 -1
  28. data/lib/rack/mime.rb +2 -2
  29. data/lib/rack/mock.rb +28 -33
  30. data/lib/rack/multipart.rb +34 -0
  31. data/lib/rack/multipart/generator.rb +93 -0
  32. data/lib/rack/multipart/parser.rb +164 -0
  33. data/lib/rack/multipart/uploaded_file.rb +30 -0
  34. data/lib/rack/request.rb +55 -19
  35. data/lib/rack/response.rb +10 -8
  36. data/lib/rack/sendfile.rb +14 -18
  37. data/lib/rack/server.rb +55 -8
  38. data/lib/rack/session/abstract/id.rb +233 -22
  39. data/lib/rack/session/cookie.rb +99 -46
  40. data/lib/rack/session/memcache.rb +30 -56
  41. data/lib/rack/session/pool.rb +22 -43
  42. data/lib/rack/showexceptions.rb +40 -11
  43. data/lib/rack/showstatus.rb +9 -2
  44. data/lib/rack/static.rb +29 -9
  45. data/lib/rack/urlmap.rb +6 -1
  46. data/lib/rack/utils.rb +67 -326
  47. data/rack.gemspec +2 -3
  48. data/test/builder/anything.rb +5 -0
  49. data/test/builder/comment.ru +4 -0
  50. data/test/builder/end.ru +3 -0
  51. data/test/builder/options.ru +2 -0
  52. data/test/cgi/lighttpd.conf +1 -1
  53. data/test/cgi/lighttpd.errors +412 -0
  54. data/test/multipart/content_type_and_no_filename +6 -0
  55. data/test/multipart/text +5 -0
  56. data/test/multipart/webkit +32 -0
  57. data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
  58. data/test/spec_auth_digest.rb +20 -5
  59. data/test/spec_builder.rb +29 -0
  60. data/test/spec_cgi.rb +11 -0
  61. data/test/spec_chunked.rb +1 -1
  62. data/test/spec_commonlogger.rb +1 -1
  63. data/test/spec_conditionalget.rb +47 -0
  64. data/test/spec_content_length.rb +0 -6
  65. data/test/spec_content_type.rb +5 -5
  66. data/test/spec_deflater.rb +46 -2
  67. data/test/spec_etag.rb +68 -1
  68. data/test/spec_fastcgi.rb +11 -0
  69. data/test/spec_file.rb +54 -3
  70. data/test/spec_handler.rb +23 -5
  71. data/test/spec_lint.rb +2 -2
  72. data/test/spec_lock.rb +111 -5
  73. data/test/spec_methodoverride.rb +2 -2
  74. data/test/spec_mock.rb +3 -3
  75. data/test/spec_mongrel.rb +1 -2
  76. data/test/spec_multipart.rb +279 -0
  77. data/test/spec_request.rb +222 -38
  78. data/test/spec_response.rb +9 -3
  79. data/test/spec_server.rb +74 -0
  80. data/test/spec_session_abstract_id.rb +43 -0
  81. data/test/spec_session_cookie.rb +97 -15
  82. data/test/spec_session_memcache.rb +60 -50
  83. data/test/spec_session_pool.rb +63 -40
  84. data/test/spec_showexceptions.rb +64 -0
  85. data/test/spec_static.rb +23 -0
  86. data/test/spec_utils.rb +65 -351
  87. data/test/spec_webrick.rb +23 -4
  88. metadata +35 -15
  89. data/test/spec_auth.rb +0 -57
@@ -12,14 +12,18 @@ module Rack
12
12
  return unless server
13
13
  server = server.to_s
14
14
 
15
+ unless @handlers.include? server
16
+ load_error = try_require('rack/handler', server)
17
+ end
18
+
15
19
  if klass = @handlers[server]
16
- obj = Object
17
- klass.split("::").each { |x| obj = obj.const_get(x) }
18
- obj
20
+ klass.split("::").inject(Object) { |o, x| o.const_get(x) }
19
21
  else
20
- try_require('rack/handler', server)
21
22
  const_get(server)
22
23
  end
24
+
25
+ rescue NameError => name_error
26
+ raise load_error || name_error
23
27
  end
24
28
 
25
29
  def self.default(options = {})
@@ -35,7 +39,7 @@ module Rack
35
39
  else
36
40
  begin
37
41
  Rack::Handler::Mongrel
38
- rescue LoadError => e
42
+ rescue LoadError
39
43
  Rack::Handler::WEBrick
40
44
  end
41
45
  end
@@ -57,12 +61,14 @@ module Rack
57
61
  gsub(/[A-Z]+[^A-Z]/, '_\&').downcase
58
62
 
59
63
  require(::File.join(prefix, file))
60
- rescue LoadError
64
+ nil
65
+ rescue LoadError => error
66
+ error
61
67
  end
62
68
 
63
69
  def self.register(server, klass)
64
70
  @handlers ||= {}
65
- @handlers[server] = klass
71
+ @handlers[server.to_s] = klass.to_s
66
72
  end
67
73
 
68
74
  autoload :CGI, "rack/handler/cgi"
@@ -10,8 +10,6 @@ module Rack
10
10
  end
11
11
 
12
12
  def self.serve(app)
13
- app = ContentLength.new(app)
14
-
15
13
  env = ENV.to_hash
16
14
  env.delete "HTTP_CONTENT_LENGTH"
17
15
 
@@ -19,16 +19,25 @@ module Rack
19
19
  module Handler
20
20
  class FastCGI
21
21
  def self.run(app, options={})
22
- file = options[:File] and STDIN.reopen(UNIXServer.new(file))
23
- port = options[:Port] and STDIN.reopen(TCPServer.new(port))
22
+ if options[:File]
23
+ STDIN.reopen(UNIXServer.new(options[:File]))
24
+ elsif options[:Port]
25
+ STDIN.reopen(TCPServer.new(options[:Host], options[:Port]))
26
+ end
24
27
  FCGI.each { |request|
25
28
  serve request, app
26
29
  }
27
30
  end
31
+
32
+ def self.valid_options
33
+ {
34
+ "Host=HOST" => "Hostname to listen on (default: localhost)",
35
+ "Port=PORT" => "Port to listen on (default: 8080)",
36
+ "File=PATH" => "Creates a Domain socket at PATH instead of a TCP socket. Ignores Host and Port if set.",
37
+ }
38
+ end
28
39
 
29
40
  def self.serve(request, app)
30
- app = Rack::ContentLength.new(app)
31
-
32
41
  env = request.env
33
42
  env.delete "HTTP_CONTENT_LENGTH"
34
43
 
@@ -11,8 +11,6 @@ module Rack
11
11
  end
12
12
  end
13
13
  def self.serve(app)
14
- app = Rack::ContentLength.new(app)
15
-
16
14
  env = ENV.to_hash
17
15
  env.delete "HTTP_CONTENT_LENGTH"
18
16
  env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
@@ -38,8 +38,18 @@ module Rack
38
38
  server.run.join
39
39
  end
40
40
 
41
+ def self.valid_options
42
+ {
43
+ "Host=HOST" => "Hostname to listen on (default: localhost)",
44
+ "Port=PORT" => "Port to listen on (default: 8080)",
45
+ "Processors=N" => "Number of concurrent processors to accept (default: 950)",
46
+ "Timeout=N" => "Time before a request is dropped for inactivity (default: 60)",
47
+ "Throttle=N" => "Throttle time between socket.accept calls in hundredths of a second (default: 0)",
48
+ }
49
+ end
50
+
41
51
  def initialize(app)
42
- @app = Rack::Chunked.new(Rack::ContentLength.new(app))
52
+ @app = app
43
53
  end
44
54
 
45
55
  def process(request, response)
@@ -60,7 +70,7 @@ module Rack
60
70
  "rack.multiprocess" => false, # ???
61
71
  "rack.run_once" => false,
62
72
 
63
- "rack.url_scheme" => "http",
73
+ "rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
64
74
  })
65
75
  env["QUERY_STRING"] ||= ""
66
76
 
@@ -9,14 +9,22 @@ module Rack
9
9
  attr_accessor :app
10
10
 
11
11
  def self.run(app, options=nil)
12
+ options[:Socket] = UNIXServer.new(options[:File]) if options[:File]
12
13
  new(options.merge(:app=>app,
13
14
  :host=>options[:Host],
14
15
  :port=>options[:Port],
15
16
  :socket=>options[:Socket])).listen
16
17
  end
18
+
19
+ def self.valid_options
20
+ {
21
+ "Host=HOST" => "Hostname to listen on (default: localhost)",
22
+ "Port=PORT" => "Port to listen on (default: 8080)",
23
+ }
24
+ end
17
25
 
18
26
  def initialize(settings = {})
19
- @app = Rack::Chunked.new(Rack::ContentLength.new(settings[:app]))
27
+ @app = settings[:app]
20
28
  super(settings)
21
29
  end
22
30
 
@@ -6,13 +6,19 @@ module Rack
6
6
  module Handler
7
7
  class Thin
8
8
  def self.run(app, options={})
9
- app = Rack::Chunked.new(Rack::ContentLength.new(app))
10
9
  server = ::Thin::Server.new(options[:Host] || '0.0.0.0',
11
10
  options[:Port] || 8080,
12
11
  app)
13
12
  yield server if block_given?
14
13
  server.start
15
14
  end
15
+
16
+ def self.valid_options
17
+ {
18
+ "Host=HOST" => "Hostname to listen on (default: localhost)",
19
+ "Port=PORT" => "Port to listen on (default: 8080)",
20
+ }
21
+ end
16
22
  end
17
23
  end
18
24
  end
@@ -12,6 +12,13 @@ module Rack
12
12
  yield @server if block_given?
13
13
  @server.start
14
14
  end
15
+
16
+ def self.valid_options
17
+ {
18
+ "Host=HOST" => "Hostname to listen on (default: localhost)",
19
+ "Port=PORT" => "Port to listen on (default: 8080)",
20
+ }
21
+ end
15
22
 
16
23
  def self.shutdown
17
24
  @server.shutdown
@@ -20,7 +27,7 @@ module Rack
20
27
 
21
28
  def initialize(server, app)
22
29
  super server
23
- @app = Rack::ContentLength.new(app)
30
+ @app = app
24
31
  end
25
32
 
26
33
  def service(req, res)
@@ -43,11 +50,11 @@ module Rack
43
50
 
44
51
  env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
45
52
  env["QUERY_STRING"] ||= ""
46
- env["REQUEST_PATH"] ||= "/"
47
53
  unless env["PATH_INFO"] == ""
48
54
  path, n = req.request_uri.path, env["SCRIPT_NAME"].length
49
55
  env["PATH_INFO"] = path[n, path.length-n]
50
56
  end
57
+ env["REQUEST_PATH"] ||= [env["SCRIPT_NAME"], env["PATH_INFO"]].join
51
58
 
52
59
  status, headers, body = @app.call(env)
53
60
  begin
@@ -56,9 +63,9 @@ module Rack
56
63
  if k.downcase == "set-cookie"
57
64
  res.cookies.concat vs.split("\n")
58
65
  else
59
- vs.split("\n").each { |v|
60
- res[k] = v
61
- }
66
+ # Since WEBrick won't accept repeated headers,
67
+ # merge the values per RFC 1945 section 4.2.
68
+ res[k] = vs.split("\n").join(", ")
62
69
  end
63
70
  }
64
71
  body.each { |part|
@@ -30,7 +30,7 @@ module Rack
30
30
 
31
31
  ## = Rack applications
32
32
 
33
- ## A Rack application is a Ruby object (not a class) that
33
+ ## A Rack application is an Ruby object (not a class) that
34
34
  ## responds to +call+.
35
35
  def call(env=nil)
36
36
  dup._call(env)
@@ -302,7 +302,7 @@ module Rack
302
302
  end
303
303
 
304
304
  ## * +read+ behaves like IO#read. Its signature is <tt>read([length, [buffer]])</tt>.
305
- ## If given, +length+ must be a non-negative Integer (>= 0) or +nil+, and +buffer+ must
305
+ ## If given, +length+ must be an non-negative Integer (>= 0) or +nil+, and +buffer+ must
306
306
  ## be a String and may not be nil. If +length+ is given and not nil, then this method
307
307
  ## reads at most +length+ bytes from the input stream. If +length+ is not given or nil,
308
308
  ## then this method reads all data until EOF.
@@ -2,15 +2,41 @@ require 'thread'
2
2
 
3
3
  module Rack
4
4
  class Lock
5
+ class Proxy < Struct.new(:target, :mutex) # :nodoc:
6
+ def each
7
+ target.each { |x| yield x }
8
+ end
9
+
10
+ def close
11
+ target.close if target.respond_to?(:close)
12
+ ensure
13
+ mutex.unlock
14
+ end
15
+
16
+ def to_path
17
+ target.to_path
18
+ end
19
+
20
+ def respond_to?(sym)
21
+ sym.to_sym == :close || target.respond_to?(sym)
22
+ end
23
+ end
24
+
5
25
  FLAG = 'rack.multithread'.freeze
6
26
 
7
- def initialize(app, lock = Mutex.new)
8
- @app, @lock = app, lock
27
+ def initialize(app, mutex = Mutex.new)
28
+ @app, @mutex = app, mutex
9
29
  end
10
30
 
11
31
  def call(env)
12
32
  old, env[FLAG] = env[FLAG], false
13
- @lock.synchronize { @app.call(env) }
33
+ @mutex.lock
34
+ response = @app.call(env)
35
+ response[2] = Proxy.new(response[2], @mutex)
36
+ response
37
+ rescue Exception
38
+ @mutex.unlock
39
+ raise
14
40
  ensure
15
41
  env[FLAG] = old
16
42
  end
@@ -1,6 +1,6 @@
1
1
  module Rack
2
2
  class MethodOverride
3
- HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS)
3
+ HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS PATCH)
4
4
 
5
5
  METHOD_OVERRIDE_PARAM_KEY = "_method".freeze
6
6
  HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE".freeze
@@ -70,7 +70,6 @@ module Rack
70
70
  ".dll" => "application/x-msdownload",
71
71
  ".dmg" => "application/octet-stream",
72
72
  ".doc" => "application/msword",
73
- ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
74
73
  ".dot" => "application/msword",
75
74
  ".dtd" => "application/xml-dtd",
76
75
  ".dvi" => "application/x-dvi",
@@ -182,6 +181,7 @@ module Rack
182
181
  ".tiff" => "image/tiff",
183
182
  ".torrent" => "application/x-bittorrent",
184
183
  ".tr" => "text/troff",
184
+ ".ttf" => "application/octet-stream",
185
185
  ".txt" => "text/plain",
186
186
  ".vcf" => "text/x-vcard",
187
187
  ".vcs" => "text/x-vcalendar",
@@ -192,12 +192,12 @@ module Rack
192
192
  ".wma" => "audio/x-ms-wma",
193
193
  ".wmv" => "video/x-ms-wmv",
194
194
  ".wmx" => "video/x-ms-wmx",
195
+ ".woff" => "application/octet-stream",
195
196
  ".wrl" => "model/vrml",
196
197
  ".wsdl" => "application/wsdl+xml",
197
198
  ".xbm" => "image/x-xbitmap",
198
199
  ".xhtml" => "application/xhtml+xml",
199
200
  ".xls" => "application/vnd.ms-excel",
200
- ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
201
201
  ".xml" => "application/xml",
202
202
  ".xpm" => "image/x-xpixmap",
203
203
  ".xsl" => "application/xml",
@@ -141,50 +141,45 @@ module Rack
141
141
  # Usually, you don't create the MockResponse on your own, but use
142
142
  # MockRequest.
143
143
 
144
- class MockResponse
145
- def initialize(status, headers, body, errors=StringIO.new(""))
146
- @status = status.to_i
147
-
148
- @original_headers = headers
149
- @headers = Rack::Utils::HeaderHash.new
150
- headers.each { |field, values|
151
- @headers[field] = values
152
- @headers[field] = "" if values.empty?
153
- }
154
-
155
- @body = ""
156
- body.each { |part| @body << part }
157
-
158
- @errors = errors.string if errors.respond_to?(:string)
159
- end
160
-
161
- # Status
162
- attr_reader :status
163
-
144
+ class MockResponse < Rack::Response
164
145
  # Headers
165
- attr_reader :headers, :original_headers
146
+ attr_reader :original_headers
166
147
 
167
- def [](field)
168
- headers[field]
169
- end
148
+ # Errors
149
+ attr_accessor :errors
170
150
 
151
+ def initialize(status, headers, body, errors=StringIO.new(""))
152
+ @original_headers = headers
153
+ @errors = errors.string if errors.respond_to?(:string)
154
+ @body_string = nil
171
155
 
172
- # Body
173
- attr_reader :body
156
+ super(body, status, headers)
157
+ end
174
158
 
175
159
  def =~(other)
176
- @body =~ other
160
+ body =~ other
177
161
  end
178
162
 
179
163
  def match(other)
180
- @body.match other
164
+ body.match other
181
165
  end
182
166
 
167
+ def body
168
+ # FIXME: apparently users of MockResponse expect the return value of
169
+ # MockResponse#body to be a string. However, the real response object
170
+ # returns the body as a list.
171
+ #
172
+ # See spec_showstatus.rb:
173
+ #
174
+ # should "not replace existing messages" do
175
+ # ...
176
+ # res.body.should == "foo!"
177
+ # end
178
+ super.join
179
+ end
183
180
 
184
- # Errors
185
- attr_accessor :errors
186
-
187
-
188
- include Response::Helpers
181
+ def empty?
182
+ [201, 204, 304].include? status
183
+ end
189
184
  end
190
185
  end
@@ -0,0 +1,34 @@
1
+ module Rack
2
+ # A multipart form data parser, adapted from IOWA.
3
+ #
4
+ # Usually, Rack::Request#POST takes care of calling this.
5
+ module Multipart
6
+ autoload :UploadedFile, 'rack/multipart/uploaded_file'
7
+ autoload :Parser, 'rack/multipart/parser'
8
+ autoload :Generator, 'rack/multipart/generator'
9
+
10
+ EOL = "\r\n"
11
+ MULTIPART_BOUNDARY = "AaB03x"
12
+ MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
13
+ TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
14
+ CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
15
+ DISPPARM = /;\s*(#{TOKEN})=("(?:\\"|[^"])*"|#{TOKEN})*/
16
+ RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
17
+ BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
18
+ BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
19
+ MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
20
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name="?([^\";]*)"?/ni
21
+ MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
22
+
23
+ class << self
24
+ def parse_multipart(env)
25
+ Parser.new(env).parse
26
+ end
27
+
28
+ def build_multipart(params, first = true)
29
+ Generator.new(params, first).dump
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,93 @@
1
+ module Rack
2
+ module Multipart
3
+ class Generator
4
+ def initialize(params, first = true)
5
+ @params, @first = params, first
6
+
7
+ if @first && !@params.is_a?(Hash)
8
+ raise ArgumentError, "value must be a Hash"
9
+ end
10
+ end
11
+
12
+ def dump
13
+ return nil if @first && !multipart?
14
+ return flattened_params if !@first
15
+
16
+ flattened_params.map do |name, file|
17
+ if file.respond_to?(:original_filename)
18
+ ::File.open(file.path, "rb") do |f|
19
+ f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
20
+ content_for_tempfile(f, file, name)
21
+ end
22
+ else
23
+ content_for_other(file, name)
24
+ end
25
+ end.join + "--#{MULTIPART_BOUNDARY}--\r"
26
+ end
27
+
28
+ private
29
+ def multipart?
30
+ multipart = false
31
+
32
+ query = lambda { |value|
33
+ case value
34
+ when Array
35
+ value.each(&query)
36
+ when Hash
37
+ value.values.each(&query)
38
+ when Rack::Multipart::UploadedFile
39
+ multipart = true
40
+ end
41
+ }
42
+ @params.values.each(&query)
43
+
44
+ multipart
45
+ end
46
+
47
+ def flattened_params
48
+ @flattened_params ||= begin
49
+ h = Hash.new
50
+ @params.each do |key, value|
51
+ k = @first ? key.to_s : "[#{key}]"
52
+
53
+ case value
54
+ when Array
55
+ value.map { |v|
56
+ Multipart.build_multipart(v, false).each { |subkey, subvalue|
57
+ h["#{k}[]#{subkey}"] = subvalue
58
+ }
59
+ }
60
+ when Hash
61
+ Multipart.build_multipart(value, false).each { |subkey, subvalue|
62
+ h[k + subkey] = subvalue
63
+ }
64
+ else
65
+ h[k] = value
66
+ end
67
+ end
68
+ h
69
+ end
70
+ end
71
+
72
+ def content_for_tempfile(io, file, name)
73
+ <<-EOF
74
+ --#{MULTIPART_BOUNDARY}\r
75
+ Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
76
+ Content-Type: #{file.content_type}\r
77
+ Content-Length: #{::File.stat(file.path).size}\r
78
+ \r
79
+ #{io.read}\r
80
+ EOF
81
+ end
82
+
83
+ def content_for_other(file, name)
84
+ <<-EOF
85
+ --#{MULTIPART_BOUNDARY}\r
86
+ Content-Disposition: form-data; name="#{name}"\r
87
+ \r
88
+ #{file}\r
89
+ EOF
90
+ end
91
+ end
92
+ end
93
+ end