lack 2.0.0

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 (72) hide show
  1. checksums.yaml +7 -0
  2. data/bin/rackup +5 -0
  3. data/lib/rack.rb +26 -0
  4. data/lib/rack/body_proxy.rb +39 -0
  5. data/lib/rack/builder.rb +166 -0
  6. data/lib/rack/handler.rb +63 -0
  7. data/lib/rack/handler/webrick.rb +120 -0
  8. data/lib/rack/mime.rb +661 -0
  9. data/lib/rack/mock.rb +198 -0
  10. data/lib/rack/multipart.rb +31 -0
  11. data/lib/rack/multipart/generator.rb +93 -0
  12. data/lib/rack/multipart/parser.rb +239 -0
  13. data/lib/rack/multipart/uploaded_file.rb +34 -0
  14. data/lib/rack/request.rb +394 -0
  15. data/lib/rack/response.rb +160 -0
  16. data/lib/rack/server.rb +258 -0
  17. data/lib/rack/server/options.rb +121 -0
  18. data/lib/rack/utils.rb +653 -0
  19. data/lib/rack/version.rb +3 -0
  20. data/spec/spec_helper.rb +1 -0
  21. data/test/builder/anything.rb +5 -0
  22. data/test/builder/comment.ru +4 -0
  23. data/test/builder/end.ru +5 -0
  24. data/test/builder/line.ru +1 -0
  25. data/test/builder/options.ru +2 -0
  26. data/test/multipart/bad_robots +259 -0
  27. data/test/multipart/binary +0 -0
  28. data/test/multipart/content_type_and_no_filename +6 -0
  29. data/test/multipart/empty +10 -0
  30. data/test/multipart/fail_16384_nofile +814 -0
  31. data/test/multipart/file1.txt +1 -0
  32. data/test/multipart/filename_and_modification_param +7 -0
  33. data/test/multipart/filename_and_no_name +6 -0
  34. data/test/multipart/filename_with_escaped_quotes +6 -0
  35. data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
  36. data/test/multipart/filename_with_percent_escaped_quotes +6 -0
  37. data/test/multipart/filename_with_unescaped_percentages +6 -0
  38. data/test/multipart/filename_with_unescaped_percentages2 +6 -0
  39. data/test/multipart/filename_with_unescaped_percentages3 +6 -0
  40. data/test/multipart/filename_with_unescaped_quotes +6 -0
  41. data/test/multipart/ie +6 -0
  42. data/test/multipart/invalid_character +6 -0
  43. data/test/multipart/mixed_files +21 -0
  44. data/test/multipart/nested +10 -0
  45. data/test/multipart/none +9 -0
  46. data/test/multipart/semicolon +6 -0
  47. data/test/multipart/text +15 -0
  48. data/test/multipart/webkit +32 -0
  49. data/test/rackup/config.ru +31 -0
  50. data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
  51. data/test/spec_body_proxy.rb +69 -0
  52. data/test/spec_builder.rb +223 -0
  53. data/test/spec_chunked.rb +101 -0
  54. data/test/spec_file.rb +221 -0
  55. data/test/spec_handler.rb +59 -0
  56. data/test/spec_head.rb +45 -0
  57. data/test/spec_lint.rb +522 -0
  58. data/test/spec_mime.rb +51 -0
  59. data/test/spec_mock.rb +277 -0
  60. data/test/spec_multipart.rb +547 -0
  61. data/test/spec_recursive.rb +72 -0
  62. data/test/spec_request.rb +1199 -0
  63. data/test/spec_response.rb +343 -0
  64. data/test/spec_rewindable_input.rb +118 -0
  65. data/test/spec_sendfile.rb +130 -0
  66. data/test/spec_server.rb +167 -0
  67. data/test/spec_utils.rb +635 -0
  68. data/test/spec_webrick.rb +184 -0
  69. data/test/testrequest.rb +78 -0
  70. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  71. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  72. metadata +240 -0
