rack 1.6.13 → 2.0.9.3

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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/COPYING +1 -1
  3. data/HISTORY.md +153 -8
  4. data/README.rdoc +35 -31
  5. data/Rakefile +6 -14
  6. data/SPEC +10 -11
  7. data/contrib/rack_logo.svg +164 -111
  8. data/example/protectedlobster.rb +1 -1
  9. data/example/protectedlobster.ru +1 -1
  10. data/lib/rack/auth/abstract/request.rb +5 -1
  11. data/lib/rack/auth/digest/params.rb +2 -3
  12. data/lib/rack/auth/digest/request.rb +1 -1
  13. data/lib/rack/body_proxy.rb +14 -9
  14. data/lib/rack/builder.rb +3 -3
  15. data/lib/rack/chunked.rb +5 -5
  16. data/lib/rack/{commonlogger.rb → common_logger.rb} +6 -3
  17. data/lib/rack/content_length.rb +2 -2
  18. data/lib/rack/deflater.rb +4 -39
  19. data/lib/rack/directory.rb +66 -54
  20. data/lib/rack/etag.rb +5 -4
  21. data/lib/rack/events.rb +154 -0
  22. data/lib/rack/file.rb +64 -40
  23. data/lib/rack/handler/cgi.rb +15 -16
  24. data/lib/rack/handler/fastcgi.rb +13 -14
  25. data/lib/rack/handler/lsws.rb +11 -11
  26. data/lib/rack/handler/scgi.rb +15 -15
  27. data/lib/rack/handler/thin.rb +3 -0
  28. data/lib/rack/handler/webrick.rb +24 -26
  29. data/lib/rack/handler.rb +3 -25
  30. data/lib/rack/head.rb +15 -17
  31. data/lib/rack/lint.rb +41 -41
  32. data/lib/rack/lobster.rb +1 -1
  33. data/lib/rack/lock.rb +15 -10
  34. data/lib/rack/logger.rb +2 -2
  35. data/lib/rack/media_type.rb +38 -0
  36. data/lib/rack/{methodoverride.rb → method_override.rb} +6 -6
  37. data/lib/rack/mime.rb +18 -5
  38. data/lib/rack/mock.rb +36 -54
  39. data/lib/rack/multipart/generator.rb +5 -5
  40. data/lib/rack/multipart/parser.rb +283 -157
  41. data/lib/rack/multipart/uploaded_file.rb +1 -2
  42. data/lib/rack/multipart.rb +36 -8
  43. data/lib/rack/{nulllogger.rb → null_logger.rb} +1 -1
  44. data/lib/rack/query_parser.rb +192 -0
  45. data/lib/rack/recursive.rb +8 -8
  46. data/lib/rack/request.rb +394 -305
  47. data/lib/rack/response.rb +130 -57
  48. data/lib/rack/rewindable_input.rb +1 -12
  49. data/lib/rack/runtime.rb +10 -18
  50. data/lib/rack/sendfile.rb +5 -7
  51. data/lib/rack/server.rb +30 -23
  52. data/lib/rack/session/abstract/id.rb +110 -75
  53. data/lib/rack/session/cookie.rb +24 -17
  54. data/lib/rack/session/memcache.rb +9 -9
  55. data/lib/rack/session/pool.rb +8 -8
  56. data/lib/rack/show_exceptions.rb +386 -0
  57. data/lib/rack/{showstatus.rb → show_status.rb} +3 -3
  58. data/lib/rack/static.rb +30 -5
  59. data/lib/rack/tempfile_reaper.rb +2 -2
  60. data/lib/rack/urlmap.rb +15 -14
  61. data/lib/rack/utils.rb +156 -217
  62. data/lib/rack.rb +70 -21
  63. data/rack.gemspec +10 -9
  64. data/test/builder/an_underscore_app.rb +5 -0
  65. data/test/builder/options.ru +1 -1
  66. data/test/cgi/test.fcgi +1 -0
  67. data/test/cgi/test.gz +0 -0
  68. data/test/helper.rb +34 -0
  69. data/test/multipart/filename_with_encoded_words +7 -0
  70. data/test/multipart/filename_with_escaped_quotes_and_modification_param +1 -1
  71. data/test/multipart/filename_with_single_quote +7 -0
  72. data/test/multipart/quoted +15 -0
  73. data/test/multipart/rack-logo.png +0 -0
  74. data/test/multipart/unity3d_wwwform +11 -0
  75. data/test/registering_handler/rack/handler/registering_myself.rb +1 -1
  76. data/test/spec_auth_basic.rb +27 -19
  77. data/test/spec_auth_digest.rb +47 -46
  78. data/test/spec_body_proxy.rb +27 -27
  79. data/test/spec_builder.rb +51 -41
  80. data/test/spec_cascade.rb +24 -22
  81. data/test/spec_cgi.rb +49 -67
  82. data/test/spec_chunked.rb +37 -35
  83. data/test/{spec_commonlogger.rb → spec_common_logger.rb} +35 -21
  84. data/test/{spec_conditionalget.rb → spec_conditional_get.rb} +29 -28
  85. data/test/spec_config.rb +3 -2
  86. data/test/spec_content_length.rb +18 -17
  87. data/test/spec_content_type.rb +13 -12
  88. data/test/spec_deflater.rb +85 -49
  89. data/test/spec_directory.rb +87 -27
  90. data/test/spec_etag.rb +32 -31
  91. data/test/spec_events.rb +133 -0
  92. data/test/spec_fastcgi.rb +50 -72
  93. data/test/spec_file.rb +120 -77
  94. data/test/spec_handler.rb +19 -34
  95. data/test/spec_head.rb +15 -14
  96. data/test/spec_lint.rb +169 -199
  97. data/test/spec_lobster.rb +24 -23
  98. data/test/spec_lock.rb +79 -39
  99. data/test/spec_logger.rb +4 -3
  100. data/test/spec_media_type.rb +42 -0
  101. data/test/{spec_methodoverride.rb → spec_method_override.rb} +34 -35
  102. data/test/spec_mime.rb +19 -19
  103. data/test/spec_mock.rb +206 -144
  104. data/test/spec_multipart.rb +329 -208
  105. data/test/{spec_nulllogger.rb → spec_null_logger.rb} +5 -4
  106. data/test/spec_recursive.rb +17 -14
  107. data/test/spec_request.rb +796 -605
  108. data/test/spec_response.rb +233 -112
  109. data/test/spec_rewindable_input.rb +50 -40
  110. data/test/spec_runtime.rb +11 -10
  111. data/test/spec_sendfile.rb +30 -35
  112. data/test/spec_server.rb +78 -52
  113. data/test/spec_session_abstract_id.rb +11 -33
  114. data/test/spec_session_abstract_session_hash.rb +45 -0
  115. data/test/spec_session_cookie.rb +99 -67
  116. data/test/spec_session_memcache.rb +67 -68
  117. data/test/spec_session_pool.rb +52 -51
  118. data/test/{spec_showexceptions.rb → spec_show_exceptions.rb} +23 -28
  119. data/test/{spec_showstatus.rb → spec_show_status.rb} +36 -35
  120. data/test/spec_static.rb +71 -32
  121. data/test/spec_tempfile_reaper.rb +11 -10
  122. data/test/spec_thin.rb +55 -50
  123. data/test/spec_urlmap.rb +79 -78
  124. data/test/spec_utils.rb +441 -346
  125. data/test/spec_version.rb +2 -8
  126. data/test/spec_webrick.rb +93 -71
  127. data/test/static/foo.html +1 -0
  128. data/test/testrequest.rb +1 -1
  129. data/test/unregistered_handler/rack/handler/unregistered.rb +1 -1
  130. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +1 -1
  131. metadata +95 -74
  132. data/KNOWN-ISSUES +0 -44
  133. data/lib/rack/backports/uri/common_18.rb +0 -56
  134. data/lib/rack/backports/uri/common_192.rb +0 -52
  135. data/lib/rack/backports/uri/common_193.rb +0 -29
  136. data/lib/rack/handler/evented_mongrel.rb +0 -8
  137. data/lib/rack/handler/mongrel.rb +0 -106
  138. data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
  139. data/lib/rack/showexceptions.rb +0 -387
  140. data/lib/rack/utils/okjson.rb +0 -600
  141. data/test/spec_mongrel.rb +0 -182
  142. /data/lib/rack/{conditionalget.rb → conditional_get.rb} +0 -0
