rack 2.0.1 → 2.2.17

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 (189) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +795 -0
  3. data/CONTRIBUTING.md +136 -0
  4. data/{COPYING → MIT-LICENSE} +4 -2
  5. data/README.rdoc +188 -145
  6. data/Rakefile +37 -23
  7. data/{SPEC → SPEC.rdoc} +46 -17
  8. data/bin/rackup +1 -0
  9. data/example/lobster.ru +2 -0
  10. data/example/protectedlobster.rb +3 -1
  11. data/example/protectedlobster.ru +2 -0
  12. data/lib/rack/auth/abstract/handler.rb +3 -1
  13. data/lib/rack/auth/abstract/request.rb +1 -1
  14. data/lib/rack/auth/basic.rb +6 -4
  15. data/lib/rack/auth/digest/md5.rb +13 -11
  16. data/lib/rack/auth/digest/nonce.rb +5 -3
  17. data/lib/rack/auth/digest/params.rb +4 -2
  18. data/lib/rack/auth/digest/request.rb +5 -3
  19. data/lib/rack/body_proxy.rb +15 -14
  20. data/lib/rack/builder.rb +116 -23
  21. data/lib/rack/cascade.rb +28 -12
  22. data/lib/rack/chunked.rb +68 -20
  23. data/lib/rack/common_logger.rb +37 -25
  24. data/lib/rack/conditional_get.rb +20 -16
  25. data/lib/rack/config.rb +2 -0
  26. data/lib/rack/content_length.rb +8 -7
  27. data/lib/rack/content_type.rb +5 -4
  28. data/lib/rack/core_ext/regexp.rb +14 -0
  29. data/lib/rack/deflater.rb +60 -70
  30. data/lib/rack/directory.rb +84 -64
  31. data/lib/rack/etag.rb +8 -5
  32. data/lib/rack/events.rb +19 -20
  33. data/lib/rack/file.rb +4 -173
  34. data/lib/rack/files.rb +218 -0
  35. data/lib/rack/handler/cgi.rb +2 -3
  36. data/lib/rack/handler/fastcgi.rb +4 -4
  37. data/lib/rack/handler/lsws.rb +3 -3
  38. data/lib/rack/handler/scgi.rb +9 -8
  39. data/lib/rack/handler/thin.rb +3 -3
  40. data/lib/rack/handler/webrick.rb +19 -10
  41. data/lib/rack/handler.rb +7 -2
  42. data/lib/rack/head.rb +1 -1
  43. data/lib/rack/lint.rb +221 -186
  44. data/lib/rack/lobster.rb +10 -10
  45. data/lib/rack/lock.rb +14 -4
  46. data/lib/rack/logger.rb +2 -0
  47. data/lib/rack/media_type.rb +23 -8
  48. data/lib/rack/method_override.rb +13 -4
  49. data/lib/rack/mime.rb +9 -1
  50. data/lib/rack/mock.rb +135 -29
  51. data/lib/rack/multipart/generator.rb +17 -13
  52. data/lib/rack/multipart/parser.rb +85 -68
  53. data/lib/rack/multipart/uploaded_file.rb +15 -7
  54. data/lib/rack/multipart.rb +6 -5
  55. data/lib/rack/null_logger.rb +2 -0
  56. data/lib/rack/query_parser.rb +108 -36
  57. data/lib/rack/recursive.rb +7 -5
  58. data/lib/rack/reloader.rb +8 -4
  59. data/lib/rack/request.rb +232 -60
  60. data/lib/rack/response.rb +127 -44
  61. data/lib/rack/rewindable_input.rb +4 -3
  62. data/lib/rack/runtime.rb +6 -4
  63. data/lib/rack/sendfile.rb +14 -10
  64. data/lib/rack/server.rb +97 -25
  65. data/lib/rack/session/abstract/id.rb +113 -25
  66. data/lib/rack/session/cookie.rb +22 -14
  67. data/lib/rack/session/memcache.rb +4 -87
  68. data/lib/rack/session/pool.rb +24 -10
  69. data/lib/rack/show_exceptions.rb +22 -18
  70. data/lib/rack/show_status.rb +9 -9
  71. data/lib/rack/static.rb +25 -12
  72. data/lib/rack/tempfile_reaper.rb +1 -1
  73. data/lib/rack/urlmap.rb +13 -7
  74. data/lib/rack/utils.rb +135 -123
  75. data/lib/rack/version.rb +29 -0
  76. data/lib/rack.rb +67 -73
  77. data/rack.gemspec +40 -29
  78. metadata +25 -184
  79. data/HISTORY.md +0 -505
  80. data/test/builder/an_underscore_app.rb +0 -5
  81. data/test/builder/anything.rb +0 -5
  82. data/test/builder/comment.ru +0 -4
  83. data/test/builder/end.ru +0 -5
  84. data/test/builder/line.ru +0 -1
  85. data/test/builder/options.ru +0 -2
  86. data/test/cgi/assets/folder/test.js +0 -1
  87. data/test/cgi/assets/fonts/font.eot +0 -1
  88. data/test/cgi/assets/images/image.png +0 -1
  89. data/test/cgi/assets/index.html +0 -1
  90. data/test/cgi/assets/javascripts/app.js +0 -1
  91. data/test/cgi/assets/stylesheets/app.css +0 -1
  92. data/test/cgi/lighttpd.conf +0 -26
  93. data/test/cgi/rackup_stub.rb +0 -6
  94. data/test/cgi/sample_rackup.ru +0 -5
  95. data/test/cgi/test +0 -9
  96. data/test/cgi/test+directory/test+file +0 -1
  97. data/test/cgi/test.fcgi +0 -9
  98. data/test/cgi/test.gz +0 -0
  99. data/test/cgi/test.ru +0 -5
  100. data/test/gemloader.rb +0 -10
  101. data/test/helper.rb +0 -34
  102. data/test/multipart/bad_robots +0 -259
  103. data/test/multipart/binary +0 -0
  104. data/test/multipart/content_type_and_no_filename +0 -6
  105. data/test/multipart/empty +0 -10
  106. data/test/multipart/fail_16384_nofile +0 -814
  107. data/test/multipart/file1.txt +0 -1
  108. data/test/multipart/filename_and_modification_param +0 -7
  109. data/test/multipart/filename_and_no_name +0 -6
  110. data/test/multipart/filename_with_encoded_words +0 -7
  111. data/test/multipart/filename_with_escaped_quotes +0 -6
  112. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  113. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  114. data/test/multipart/filename_with_single_quote +0 -7
  115. data/test/multipart/filename_with_unescaped_percentages +0 -6
  116. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  117. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  118. data/test/multipart/filename_with_unescaped_quotes +0 -6
  119. data/test/multipart/ie +0 -6
  120. data/test/multipart/invalid_character +0 -6
  121. data/test/multipart/mixed_files +0 -21
  122. data/test/multipart/nested +0 -10
  123. data/test/multipart/none +0 -9
  124. data/test/multipart/quoted +0 -15
  125. data/test/multipart/rack-logo.png +0 -0
  126. data/test/multipart/semicolon +0 -6
  127. data/test/multipart/text +0 -15
  128. data/test/multipart/three_files_three_fields +0 -31
  129. data/test/multipart/unity3d_wwwform +0 -11
  130. data/test/multipart/webkit +0 -32
  131. data/test/rackup/config.ru +0 -31
  132. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  133. data/test/spec_auth_basic.rb +0 -89
  134. data/test/spec_auth_digest.rb +0 -260
  135. data/test/spec_body_proxy.rb +0 -85
  136. data/test/spec_builder.rb +0 -233
  137. data/test/spec_cascade.rb +0 -63
  138. data/test/spec_cgi.rb +0 -84
  139. data/test/spec_chunked.rb +0 -103
  140. data/test/spec_common_logger.rb +0 -95
  141. data/test/spec_conditional_get.rb +0 -103
  142. data/test/spec_config.rb +0 -23
  143. data/test/spec_content_length.rb +0 -86
  144. data/test/spec_content_type.rb +0 -46
  145. data/test/spec_deflater.rb +0 -365
  146. data/test/spec_directory.rb +0 -148
  147. data/test/spec_etag.rb +0 -108
  148. data/test/spec_events.rb +0 -133
  149. data/test/spec_fastcgi.rb +0 -85
  150. data/test/spec_file.rb +0 -251
  151. data/test/spec_handler.rb +0 -57
  152. data/test/spec_head.rb +0 -46
  153. data/test/spec_lint.rb +0 -515
  154. data/test/spec_lobster.rb +0 -59
  155. data/test/spec_lock.rb +0 -194
  156. data/test/spec_logger.rb +0 -24
  157. data/test/spec_media_type.rb +0 -42
  158. data/test/spec_method_override.rb +0 -83
  159. data/test/spec_mime.rb +0 -51
  160. data/test/spec_mock.rb +0 -342
  161. data/test/spec_multipart.rb +0 -716
  162. data/test/spec_null_logger.rb +0 -21
  163. data/test/spec_recursive.rb +0 -75
  164. data/test/spec_request.rb +0 -1393
  165. data/test/spec_response.rb +0 -510
  166. data/test/spec_rewindable_input.rb +0 -128
  167. data/test/spec_runtime.rb +0 -50
  168. data/test/spec_sendfile.rb +0 -125
  169. data/test/spec_server.rb +0 -193
  170. data/test/spec_session_abstract_id.rb +0 -31
  171. data/test/spec_session_abstract_session_hash.rb +0 -28
  172. data/test/spec_session_cookie.rb +0 -442
  173. data/test/spec_session_memcache.rb +0 -320
  174. data/test/spec_session_pool.rb +0 -210
  175. data/test/spec_show_exceptions.rb +0 -80
  176. data/test/spec_show_status.rb +0 -104
  177. data/test/spec_static.rb +0 -184
  178. data/test/spec_tempfile_reaper.rb +0 -64
  179. data/test/spec_thin.rb +0 -96
  180. data/test/spec_urlmap.rb +0 -237
  181. data/test/spec_utils.rb +0 -742
  182. data/test/spec_version.rb +0 -11
  183. data/test/spec_webrick.rb +0 -208
  184. data/test/static/another/index.html +0 -1
  185. data/test/static/foo.html +0 -1
  186. data/test/static/index.html +0 -1
  187. data/test/testrequest.rb +0 -78
  188. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  189. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