@@ -0,0 +1,51 @@
1
+ require 'rack/mime'
2
+
3
+ describe Rack::Mime do
4
+
5
+ it "should return the fallback mime-type for files with no extension" do
6
+ fallback = 'image/jpg'
7
+ Rack::Mime.mime_type(File.extname('no_ext'), fallback).should.equal fallback
8
+ end
9
+
10
+ it "should always return 'application/octet-stream' for unknown file extensions" do
11
+ unknown_ext = File.extname('unknown_ext.abcdefg')
12
+ Rack::Mime.mime_type(unknown_ext).should.equal 'application/octet-stream'
13
+ end
14
+
15
+ it "should return the mime-type for a given extension" do
16
+ # sanity check. it would be infeasible test every single mime-type.
17
+ Rack::Mime.mime_type(File.extname('image.jpg')).should.equal 'image/jpeg'
18
+ end
19
+
20
+ it "should support null fallbacks" do
21
+ Rack::Mime.mime_type('.nothing', nil).should.equal nil
22
+ end
23
+
24
+ it "should match exact mimes" do
25
+ Rack::Mime.match?('text/html', 'text/html').should.equal true
26
+ Rack::Mime.match?('text/html', 'text/meme').should.equal false
27
+ Rack::Mime.match?('text', 'text').should.equal true
28
+ Rack::Mime.match?('text', 'binary').should.equal false
29
+ end
30
+
31
+ it "should match class wildcard mimes" do
32
+ Rack::Mime.match?('text/html', 'text/*').should.equal true
33
+ Rack::Mime.match?('text/plain', 'text/*').should.equal true
34
+ Rack::Mime.match?('application/json', 'text/*').should.equal false
35
+ Rack::Mime.match?('text/html', 'text').should.equal true
36
+ end
37
+
38
+ it "should match full wildcards" do
39
+ Rack::Mime.match?('text/html', '*').should.equal true
40
+ Rack::Mime.match?('text/plain', '*').should.equal true
41
+ Rack::Mime.match?('text/html', '*/*').should.equal true
42
+ Rack::Mime.match?('text/plain', '*/*').should.equal true
43
+ end
44
+
45
+ it "should match type wildcard mimes" do
46
+ Rack::Mime.match?('text/html', '*/html').should.equal true
47
+ Rack::Mime.match?('text/plain', '*/plain').should.equal true
48
+ end
49
+
50
+ end
51
+
@@ -0,0 +1,277 @@
1
+ require 'yaml'
2
+ require 'rack/lint'
3
+ require 'rack/mock'
4
+ require 'stringio'
5
+
6
+ app = Rack::Lint.new(lambda { |env|
7
+ req = Rack::Request.new(env)
8
+
9
+ env["mock.postdata"] = env["rack.input"].read
10
+ if req.GET["error"]
11
+ env["rack.errors"].puts req.GET["error"]
12
+ env["rack.errors"].flush
13
+ end
14
+
15
+ body = req.head? ? "" : env.to_yaml
16
+ Rack::Response.new(body,
17
+ req.GET["status"] || 200,
18
+ "Content-Type" => "text/yaml").finish
19
+ })
20
+
21
+ describe Rack::MockRequest do
22
+ should "return a MockResponse" do
23
+ res = Rack::MockRequest.new(app).get("")
24
+ res.should.be.kind_of Rack::MockResponse
25
+ end
26
+
27
+ should "be able to only return the environment" do
28
+ env = Rack::MockRequest.env_for("")
29
+ env.should.be.kind_of Hash
30
+ env.should.include "rack.version"
31
+ end
32
+
33
+ should "return an environment with a path" do
34
+ env = Rack::MockRequest.env_for("http://www.example.com/parse?location[]=1&location[]=2&age_group[]=2")
35
+ env["QUERY_STRING"].should.equal "location[]=1&location[]=2&age_group[]=2"
36
+ env["PATH_INFO"].should.equal "/parse"
37
+ env.should.be.kind_of Hash
38
+ env.should.include "rack.version"
39
+ end
40
+
41
+ should "provide sensible defaults" do
42
+ res = Rack::MockRequest.new(app).request
43
+
44
+ env = YAML.load(res.body)
45
+ env["REQUEST_METHOD"].should.equal "GET"
46
+ env["SERVER_NAME"].should.equal "example.org"
47
+ env["SERVER_PORT"].should.equal "80"
48
+ env["QUERY_STRING"].should.equal ""
49
+ env["PATH_INFO"].should.equal "/"
50
+ env["SCRIPT_NAME"].should.equal ""
51
+ env["rack.url_scheme"].should.equal "http"
52
+ env["mock.postdata"].should.be.empty
53
+ end
54
+
55
+ should "allow GET/POST/PUT/DELETE/HEAD" do
56
+ res = Rack::MockRequest.new(app).get("", :input => "foo")
57
+ env = YAML.load(res.body)
58
+ env["REQUEST_METHOD"].should.equal "GET"
59
+
60
+ res = Rack::MockRequest.new(app).post("", :input => "foo")
61
+ env = YAML.load(res.body)
62
+ env["REQUEST_METHOD"].should.equal "POST"
63
+
64
+ res = Rack::MockRequest.new(app).put("", :input => "foo")
65
+ env = YAML.load(res.body)
66
+ env["REQUEST_METHOD"].should.equal "PUT"
67
+
68
+ res = Rack::MockRequest.new(app).patch("", :input => "foo")
69
+ env = YAML.load(res.body)
70
+ env["REQUEST_METHOD"].should.equal "PATCH"
71
+
72
+ res = Rack::MockRequest.new(app).delete("", :input => "foo")
73
+ env = YAML.load(res.body)
74
+ env["REQUEST_METHOD"].should.equal "DELETE"
75
+
76
+ Rack::MockRequest.env_for("/", :method => "HEAD")["REQUEST_METHOD"].
77
+ should.equal "HEAD"
78
+
79
+ Rack::MockRequest.env_for("/", :method => "OPTIONS")["REQUEST_METHOD"].
80
+ should.equal "OPTIONS"
81
+ end
82
+
83
+ should "set content length" do
84
+ env = Rack::MockRequest.env_for("/", :input => "foo")
85
+ env["CONTENT_LENGTH"].should.equal "3"
86
+ end
87
+
88
+ should "allow posting" do
89
+ res = Rack::MockRequest.new(app).get("", :input => "foo")
90
+ env = YAML.load(res.body)
91
+ env["mock.postdata"].should.equal "foo"
92
+
93
+ res = Rack::MockRequest.new(app).post("", :input => StringIO.new("foo"))
94
+ env = YAML.load(res.body)
95
+ env["mock.postdata"].should.equal "foo"
96
+ end
97
+
98
+ should "use all parts of an URL" do
99
+ res = Rack::MockRequest.new(app).
100
+ get("https://bla.example.org:9292/meh/foo?bar")
101
+ res.should.be.kind_of Rack::MockResponse
102
+
103
+ env = YAML.load(res.body)
104
+ env["REQUEST_METHOD"].should.equal "GET"
105
+ env["SERVER_NAME"].should.equal "bla.example.org"
106
+ env["SERVER_PORT"].should.equal "9292"
107
+ env["QUERY_STRING"].should.equal "bar"
108
+ env["PATH_INFO"].should.equal "/meh/foo"
109
+ env["rack.url_scheme"].should.equal "https"
110
+ end
111
+
112
+ should "set SSL port and HTTP flag on when using https" do
113
+ res = Rack::MockRequest.new(app).
114
+ get("https://example.org/foo")
115
+ res.should.be.kind_of Rack::MockResponse
116
+
117
+ env = YAML.load(res.body)
118
+ env["REQUEST_METHOD"].should.equal "GET"
119
+ env["SERVER_NAME"].should.equal "example.org"
120
+ env["SERVER_PORT"].should.equal "443"
121
+ env["QUERY_STRING"].should.equal ""
122
+ env["PATH_INFO"].should.equal "/foo"
123
+ env["rack.url_scheme"].should.equal "https"
124
+ env["HTTPS"].should.equal "on"
125
+ end
126
+
127
+ should "prepend slash to uri path" do
128
+ res = Rack::MockRequest.new(app).
129
+ get("foo")
130
+ res.should.be.kind_of Rack::MockResponse
131
+
132
+ env = YAML.load(res.body)
133
+ env["REQUEST_METHOD"].should.equal "GET"
134
+ env["SERVER_NAME"].should.equal "example.org"
135
+ env["SERVER_PORT"].should.equal "80"
136
+ env["QUERY_STRING"].should.equal ""
137
+ env["PATH_INFO"].should.equal "/foo"
138
+ env["rack.url_scheme"].should.equal "http"
139
+ end
140
+
141
+ should "properly convert method name to an uppercase string" do
142
+ res = Rack::MockRequest.new(app).request(:get)
143
+ env = YAML.load(res.body)
144
+ env["REQUEST_METHOD"].should.equal "GET"
145
+ end
146
+
147
+ should "accept params and build query string for GET requests" do
148
+ res = Rack::MockRequest.new(app).get("/foo?baz=2", :params => {:foo => {:bar => "1"}})
149
+ env = YAML.load(res.body)
150
+ env["REQUEST_METHOD"].should.equal "GET"
151
+ env["QUERY_STRING"].should.include "baz=2"
152
+ env["QUERY_STRING"].should.include "foo[bar]=1"
153
+ env["PATH_INFO"].should.equal "/foo"
154
+ env["mock.postdata"].should.equal ""
155
+ end
156
+
157
+ should "accept raw input in params for GET requests" do
158
+ res = Rack::MockRequest.new(app).get("/foo?baz=2", :params => "foo[bar]=1")
159
+ env = YAML.load(res.body)
160
+ env["REQUEST_METHOD"].should.equal "GET"
161
+ env["QUERY_STRING"].should.include "baz=2"
162
+ env["QUERY_STRING"].should.include "foo[bar]=1"
163
+ env["PATH_INFO"].should.equal "/foo"
164
+ env["mock.postdata"].should.equal ""
165
+ end
166
+
167
+ should "accept params and build url encoded params for POST requests" do
168
+ res = Rack::MockRequest.new(app).post("/foo", :params => {:foo => {:bar => "1"}})
169
+ env = YAML.load(res.body)
170
+ env["REQUEST_METHOD"].should.equal "POST"
171
+ env["QUERY_STRING"].should.equal ""
172
+ env["PATH_INFO"].should.equal "/foo"
173
+ env["CONTENT_TYPE"].should.equal "application/x-www-form-urlencoded"
174
+ env["mock.postdata"].should.equal "foo[bar]=1"
175
+ end
176
+
177
+ should "accept raw input in params for POST requests" do
178
+ res = Rack::MockRequest.new(app).post("/foo", :params => "foo[bar]=1")
179
+ env = YAML.load(res.body)
180
+ env["REQUEST_METHOD"].should.equal "POST"
181
+ env["QUERY_STRING"].should.equal ""
182
+ env["PATH_INFO"].should.equal "/foo"
183
+ env["CONTENT_TYPE"].should.equal "application/x-www-form-urlencoded"
184
+ env["mock.postdata"].should.equal "foo[bar]=1"
185
+ end
186
+
187
+ should "accept params and build multipart encoded params for POST requests" do
188
+ files = Rack::Multipart::UploadedFile.new(File.join(File.dirname(__FILE__), "multipart", "file1.txt"))
189
+ res = Rack::MockRequest.new(app).post("/foo", :params => { "submit-name" => "Larry", "files" => files })
190
+ env = YAML.load(res.body)
191
+ env["REQUEST_METHOD"].should.equal "POST"
192
+ env["QUERY_STRING"].should.equal ""
193
+ env["PATH_INFO"].should.equal "/foo"
194
+ env["CONTENT_TYPE"].should.equal "multipart/form-data; boundary=AaB03x"
195
+ # The gsub accounts for differences in YAMLs affect on the data.
196
+ env["mock.postdata"].gsub("\r", "").length.should.equal 206
197
+ end
198
+
199
+ should "behave valid according to the Rack spec" do
200
+ lambda {
201
+ Rack::MockRequest.new(app).
202
+ get("https://bla.example.org:9292/meh/foo?bar", :lint => true)
203
+ }.should.not.raise(Rack::Lint::LintError)
204
+ end
205
+
206
+ should "call close on the original body object" do
207
+ called = false
208
+ body = Rack::BodyProxy.new(['hi']) { called = true }
209
+ capp = proc { |e| [200, {'Content-Type' => 'text/plain'}, body] }
210
+ called.should.equal false
211
+ Rack::MockRequest.new(capp).get('/', :lint => true)
212
+ called.should.equal true
213
+ end
214
+ end
215
+
216
+ describe Rack::MockResponse do
217
+ should "provide access to the HTTP status" do
218
+ res = Rack::MockRequest.new(app).get("")
219
+ res.should.be.successful
220
+ res.should.be.ok
221
+
222
+ res = Rack::MockRequest.new(app).get("/?status=404")
223
+ res.should.not.be.successful
224
+ res.should.be.client_error
225
+ res.should.be.not_found
226
+
227
+ res = Rack::MockRequest.new(app).get("/?status=501")
228
+ res.should.not.be.successful
229
+ res.should.be.server_error
230
+
231
+ res = Rack::MockRequest.new(app).get("/?status=307")
232
+ res.should.be.redirect
233
+
234
+ res = Rack::MockRequest.new(app).get("/?status=201", :lint => true)
235
+ res.should.be.empty
236
+ end
237
+
238
+ should "provide access to the HTTP headers" do
239
+ res = Rack::MockRequest.new(app).get("")
240
+ res.should.include "Content-Type"
241
+ res.headers["Content-Type"].should.equal "text/yaml"
242
+ res.original_headers["Content-Type"].should.equal "text/yaml"
243
+ res["Content-Type"].should.equal "text/yaml"
244
+ res.content_type.should.equal "text/yaml"
245
+ res.content_length.should.not.equal 0
246
+ res.location.should.be.nil
247
+ end
248
+
249
+ should "provide access to the HTTP body" do
250
+ res = Rack::MockRequest.new(app).get("")
251
+ res.body.should =~ /rack/
252
+ res.should =~ /rack/
253
+ res.should.match(/rack/)
254
+ res.should.satisfy { |r| r.match(/rack/) }
255
+ end
256
+
257
+ should "provide access to the Rack errors" do
258
+ res = Rack::MockRequest.new(app).get("/?error=foo", :lint => true)
259
+ res.should.be.ok
260
+ res.errors.should.not.be.empty
261
+ res.errors.should.include "foo"
262
+ end
263
+
264
+ should "allow calling body.close afterwards" do
265
+ # this is exactly what rack-test does
266
+ body = StringIO.new("hi")
267
+ res = Rack::MockResponse.new(200, {}, body)
268
+ body.close if body.respond_to?(:close)
269
+ res.body.should == 'hi'
270
+ end
271
+
272
+ should "optionally make Rack errors fatal" do
273
+ lambda {
274
+ Rack::MockRequest.new(app).get("/?error=foo", :fatal => true)
275
+ }.should.raise(Rack::MockRequest::FatalWarning)
276
+ end
277
+ end
@@ -0,0 +1,547 @@
1
+ require 'rack/utils'
2
+ require 'rack/mock'
3
+
4
+ describe Rack::Multipart do
5
+ def multipart_fixture(name, boundary = "AaB03x")
6
+ file = multipart_file(name)
7
+ data = File.open(file, 'rb') { |io| io.read }
8
+
9
+ type = "multipart/form-data; boundary=#{boundary}"
10
+ length = data.respond_to?(:bytesize) ? data.bytesize : data.size
11
+
12
+ { "CONTENT_TYPE" => type,
13
+ "CONTENT_LENGTH" => length.to_s,
14
+ :input => StringIO.new(data) }
15
+ end
16
+
17
+ def multipart_file(name)
18
+ File.join(File.dirname(__FILE__), "multipart", name.to_s)
19
+ end
20
+
21
+ should "return nil if content type is not multipart" do
22
+ env = Rack::MockRequest.env_for("/",
23
+ "CONTENT_TYPE" => 'application/x-www-form-urlencoded')
24
+ Rack::Multipart.parse_multipart(env).should.equal nil
25
+ end
26
+
27
+ should "parse multipart content when content type present but filename is not" do
28
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename))
29
+ params = Rack::Multipart.parse_multipart(env)
30
+ params["text"].should.equal "contents"
31
+ end
32
+
33
+ if "<3".respond_to?(:force_encoding)
34
+ should "set US_ASCII encoding based on charset" do
35
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename))
36
+ params = Rack::Multipart.parse_multipart(env)
37
+ params["text"].encoding.should.equal Encoding::US_ASCII
38
+
39
+ # I'm not 100% sure if making the param name encoding match the
40
+ # Content-Type charset is the right thing to do. We should revisit this.
41
+ params.keys.each do |key|
42
+ key.encoding.should.equal Encoding::US_ASCII
43
+ end
44
+ end
45
+
46
+ should "set BINARY encoding on things without content type" do
47
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:none))
48
+ params = Rack::Multipart.parse_multipart(env)
49
+ params["submit-name"].encoding.should.equal Encoding::UTF_8
50
+ end
51
+
52
+ should "set UTF8 encoding on names of things without content type" do
53
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:none))
54
+ params = Rack::Multipart.parse_multipart(env)
55
+ params.keys.each do |key|
56
+ key.encoding.should.equal Encoding::UTF_8
57
+ end
58
+ end
59
+
60
+ should "default text to UTF8" do
61
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:text))
62
+ params = Rack::Multipart.parse_multipart(env)
63
+ params['submit-name'].encoding.should.equal Encoding::UTF_8
64
+ params['submit-name-with-content'].encoding.should.equal Encoding::UTF_8
65
+ params.keys.each do |key|
66
+ key.encoding.should.equal Encoding::UTF_8
67
+ end
68
+ end
69
+ end
70
+
71
+ should "raise RangeError if the key space is exhausted" do
72
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename))
73
+
74
+ old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 1
75
+ begin
76
+ lambda { Rack::Multipart.parse_multipart(env) }.should.raise(RangeError)
77
+ ensure
78
+ Rack::Utils.key_space_limit = old
79
+ end
80
+ end
81
+
82
+ should "parse multipart form webkit style" do
83
+ env = Rack::MockRequest.env_for '/', multipart_fixture(:webkit)
84
+ env['CONTENT_TYPE'] = "multipart/form-data; boundary=----WebKitFormBoundaryWLHCs9qmcJJoyjKR"
85
+ params = Rack::Multipart.parse_multipart(env)
86
+ params['profile']['bio'].should.include 'hello'
87
+ end
88
+
89
+ should "reject insanely long boundaries" do
90
+ # using a pipe since a tempfile can use up too much space
91
+ rd, wr = IO.pipe
92
+
93
+ # we only call rewind once at start, so make sure it succeeds
94
+ # and doesn't hit ESPIPE
95
+ def rd.rewind; end
96
+ wr.sync = true
97
+
98
+ # mock out length to make this pipe look like a Tempfile
99
+ def rd.length
100
+ 1024 * 1024 * 8
101
+ end
102
+
103
+ # write to a pipe in a background thread, this will write a lot
104
+ # unless Rack (properly) shuts down the read end
105
+ thr = Thread.new do
106
+ begin
107
+ wr.write("--AaB03x")
108
+
109
+ # make the initial boundary a few gigs long
110
+ longer = "0123456789" * 1024 * 1024
111
+ (1024 * 1024).times { wr.write(longer) }
112
+
113
+ wr.write("\r\n")
114
+ wr.write('Content-Disposition: form-data; name="a"; filename="a.txt"')
115
+ wr.write("\r\n")
116
+ wr.write("Content-Type: text/plain\r\n")
117
+ wr.write("\r\na")
118
+ wr.write("--AaB03x--\r\n")
119
+ wr.close
120
+ rescue => err # this is EPIPE if Rack shuts us down
121
+ err
122
+ end
123
+ end
124
+
125
+ fixture = {
126
+ "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
127
+ "CONTENT_LENGTH" => rd.length.to_s,
128
+ :input => rd,
129
+ }
130
+
131
+ env = Rack::MockRequest.env_for '/', fixture
132
+ lambda {
133
+ Rack::Multipart.parse_multipart(env)
134
+ }.should.raise(EOFError)
135
+ rd.close
136
+
137
+ err = thr.value
138
+ err.should.be.instance_of Errno::EPIPE
139
+ wr.close
140
+ end
141
+
142
+ should "parse multipart upload with text file" do
143
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:text))
144
+ params = Rack::Multipart.parse_multipart(env)
145
+ params["submit-name"].should.equal "Larry"
146
+ params["submit-name-with-content"].should.equal "Berry"
147
+ params["files"][:type].should.equal "text/plain"
148
+ params["files"][:filename].should.equal "file1.txt"
149
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
150
+ "name=\"files\"; filename=\"file1.txt\"\r\n" +
151
+ "Content-Type: text/plain\r\n"
152
+ params["files"][:name].should.equal "files"
153
+ params["files"][:tempfile].read.should.equal "contents"
154
+ end
155
+
156
+ should "preserve extension in the created tempfile" do
157
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:text))
158
+ params = Rack::Multipart.parse_multipart(env)
159
+ File.extname(params["files"][:tempfile].path).should.equal ".txt"
160
+ end
161
+
162
+ should "parse multipart upload with text file with no name field" do
163
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_and_no_name))
164
+ params = Rack::Multipart.parse_multipart(env)
165
+ params["file1.txt"][:type].should.equal "text/plain"
166
+ params["file1.txt"][:filename].should.equal "file1.txt"
167
+ params["file1.txt"][:head].should.equal "Content-Disposition: form-data; " +
168
+ "filename=\"file1.txt\"\r\n" +
169
+ "Content-Type: text/plain\r\n"
170
+ params["file1.txt"][:name].should.equal "file1.txt"
171
+ params["file1.txt"][:tempfile].read.should.equal "contents"
172
+ end
173
+
174
+ should "parse multipart upload with nested parameters" do
175
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:nested))
176
+ params = Rack::Multipart.parse_multipart(env)
177
+ params["foo"]["submit-name"].should.equal "Larry"
178
+ params["foo"]["files"][:type].should.equal "text/plain"
179
+ params["foo"]["files"][:filename].should.equal "file1.txt"
180
+ params["foo"]["files"][:head].should.equal "Content-Disposition: form-data; " +
181
+ "name=\"foo[files]\"; filename=\"file1.txt\"\r\n" +
182
+ "Content-Type: text/plain\r\n"
183
+ params["foo"]["files"][:name].should.equal "foo[files]"
184
+ params["foo"]["files"][:tempfile].read.should.equal "contents"
185
+ end
186
+
187
+ should "parse multipart upload with binary file" do
188
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:binary))
189
+ params = Rack::Multipart.parse_multipart(env)
190
+ params["submit-name"].should.equal "Larry"
191
+ params["files"][:type].should.equal "image/png"
192
+ params["files"][:filename].should.equal "rack-logo.png"
193
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
194
+ "name=\"files\"; filename=\"rack-logo.png\"\r\n" +
195
+ "Content-Type: image/png\r\n"
196
+ params["files"][:name].should.equal "files"
197
+ params["files"][:tempfile].read.length.should.equal 26473
198
+ end
199
+
200
+ should "parse multipart upload with empty file" do
201
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:empty))
202
+ params = Rack::Multipart.parse_multipart(env)
203
+ params["submit-name"].should.equal "Larry"
204
+ params["files"][:type].should.equal "text/plain"
205
+ params["files"][:filename].should.equal "file1.txt"
206
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
207
+ "name=\"files\"; filename=\"file1.txt\"\r\n" +
208
+ "Content-Type: text/plain\r\n"
209
+ params["files"][:name].should.equal "files"
210
+ params["files"][:tempfile].read.should.equal ""
211
+ end
212
+
213
+ should "parse multipart upload with filename with semicolons" do
214
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:semicolon))
215
+ params = Rack::Multipart.parse_multipart(env)
216
+ params["files"][:type].should.equal "text/plain"
217
+ params["files"][:filename].should.equal "fi;le1.txt"
218
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
219
+ "name=\"files\"; filename=\"fi;le1.txt\"\r\n" +
220
+ "Content-Type: text/plain\r\n"
221
+ params["files"][:name].should.equal "files"
222
+ params["files"][:tempfile].read.should.equal "contents"
223
+ end
224
+
225
+ should "parse multipart upload with filename with invalid characters" do
226
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:invalid_character))
227
+ params = Rack::Multipart.parse_multipart(env)
228
+ params["files"][:type].should.equal "text/plain"
229
+ params["files"][:filename].should.match(/invalid/)
230
+ head = "Content-Disposition: form-data; " +
231
+ "name=\"files\"; filename=\"invalid\xC3.txt\"\r\n" +
232
+ "Content-Type: text/plain\r\n"
233
+ head = head.force_encoding("ASCII-8BIT") if head.respond_to?(:force_encoding)
234
+ params["files"][:head].should.equal head
235
+ params["files"][:name].should.equal "files"
236
+ params["files"][:tempfile].read.should.equal "contents"
237
+ end
238
+
239
+ should "not include file params if no file was selected" do
240
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:none))
241
+ params = Rack::Multipart.parse_multipart(env)
242
+ params["submit-name"].should.equal "Larry"
243
+ params["files"].should.equal nil
244
+ params.keys.should.not.include "files"
245
+ end
246
+
247
+ should "parse multipart/mixed" do
248
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:mixed_files))
249
+ params = Rack::Utils::Multipart.parse_multipart(env)
250
+ params["foo"].should.equal "bar"
251
+ params["files"].should.be.instance_of String
252
+ params["files"].size.should.equal 252
253
+ end
254
+
255
+ should "parse multipart/mixed" do
256
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:mixed_files))
257
+ params = Rack::Utils::Multipart.parse_multipart(env)
258
+ params["foo"].should.equal "bar"
259
+ params["files"].should.be.instance_of String
260
+ params["files"].size.should.equal 252
261
+ end
262
+
263
+ should "parse IE multipart upload and clean up filename" do
264
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:ie))
265
+ params = Rack::Multipart.parse_multipart(env)
266
+ params["files"][:type].should.equal "text/plain"
267
+ params["files"][:filename].should.equal "file1.txt"
268
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
269
+ "name=\"files\"; " +
270
+ 'filename="C:\Documents and Settings\Administrator\Desktop\file1.txt"' +
271
+ "\r\nContent-Type: text/plain\r\n"
272
+ params["files"][:name].should.equal "files"
273
+ params["files"][:tempfile].read.should.equal "contents"
274
+ end
275
+
276
+ should "parse filename and modification param" do
277
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_and_modification_param))
278
+ params = Rack::Multipart.parse_multipart(env)
279
+ params["files"][:type].should.equal "image/jpeg"
280
+ params["files"][:filename].should.equal "genome.jpeg"
281
+ params["files"][:head].should.equal "Content-Type: image/jpeg\r\n" +
282
+ "Content-Disposition: attachment; " +
283
+ "name=\"files\"; " +
284
+ "filename=genome.jpeg; " +
285
+ "modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";\r\n" +
286
+ "Content-Description: a complete map of the human genome\r\n"
287
+ params["files"][:name].should.equal "files"
288
+ params["files"][:tempfile].read.should.equal "contents"
289
+ end
290
+
291
+ should "parse filename with escaped quotes" do
292
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_escaped_quotes))
293
+ params = Rack::Multipart.parse_multipart(env)
294
+ params["files"][:type].should.equal "application/octet-stream"
295
+ params["files"][:filename].should.equal "escape \"quotes"
296
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
297
+ "name=\"files\"; " +
298
+ "filename=\"escape \\\"quotes\"\r\n" +
299
+ "Content-Type: application/octet-stream\r\n"
300
+ params["files"][:name].should.equal "files"
301
+ params["files"][:tempfile].read.should.equal "contents"
302
+ end
303
+
304
+ should "parse filename with percent escaped quotes" do
305
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_percent_escaped_quotes))
306
+ params = Rack::Multipart.parse_multipart(env)
307
+ params["files"][:type].should.equal "application/octet-stream"
308
+ params["files"][:filename].should.equal "escape \"quotes"
309
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
310
+ "name=\"files\"; " +
311
+ "filename=\"escape %22quotes\"\r\n" +
312
+ "Content-Type: application/octet-stream\r\n"
313
+ params["files"][:name].should.equal "files"
314
+ params["files"][:tempfile].read.should.equal "contents"
315
+ end
316
+
317
+ should "parse filename with unescaped quotes" do
318
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_unescaped_quotes))
319
+ params = Rack::Multipart.parse_multipart(env)
320
+ params["files"][:type].should.equal "application/octet-stream"
321
+ params["files"][:filename].should.equal "escape \"quotes"
322
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
323
+ "name=\"files\"; " +
324
+ "filename=\"escape \"quotes\"\r\n" +
325
+ "Content-Type: application/octet-stream\r\n"
326
+ params["files"][:name].should.equal "files"
327
+ params["files"][:tempfile].read.should.equal "contents"
328
+ end
329
+
330
+ should "parse filename with escaped quotes and modification param" do
331
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_escaped_quotes_and_modification_param))
332
+ params = Rack::Multipart.parse_multipart(env)
333
+ params["files"][:type].should.equal "image/jpeg"
334
+ params["files"][:filename].should.equal "\"human\" genome.jpeg"
335
+ params["files"][:head].should.equal "Content-Type: image/jpeg\r\n" +
336
+ "Content-Disposition: attachment; " +
337
+ "name=\"files\"; " +
338
+ "filename=\"\"human\" genome.jpeg\"; " +
339
+ "modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";\r\n" +
340
+ "Content-Description: a complete map of the human genome\r\n"
341
+ params["files"][:name].should.equal "files"
342
+ params["files"][:tempfile].read.should.equal "contents"
343
+ end
344
+
345
+ should "parse filename with unescaped percentage characters" do
346
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_unescaped_percentages, "----WebKitFormBoundary2NHc7OhsgU68l3Al"))
347
+ params = Rack::Multipart.parse_multipart(env)
348
+ files = params["document"]["attachment"]
349
+ files[:type].should.equal "image/jpeg"
350
+ files[:filename].should.equal "100% of a photo.jpeg"
351
+ files[:head].should.equal <<-MULTIPART
352
+ Content-Disposition: form-data; name="document[attachment]"; filename="100% of a photo.jpeg"\r
353
+ Content-Type: image/jpeg\r
354
+ MULTIPART
355
+
356
+ files[:name].should.equal "document[attachment]"
357
+ files[:tempfile].read.should.equal "contents"
358
+ end
359
+
360
+ should "parse filename with unescaped percentage characters that look like partial hex escapes" do
361
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_unescaped_percentages2, "----WebKitFormBoundary2NHc7OhsgU68l3Al"))
362
+ params = Rack::Multipart.parse_multipart(env)
363
+ files = params["document"]["attachment"]
364
+ files[:type].should.equal "image/jpeg"
365
+ files[:filename].should.equal "100%a"
366
+ files[:head].should.equal <<-MULTIPART
367
+ Content-Disposition: form-data; name="document[attachment]"; filename="100%a"\r
368
+ Content-Type: image/jpeg\r
369
+ MULTIPART
370
+
371
+ files[:name].should.equal "document[attachment]"
372
+ files[:tempfile].read.should.equal "contents"
373
+ end
374
+
375
+ should "parse filename with unescaped percentage characters that look like partial hex escapes" do
376
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_unescaped_percentages3, "----WebKitFormBoundary2NHc7OhsgU68l3Al"))
377
+ params = Rack::Multipart.parse_multipart(env)
378
+ files = params["document"]["attachment"]
379
+ files[:type].should.equal "image/jpeg"
380
+ files[:filename].should.equal "100%"
381
+ files[:head].should.equal <<-MULTIPART
382
+ Content-Disposition: form-data; name="document[attachment]"; filename="100%"\r
383
+ Content-Type: image/jpeg\r
384
+ MULTIPART
385
+
386
+ files[:name].should.equal "document[attachment]"
387
+ files[:tempfile].read.should.equal "contents"
388
+ end
389
+
390
+ it "rewinds input after parsing upload" do
391
+ options = multipart_fixture(:text)
392
+ input = options[:input]
393
+ env = Rack::MockRequest.env_for("/", options)
394
+ params = Rack::Multipart.parse_multipart(env)
395
+ params["submit-name"].should.equal "Larry"
396
+ params["files"][:filename].should.equal "file1.txt"
397
+ input.read.length.should.equal 307
398
+ end
399
+
400
+ it "builds multipart body" do
401
+ files = Rack::Multipart::UploadedFile.new(multipart_file("file1.txt"))
402
+ data = Rack::Multipart.build_multipart("submit-name" => "Larry", "files" => files)
403
+
404
+ options = {
405
+ "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
406
+ "CONTENT_LENGTH" => data.length.to_s,
407
+ :input => StringIO.new(data)
408
+ }
409
+ env = Rack::MockRequest.env_for("/", options)
410
+ params = Rack::Multipart.parse_multipart(env)
411
+ params["submit-name"].should.equal "Larry"
412
+ params["files"][:filename].should.equal "file1.txt"
413
+ params["files"][:tempfile].read.should.equal "contents"
414
+ end
415
+
416
+ it "builds nested multipart body" do
417
+ files = Rack::Multipart::UploadedFile.new(multipart_file("file1.txt"))
418
+ data = Rack::Multipart.build_multipart("people" => [{"submit-name" => "Larry", "files" => files}])
419
+
420
+ options = {
421
+ "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
422
+ "CONTENT_LENGTH" => data.length.to_s,
423
+ :input => StringIO.new(data)
424
+ }
425
+ env = Rack::MockRequest.env_for("/", options)
426
+ params = Rack::Multipart.parse_multipart(env)
427
+ params["people"][0]["submit-name"].should.equal "Larry"
428
+ params["people"][0]["files"][:filename].should.equal "file1.txt"
429
+ params["people"][0]["files"][:tempfile].read.should.equal "contents"
430
+ end
431
+
432
+ it "can parse fields that end at the end of the buffer" do
433
+ input = File.read(multipart_file("bad_robots"))
434
+
435
+ req = Rack::Request.new Rack::MockRequest.env_for("/",
436
+ "CONTENT_TYPE" => "multipart/form-data, boundary=1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon",
437
+ "CONTENT_LENGTH" => input.size,
438
+ :input => input)
439
+
440
+ req.POST['file.path'].should.equal "/var/tmp/uploads/4/0001728414"
441
+ req.POST['addresses'].should.not.equal nil
442
+ end
443
+
444
+ it "builds complete params with the chunk size of 16384 slicing exactly on boundary" do
445
+ data = File.open(multipart_file("fail_16384_nofile"), 'rb') { |f| f.read }.gsub(/\n/, "\r\n")
446
+ options = {
447
+ "CONTENT_TYPE" => "multipart/form-data; boundary=----WebKitFormBoundaryWsY0GnpbI5U7ztzo",
448
+ "CONTENT_LENGTH" => data.length.to_s,
449
+ :input => StringIO.new(data)
450
+ }
451
+ env = Rack::MockRequest.env_for("/", options)
452
+ params = Rack::Multipart.parse_multipart(env)
453
+
454
+ params.should.not.equal nil
455
+ params.keys.should.include "AAAAAAAAAAAAAAAAAAA"
456
+ params["AAAAAAAAAAAAAAAAAAA"].keys.should.include "PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"
457
+ params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"].keys.should.include "new"
458
+ params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"]["new"].keys.should.include "-2"
459
+ params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"]["new"]["-2"].keys.should.include "ba_unit_id"
460
+ params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"]["new"]["-2"]["ba_unit_id"].should.equal "1017"
461
+ end
462
+
463
+ should "return nil if no UploadedFiles were used" do
464
+ data = Rack::Multipart.build_multipart("people" => [{"submit-name" => "Larry", "files" => "contents"}])
465
+ data.should.equal nil
466
+ end
467
+
468
+ should "raise ArgumentError if params is not a Hash" do
469
+ lambda { Rack::Multipart.build_multipart("foo=bar") }.
470
+ should.raise(ArgumentError).
471
+ message.should.equal "value must be a Hash"
472
+ end
473
+
474
+ it "can parse fields with a content type" do
475
+ data = <<-EOF
476
+ --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon\r
477
+ Content-Disposition: form-data; name="description"\r
478
+ Content-Type: text/plain"\r
479
+ \r
480
+ Very very blue\r
481
+ --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon--\r
482
+ EOF
483
+ options = {
484
+ "CONTENT_TYPE" => "multipart/form-data; boundary=1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon",
485
+ "CONTENT_LENGTH" => data.length.to_s,
486
+ :input => StringIO.new(data)
487
+ }
488
+ env = Rack::MockRequest.env_for("/", options)
489
+ params = Rack::Utils::Multipart.parse_multipart(env)
490
+
491
+ params.should.equal({"description"=>"Very very blue"})
492
+ end
493
+
494
+ should "parse multipart upload with no content-length header" do
495
+ env = Rack::MockRequest.env_for '/', multipart_fixture(:webkit)
496
+ env['CONTENT_TYPE'] = "multipart/form-data; boundary=----WebKitFormBoundaryWLHCs9qmcJJoyjKR"
497
+ env.delete 'CONTENT_LENGTH'
498
+ params = Rack::Multipart.parse_multipart(env)
499
+ params['profile']['bio'].should.include 'hello'
500
+ end
501
+
502
+ should "parse very long unquoted multipart file names" do
503
+ data = <<-EOF
504
+ --AaB03x\r
505
+ Content-Type: text/plain\r
506
+ Content-Disposition: attachment; name=file; filename=#{'long' * 100}\r
507
+ \r
508
+ contents\r
509
+ --AaB03x--\r
510
+ EOF
511
+
512
+ options = {
513
+ "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
514
+ "CONTENT_LENGTH" => data.length.to_s,
515
+ :input => StringIO.new(data)
516
+ }
517
+ env = Rack::MockRequest.env_for("/", options)
518
+ params = Rack::Utils::Multipart.parse_multipart(env)
519
+
520
+ params["file"][:filename].should.equal('long' * 100)
521
+ end
522
+
523
+ should "support mixed case metadata" do
524
+ file = multipart_file(:text)
525
+ data = File.open(file, 'rb') { |io| io.read }
526
+
527
+ type = "Multipart/Form-Data; Boundary=AaB03x"
528
+ length = data.respond_to?(:bytesize) ? data.bytesize : data.size
529
+
530
+ e = { "CONTENT_TYPE" => type,
531
+ "CONTENT_LENGTH" => length.to_s,
532
+ :input => StringIO.new(data) }
533
+
534
+ env = Rack::MockRequest.env_for("/", e)
535
+ params = Rack::Multipart.parse_multipart(env)
536
+ params["submit-name"].should.equal "Larry"
537
+ params["submit-name-with-content"].should.equal "Berry"
538
+ params["files"][:type].should.equal "text/plain"
539
+ params["files"][:filename].should.equal "file1.txt"
540
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
541
+ "name=\"files\"; filename=\"file1.txt\"\r\n" +
542
+ "Content-Type: text/plain\r\n"
543
+ params["files"][:name].should.equal "files"
544
+ params["files"][:tempfile].read.should.equal "contents"
545
+ end
546
+
547
+ end