@@ -3,164 +3,320 @@ require 'rack/utils'
3
3
  module Rack
4
4
  module Multipart
5
5
  class MultipartPartLimitError < Errno::EMFILE; end
6
+ class MultipartTotalPartLimitError < StandardError; end
6
7
 
7
8
  class Parser
8
9
  BUFSIZE = 16384
9
- DUMMY = Struct.new(:parse).new
10
+ TEXT_PLAIN = "text/plain"
11
+ TEMPFILE_FACTORY = lambda { |filename, content_type|
12
+ Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))])
13
+ }
14
+
15
+ class BoundedIO # :nodoc:
16
+ def initialize(io, content_length)
17
+ @io = io
18
+ @content_length = content_length
19
+ @cursor = 0
20
+ end
10
21
 
11
- def self.create(env)
12
- return DUMMY unless env['CONTENT_TYPE'] =~ MULTIPART
22
+ def read(size)
23
+ return if @cursor >= @content_length
13
24
 
14
- io = env['rack.input']
15
- io.rewind
25
+ left = @content_length - @cursor
26
+
27
+ str = if left < size
28
+ @io.read left
29
+ else
30
+ @io.read size
31
+ end
16
32
 
17
- content_length = env['CONTENT_LENGTH']
18
- content_length = content_length.to_i if content_length
33
+ if str
34
+ @cursor += str.bytesize
35
+ else
36
+ # Raise an error for mismatching Content-Length and actual contents
37
+ raise EOFError, "bad content body"
38
+ end
19
39
 
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
40
+ str
41
+ end
23
42
 