@@ -1,16 +1,25 @@
1
- require 'rack/utils'
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
2
4
 
3
5
  module Rack
4
6
  module Multipart
5
7
  class MultipartPartLimitError < Errno::EMFILE; end
8
+ class MultipartTotalPartLimitError < StandardError; end
6
9
 
7
10
  class Parser
8
- BUFSIZE = 16384
11
+ (require_relative '../core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
12
+
13
+ BUFSIZE = 1_048_576
9
14
  TEXT_PLAIN = "text/plain"
10
15
  TEMPFILE_FACTORY = lambda { |filename, content_type|
11
- Tempfile.new(["RackMultipart", ::File.extname(filename)])
16
+ extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
17
+
18
+ Tempfile.new(["RackMultipart", extension])
12
19
  }
13
20
 
21
+ BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
22
+
14
23
  class BoundedIO # :nodoc:
15
24
  def initialize(io, content_length)
16
25
  @io = io
@@ -18,15 +27,15 @@ module Rack
18
27
  @cursor = 0
19
28
  end
20
29
 
21
- def read(size)
30
+ def read(size, outbuf = nil)
22
31
  return if @cursor >= @content_length
23
32
 
24
33
  left = @content_length - @cursor
25
34
 
26
35
  str = if left < size
27
- @io.read left
36
+ @io.read left, outbuf
28
37
  else
29
- @io.read size
38
+ @io.read size, outbuf
30
39
  end
31
40
 
32
41
  if str
@@ -39,8 +48,6 @@ module Rack
39
48
  str
40
49
  end
41
50
 
42
- def eof?; @content_length == @cursor; end
43
-
44
51
  def rewind
45
52
  @io.rewind
46
53
  end
@@ -63,13 +70,14 @@ module Rack
63
70
  return EMPTY unless boundary
64
71
 
65
72
  io = BoundedIO.new(io, content_length) if content_length
73
+ outbuf = String.new
66
74
 
67
75
  parser = new(boundary, tmpfile, bufsize, qp)
68
- parser.on_read io.read(bufsize), io.eof?
76
+ parser.on_read io.read(bufsize, outbuf)
69
77
 
70
78
  loop do
71
79
  break if parser.state == :DONE
72
- parser.on_read io.read(bufsize), io.eof?
80
+ parser.on_read io.read(bufsize, outbuf)
73
81
  end
74
82
 
75
83
  io.rewind
@@ -92,14 +100,8 @@ module Rack
92
100
  # those which give the lone filename.
93
101
  fn = filename.split(/[\/\\]/).last
94
102
 
95
- data = {:filename => fn, :type => content_type,
96
- :name => name, :tempfile => body, :head => head}
97
- elsif !filename && content_type && body.is_a?(IO)
98
- body.rewind
99
-
100
- # Generic multipart cases, not coming from a form
101
- data = {:type => content_type,
102
- :name => name, :tempfile => body, :head => head}
103
+ data = { filename: fn, type: content_type,
104
+ name: name, tempfile: body, head: head }
103
105
  end
104
106
 
105
107
  yield data
@@ -118,7 +120,7 @@ module Rack
118
120
 
119
121
  include Enumerable
120
122
 
121
- def initialize tempfile
123
+ def initialize(tempfile)
122
124
  @tempfile = tempfile
123
125
  @mime_parts = []
124
126
  @open_files = 0
@@ -128,62 +130,75 @@ module Rack
128
130
  @mime_parts.each { |part| yield part }
129
131
  end
130
132
 
131
- def on_mime_head mime_index, head, filename, content_type, name
133
+ def on_mime_head(mime_index, head, filename, content_type, name)
132
134
  if filename
133
135
  body = @tempfile.call(filename, content_type)
134
136
  body.binmode if body.respond_to?(:binmode)
135
137
  klass = TempfilePart
136
138
  @open_files += 1
137
139
  else
138
- body = ''.force_encoding(Encoding::ASCII_8BIT)
140
+ body = String.new
139
141
  klass = BufferPart
140
142
  end
141
143
 
142
144
  @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
143
- check_open_files
145
+
146
+ check_part_limits
144
147
  end
145
148
 
146
- def on_mime_body mime_index, content
149
+ def on_mime_body(mime_index, content)
147
150
  @mime_parts[mime_index].body << content
148
151
  end
149
152
 
150
- def on_mime_finish mime_index
153
+ def on_mime_finish(mime_index)
151
154
  end
152
155
 
153
156
  private
154
157
 
155
- def check_open_files
156
- if Utils.multipart_part_limit > 0
157
- if @open_files >= Utils.multipart_part_limit
158
+ def check_part_limits
159
+ file_limit = Utils.multipart_file_limit
160
+ part_limit = Utils.multipart_total_part_limit
161
+
162
+ if file_limit && file_limit > 0
163
+ if @open_files >= file_limit
158
164
  @mime_parts.each(&:close)
159
165
  raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
160
166
  end
161
167
  end
168
+
169
+ if part_limit && part_limit > 0
170
+ if @mime_parts.size >= part_limit
171
+ @mime_parts.each(&:close)
172
+ raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
173
+ end
174
+ end
162
175
  end
163
176
  end
164
177
 
165
178
  attr_reader :state
166
179
 
167
180
  def initialize(boundary, tempfile, bufsize, query_parser)
168
- @buf = "".force_encoding(Encoding::ASCII_8BIT)
169
-
170
181
  @query_parser = query_parser
171
182
  @params = query_parser.make_params
172
183
  @boundary = "--#{boundary}"
173
- @boundary_size = @boundary.bytesize + EOL.size
174
184
  @bufsize = bufsize
175
185
 
176
- @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
177
186
  @full_boundary = @boundary
178
187
  @end_boundary = @boundary + '--'
179
188
  @state = :FAST_FORWARD
180
189
  @mime_index = 0
181
190
  @collector = Collector.new tempfile
191
+
192
+ @sbuf = StringScanner.new("".dup)
193
+ @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
194
+ @end_boundary_size = boundary.bytesize + 6 # (-- at start, -- at finish, EOL at end)
195
+ @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
196
+ @head_regex = /(.*?#{EOL})#{EOL}/m
182
197
  end
183
198
 
184
- def on_read content, eof
185
- handle_empty_content!(content, eof)
186
- @buf << content
199
+ def on_read(content)
200
+ handle_empty_content!(content)
201
+ @sbuf.concat content
187
202
  run_parser
188
203
  end
189
204
 
@@ -194,7 +209,6 @@ module Rack
194
209
  @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
195
210
  end
196
211
  end
197
-
198
212
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
199
213
  end
200
214
 
@@ -218,10 +232,15 @@ module Rack
218
232
  end
219
233
 
220
234
  def handle_fast_forward
221
- if consume_boundary
235
+ tok = consume_boundary
236
+
237
+ if tok == :END_BOUNDARY && @sbuf.pos == @end_boundary_size && @sbuf.eos?
238
+ # stop parsing a buffer if a buffer is only an end boundary.
239
+ @state = :DONE
240
+ elsif tok
222
241
  @state = :MIME_HEAD
223
242
  else
224
- raise EOFError, "bad content body" if @buf.bytesize >= @bufsize
243
+ raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
225
244
  :want_read
226
245
  end
227
246
  end
@@ -229,19 +248,16 @@ module Rack
229
248
  def handle_consume_token
230
249
  tok = consume_boundary
231
250
  # break if we're at the end of a buffer, but not if it is the end of a field
232
- if tok == :END_BOUNDARY || (@buf.empty? && tok != :BOUNDARY)
233
- @state = :DONE
251
+ @state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY)
252
+ :DONE
234
253
  else
235
- @state = :MIME_HEAD
254
+ :MIME_HEAD
236
255
  end
237
256
  end
238
257
 
239
258
  def handle_mime_head
240
- if @buf.index(EOL + EOL)
241
- i = @buf.index(EOL+EOL)
242
- head = @buf.slice!(0, i+2) # First \r\n
243
- @buf.slice!(0, 2) # Second \r\n
244
-
259
+ if @sbuf.scan_until(@head_regex)
260
+ head = @sbuf[1]
245
261
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
246
262
  if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
247
263
  name = Rack::Auth::Digest::Params::dequote(name)
@@ -252,7 +268,7 @@ module Rack
252
268
  filename = get_filename(head)
253
269
 
254
270
  if name.nil? || name.empty?
255
- name = filename || "#{content_type || TEXT_PLAIN}[]"
271
+ name = filename || "#{content_type || TEXT_PLAIN}[]".dup
256
272
  end
257
273
 
258
274
  @collector.on_mime_head @mime_index, head, filename, content_type, name
@@ -263,31 +279,33 @@ module Rack
263
279
  end
264
280
 
265
281
  def handle_mime_body
266
- if @buf =~ rx
267
- # Save the rest.
268
- if i = @buf.index(rx)
269
- @collector.on_mime_body @mime_index, @buf.slice!(0, i)
270
- @buf.slice!(0, 2) # Remove \r\n after the content
271
- end
282
+ if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
283
+ body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
284
+ @collector.on_mime_body @mime_index, body
285
+ @sbuf.pos += body.length + 2 # skip \r\n after the content
272
286
  @state = :CONSUME_TOKEN
273
287
  @mime_index += 1
274
288
  else
289
+ # Save what we have so far
290
+ if @rx_max_size < @sbuf.rest_size
291
+ delta = @sbuf.rest_size - @rx_max_size
292
+ @collector.on_mime_body @mime_index, @sbuf.peek(delta)
293
+ @sbuf.pos += delta
294
+ @sbuf.string = @sbuf.rest
295
+ end
275
296
  :want_read
276
297
  end
277
298
  end
278
299
 
279
300
  def full_boundary; @full_boundary; end
280
301
 
281
- def rx; @rx; end
282
-
283
302
  def consume_boundary
284
- while @buf.gsub!(/\A([^\n]*(?:\n|\Z))/, '')
285
- read_buffer = $1
303
+ while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
286
304
  case read_buffer.strip
287
305
  when full_boundary then return :BOUNDARY
288
306
  when @end_boundary then return :END_BOUNDARY
289
307
  end
290
- return if @buf.empty?
308
+ return if @sbuf.eos?
291
309
  end
292
310
  end
293
311
 
@@ -302,14 +320,15 @@ module Rack
302
320
  elsif filename = params['filename*']
303
321
  encoding, _, filename = filename.split("'", 3)
304
322
  end
305
- when BROKEN_QUOTED, BROKEN_UNQUOTED
323
+ when BROKEN
306
324
  filename = $1
325
+ filename = $1 if filename =~ /^"(.*)"$/
307
326
  end
308
327
 
309
328
  return unless filename
310
329
 
311
- if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
312
- filename = Utils.unescape(filename)
330
+ if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
331
+ filename = Utils.unescape_path(filename)
313
332
  end
314
333
 
315
334
  filename.scrub!
@@ -325,7 +344,7 @@ module Rack
325
344
  filename
326
345
  end
327
346
 
328
- CHARSET = "charset"
347
+ CHARSET = "charset"
329
348
 
330
349
  def tag_multipart_encoding(filename, content_type, name, body)
331
350
  name = name.to_s
@@ -340,12 +359,12 @@ module Rack
340
359
  type_subtype = list.first
341
360
  type_subtype.strip!
342
361
  if TEXT_PLAIN == type_subtype
343
- rest = list.drop 1
362
+ rest = list.drop 1
344
363
  rest.each do |param|
345
- k,v = param.split('=', 2)
364
+ k, v = param.split('=', 2)
346
365
  k.strip!
347
366
  v.strip!
348
- v = v[1..-2] if v[0] == '"' && v[-1] == '"'
367
+ v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
349
368
  encoding = Encoding.find v if k == CHARSET
350
369
  end
351
370
  end
@@ -355,11 +374,9 @@ module Rack
355
374
  body.force_encoding(encoding)
356
375
  end
357
376
 
358
-
359
- def handle_empty_content!(content, eof)
377
+ def handle_empty_content!(content)
360
378
  if content.nil? || content.empty?
361
- raise EOFError if eof
362
- return true
379
+ raise EOFError
363
380
  end
364
381
  end
365
382
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  module Multipart
3
5
  class UploadedFile
@@ -7,17 +9,23 @@ module Rack
7
9
  # The content type of the "uploaded" file
8
10
  attr_accessor :content_type
9
11
 
10
- def initialize(path, content_type = "text/plain", binary = false)
11
- raise "#{path} file does not exist" unless ::File.exist?(path)
12
+ def initialize(filepath = nil, ct = "text/plain", bin = false,
13
+ path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
14
+ if io
15
+ @tempfile = io
16
+ @original_filename = filename
17
+ else
18
+ raise "#{path} file does not exist" unless ::File.exist?(path)
19
+ @original_filename = filename || ::File.basename(path)
20
+ @tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
21
+ @tempfile.binmode if binary
22
+ FileUtils.copy_file(path, @tempfile.path)
23
+ end
12
24
  @content_type = content_type
13
- @original_filename = ::File.basename(path)
14
- @tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
15
- @tempfile.binmode if binary
16
- FileUtils.copy_file(path, @tempfile.path)
17
25
  end
18
26
 
19
27
  def path
20
- @tempfile.path
28
+ @tempfile.path if @tempfile.respond_to?(:path)
21
29
  end
22
30
  alias_method :local_path, :path
23
31
 
@@ -1,4 +1,6 @@
1
- require 'rack/multipart/parser'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'multipart/parser'
2
4
 
3
5
  module Rack
4
6
  # A multipart form data parser, adapted from IOWA.
@@ -14,13 +16,12 @@ module Rack
14
16
  TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
15
17
  CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
16
18
  VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
17
- BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
18
- BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
19
+ BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
19
20
  MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
20
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name=(#{VALUE})/ni
21
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
21
22
  MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
22
23
  # Updated definitions from RFC 2231
23
- ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
24
+ ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
24
25
  ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
25
26
  SECTION = /\*[0-9]+/
26
27
  REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class NullLogger
3
5
  def initialize(app)
@@ -1,5 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class QueryParser
5
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
6
+
3
7
  DEFAULT_SEP = /[&;] */n
4
8
  COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
5
9
 
@@ -12,16 +16,47 @@ module Rack
12
16
  # sequence.
13
17
  class InvalidParameterError < ArgumentError; end
14
18
 
15
- def self.make_default(key_space_limit, param_depth_limit)
16
- new Params, key_space_limit, param_depth_limit
19
+ # QueryLimitError is for errors raised when the query provided exceeds one
20
+ # of the query parser limits.
21
+ class QueryLimitError < RangeError
22
+ end
23
+
24
+ # ParamsTooDeepError is the old name for the error that is raised when params
25
+ # are recursively nested over the specified limit. Make it the same as
26
+ # as QueryLimitError, so that code that rescues ParamsTooDeepError error
27
+ # to handle bad query strings also now handles other limits.
28
+ ParamsTooDeepError = QueryLimitError
29
+
30
+ def self.make_default(key_space_limit, param_depth_limit, **options)
31
+ new(Params, key_space_limit, param_depth_limit, **options)
17
32
  end
18
33
 
19
34
  attr_reader :key_space_limit, :param_depth_limit
20
35
 
21
- def initialize(params_class, key_space_limit, param_depth_limit)
36
+ env_int = lambda do |key, val|
37
+ if str_val = ENV[key]
38
+ begin
39
+ val = Integer(str_val, 10)
40
+ rescue ArgumentError
41
+ raise ArgumentError, "non-integer value provided for environment variable #{key}"
42
+ end
43
+ end
44
+
45
+ val
46
+ end
47
+
48
+ BYTESIZE_LIMIT = env_int.call("RACK_QUERY_PARSER_BYTESIZE_LIMIT", 4194304)
49
+ private_constant :BYTESIZE_LIMIT
50
+
51
+ PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
52
+ private_constant :PARAMS_LIMIT
53
+
54
+ def initialize(params_class, key_space_limit, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
22
55
  @params_class = params_class
23
56
  @key_space_limit = key_space_limit
24
57
  @param_depth_limit = param_depth_limit
58
+ @bytesize_limit = bytesize_limit
59
+ @params_limit = params_limit
25
60
  end
26
61
 
27
62
  # Stolen from Mongrel, with some small modifications:
@@ -34,9 +69,9 @@ module Rack
34
69
 
35
70
  params = make_params
36
71
 
37
- (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
72
+ check_query_string(qs, d).split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
38
73
  next if p.empty?
39
- k, v = p.split('='.freeze, 2).map!(&unescaper)
74
+ k, v = p.split('=', 2).map!(&unescaper)
40
75
 
41
76
  if cur = params[k]
42
77
  if cur.class == Array
@@ -49,7 +84,7 @@ module Rack
49
84
  end
50
85
  end
51
86
 
52
- return params.to_params_hash
87
+ return params.to_h
53
88
  end
54
89
 
55
90
  # parse_nested_query expands a query string into structural types. Supported
@@ -58,43 +93,44 @@ module Rack
58
93
  # ParameterTypeError is raised. Users are encouraged to return a 400 in this
59
94
  # case.
60
95
  def parse_nested_query(qs, d = nil)
61
- return {} if qs.nil? || qs.empty?
62
96
  params = make_params
63
97
 
64
- (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
65
- k, v = p.split('='.freeze, 2).map! { |s| unescape(s) }
98
+ unless qs.nil? || qs.empty?
99
+ check_query_string(qs, d).split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
100
+ k, v = p.split('=', 2).map! { |s| unescape(s) }
66
101
 
67
- normalize_params(params, k, v, param_depth_limit)
102
+ normalize_params(params, k, v, param_depth_limit)
103
+ end
68
104
  end
69
105
 
70
- return params.to_params_hash
106
+ return params.to_h
71
107
  rescue ArgumentError => e
72
- raise InvalidParameterError, e.message
108
+ raise InvalidParameterError, e.message, e.backtrace
73
109
  end
74
110
 
75
111
  # normalize_params recursively expands parameters into structural types. If
76
112
  # the structural types represented by two different parameter names are in
77
113
  # conflict, a ParameterTypeError is raised.
78
114
  def normalize_params(params, name, v, depth)
79
- raise RangeError if depth <= 0
115
+ raise ParamsTooDeepError if depth <= 0
80
116
 
81
117
  name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
82
- k = $1 || ''.freeze
83
- after = $' || ''.freeze
118
+ k = $1 || ''
119
+ after = $' || ''
84
120
 
85
121
  if k.empty?
86
- if !v.nil? && name == "[]".freeze
122
+ if !v.nil? && name == "[]"
87
123
  return Array(v)
88
124
  else
89
125
  return
90
126
  end
91
127
  end
92
128
 
93
- if after == ''.freeze
129
+ if after == ''
94
130
  params[k] = v
95
- elsif after == "[".freeze
131
+ elsif after == "["
96
132
  params[name] = v
97
- elsif after == "[]".freeze
133
+ elsif after == "[]"
98
134
  params[k] ||= []
99
135
  raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
100
136
  params[k] << v
@@ -135,7 +171,7 @@ module Rack
135
171
  end
136
172
 
137
173
  def params_hash_has_key?(hash, key)
138
- return false if key =~ /\[\]/
174
+ return false if /\[\]/.match?(key)
139
175
 
140
176
  key.split(/[\[\]]+/).inject(hash) do |h, part|
141
177
  next h if part == ''
@@ -146,8 +182,24 @@ module Rack
146
182
  true
147
183
  end
148
184
 
149
- def unescape(s)
150
- Utils.unescape(s)
185
+ def check_query_string(qs, sep)
186
+ if qs
187
+ if qs.bytesize > @bytesize_limit
188
+ raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
189
+ end
190
+
191
+ if (param_count = qs.count(sep.is_a?(String) ? sep : '&')) >= @params_limit
192
+ raise QueryLimitError, "total number of query parameters (#{param_count+1}) exceeds limit (#{@params_limit})"
193
+ end
194
+
195
+ qs
196
+ else
197
+ ''
198
+ end
199
+ end
200
+
201
+ def unescape(string)
202
+ Utils.unescape(string)
151
203
  end
152
204
 
153
205
  class Params
@@ -163,7 +215,7 @@ module Rack
163
215
 
164
216
  def []=(key, value)
165
217
  @size += key.size if key && !@params.key?(key)
166
- raise RangeError, 'exceeded available parameter key space' if @size > @limit
218
+ raise ParamsTooDeepError, 'exceeded available parameter key space' if @size > @limit
167
219
  @params[key] = value
168
220
  end
169
221
 
@@ -171,22 +223,42 @@ module Rack
171
223
  @params.key?(key)
172
224
  end
173
225
 
174
- def to_params_hash
175
- hash = @params
176
- hash.keys.each do |key|
177
- value = hash[key]
178
- if value.kind_of?(self.class)
179
- if value.object_id == self.object_id
180
- hash[key] = hash
181
- else
182
- hash[key] = value.to_params_hash
183
- end
184
- elsif value.kind_of?(Array)
185
- value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x}
226
+ # Recursively unwraps nested `Params` objects and constructs an object
227
+ # of the same shape, but using the objects' internal representations
228
+ # (Ruby hashes) in place of the objects. The result is a hash consisting
229
+ # purely of Ruby primitives.
230
+ #
231
+ # Mutation warning!
232
+ #
233
+ # 1. This method mutates the internal representation of the `Params`
234
+ # objects in order to save object allocations.
235
+ #
236
+ # 2. The value you get back is a reference to the internal hash
237
+ # representation, not a copy.
238
+ #
239
+ # 3. Because the `Params` object's internal representation is mutable
240
+ # through the `#[]=` method, it is not thread safe. The result of
241
+ # getting the hash representation while another thread is adding a
242
+ # key to it is non-deterministic.
243
+ #
244
+ def to_h
245
+ @params.each do |key, value|
246
+ case value
247
+ when self
248
+ # Handle circular references gracefully.
249
+ @params[key] = @params
250
+ when Params
251
+ @params[key] = value.to_h
252
+ when Array
253
+ value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
254
+ else
255
+ # Ignore anything that is not a `Params` object or
256
+ # a collection that can contain one.
186
257
  end
187
258
  end
188
- hash
259
+ @params
189
260
  end
261
+ alias_method :to_params_hash, :to_h
190
262
  end
191
263
  end
192
264
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
 
3
5
  module Rack
@@ -10,14 +12,14 @@ module Rack
10
12
  class ForwardRequest < Exception
11
13
  attr_reader :url, :env
12
14
 
13
- def initialize(url, env={})
15
+ def initialize(url, env = {})
14
16
  @url = URI(url)
15
17
  @env = env
16
18
 
17
- @env[PATH_INFO] = @url.path
18
- @env[QUERY_STRING] = @url.query if @url.query
19
- @env[HTTP_HOST] = @url.host if @url.host
20
- @env["HTTP_PORT"] = @url.port if @url.port
19
+ @env[PATH_INFO] = @url.path
20
+ @env[QUERY_STRING] = @url.query if @url.query
21
+ @env[HTTP_HOST] = @url.host if @url.host
22
+ @env[HTTP_PORT] = @url.port if @url.port
21
23
  @env[RACK_URL_SCHEME] = @url.scheme if @url.scheme
22
24
 
23
25
  super "forwarding to #{url}"