rack 1.1.6 → 1.6.9

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 (212) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +1 -1
  3. data/HISTORY.md +375 -0
  4. data/KNOWN-ISSUES +23 -0
  5. data/README.rdoc +312 -0
  6. data/Rakefile +124 -0
  7. data/SPEC +125 -32
  8. data/contrib/rack.png +0 -0
  9. data/contrib/rack.svg +150 -0
  10. data/contrib/rack_logo.svg +1 -1
  11. data/contrib/rdoc.css +412 -0
  12. data/example/protectedlobster.rb +1 -1
  13. data/lib/rack/auth/abstract/handler.rb +4 -4
  14. data/lib/rack/auth/abstract/request.rb +7 -5
  15. data/lib/rack/auth/basic.rb +1 -1
  16. data/lib/rack/auth/digest/md5.rb +7 -3
  17. data/lib/rack/auth/digest/nonce.rb +1 -1
  18. data/lib/rack/auth/digest/params.rb +7 -9
  19. data/lib/rack/auth/digest/request.rb +10 -9
  20. data/lib/rack/backports/uri/common_18.rb +56 -0
  21. data/lib/rack/backports/uri/common_192.rb +52 -0
  22. data/lib/rack/backports/uri/common_193.rb +29 -0
  23. data/lib/rack/body_proxy.rb +39 -0
  24. data/lib/rack/builder.rb +106 -22
  25. data/lib/rack/cascade.rb +17 -6
  26. data/lib/rack/chunked.rb +44 -24
  27. data/lib/rack/commonlogger.rb +36 -13
  28. data/lib/rack/conditionalget.rb +49 -17
  29. data/lib/rack/config.rb +5 -0
  30. data/lib/rack/content_length.rb +14 -6
  31. data/lib/rack/content_type.rb +7 -1
  32. data/lib/rack/deflater.rb +73 -15
  33. data/lib/rack/directory.rb +18 -8
  34. data/lib/rack/etag.rb +59 -9
  35. data/lib/rack/file.rb +106 -44
  36. data/lib/rack/handler/cgi.rb +11 -11
  37. data/lib/rack/handler/fastcgi.rb +18 -6
  38. data/lib/rack/handler/lsws.rb +2 -4
  39. data/lib/rack/handler/mongrel.rb +22 -6
  40. data/lib/rack/handler/scgi.rb +16 -8
  41. data/lib/rack/handler/thin.rb +19 -4
  42. data/lib/rack/handler/webrick.rb +72 -19
  43. data/lib/rack/handler.rb +47 -14
  44. data/lib/rack/head.rb +10 -2
  45. data/lib/rack/lint.rb +260 -75
  46. data/lib/rack/lobster.rb +13 -8
  47. data/lib/rack/lock.rb +13 -3
  48. data/lib/rack/logger.rb +0 -2
  49. data/lib/rack/methodoverride.rb +27 -8
  50. data/lib/rack/mime.rb +625 -167
  51. data/lib/rack/mock.rb +78 -53
  52. data/lib/rack/multipart/generator.rb +93 -0
  53. data/lib/rack/multipart/parser.rb +253 -0
  54. data/lib/rack/multipart/uploaded_file.rb +34 -0
  55. data/lib/rack/multipart.rb +34 -0
  56. data/lib/rack/nulllogger.rb +21 -2
  57. data/lib/rack/recursive.rb +10 -5
  58. data/lib/rack/reloader.rb +3 -2
  59. data/lib/rack/request.rb +201 -74
  60. data/lib/rack/response.rb +41 -28
  61. data/lib/rack/rewindable_input.rb +15 -11
  62. data/lib/rack/runtime.rb +16 -3
  63. data/lib/rack/sendfile.rb +47 -29
  64. data/lib/rack/server.rb +223 -47
  65. data/lib/rack/session/abstract/id.rb +289 -30
  66. data/lib/rack/session/cookie.rb +133 -44
  67. data/lib/rack/session/memcache.rb +30 -56
  68. data/lib/rack/session/pool.rb +19 -43
  69. data/lib/rack/showexceptions.rb +53 -15
  70. data/lib/rack/showstatus.rb +14 -7
  71. data/lib/rack/static.rb +124 -12
  72. data/lib/rack/tempfile_reaper.rb +22 -0
  73. data/lib/rack/urlmap.rb +49 -15
  74. data/lib/rack/utils/okjson.rb +600 -0
  75. data/lib/rack/utils.rb +363 -361
  76. data/lib/rack.rb +17 -23
  77. data/rack.gemspec +11 -20
  78. data/test/builder/anything.rb +5 -0
  79. data/test/builder/comment.ru +4 -0
  80. data/test/builder/end.ru +5 -0
  81. data/test/builder/line.ru +1 -0
  82. data/test/builder/options.ru +2 -0
  83. data/test/cgi/assets/folder/test.js +1 -0
  84. data/test/cgi/assets/fonts/font.eot +1 -0
  85. data/test/cgi/assets/images/image.png +1 -0
  86. data/test/cgi/assets/index.html +1 -0
  87. data/test/cgi/assets/javascripts/app.js +1 -0
  88. data/test/cgi/assets/stylesheets/app.css +1 -0
  89. data/test/cgi/lighttpd.conf +26 -0
  90. data/test/cgi/rackup_stub.rb +6 -0
  91. data/test/cgi/sample_rackup.ru +5 -0
  92. data/test/cgi/test +9 -0
  93. data/test/cgi/test+directory/test+file +1 -0
  94. data/test/cgi/test.fcgi +8 -0
  95. data/test/cgi/test.ru +5 -0
  96. data/test/gemloader.rb +10 -0
  97. data/test/multipart/bad_robots +259 -0
  98. data/test/multipart/binary +0 -0
  99. data/test/multipart/content_type_and_no_filename +6 -0
  100. data/test/multipart/empty +10 -0
  101. data/test/multipart/fail_16384_nofile +814 -0
  102. data/test/multipart/file1.txt +1 -0
  103. data/test/multipart/filename_and_modification_param +7 -0
  104. data/test/multipart/filename_and_no_name +6 -0
  105. data/test/multipart/filename_with_escaped_quotes +6 -0
  106. data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
  107. data/test/multipart/filename_with_null_byte +7 -0
  108. data/test/multipart/filename_with_percent_escaped_quotes +6 -0
  109. data/test/multipart/filename_with_unescaped_percentages +6 -0
  110. data/test/multipart/filename_with_unescaped_percentages2 +6 -0
  111. data/test/multipart/filename_with_unescaped_percentages3 +6 -0
  112. data/test/multipart/filename_with_unescaped_quotes +6 -0
  113. data/test/multipart/ie +6 -0
  114. data/test/multipart/invalid_character +6 -0
  115. data/test/multipart/mixed_files +21 -0
  116. data/test/multipart/nested +10 -0
  117. data/test/multipart/none +9 -0
  118. data/test/multipart/semicolon +6 -0
  119. data/test/multipart/text +15 -0
  120. data/test/multipart/three_files_three_fields +31 -0
  121. data/test/multipart/webkit +32 -0
  122. data/test/rackup/config.ru +31 -0
  123. data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
  124. data/test/{spec_rack_auth_basic.rb → spec_auth_basic.rb} +23 -15
  125. data/test/{spec_rack_auth_digest.rb → spec_auth_digest.rb} +56 -29
  126. data/test/spec_body_proxy.rb +85 -0
  127. data/test/spec_builder.rb +223 -0
  128. data/test/{spec_rack_cascade.rb → spec_cascade.rb} +28 -15
  129. data/test/{spec_rack_cgi.rb → spec_cgi.rb} +44 -31
  130. data/test/spec_chunked.rb +101 -0
  131. data/test/spec_commonlogger.rb +93 -0
  132. data/test/spec_conditionalget.rb +102 -0
  133. data/test/{spec_rack_config.rb → spec_config.rb} +6 -8
  134. data/test/spec_content_length.rb +85 -0
  135. data/test/spec_content_type.rb +45 -0
  136. data/test/spec_deflater.rb +339 -0
  137. data/test/{spec_rack_directory.rb → spec_directory.rb} +37 -10
  138. data/test/spec_etag.rb +107 -0
  139. data/test/{spec_rack_fastcgi.rb → spec_fastcgi.rb} +47 -29
  140. data/test/spec_file.rb +221 -0
  141. data/test/spec_handler.rb +72 -0
  142. data/test/spec_head.rb +45 -0
  143. data/test/{spec_rack_lint.rb → spec_lint.rb} +82 -60
  144. data/test/spec_lobster.rb +58 -0
  145. data/test/spec_lock.rb +164 -0
  146. data/test/spec_logger.rb +23 -0
  147. data/test/spec_methodoverride.rb +95 -0
  148. data/test/spec_mime.rb +51 -0
  149. data/test/{spec_rack_mock.rb → spec_mock.rb} +92 -38
  150. data/test/{spec_rack_mongrel.rb → spec_mongrel.rb} +46 -53
  151. data/test/spec_multipart.rb +600 -0
  152. data/test/spec_nulllogger.rb +20 -0
  153. data/test/spec_recursive.rb +72 -0
  154. data/test/spec_request.rb +1227 -0
  155. data/test/spec_response.rb +407 -0
  156. data/test/spec_rewindable_input.rb +118 -0
  157. data/test/spec_runtime.rb +49 -0
  158. data/test/spec_sendfile.rb +130 -0
  159. data/test/spec_server.rb +167 -0
  160. data/test/spec_session_abstract_id.rb +53 -0
  161. data/test/spec_session_cookie.rb +410 -0
  162. data/test/{spec_rack_session_memcache.rb → spec_session_memcache.rb} +119 -71
  163. data/test/{spec_rack_session_pool.rb → spec_session_pool.rb} +106 -69
  164. data/test/spec_showexceptions.rb +85 -0
  165. data/test/spec_showstatus.rb +103 -0
  166. data/test/spec_static.rb +145 -0
  167. data/test/spec_tempfile_reaper.rb +63 -0
  168. data/test/{spec_rack_thin.rb → spec_thin.rb} +35 -35
  169. data/test/{spec_rack_urlmap.rb → spec_urlmap.rb} +40 -19
  170. data/test/spec_utils.rb +647 -0
  171. data/test/spec_version.rb +17 -0
  172. data/test/spec_webrick.rb +184 -0
  173. data/test/static/another/index.html +1 -0
  174. data/test/static/index.html +1 -0
  175. data/test/testrequest.rb +78 -0
  176. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  177. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  178. metadata +220 -239
  179. data/RDOX +0 -0
  180. data/README +0 -592
  181. data/lib/rack/adapter/camping.rb +0 -22
  182. data/test/spec_auth.rb +0 -57
  183. data/test/spec_rack_builder.rb +0 -84
  184. data/test/spec_rack_camping.rb +0 -55
  185. data/test/spec_rack_chunked.rb +0 -62
  186. data/test/spec_rack_commonlogger.rb +0 -61
  187. data/test/spec_rack_conditionalget.rb +0 -41
  188. data/test/spec_rack_content_length.rb +0 -43
  189. data/test/spec_rack_content_type.rb +0 -30
  190. data/test/spec_rack_deflater.rb +0 -127
  191. data/test/spec_rack_etag.rb +0 -17
  192. data/test/spec_rack_file.rb +0 -75
  193. data/test/spec_rack_handler.rb +0 -43
  194. data/test/spec_rack_head.rb +0 -30
  195. data/test/spec_rack_lobster.rb +0 -45
  196. data/test/spec_rack_lock.rb +0 -38
  197. data/test/spec_rack_logger.rb +0 -21
  198. data/test/spec_rack_methodoverride.rb +0 -60
  199. data/test/spec_rack_nulllogger.rb +0 -13
  200. data/test/spec_rack_recursive.rb +0 -77
  201. data/test/spec_rack_request.rb +0 -594
  202. data/test/spec_rack_response.rb +0 -221
  203. data/test/spec_rack_rewindable_input.rb +0 -118
  204. data/test/spec_rack_runtime.rb +0 -35
  205. data/test/spec_rack_sendfile.rb +0 -86
  206. data/test/spec_rack_session_cookie.rb +0 -92
  207. data/test/spec_rack_showexceptions.rb +0 -21
  208. data/test/spec_rack_showstatus.rb +0 -72
  209. data/test/spec_rack_static.rb +0 -37
  210. data/test/spec_rack_utils.rb +0 -557
  211. data/test/spec_rack_webrick.rb +0 -130
  212. data/test/spec_rackup.rb +0 -164