24
- new($1, io, content_length, env, tempfile, bufsize)
43
+ def rewind
44
+ @io.rewind
45
+ end
25
46
  end
26
47
 
27
- def initialize(boundary, io, content_length, env, tempfile, bufsize)
28
- @buf = ""
48
+ MultipartInfo = Struct.new :params, :tmp_files
49
+ EMPTY = MultipartInfo.new(nil, [])
29
50
 
30
- if @buf.respond_to? :force_encoding
31
- @buf.force_encoding Encoding::ASCII_8BIT
32
- end
51
+ def self.parse_boundary(content_type)
52
+ return unless content_type
53
+ data = content_type.match(MULTIPART)
54
+ return unless data
55
+ data[1]
56
+ end
33
57
 
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
58
+ def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
59
+ return EMPTY if 0 == content_length
42
60
 
43
- if @content_length
44
- @content_length -= @boundary_size
45
- end
61
+ boundary = parse_boundary content_type
62
+ return EMPTY unless boundary
46
63
 
47
- @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
48
- @full_boundary = @boundary + EOL
49
- end
64
+ io = BoundedIO.new(io, content_length) if content_length
50
65
 
51
- def parse
52
- fast_forward_to_first_boundary
66
+ parser = new(boundary, tmpfile, bufsize, qp)
67
+ parser.on_read io.read(bufsize)
53
68
 
54
- opened_files = 0
55
69
  loop do
70
+ break if parser.state == :DONE
71
+ parser.on_read io.read(bufsize)
72
+ end
56
73
 
