unicorn 0.8.4 → 0.9.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.
- data/.document +1 -0
- data/CHANGELOG +1 -3
- data/COPYING +339 -0
- data/GNUmakefile +12 -8
- data/LICENSE +3 -3
- data/Manifest +9 -0
- data/README +20 -8
- data/TODO +5 -13
- data/examples/echo.ru +32 -0
- data/examples/git.ru +13 -0
- data/ext/unicorn/http11/http11.c +9 -2
- data/lib/unicorn.rb +35 -17
- data/lib/unicorn/app/exec_cgi.rb +10 -7
- data/lib/unicorn/app/inetd.rb +108 -0
- data/lib/unicorn/chunked_reader.rb +94 -0
- data/lib/unicorn/configurator.rb +1 -1
- data/lib/unicorn/const.rb +5 -1
- data/lib/unicorn/http_request.rb +16 -60
- data/lib/unicorn/http_response.rb +2 -3
- data/lib/unicorn/tee_input.rb +135 -0
- data/lib/unicorn/trailer_parser.rb +52 -0
- data/lib/unicorn/util.rb +0 -17
- data/local.mk.sample +3 -3
- data/test/rails/test_rails.rb +18 -12
- data/test/test_helper.rb +26 -0
- data/test/unit/test_chunked_reader.rb +180 -0
- data/test/unit/test_configurator.rb +1 -1
- data/test/unit/test_http_parser.rb +30 -0
- data/test/unit/test_request.rb +6 -1
- data/test/unit/test_server.rb +12 -1
- data/test/unit/test_signals.rb +2 -0
- data/test/unit/test_trailer_parser.rb +52 -0
- data/test/unit/test_upload.rb +130 -104
- data/test/unit/test_util.rb +28 -30
- data/unicorn.gemspec +7 -6
- metadata +19 -3
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'unicorn'
|
3
|
+
require 'unicorn/http11'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'io/nonblock'
|
6
|
+
require 'digest/sha1'
|
7
|
+
|
8
|
+
class TestChunkedReader < Test::Unit::TestCase
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@env = {}
|
12
|
+
@rd, @wr = IO.pipe
|
13
|
+
@rd.binmode
|
14
|
+
@wr.binmode
|
15
|
+
@rd.sync = @wr.sync = true
|
16
|
+
@start_pid = $$
|
17
|
+
end
|
18
|
+
|
19
|
+
def teardown
|
20
|
+
return if $$ != @start_pid
|
21
|
+
@rd.close rescue nil
|
22
|
+
@wr.close rescue nil
|
23
|
+
begin
|
24
|
+
Process.wait
|
25
|
+
rescue Errno::ECHILD
|
26
|
+
break
|
27
|
+
end while true
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_error
|
31
|
+
cr = bin_reader("8\r\nasdfasdf\r\n8\r\nasdfasdfa#{'a' * 1024}")
|
32
|
+
a = nil
|
33
|
+
assert_nothing_raised { a = cr.readpartial(8192) }
|
34
|
+
assert_equal 'asdfasdf', a
|
35
|
+
assert_nothing_raised { a = cr.readpartial(8192) }
|
36
|
+
assert_equal 'asdfasdf', a
|
37
|
+
assert_raises(Unicorn::HttpParserError) { cr.readpartial(8192) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_eof1
|
41
|
+
cr = bin_reader("0\r\n")
|
42
|
+
assert_raises(EOFError) { cr.readpartial(8192) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_eof2
|
46
|
+
cr = bin_reader("0\r\n\r\n")
|
47
|
+
assert_raises(EOFError) { cr.readpartial(8192) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_readpartial1
|
51
|
+
cr = bin_reader("4\r\nasdf\r\n0\r\n")
|
52
|
+
assert_equal 'asdf', cr.readpartial(8192)
|
53
|
+
assert_raises(EOFError) { cr.readpartial(8192) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_gets1
|
57
|
+
cr = bin_reader("4\r\nasdf\r\n0\r\n")
|
58
|
+
STDOUT.sync = true
|
59
|
+
assert_equal 'asdf', cr.gets
|
60
|
+
assert_raises(EOFError) { cr.readpartial(8192) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_gets2
|
64
|
+
cr = bin_reader("4\r\nasd\n\r\n0\r\n\r\n")
|
65
|
+
assert_equal "asd\n", cr.gets
|
66
|
+
assert_nil cr.gets
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_gets3
|
70
|
+
max = Unicorn::Const::CHUNK_SIZE * 2
|
71
|
+
str = ('a' * max).freeze
|
72
|
+
first = 5
|
73
|
+
last = str.size - first
|
74
|
+
cr = bin_reader(
|
75
|
+
"#{'%x' % first}\r\n#{str[0, first]}\r\n" \
|
76
|
+
"#{'%x' % last}\r\n#{str[-last, last]}\r\n" \
|
77
|
+
"0\r\n")
|
78
|
+
assert_equal str, cr.gets
|
79
|
+
assert_nil cr.gets
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_readpartial_gets_mixed1
|
83
|
+
max = Unicorn::Const::CHUNK_SIZE * 2
|
84
|
+
str = ('a' * max).freeze
|
85
|
+
first = 5
|
86
|
+
last = str.size - first
|
87
|
+
cr = bin_reader(
|
88
|
+
"#{'%x' % first}\r\n#{str[0, first]}\r\n" \
|
89
|
+
"#{'%x' % last}\r\n#{str[-last, last]}\r\n" \
|
90
|
+
"0\r\n")
|
91
|
+
partial = cr.readpartial(16384)
|
92
|
+
assert String === partial
|
93
|
+
|
94
|
+
len = max - partial.size
|
95
|
+
assert_equal(str[-len, len], cr.gets)
|
96
|
+
assert_raises(EOFError) { cr.readpartial(1) }
|
97
|
+
assert_nil cr.gets
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_gets_mixed_readpartial
|
101
|
+
max = 10
|
102
|
+
str = ("z\n" * max).freeze
|
103
|
+
first = 5
|
104
|
+
last = str.size - first
|
105
|
+
cr = bin_reader(
|
106
|
+
"#{'%x' % first}\r\n#{str[0, first]}\r\n" \
|
107
|
+
"#{'%x' % last}\r\n#{str[-last, last]}\r\n" \
|
108
|
+
"0\r\n")
|
109
|
+
assert_equal("z\n", cr.gets)
|
110
|
+
assert_equal("z\n", cr.gets)
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_dd
|
114
|
+
cr = bin_reader("6\r\nhello\n\r\n")
|
115
|
+
tmp = Tempfile.new('test_dd')
|
116
|
+
tmp.sync = true
|
117
|
+
|
118
|
+
pid = fork {
|
119
|
+
crd, cwr = IO.pipe
|
120
|
+
crd.binmode
|
121
|
+
cwr.binmode
|
122
|
+
crd.sync = cwr.sync = true
|
123
|
+
|
124
|
+
pid = fork {
|
125
|
+
STDOUT.reopen(cwr)
|
126
|
+
crd.close
|
127
|
+
cwr.close
|
128
|
+
exec('dd', 'if=/dev/urandom', 'bs=93390', 'count=16')
|
129
|
+
}
|
130
|
+
cwr.close
|
131
|
+
begin
|
132
|
+
buf = crd.readpartial(16384)
|
133
|
+
tmp.write(buf)
|
134
|
+
@wr.write("#{'%x' % buf.size}\r\n#{buf}\r\n")
|
135
|
+
rescue EOFError
|
136
|
+
@wr.write("0\r\n\r\n")
|
137
|
+
Process.waitpid(pid)
|
138
|
+
exit 0
|
139
|
+
end while true
|
140
|
+
}
|
141
|
+
assert_equal "hello\n", cr.gets
|
142
|
+
sha1 = Digest::SHA1.new
|
143
|
+
buf = Unicorn::Z.dup
|
144
|
+
begin
|
145
|
+
cr.readpartial(16384, buf)
|
146
|
+
sha1.update(buf)
|
147
|
+
rescue EOFError
|
148
|
+
break
|
149
|
+
end while true
|
150
|
+
|
151
|
+
assert_nothing_raised { Process.waitpid(pid) }
|
152
|
+
sha1_file = Digest::SHA1.new
|
153
|
+
File.open(tmp.path, 'rb') { |fp|
|
154
|
+
while fp.read(16384, buf)
|
155
|
+
sha1_file.update(buf)
|
156
|
+
end
|
157
|
+
}
|
158
|
+
assert_equal sha1_file.hexdigest, sha1.hexdigest
|
159
|
+
end
|
160
|
+
|
161
|
+
def test_trailer
|
162
|
+
@env['HTTP_TRAILER'] = 'Content-MD5'
|
163
|
+
pid = fork { @wr.syswrite("Content-MD5: asdf\r\n") }
|
164
|
+
cr = bin_reader("8\r\nasdfasdf\r\n8\r\nasdfasdf\r\n0\r\n")
|
165
|
+
assert_equal 'asdfasdf', cr.readpartial(4096)
|
166
|
+
assert_equal 'asdfasdf', cr.readpartial(4096)
|
167
|
+
assert_raises(EOFError) { cr.readpartial(4096) }
|
168
|
+
pid, status = Process.waitpid2(pid)
|
169
|
+
assert status.success?
|
170
|
+
assert_equal 'asdf', @env['HTTP_CONTENT_MD5']
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
def bin_reader(buf)
|
176
|
+
buf.force_encoding(Encoding::BINARY) if buf.respond_to?(:force_encoding)
|
177
|
+
Unicorn::ChunkedReader.new(@env, @rd, buf)
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
@@ -23,6 +23,7 @@ class HttpParserTest < Test::Unit::TestCase
|
|
23
23
|
assert_equal 'GET', req['REQUEST_METHOD']
|
24
24
|
assert_nil req['FRAGMENT']
|
25
25
|
assert_equal '', req['QUERY_STRING']
|
26
|
+
assert_nil req[:http_body]
|
26
27
|
|
27
28
|
parser.reset
|
28
29
|
req.clear
|
@@ -41,6 +42,7 @@ class HttpParserTest < Test::Unit::TestCase
|
|
41
42
|
assert_equal 'GET', req['REQUEST_METHOD']
|
42
43
|
assert_nil req['FRAGMENT']
|
43
44
|
assert_equal '', req['QUERY_STRING']
|
45
|
+
assert_nil req[:http_body]
|
44
46
|
end
|
45
47
|
|
46
48
|
def test_parse_server_host_default_port
|
@@ -49,6 +51,7 @@ class HttpParserTest < Test::Unit::TestCase
|
|
49
51
|
assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo\r\n\r\n")
|
50
52
|
assert_equal 'foo', req['SERVER_NAME']
|
51
53
|
assert_equal '80', req['SERVER_PORT']
|
54
|
+
assert_nil req[:http_body]
|
52
55
|
end
|
53
56
|
|
54
57
|
def test_parse_server_host_alt_port
|
@@ -57,6 +60,7 @@ class HttpParserTest < Test::Unit::TestCase
|
|
57
60
|
assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n")
|
58
61
|
assert_equal 'foo', req['SERVER_NAME']
|
59
62
|
assert_equal '999', req['SERVER_PORT']
|
63
|
+
assert_nil req[:http_body]
|
60
64
|
end
|
61
65
|
|
62
66
|
def test_parse_server_host_empty_port
|
@@ -65,6 +69,7 @@ class HttpParserTest < Test::Unit::TestCase
|
|
65
69
|
assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n")
|
66
70
|
assert_equal 'foo', req['SERVER_NAME']
|
67
71
|
assert_equal '80', req['SERVER_PORT']
|
72
|
+
assert_nil req[:http_body]
|
68
73
|
end
|
69
74
|
|
70
75
|
def test_parse_server_host_xfp_https
|
@@ -74,6 +79,7 @@ class HttpParserTest < Test::Unit::TestCase
|
|
74
79
|
"X-Forwarded-Proto: https\r\n\r\n")
|
75
80
|
assert_equal 'foo', req['SERVER_NAME']
|
76
81
|
assert_equal '443', req['SERVER_PORT']
|
82
|
+
assert_nil req[:http_body]
|
77
83
|
end
|
78
84
|
|
79
85
|
def test_parse_strange_headers
|
@@ -81,6 +87,7 @@ class HttpParserTest < Test::Unit::TestCase
|
|
81
87
|
req = {}
|
82
88
|
should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n"
|
83
89
|
assert parser.execute(req, should_be_good)
|
90
|
+
assert_nil req[:http_body]
|
84
91
|
|
85
92
|
# ref: http://thread.gmane.org/gmane.comp.lang.ruby.mongrel.devel/37/focus=45
|
86
93
|
# (note we got 'pen' mixed up with 'pound' in that thread,
|
@@ -104,6 +111,7 @@ class HttpParserTest < Test::Unit::TestCase
|
|
104
111
|
req = {}
|
105
112
|
sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n)
|
106
113
|
assert parser.execute(req, sorta_safe)
|
114
|
+
assert_nil req[:http_body]
|
107
115
|
end
|
108
116
|
end
|
109
117
|
|
@@ -115,6 +123,7 @@ class HttpParserTest < Test::Unit::TestCase
|
|
115
123
|
assert_raises(HttpParserError) { parser.execute(req, bad_http) }
|
116
124
|
parser.reset
|
117
125
|
assert(parser.execute({}, "GET / HTTP/1.0\r\n\r\n"))
|
126
|
+
assert_nil req[:http_body]
|
118
127
|
end
|
119
128
|
|
120
129
|
def test_piecemeal
|
@@ -134,6 +143,7 @@ class HttpParserTest < Test::Unit::TestCase
|
|
134
143
|
assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
|
135
144
|
assert_nil req['FRAGMENT']
|
136
145
|
assert_equal '', req['QUERY_STRING']
|
146
|
+
assert_nil req[:http_body]
|
137
147
|
end
|
138
148
|
|
139
149
|
# not common, but underscores do appear in practice
|
@@ -150,6 +160,7 @@ class HttpParserTest < Test::Unit::TestCase
|
|
150
160
|
assert_equal 'under_score.example.com', req['HTTP_HOST']
|
151
161
|
assert_equal 'under_score.example.com', req['SERVER_NAME']
|
152
162
|
assert_equal '80', req['SERVER_PORT']
|
163
|
+
assert_nil req[:http_body]
|
153
164
|
end
|
154
165
|
|
155
166
|
def test_absolute_uri
|
@@ -243,6 +254,24 @@ class HttpParserTest < Test::Unit::TestCase
|
|
243
254
|
assert_equal "", req[:http_body]
|
244
255
|
end
|
245
256
|
|
257
|
+
def test_unknown_methods
|
258
|
+
%w(GETT HEADR XGET XHEAD).each { |m|
|
259
|
+
parser = HttpParser.new
|
260
|
+
req = {}
|
261
|
+
s = "#{m} /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n"
|
262
|
+
ok = false
|
263
|
+
assert_nothing_raised do
|
264
|
+
ok = parser.execute(req, s)
|
265
|
+
end
|
266
|
+
assert ok
|
267
|
+
assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI']
|
268
|
+
assert_equal 'posts-17408', req['FRAGMENT']
|
269
|
+
assert_equal 'page=1', req['QUERY_STRING']
|
270
|
+
assert_equal "", req[:http_body]
|
271
|
+
assert_equal m, req['REQUEST_METHOD']
|
272
|
+
}
|
273
|
+
end
|
274
|
+
|
246
275
|
def test_fragment_in_uri
|
247
276
|
parser = HttpParser.new
|
248
277
|
req = {}
|
@@ -255,6 +284,7 @@ class HttpParserTest < Test::Unit::TestCase
|
|
255
284
|
assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI']
|
256
285
|
assert_equal 'posts-17408', req['FRAGMENT']
|
257
286
|
assert_equal 'page=1', req['QUERY_STRING']
|
287
|
+
assert_nil req[:http_body]
|
258
288
|
end
|
259
289
|
|
260
290
|
# lame random garbage maker
|
data/test/unit/test_request.rb
CHANGED
@@ -16,6 +16,7 @@ class RequestTest < Test::Unit::TestCase
|
|
16
16
|
|
17
17
|
class MockRequest < StringIO
|
18
18
|
alias_method :readpartial, :sysread
|
19
|
+
alias_method :read_nonblock, :sysread
|
19
20
|
end
|
20
21
|
|
21
22
|
def setup
|
@@ -149,7 +150,11 @@ class RequestTest < Test::Unit::TestCase
|
|
149
150
|
assert_nothing_raised { env = @request.read(client) }
|
150
151
|
assert ! env.include?(:http_body)
|
151
152
|
assert_equal length, env['rack.input'].size
|
152
|
-
count.times {
|
153
|
+
count.times {
|
154
|
+
tmp = env['rack.input'].read(bs)
|
155
|
+
tmp << env['rack.input'].read(bs - tmp.size) if tmp.size != bs
|
156
|
+
assert_equal buf, tmp
|
157
|
+
}
|
153
158
|
assert_nil env['rack.input'].read(bs)
|
154
159
|
assert_nothing_raised { env['rack.input'].rewind }
|
155
160
|
assert_nothing_raised { res = @lint.call(env) }
|
data/test/unit/test_server.rb
CHANGED
@@ -12,6 +12,8 @@ class TestHandler
|
|
12
12
|
|
13
13
|
def call(env)
|
14
14
|
# response.socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello!\n")
|
15
|
+
while env['rack.input'].read(4096)
|
16
|
+
end
|
15
17
|
[200, { 'Content-Type' => 'text/plain' }, ['hello!\n']]
|
16
18
|
end
|
17
19
|
end
|
@@ -152,9 +154,18 @@ class WebServerTest < Test::Unit::TestCase
|
|
152
154
|
|
153
155
|
def test_file_streamed_request
|
154
156
|
body = "a" * (Unicorn::Const::MAX_BODY * 2)
|
155
|
-
long = "
|
157
|
+
long = "PUT /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body
|
156
158
|
do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400)
|
157
159
|
end
|
158
160
|
|
161
|
+
def test_file_streamed_request_bad_method
|
162
|
+
body = "a" * (Unicorn::Const::MAX_BODY * 2)
|
163
|
+
long = "GET /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body
|
164
|
+
assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,
|
165
|
+
Errno::EBADF) {
|
166
|
+
do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400)
|
167
|
+
}
|
168
|
+
end
|
169
|
+
|
159
170
|
end
|
160
171
|
|
data/test/unit/test_signals.rb
CHANGED
@@ -158,6 +158,8 @@ class SignalsTest < Test::Unit::TestCase
|
|
158
158
|
|
159
159
|
def test_request_read
|
160
160
|
app = lambda { |env|
|
161
|
+
while env['rack.input'].read(4096)
|
162
|
+
end
|
161
163
|
[ 200, {'Content-Type'=>'text/plain', 'X-Pid'=>Process.pid.to_s}, [] ]
|
162
164
|
}
|
163
165
|
redirect_test_io { @server = HttpServer.new(app, @server_opts).start }
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'unicorn'
|
3
|
+
require 'unicorn/http11'
|
4
|
+
require 'unicorn/trailer_parser'
|
5
|
+
|
6
|
+
class TestTrailerParser < Test::Unit::TestCase
|
7
|
+
|
8
|
+
def test_basic
|
9
|
+
tp = Unicorn::TrailerParser.new('Content-MD5')
|
10
|
+
env = {}
|
11
|
+
assert ! tp.execute!(env, "Content-MD5: asdf")
|
12
|
+
assert env.empty?
|
13
|
+
assert tp.execute!(env, "Content-MD5: asdf\r\n")
|
14
|
+
assert_equal 'asdf', env['HTTP_CONTENT_MD5']
|
15
|
+
assert_equal 1, env.size
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_invalid_trailer
|
19
|
+
tp = Unicorn::TrailerParser.new('Content-MD5')
|
20
|
+
env = {}
|
21
|
+
assert_raises(Unicorn::HttpParserError) {
|
22
|
+
tp.execute!(env, "Content-MD: asdf\r\n")
|
23
|
+
}
|
24
|
+
assert env.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_multiple_trailer
|
28
|
+
tp = Unicorn::TrailerParser.new('Foo,Bar')
|
29
|
+
env = {}
|
30
|
+
buf = "Bar: a\r\nFoo: b\r\n"
|
31
|
+
assert tp.execute!(env, buf)
|
32
|
+
assert_equal 'a', env['HTTP_BAR']
|
33
|
+
assert_equal 'b', env['HTTP_FOO']
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_too_big_key
|
37
|
+
tp = Unicorn::TrailerParser.new('Foo,Bar')
|
38
|
+
env = {}
|
39
|
+
buf = "Bar#{'a' * 1024}: a\r\nFoo: b\r\n"
|
40
|
+
assert_raises(Unicorn::HttpParserError) { tp.execute!(env, buf) }
|
41
|
+
assert env.empty?
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_too_big_value
|
45
|
+
tp = Unicorn::TrailerParser.new('Foo,Bar')
|
46
|
+
env = {}
|
47
|
+
buf = "Bar: #{'a' * (1024 * 1024)}: a\r\nFoo: b\r\n"
|
48
|
+
assert_raises(Unicorn::HttpParserError) { tp.execute!(env, buf) }
|
49
|
+
assert env.empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
data/test/unit/test_upload.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# Copyright (c) 2009 Eric Wong
|
2
2
|
require 'test/test_helper'
|
3
|
+
require 'digest/md5'
|
3
4
|
|
4
5
|
include Unicorn
|
5
6
|
|
@@ -18,29 +19,33 @@ class UploadTest < Test::Unit::TestCase
|
|
18
19
|
@sha1 = Digest::SHA1.new
|
19
20
|
@sha1_app = lambda do |env|
|
20
21
|
input = env['rack.input']
|
21
|
-
resp = {
|
22
|
+
resp = {}
|
22
23
|
|
23
|
-
# sysread
|
24
24
|
@sha1.reset
|
25
|
-
|
26
|
-
|
27
|
-
rescue EOFError
|
25
|
+
while buf = input.read(@bs)
|
26
|
+
@sha1.update(buf)
|
28
27
|
end
|
29
28
|
resp[:sha1] = @sha1.hexdigest
|
30
29
|
|
31
|
-
# read
|
32
|
-
input.sysseek(0) if input.respond_to?(:sysseek)
|
30
|
+
# rewind and read again
|
33
31
|
input.rewind
|
34
32
|
@sha1.reset
|
35
|
-
|
36
|
-
buf = input.read(@bs) or break
|
33
|
+
while buf = input.read(@bs)
|
37
34
|
@sha1.update(buf)
|
38
|
-
|
35
|
+
end
|
39
36
|
|
40
37
|
if resp[:sha1] == @sha1.hexdigest
|
41
38
|
resp[:sysread_read_byte_match] = true
|
42
39
|
end
|
43
40
|
|
41
|
+
if expect_size = env['HTTP_X_EXPECT_SIZE']
|
42
|
+
if expect_size.to_i == input.size
|
43
|
+
resp[:expect_size_match] = true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
resp[:size] = input.size
|
47
|
+
resp[:content_md5] = env['HTTP_CONTENT_MD5']
|
48
|
+
|
44
49
|
[ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ]
|
45
50
|
end
|
46
51
|
end
|
@@ -54,7 +59,7 @@ class UploadTest < Test::Unit::TestCase
|
|
54
59
|
start_server(@sha1_app)
|
55
60
|
sock = TCPSocket.new(@addr, @port)
|
56
61
|
sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
|
57
|
-
@count.times do
|
62
|
+
@count.times do |i|
|
58
63
|
buf = @random.sysread(@bs)
|
59
64
|
@sha1.update(buf)
|
60
65
|
sock.syswrite(buf)
|
@@ -63,10 +68,34 @@ class UploadTest < Test::Unit::TestCase
|
|
63
68
|
assert_equal "HTTP/1.1 200 OK", read[0]
|
64
69
|
resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
|
65
70
|
assert_equal length, resp[:size]
|
66
|
-
assert_equal 0, resp[:pos]
|
67
71
|
assert_equal @sha1.hexdigest, resp[:sha1]
|
68
72
|
end
|
69
73
|
|
74
|
+
def test_put_content_md5
|
75
|
+
md5 = Digest::MD5.new
|
76
|
+
start_server(@sha1_app)
|
77
|
+
sock = TCPSocket.new(@addr, @port)
|
78
|
+
sock.syswrite("PUT / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n" \
|
79
|
+
"Trailer: Content-MD5\r\n\r\n")
|
80
|
+
@count.times do |i|
|
81
|
+
buf = @random.sysread(@bs)
|
82
|
+
@sha1.update(buf)
|
83
|
+
md5.update(buf)
|
84
|
+
sock.syswrite("#{'%x' % buf.size}\r\n")
|
85
|
+
sock.syswrite(buf << "\r\n")
|
86
|
+
end
|
87
|
+
sock.syswrite("0\r\n")
|
88
|
+
|
89
|
+
content_md5 = [ md5.digest! ].pack('m').strip.freeze
|
90
|
+
sock.syswrite("Content-MD5: #{content_md5}\r\n")
|
91
|
+
read = sock.read.split(/\r\n/)
|
92
|
+
assert_equal "HTTP/1.1 200 OK", read[0]
|
93
|
+
resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
|
94
|
+
assert_equal length, resp[:size]
|
95
|
+
assert_equal @sha1.hexdigest, resp[:sha1]
|
96
|
+
assert_equal content_md5, resp[:content_md5]
|
97
|
+
end
|
98
|
+
|
70
99
|
def test_put_trickle_small
|
71
100
|
@count, @bs = 2, 128
|
72
101
|
start_server(@sha1_app)
|
@@ -85,42 +114,7 @@ class UploadTest < Test::Unit::TestCase
|
|
85
114
|
assert_equal "HTTP/1.1 200 OK", read[0]
|
86
115
|
resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
|
87
116
|
assert_equal length, resp[:size]
|
88
|
-
assert_equal 0, resp[:pos]
|
89
117
|
assert_equal @sha1.hexdigest, resp[:sha1]
|
90
|
-
assert_equal StringIO, resp[:class]
|
91
|
-
end
|
92
|
-
|
93
|
-
def test_tempfile_unlinked
|
94
|
-
spew_path = lambda do |env|
|
95
|
-
if orig = env['HTTP_X_OLD_PATH']
|
96
|
-
assert orig != env['rack.input'].path
|
97
|
-
end
|
98
|
-
assert_equal length, env['rack.input'].size
|
99
|
-
[ 200, @hdr.merge('X-Tempfile-Path' => env['rack.input'].path), [] ]
|
100
|
-
end
|
101
|
-
start_server(spew_path)
|
102
|
-
sock = TCPSocket.new(@addr, @port)
|
103
|
-
sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
|
104
|
-
@count.times { sock.syswrite(' ' * @bs) }
|
105
|
-
path = sock.read[/^X-Tempfile-Path: (\S+)/, 1]
|
106
|
-
sock.close
|
107
|
-
|
108
|
-
# send another request to ensure we hit the next request
|
109
|
-
sock = TCPSocket.new(@addr, @port)
|
110
|
-
sock.syswrite("PUT / HTTP/1.0\r\nX-Old-Path: #{path}\r\n" \
|
111
|
-
"Content-Length: #{length}\r\n\r\n")
|
112
|
-
@count.times { sock.syswrite(' ' * @bs) }
|
113
|
-
path2 = sock.read[/^X-Tempfile-Path: (\S+)/, 1]
|
114
|
-
sock.close
|
115
|
-
assert path != path2
|
116
|
-
|
117
|
-
# make sure the next request comes in so the unlink got processed
|
118
|
-
sock = TCPSocket.new(@addr, @port)
|
119
|
-
sock.syswrite("GET ?lasdf\r\n\r\n\r\n\r\n")
|
120
|
-
sock.sysread(4096) rescue nil
|
121
|
-
sock.close
|
122
|
-
|
123
|
-
assert ! File.exist?(path)
|
124
118
|
end
|
125
119
|
|
126
120
|
def test_put_keepalive_truncates_small_overwrite
|
@@ -136,75 +130,31 @@ class UploadTest < Test::Unit::TestCase
|
|
136
130
|
sock.syswrite('12345') # write 4 bytes more than we expected
|
137
131
|
@sha1.update('1')
|
138
132
|
|
139
|
-
|
133
|
+
buf = sock.readpartial(4096)
|
134
|
+
while buf !~ /\r\n\r\n/
|
135
|
+
buf << sock.readpartial(4096)
|
136
|
+
end
|
137
|
+
read = buf.split(/\r\n/)
|
140
138
|
assert_equal "HTTP/1.1 200 OK", read[0]
|
141
139
|
resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
|
142
140
|
assert_equal to_upload, resp[:size]
|
143
|
-
assert_equal 0, resp[:pos]
|
144
141
|
assert_equal @sha1.hexdigest, resp[:sha1]
|
145
142
|
end
|
146
143
|
|
147
144
|
def test_put_excessive_overwrite_closed
|
148
|
-
start_server(lambda { |env| [ 200, @hdr, [] ] })
|
149
|
-
sock = TCPSocket.new(@addr, @port)
|
150
|
-
buf = ' ' * @bs
|
151
|
-
sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
|
152
|
-
@count.times { sock.syswrite(buf) }
|
153
|
-
assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
|
154
|
-
::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) }
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
def test_put_handler_closed_file
|
159
|
-
nr = '0'
|
160
145
|
start_server(lambda { |env|
|
161
|
-
env['rack.input'].
|
162
|
-
|
163
|
-
[ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ]
|
146
|
+
while env['rack.input'].read(65536); end
|
147
|
+
[ 200, @hdr, [] ]
|
164
148
|
})
|
165
149
|
sock = TCPSocket.new(@addr, @port)
|
166
150
|
buf = ' ' * @bs
|
167
151
|
sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
|
168
|
-
@count.times { sock.syswrite(buf) }
|
169
|
-
read = sock.read.split(/\r\n/)
|
170
|
-
assert_equal "HTTP/1.1 200 OK", read[0]
|
171
|
-
resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
|
172
|
-
assert_equal '1', resp[:nr]
|
173
|
-
|
174
|
-
# server still alive?
|
175
|
-
sock = TCPSocket.new(@addr, @port)
|
176
|
-
sock.syswrite("GET / HTTP/1.0\r\n\r\n")
|
177
|
-
read = sock.read.split(/\r\n/)
|
178
|
-
assert_equal "HTTP/1.1 200 OK", read[0]
|
179
|
-
resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
|
180
|
-
assert_equal '2', resp[:nr]
|
181
|
-
end
|
182
152
|
|
183
|
-
def test_renamed_file_not_closed
|
184
|
-
start_server(lambda { |env|
|
185
|
-
new_tmp = Tempfile.new('unicorn_test')
|
186
|
-
input = env['rack.input']
|
187
|
-
File.rename(input.path, new_tmp.path)
|
188
|
-
resp = {
|
189
|
-
:inode => input.stat.ino,
|
190
|
-
:size => input.stat.size,
|
191
|
-
:new_tmp => new_tmp.path,
|
192
|
-
:old_tmp => input.path,
|
193
|
-
}
|
194
|
-
[ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ]
|
195
|
-
})
|
196
|
-
sock = TCPSocket.new(@addr, @port)
|
197
|
-
buf = ' ' * @bs
|
198
|
-
sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
|
199
153
|
@count.times { sock.syswrite(buf) }
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
assert_equal resp[:inode], new_tmp.stat.ino
|
205
|
-
assert_equal length, resp[:size]
|
206
|
-
assert ! File.exist?(resp[:old_tmp])
|
207
|
-
assert_equal resp[:size], new_tmp.stat.size
|
154
|
+
assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
|
155
|
+
::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) }
|
156
|
+
end
|
157
|
+
assert_equal "HTTP/1.1 200 OK\r\n", sock.gets
|
208
158
|
end
|
209
159
|
|
210
160
|
# Despite reading numerous articles and inspecting the 1.9.1-p0 C
|
@@ -233,7 +183,6 @@ class UploadTest < Test::Unit::TestCase
|
|
233
183
|
resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
|
234
184
|
assert $?.success?, 'curl ran OK'
|
235
185
|
assert_match(%r!\b#{sha1}\b!, resp)
|
236
|
-
assert_match(/Tempfile/, resp)
|
237
186
|
assert_match(/sysread_read_byte_match/, resp)
|
238
187
|
|
239
188
|
# small StringIO path
|
@@ -249,10 +198,87 @@ class UploadTest < Test::Unit::TestCase
|
|
249
198
|
resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
|
250
199
|
assert $?.success?, 'curl ran OK'
|
251
200
|
assert_match(%r!\b#{sha1}\b!, resp)
|
252
|
-
assert_match(/StringIO/, resp)
|
253
201
|
assert_match(/sysread_read_byte_match/, resp)
|
254
202
|
end
|
255
203
|
|
204
|
+
def test_chunked_upload_via_curl
|
205
|
+
# POSIX doesn't require all of these to be present on a system
|
206
|
+
which('curl') or return
|
207
|
+
which('sha1sum') or return
|
208
|
+
which('dd') or return
|
209
|
+
|
210
|
+
start_server(@sha1_app)
|
211
|
+
|
212
|
+
tmp = Tempfile.new('dd_dest')
|
213
|
+
assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
|
214
|
+
"bs=#{@bs}", "count=#{@count}"),
|
215
|
+
"dd #@random to #{tmp}")
|
216
|
+
sha1_re = %r!\b([a-f0-9]{40})\b!
|
217
|
+
sha1_out = `sha1sum #{tmp.path}`
|
218
|
+
assert $?.success?, 'sha1sum ran OK'
|
219
|
+
|
220
|
+
assert_match(sha1_re, sha1_out)
|
221
|
+
sha1 = sha1_re.match(sha1_out)[1]
|
222
|
+
cmd = "curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \
|
223
|
+
-isSf --no-buffer -T- " \
|
224
|
+
"http://#@addr:#@port/"
|
225
|
+
resp = Tempfile.new('resp')
|
226
|
+
resp.sync = true
|
227
|
+
|
228
|
+
rd, wr = IO.pipe
|
229
|
+
wr.sync = rd.sync = true
|
230
|
+
pid = fork {
|
231
|
+
STDIN.reopen(rd)
|
232
|
+
rd.close
|
233
|
+
wr.close
|
234
|
+
STDOUT.reopen(resp)
|
235
|
+
exec cmd
|
236
|
+
}
|
237
|
+
rd.close
|
238
|
+
|
239
|
+
tmp.rewind
|
240
|
+
@count.times { |i|
|
241
|
+
wr.write(tmp.read(@bs))
|
242
|
+
sleep(rand / 10) if 0 == i % 8
|
243
|
+
}
|
244
|
+
wr.close
|
245
|
+
pid, status = Process.waitpid2(pid)
|
246
|
+
|
247
|
+
resp.rewind
|
248
|
+
resp = resp.read
|
249
|
+
assert status.success?, 'curl ran OK'
|
250
|
+
assert_match(%r!\b#{sha1}\b!, resp)
|
251
|
+
assert_match(/sysread_read_byte_match/, resp)
|
252
|
+
assert_match(/expect_size_match/, resp)
|
253
|
+
end
|
254
|
+
|
255
|
+
def test_curl_chunked_small
|
256
|
+
# POSIX doesn't require all of these to be present on a system
|
257
|
+
which('curl') or return
|
258
|
+
which('sha1sum') or return
|
259
|
+
which('dd') or return
|
260
|
+
|
261
|
+
start_server(@sha1_app)
|
262
|
+
|
263
|
+
tmp = Tempfile.new('dd_dest')
|
264
|
+
# small StringIO path
|
265
|
+
assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
|
266
|
+
"bs=1024", "count=1"),
|
267
|
+
"dd #@random to #{tmp}")
|
268
|
+
sha1_re = %r!\b([a-f0-9]{40})\b!
|
269
|
+
sha1_out = `sha1sum #{tmp.path}`
|
270
|
+
assert $?.success?, 'sha1sum ran OK'
|
271
|
+
|
272
|
+
assert_match(sha1_re, sha1_out)
|
273
|
+
sha1 = sha1_re.match(sha1_out)[1]
|
274
|
+
resp = `curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \
|
275
|
+
-isSf --no-buffer -T- http://#@addr:#@port/ < #{tmp.path}`
|
276
|
+
assert $?.success?, 'curl ran OK'
|
277
|
+
assert_match(%r!\b#{sha1}\b!, resp)
|
278
|
+
assert_match(/sysread_read_byte_match/, resp)
|
279
|
+
assert_match(/expect_size_match/, resp)
|
280
|
+
end
|
281
|
+
|
256
282
|
private
|
257
283
|
|
258
284
|
def length
|