data/lib/rack/mock.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'uri'
2
2
  require 'stringio'
3
+ require 'rack'
3
4
  require 'rack/lint'
4
5
  require 'rack/utils'
5
6
  require 'rack/response'
@@ -8,12 +9,12 @@ module Rack
8
9
  # Rack::MockRequest helps testing your Rack application without
9
10
  # actually using HTTP.
10
11
  #
11
- # After performing a request on a URL with get/post/put/delete, it
12
+ # After performing a request on a URL with get/post/put/patch/delete, it
12
13
  # returns a MockResponse with useful helper methods for effective
13
14
  # testing.
14
15
  #
15
16
  # You can pass a hash with additional configuration to the
16
- # get/post/put/delete.
17
+ # get/post/put/patch/delete.
17
18
  # <tt>:input</tt>:: A String or IO-like to be used as rack.input.
18
19
  # <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
19
20
  # <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
@@ -40,7 +41,7 @@ module Rack
40
41
  end
41
42
 
42
43
  DEFAULT_ENV = {
43
- "rack.version" => [1,1],
44
+ "rack.version" => Rack::VERSION,
44
45
  "rack.input" => StringIO.new,
45
46
  "rack.errors" => StringIO.new,
46
47
  "rack.multithread" => true,
@@ -52,10 +53,13 @@ module Rack
52
53
  @app = app
53
54
  end
54
55
 
55
- def get(uri, opts={}) request("GET", uri, opts) end
56
- def post(uri, opts={}) request("POST", uri, opts) end
57
- def put(uri, opts={}) request("PUT", uri, opts) end
58
- def delete(uri, opts={}) request("DELETE", uri, opts) end
56
+ def get(uri, opts={}) request("GET", uri, opts) end
57
+ def post(uri, opts={}) request("POST", uri, opts) end
58
+ def put(uri, opts={}) request("PUT", uri, opts) end
59
+ def patch(uri, opts={}) request("PATCH", uri, opts) end
60
+ def delete(uri, opts={}) request("DELETE", uri, opts) end
61
+ def head(uri, opts={}) request("HEAD", uri, opts) end
62
+ def options(uri, opts={}) request("OPTIONS", uri, opts) end
59
63
 
60
64
  def request(method="GET", uri="", opts={})
61
65
  env = self.class.env_for(uri, opts.merge(:method => method))
@@ -67,25 +71,29 @@ module Rack
67
71
  end
68
72
 
69
73
  errors = env["rack.errors"]
70
- MockResponse.new(*(app.call(env) + [errors]))
74
+ status, headers, body = app.call(env)
75
+ MockResponse.new(status, headers, body, errors)
76
+ ensure
77
+ body.close if body.respond_to?(:close)
78
+ end
79
+
80
+ # For historical reasons, we're pinning to RFC 2396. It's easier for users
81
+ # and we get support from ruby 1.8 to 2.2 using this method.
82
+ def self.parse_uri_rfc2396(uri)
83
+ @parser ||= defined?(URI::RFC2396_Parser) ? URI::RFC2396_Parser.new : URI
84
+ @parser.parse(uri)
71
85
  end
72
86
 
73
87
  # Return the Rack environment used for a request to +uri+.
74
88
  def self.env_for(uri="", opts={})
75
- uri = URI(uri)
89
+ uri = parse_uri_rfc2396(uri)
76
90
  uri.path = "/#{uri.path}" unless uri.path[0] == ?/
77
91
 
78
92
  env = DEFAULT_ENV.dup
79
93
 
80
- env["REQUEST_METHOD"] = opts[:method] ? opts[:method].to_s.upcase : "GET"
81
- env["SERVER_NAME"] = uri.host || "example.org"
82
- env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
83
- env["QUERY_STRING"] = uri.query.to_s
84
- env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path
85
- env["rack.url_scheme"] = uri.scheme || "http"
86
- env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
94
+ env_with_encoding(env, opts, uri)
87
95
 
88
- env["SCRIPT_NAME"] = opts[:script_name] || ""
96
+ env[SCRIPT_NAME] = opts[:script_name] || ""
89
97
 
90
98
  if opts[:fatal]
91
99
  env["rack.errors"] = FatalWarner.new
@@ -94,10 +102,10 @@ module Rack
94
102
  end
95
103
 
96
104
  if params = opts[:params]
97
- if env["REQUEST_METHOD"] == "GET"
105
+ if env[REQUEST_METHOD] == "GET"
98
106
  params = Utils.parse_nested_query(params) if params.is_a?(String)
99
- params.update(Utils.parse_nested_query(env["QUERY_STRING"]))
100
- env["QUERY_STRING"] = Utils.build_nested_query(params)
107
+ params.update(Utils.parse_nested_query(env[QUERY_STRING]))
108
+ env[QUERY_STRING] = Utils.build_nested_query(params)
101
109
  elsif !opts.has_key?(:input)
102
110
  opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
103
111
  if params.is_a?(Hash)
@@ -134,56 +142,73 @@ module Rack
134
142
 
135
143
  env
136
144
  end
145
+
146
+ if "<3".respond_to? :b
147
+ def self.env_with_encoding(env, opts, uri)
148
+ env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : "GET").b
149
+ env["SERVER_NAME"] = (uri.host || "example.org").b
150
+ env["SERVER_PORT"] = (uri.port ? uri.port.to_s : "80").b
151
+ env[QUERY_STRING] = (uri.query.to_s).b
152
+ env[PATH_INFO] = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b
153
+ env["rack.url_scheme"] = (uri.scheme || "http").b
154
+ env["HTTPS"] = (env["rack.url_scheme"] == "https" ? "on" : "off").b
155
+ end
156
+ else
157
+ def self.env_with_encoding(env, opts, uri)
158
+ env[REQUEST_METHOD] = opts[:method] ? opts[:method].to_s.upcase : "GET"
159
+ env["SERVER_NAME"] = uri.host || "example.org"
160
+ env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
161
+ env[QUERY_STRING] = uri.query.to_s
162
+ env[PATH_INFO] = (!uri.path || uri.path.empty?) ? "/" : uri.path
163
+ env["rack.url_scheme"] = uri.scheme || "http"
164
+ env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
165
+ end
166
+ end
137
167
  end