57
- head, filename, content_type, name, body =
58
- get_current_head_and_filename_and_content_type_and_name_and_body
74
+ io.rewind
75
+ parser.result
76
+ end
59
77
 
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
78
+ class Collector
79
+ class MimePart < Struct.new(:body, :head, :filename, :content_type, :name)
80
+ def get_data
81
+ data = body
82
+ if filename == ""
83
+ # filename is blank which means no file has been selected
84
+ return
85
+ elsif filename
86
+ body.rewind if body.respond_to?(:rewind)
87
+
88
+ # Take the basename of the upload's original filename.
89
+ # This handles the full Windows paths given by Internet Explorer
90
+ # (and perhaps other broken user agents) without affecting
91
+ # those which give the lone filename.
92
+ fn = filename.split(/[\/\\]/).last
93
+
94
+ data = {:filename => fn, :type => content_type,
95
+ :name => name, :tempfile => body, :head => head}
96
+ elsif !filename && content_type && body.is_a?(IO)
97
+ body.rewind
98
+
99
+ # Generic multipart cases, not coming from a form
100
+ data = {:type => content_type,
101
+ :name => name, :tempfile => body, :head => head}
102
+ end
103
+
104
+ yield data
63
105
  end
106
+ end
64
107
 
65
- # Save the rest.
66
- if i = @buf.index(rx)
67
- body << @buf.slice!(0, i)
68
- @buf.slice!(0, @boundary_size+2)
108
+ class BufferPart < MimePart
109
+ def file?; false; end
110
+ def close; end
111
+ end
69
112
 
70
- @content_length = -1 if $1 == "--"
71
- end
113
+ class TempfilePart < MimePart
114
+ def file?; true; end
115
+ def close; body.close; end
116
+ end
72
117
 
73
- get_data(filename, body, content_type, name, head) do |data|
74
- tag_multipart_encoding(filename, content_type, name, data)
118
+ include Enumerable
75
119
 
76
- Utils.normalize_params(@params, name, data)
77
- end
120
+ def initialize tempfile
121
+ @tempfile = tempfile
122
+ @mime_parts = []
123
+ @open_files = 0
124
+ end
78
125
 
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
126
+ def each
127
+ @mime_parts.each { |part| yield part }
81
128
  end
82
129
 
83
- @io.rewind
130
+ def on_mime_head mime_index, head, filename, content_type, name
131
+ if filename
132
+ body = @tempfile.call(filename, content_type)
133
+ body.binmode if body.respond_to?(:binmode)
134
+ klass = TempfilePart
135
+ @open_files += 1
136
+ else
137
+ body = String.new
138
+ klass = BufferPart
139
+ end
84
140
 
85
- @params.to_params_hash
86
- end
141
+ @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
87
142
 
88
- private
89
- def full_boundary; @full_boundary; end
143
+ check_part_limits
144
+ end
90
145
 
91
- def rx; @rx; end
146
+ def on_mime_body mime_index, content
147
+ @mime_parts[mime_index].body << content
148
+ end
92
149
 
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
150
+ def on_mime_finish mime_index
151
+ end
152
+
153
+ private
154
+
155
+ def check_part_limits
156
+ file_limit = Utils.multipart_file_limit
157
+ part_limit = Utils.multipart_total_part_limit
98
158
 
99
- while @buf.gsub!(/\A([^\n]*\n)/, '')
100
- read_buffer = $1
101
- return if read_buffer == full_boundary
159
+ if file_limit && file_limit > 0
160
+ if @open_files >= file_limit
161
+ @mime_parts.each(&:close)
162
+ raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
163
+ end
102
164
  end
103
165
 
104
- raise EOFError, "bad content body" if Utils.bytesize(@buf) >= @bufsize
166
+ if part_limit && part_limit > 0
167
+ if @mime_parts.size >= part_limit
168
+ @mime_parts.each(&:close)
169
+ raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
170
+ end
171
+ end
105
172
  end
106
173
  end
107
174
 
108
- def get_current_head_and_filename_and_content_type_and_name_and_body
109
- head = nil
110
- body = ''
175
+ attr_reader :state
176
+
177
+ def initialize(boundary, tempfile, bufsize, query_parser)
178
+ @buf = String.new
179
+
180
+ @query_parser = query_parser
181
+ @params = query_parser.make_params
182
+ @boundary = "--#{boundary}"
183
+ @bufsize = bufsize
184
+
185
+ @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
186
+ @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
187
+ @full_boundary = @boundary
188
+ @end_boundary = @boundary + '--'
189
+ @state = :FAST_FORWARD
190
+ @mime_index = 0
191
+ @collector = Collector.new tempfile
192
+ end
193
+
194
+ def on_read content
195
+ handle_empty_content!(content)
196
+ @buf << content
197
+ run_parser
198
+ end
111
199
 
112
- if body.respond_to? :force_encoding
113
- body.force_encoding Encoding::ASCII_8BIT
200
+ def result
201
+ @collector.each do |part|
202
+ part.get_data do |data|
203
+ tag_multipart_encoding(part.filename, part.content_type, part.name, data)
204
+ @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
205
+ end
114
206
  end
115
207
 
116
- filename = content_type = name = nil
208
+ MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
209
+ end
117
210
 
118
- until head && @buf =~ rx
119
- if !head && i = @buf.index(EOL+EOL)
120
- head = @buf.slice!(0, i+2) # First \r\n
211
+ private
121
212
 
122
- @buf.slice!(0, 2) # Second \r\n
213
+ def run_parser
214
+ loop do
215
+ case @state
216
+ when :FAST_FORWARD
217
+ break if handle_fast_forward == :want_read
218
+ when :CONSUME_TOKEN
219
+ break if handle_consume_token == :want_read
220
+ when :MIME_HEAD
221
+ break if handle_mime_head == :want_read
222
+ when :MIME_BODY
223
+ break if handle_mime_body == :want_read
224
+ when :DONE
225
+ break
226
+ end
227
+ end
228
+ end
123
229
 
124
- content_type = head[MULTIPART_CONTENT_TYPE, 1]
125
- name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
230
+ def handle_fast_forward
231
+ if consume_boundary
232
+ @state = :MIME_HEAD
233
+ else
234
+ raise EOFError, "bad content body" if @buf.bytesize >= @bufsize
235
+ :want_read
236
+ end
237
+ end
126
238
 
127
- filename = get_filename(head)
239
+ def handle_consume_token
240
+ tok = consume_boundary
241
+ # break if we're at the end of a buffer, but not if it is the end of a field
242
+ if tok == :END_BOUNDARY || (@buf.empty? && tok != :BOUNDARY)
243
+ @state = :DONE
244
+ else
245
+ @state = :MIME_HEAD
246
+ end
247
+ end
128
248
 
129
- if name.nil? || name.empty? && filename
130
- name = filename
131
- end
249
+ def handle_mime_head
250
+ if @buf.index(EOL + EOL)
251
+ i = @buf.index(EOL+EOL)
252
+ head = @buf.slice!(0, i+2) # First \r\n
253
+ @buf.slice!(0, 2) # Second \r\n
254
+
255
+ content_type = head[MULTIPART_CONTENT_TYPE, 1]
256
+ if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
257
+ name = Rack::Auth::Digest::Params::dequote(name)
258
+ else
259
+ name = head[MULTIPART_CONTENT_ID, 1]
260
+ end
132
261
 
133
- if filename
134
- (@env['rack.tempfiles'] ||= []) << body = @tempfile.call(filename, content_type)
135
- body.binmode if body.respond_to?(:binmode)
136
- end
262
+ filename = get_filename(head)
137
263
 
138
- next
264
+ if name.nil? || name.empty?
265
+ name = filename || "#{content_type || TEXT_PLAIN}[]"
139
266
  end
140
267
 