138
168
 
139
169
  # Rack::MockResponse provides useful helpers for testing your apps.
140
170
  # Usually, you don't create the MockResponse on your own, but use
141
171
  # MockRequest.
142
172
 
143
- class MockResponse
144
- def initialize(status, headers, body, errors=StringIO.new(""))
145
- @status = status.to_i
146
-
147
- @original_headers = headers
148
- @headers = Rack::Utils::HeaderHash.new
149
- headers.each { |field, values|
150
- @headers[field] = values
151
- @headers[field] = "" if values.empty?
152
- }
153
-
154
- @body = ""
155
- body.each { |part| @body << part }
156
-
157
- @errors = errors.string if errors.respond_to?(:string)
158
- end
159
-
160
- # Status
161
- attr_reader :status
162
-
173
+ class MockResponse < Rack::Response
163
174
  # Headers
164
- attr_reader :headers, :original_headers
175
+ attr_reader :original_headers
165
176
 
166
- def [](field)
167
- headers[field]
168
- end
177
+ # Errors
178
+ attr_accessor :errors
169
179
 
180
+ def initialize(status, headers, body, errors=StringIO.new(""))
181
+ @original_headers = headers
182
+ @errors = errors.string if errors.respond_to?(:string)
183
+ @body_string = nil
170
184
 