268
+ @collector.on_mime_head @mime_index, head, filename, content_type, name
269
+ @state = :MIME_BODY
270
+ else
271
+ :want_read
272
+ end
273
+ end
274
+
275
+ def handle_mime_body
276
+ if i = @buf.index(rx)
277
+ # Save the rest.
278
+ @collector.on_mime_body @mime_index, @buf.slice!(0, i)
279
+ @buf.slice!(0, 2) # Remove \r\n after the content
280
+ @state = :CONSUME_TOKEN
281
+ @mime_index += 1
282
+ else
141
283
  # Save the read body part.
142
- if head && (@boundary_size+4 < @buf.size)
143
- body << @buf.slice!(0, @buf.size - (@boundary_size+4))
284
+ if @rx_max_size < @buf.size
285
+ @collector.on_mime_body @mime_index, @buf.slice!(0, @buf.size - @rx_max_size)
144
286
  end
287
+ :want_read
288
+ end
289
+ end
145
290
 
146
- content = @io.read(@content_length && @bufsize >= @content_length ? @content_length : @bufsize)
147
- raise EOFError, "bad content body" if content.nil? || content.empty?
291
+ def full_boundary; @full_boundary; end
148
292
 
149
- @buf << content
150
- @content_length -= content.size if @content_length
151
- end
293
+ def rx; @rx; end
152
294
 
153
- [head, filename, content_type, name, body]
295
+ def consume_boundary
296
+ while @buf.gsub!(/\A([^\n]*(?:\n|\Z))/, '')
297
+ read_buffer = $1
298
+ case read_buffer.strip
299
+ when full_boundary then return :BOUNDARY
300
+ when @end_boundary then return :END_BOUNDARY
301
+ end
302
+ return if @buf.empty?
303
+ end
154
304
  end
155
305
 
156
306
  def get_filename(head)
157
307
  filename = nil
158
308
  case head
159
309
  when RFC2183
160
- filename = Hash[head.scan(DISPPARM)]['filename']
161
- filename = $1 if filename and filename =~ /^"(.*)"$/
162
- when BROKEN_QUOTED, BROKEN_UNQUOTED
310
+ params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
311
+
312
+ if filename = params['filename']
313
+ filename = $1 if filename =~ /^"(.*)"$/
314
+ elsif filename = params['filename*']
315
+ encoding, _, filename = filename.split("'", 3)
316
+ end
317
+ when BROKEN
163
318
  filename = $1
319
+ filename = $1 if filename =~ /^"(.*)"$/
164
320
  end
165
321
 
166
322
  return unless filename
@@ -169,84 +325,54 @@ module Rack
169
325
  filename = Utils.unescape(filename)
170
326
  end
171
327
 
172
- scrub_filename filename
328
+ filename.scrub!
173
329
 
174
330
  if filename !~ /\\[^\\"]/
175
331
  filename = filename.gsub(/\\(.)/, '\1')
176
332
  end
177
- filename
178
- end
179
333
 
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
334
+ if encoding
335
+ filename.force_encoding ::Encoding.find(encoding)
188
336
  end
189
337
 
190
- CHARSET = "charset"
191
- TEXT_PLAIN = "text/plain"
338
+ filename
339
+ end
340
+
341
+ CHARSET = "charset"
192
342
 
193
- def tag_multipart_encoding(filename, content_type, name, body)
194
- name.force_encoding Encoding::UTF_8
343
+ def tag_multipart_encoding(filename, content_type, name, body)
344
+ name = name.to_s
345
+ encoding = Encoding::UTF_8
195
346
 
196
- return if filename
347
+ name.force_encoding(encoding)
197
348
 
198
- encoding = Encoding::UTF_8
349
+ return if filename
199
350
 
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
351
+ if content_type
352
+ list = content_type.split(';')
353
+ type_subtype = list.first
354
+ type_subtype.strip!
355
+ if TEXT_PLAIN == type_subtype
356
+ rest = list.drop 1
357
+ rest.each do |param|
358
+ k,v = param.split('=', 2)
359
+ k.strip!
360
+ v.strip!
361
+ v = v[1..-2] if v[0] == '"' && v[-1] == '"'
362
+ encoding = Encoding.find v if k == CHARSET
212
363
  end
213
364
  end
214
-
215
- name.force_encoding encoding
216
- body.force_encoding encoding
217
- end
218
- else
219
- def scrub_filename(filename)
220
365
  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
366
 
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
367
+ name.force_encoding(encoding)
368
+ body.force_encoding(encoding)
369
+ end
238
370
 
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
371
 
244
- # Generic multipart cases, not coming from a form
245
- data = {:type => content_type,
246
- :name => name, :tempfile => body, :head => head}
372
+ def handle_empty_content!(content)
373
+ if content.nil? || content.empty?
374
+ raise EOFError
247
375
  end
248
-
249
- yield data
250
376
  end
251
377
  end
252
378
  end
@@ -11,8 +11,7 @@ module Rack
11
11
  raise "#{path} file does not exist" unless ::File.exist?(path)
12
12
  @content_type = content_type
13
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)
14
+ @tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
16
15
  @tempfile.binmode if binary
17
16
  FileUtils.copy_file(path, @tempfile.path)
18
17
  end
@@ -1,10 +1,11 @@
1
+ require 'rack/multipart/parser'
2
+
1
3
  module Rack
2
4
  # A multipart form data parser, adapted from IOWA.
3
5
  #
4
6
  # Usually, Rack::Request#POST takes care of calling this.
5
7
  module Multipart
6
8
  autoload :UploadedFile, 'rack/multipart/uploaded_file'
7
- autoload :Parser, 'rack/multipart/parser'
8
9
  autoload :Generator, 'rack/multipart/generator'
9
10
 
10
11
  EOL = "\r\n"
@@ -12,17 +13,44 @@ module Rack
12
13
  MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
13
14
  TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
14
15
  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
16
+ VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
17
+ BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
19
18
  MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
20
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name="?([^\";]*)"?/ni
19
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s+name=(#{VALUE})/ni
21
20
  MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
21
+ # Updated definitions from RFC 2231
22
+ ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
23
+ ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
24
+ SECTION = /\*[0-9]+/
25
+ REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
26
+ REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
27
+ EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
28
+ EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
29
+ EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
30
+ EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
31
+ EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
32
+ EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
33
+ EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
34
+ DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
35
+ RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
22
36
 
23
37
  class << self
24
- def parse_multipart(env)
25
- Parser.create(env).parse
38
+ def parse_multipart(env, params = Rack::Utils.default_query_parser)
39
+ extract_multipart Rack::Request.new(env), params
40
+ end
41
+
42
+ def extract_multipart(req, params = Rack::Utils.default_query_parser)
43
+ io = req.get_header(RACK_INPUT)
44
+ io.rewind
45
+ content_length = req.content_length
46
+ content_length = content_length.to_i if content_length
47
+
48
+ tempfile = req.get_header(RACK_MULTIPART_TEMPFILE_FACTORY) || Parser::TEMPFILE_FACTORY
49
+ bufsize = req.get_header(RACK_MULTIPART_BUFFER_SIZE) || Parser::BUFSIZE
50
+
51
+ info = Parser.parse io, content_length, req.get_header('CONTENT_TYPE'), tempfile, bufsize, params
52
+ req.set_header(RACK_TEMPFILES, info.tmp_files)
53
+ info.params
26
54
  end
27
55
 
28
56
  def build_multipart(params, first = true)
@@ -5,7 +5,7 @@ module Rack
5
5
  end
6
6
 
7
7
  def call(env)
8
- env['rack.logger'] = self
8
+ env[RACK_LOGGER] = self
9
9
  @app.call(env)
10
10
  end
11
11