171
- # Body
172
- attr_reader :body
185
+ super(body, status, headers)
186
+ end
173
187
 
174
188
  def =~(other)
175
- @body =~ other
189
+ body =~ other
176
190
  end
177
191
 
178
192
  def match(other)
179
- @body.match other
193
+ body.match other
180
194
  end
181
195
 
196
+ def body
197
+ # FIXME: apparently users of MockResponse expect the return value of
198
+ # MockResponse#body to be a string. However, the real response object
199
+ # returns the body as a list.
200
+ #
201
+ # See spec_showstatus.rb:
202
+ #
203
+ # should "not replace existing messages" do
204
+ # ...
205
+ # res.body.should == "foo!"
206
+ # end
207
+ super.join
208
+ end
182
209
 
183
- # Errors
184
- attr_accessor :errors
185
-
186
-
187
- include Response::Helpers
210
+ def empty?
211
+ [201, 204, 205, 304].include? status
212
+ end
188
213
  end
189
214
  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
@@ -0,0 +1,253 @@
1
+ require 'rack/utils'
2
+
3
+ module Rack
4
+ module Multipart
5
+ class MultipartPartLimitError < Errno::EMFILE; end
6
+
7
+ class Parser
8
+ BUFSIZE = 16384
9
+ DUMMY = Struct.new(:parse).new
10
+
11
+ def self.create(env)
12
+ return DUMMY unless env['CONTENT_TYPE'] =~ MULTIPART
13
+
14
+ io = env['rack.input']
15
+ io.rewind
16
+
17
+ content_length = env['CONTENT_LENGTH']
18
+ content_length = content_length.to_i if content_length
19
+
20
+ tempfile = env['rack.multipart.tempfile_factory'] ||
21
+ lambda { |filename, content_type| Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))]) }
22
+ bufsize = env['rack.multipart.buffer_size'] || BUFSIZE
23
+
24
+ new($1, io, content_length, env, tempfile, bufsize)
25
+ end
26
+
27
+ def initialize(boundary, io, content_length, env, tempfile, bufsize)
28
+ @buf = ""
29
+
30
+ if @buf.respond_to? :force_encoding
31
+ @buf.force_encoding Encoding::ASCII_8BIT
32
+ end
33
+
34
+ @params = Utils::KeySpaceConstrainedParams.new
35
+ @boundary = "--#{boundary}"
36
+ @io = io
37
+ @content_length = content_length
38
+ @boundary_size = Utils.bytesize(@boundary) + EOL.size
39
+ @env = env
40
+ @tempfile = tempfile
41
+ @bufsize = bufsize
42
+
43
+ if @content_length
44
+ @content_length -= @boundary_size
45
+ end
46
+
47
+ @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
48
+ @full_boundary = @boundary + EOL
49
+ end
50
+
51
+ def parse
52
+ fast_forward_to_first_boundary
53
+
54
+ opened_files = 0
55
+ loop do
56
+
57
+ head, filename, content_type, name, body =
58
+ get_current_head_and_filename_and_content_type_and_name_and_body
59
+
60
+ if Utils.multipart_part_limit > 0
61
+ opened_files += 1 if filename
62
+ raise MultipartPartLimitError, 'Maximum file multiparts in content reached' if opened_files >= Utils.multipart_part_limit
63
+ end
64
+
65
+ # Save the rest.
66
+ if i = @buf.index(rx)
67
+ body << @buf.slice!(0, i)
68
+ @buf.slice!(0, @boundary_size+2)
69
+
70
+ @content_length = -1 if $1 == "--"
71
+ end
72
+
73
+ get_data(filename, body, content_type, name, head) do |data|
74
+ tag_multipart_encoding(filename, content_type, name, data)
75
+
76
+ Utils.normalize_params(@params, name, data)
77
+ end
78
+
79
+ # break if we're at the end of a buffer, but not if it is the end of a field
80
+ break if (@buf.empty? && $1 != EOL) || @content_length == -1
81
+ end
82
+
83
+ @io.rewind
84
+
85
+ @params.to_params_hash
86
+ end
87
+
88
+ private
89
+ def full_boundary; @full_boundary; end
90
+
91
+ def rx; @rx; end
92
+
93
+ def fast_forward_to_first_boundary
94
+ loop do
95
+ content = @io.read(@bufsize)
96
+ raise EOFError, "bad content body" unless content
97
+ @buf << content
98
+
99
+ while @buf.gsub!(/\A([^\n]*\n)/, '')
100
+ read_buffer = $1
101
+ return if read_buffer == full_boundary
102
+ end
103
+
104
+ raise EOFError, "bad content body" if Utils.bytesize(@buf) >= @bufsize
105
+ end
106
+ end
107
+
108
+ def get_current_head_and_filename_and_content_type_and_name_and_body
109
+ head = nil
110
+ body = ''
111
+
112
+ if body.respond_to? :force_encoding
113
+ body.force_encoding Encoding::ASCII_8BIT
114
+ end
115
+
116
+ filename = content_type = name = nil
117
+
118
+ until head && @buf =~ rx
119
+ if !head && i = @buf.index(EOL+EOL)
120
+ head = @buf.slice!(0, i+2) # First \r\n
121
+
122
+ @buf.slice!(0, 2) # Second \r\n
123
+
124
+ content_type = head[MULTIPART_CONTENT_TYPE, 1]
125
+ name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
126
+
127
+ filename = get_filename(head)
128
+
129
+ if name.nil? || name.empty? && filename
130
+ name = filename
131
+ end
132
+
133
+ if filename
134
+ (@env['rack.tempfiles'] ||= []) << body = @tempfile.call(filename, content_type)
135
+ body.binmode if body.respond_to?(:binmode)
136
+ end
137
+
138
+ next
139
+ end
140
+
141
+ # Save the read body part.
142
+ if head && (@boundary_size+4 < @buf.size)
143
+ body << @buf.slice!(0, @buf.size - (@boundary_size+4))
144
+ end
145
+
146
+ content = @io.read(@content_length && @bufsize >= @content_length ? @content_length : @bufsize)
147
+ raise EOFError, "bad content body" if content.nil? || content.empty?
148
+
149
+ @buf << content
150
+ @content_length -= content.size if @content_length
151
+ end
152
+
153
+ [head, filename, content_type, name, body]
154
+ end
155
+
156
+ def get_filename(head)
157
+ filename = nil
158
+ case head
159
+ when RFC2183
160
+ filename = Hash[head.scan(DISPPARM)]['filename']
161
+ filename = $1 if filename and filename =~ /^"(.*)"$/
162
+ when BROKEN_QUOTED, BROKEN_UNQUOTED
163
+ filename = $1
164
+ end
165
+
166
+ return unless filename
167
+
168
+ if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
169
+ filename = Utils.unescape(filename)
170
+ end
171
+
172
+ scrub_filename filename
173
+
174
+ if filename !~ /\\[^\\"]/
175
+ filename = filename.gsub(/\\(.)/, '\1')
176
+ end
177
+ filename
178
+ end
179
+
180
+ if "<3".respond_to? :valid_encoding?
181
+ def scrub_filename(filename)
182
+ unless filename.valid_encoding?
183
+ # FIXME: this force_encoding is for Ruby 2.0 and 1.9 support.
184
+ # We can remove it after they are dropped
185
+ filename.force_encoding(Encoding::ASCII_8BIT)
186
+ filename.encode!(:invalid => :replace, :undef => :replace)
187
+ end
188
+ end
189
+
190
+ CHARSET = "charset"
191
+ TEXT_PLAIN = "text/plain"
192
+
193
+ def tag_multipart_encoding(filename, content_type, name, body)
194
+ name.force_encoding Encoding::UTF_8
195
+
196
+ return if filename
197
+
198
+ encoding = Encoding::UTF_8
199
+
200
+ if content_type
201
+ list = content_type.split(';')
202
+ type_subtype = list.first
203
+ type_subtype.strip!
204
+ if TEXT_PLAIN == type_subtype
205
+ rest = list.drop 1
206
+ rest.each do |param|
207
+ k,v = param.split('=', 2)
208
+ k.strip!
209
+ v.strip!
210
+ encoding = Encoding.find v if k == CHARSET
211
+ end
212
+ end
213
+ end
214
+
215
+ name.force_encoding encoding
216
+ body.force_encoding encoding
217
+ end
218
+ else
219
+ def scrub_filename(filename)
220
+ end
221
+ def tag_multipart_encoding(filename, content_type, name, body)
222
+ end
223
+ end
224
+
225
+ def get_data(filename, body, content_type, name, head)
226
+ data = body
227
+ if filename == ""
228
+ # filename is blank which means no file has been selected
229
+ return
230
+ elsif filename
231
+ body.rewind if body.respond_to?(:rewind)
232
+
233
+ # Take the basename of the upload's original filename.
234
+ # This handles the full Windows paths given by Internet Explorer
235
+ # (and perhaps other broken user agents) without affecting
236
+ # those which give the lone filename.
237
+ filename = filename.split(/[\/\\]/).last
238
+
239
+ data = {:filename => filename, :type => content_type,
240
+ :name => name, :tempfile => body, :head => head}
241
+ elsif !filename && content_type && body.is_a?(IO)
242
+ body.rewind
243
+
244
+ # Generic multipart cases, not coming from a form
245
+ data = {:type => content_type,
246
+ :name => name, :tempfile => body, :head => head}
247
+ end
248
+
249
+ yield data
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,34 @@
1
+ module Rack
2
+ module Multipart
3
+ class UploadedFile
4
+ # The filename, *not* including the path, of the "uploaded" file
5
+ attr_reader :original_filename
6
+
7
+ # The content type of the "uploaded" file
8
+ attr_accessor :content_type
9
+
10
+ def initialize(path, content_type = "text/plain", binary = false)
11
+ raise "#{path} file does not exist" unless ::File.exist?(path)
12
+ @content_type = content_type
13
+ @original_filename = ::File.basename(path)
14
+ @tempfile = Tempfile.new([@original_filename, ::File.extname(path)])
15
+ @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
16
+ @tempfile.binmode if binary
17
+ FileUtils.copy_file(path, @tempfile.path)
18
+ end
19
+
20
+ def path
21
+ @tempfile.path
22
+ end
23
+ alias_method :local_path, :path
24
+
25
+ def respond_to?(*args)
26
+ super or @tempfile.respond_to?(*args)
27
+ end
28
+
29
+ def method_missing(method_name, *args, &block) #:nodoc:
30
+ @tempfile.__send__(method_name, *args, &block)
31
+ end
32
+ end
33
+ end
34
+ 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=\"?([^\";,]+)\"?|ni
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.create(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
@@ -9,10 +9,29 @@ module Rack
9
9
  @app.call(env)
10
10
  end
11
11
 
12
- def info(progname = nil, &block); end
12
+ def info(progname = nil, &block); end
13
13
  def debug(progname = nil, &block); end
14
- def warn(progname = nil, &block); end
14
+ def warn(progname = nil, &block); end
15
15
  def error(progname = nil, &block); end
16
16
  def fatal(progname = nil, &block); end
17
+ def unknown(progname = nil, &block); end
18
+ def info? ; end
19
+ def debug? ; end
20
+ def warn? ; end
21
+ def error? ; end
22
+ def fatal? ; end
23
+ def level ; end
24
+ def progname ; end
25
+ def datetime_format ; end
26
+ def formatter ; end
27
+ def sev_threshold ; end
28
+ def level=(level); end
29
+ def progname=(progname); end
30
+ def datetime_format=(datetime_format); end
31
+ def formatter=(formatter); end
32
+ def sev_threshold=(sev_threshold); end
33
+ def close ; end
34
+ def add(severity, message = nil, progname = nil, &block); end
35
+ def <<(msg); end
17
36
  end
18
37
  end
@@ -14,8 +14,8 @@ module Rack
14
14
  @url = URI(url)
15
15
  @env = env
16
16
 
17
- @env["PATH_INFO"] = @url.path
18
- @env["QUERY_STRING"] = @url.query if @url.query
17
+ @env[PATH_INFO] = @url.path
18
+ @env[QUERY_STRING] = @url.query if @url.query
19
19
  @env["HTTP_HOST"] = @url.host if @url.host
20
20
  @env["HTTP_PORT"] = @url.port if @url.port
21
21
  @env["rack.url_scheme"] = @url.scheme if @url.scheme
@@ -35,7 +35,11 @@ module Rack
35
35
  end
36
36
 
37
37
  def call(env)
38
- @script_name = env["SCRIPT_NAME"]
38
+ dup._call(env)
39
+ end
40
+
41
+ def _call(env)
42
+ @script_name = env[SCRIPT_NAME]
39
43
  @app.call(env.merge('rack.recursive.include' => method(:include)))
40
44
  rescue ForwardRequest => req
41
45
  call(env.merge(req.env))
@@ -47,8 +51,9 @@ module Rack
47
51
  raise ArgumentError, "can only include below #{@script_name}, not #{path}"
48
52
  end
49
53
 
50
- env = env.merge("PATH_INFO" => path, "SCRIPT_NAME" => @script_name,
51
- "REQUEST_METHOD" => "GET",
54
+ env = env.merge(PATH_INFO => path,
55
+ SCRIPT_NAME => @script_name,
56
+ REQUEST_METHOD => "GET",
52
57
  "CONTENT_LENGTH" => "0", "CONTENT_TYPE" => "",
53
58
  "rack.input" => StringIO.new(""))
54
59
  @app.call(env)