yahns 1.6.0 → 1.7.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.
- checksums.yaml +4 -4
- data/Documentation/yahns_config.txt +3 -0
- data/GIT-VERSION-GEN +1 -1
- data/extras/proxy_pass.rb +22 -16
- data/lib/yahns/client_expire_tcpi.rb +1 -1
- data/lib/yahns/config.rb +4 -5
- data/lib/yahns/fdmap.rb +9 -0
- data/lib/yahns/http_client.rb +19 -19
- data/lib/yahns/http_context.rb +11 -18
- data/lib/yahns/http_response.rb +2 -2
- data/lib/yahns/openssl_client.rb +26 -6
- data/lib/yahns/proxy_http_response.rb +293 -0
- data/lib/yahns/proxy_pass.rb +248 -0
- data/lib/yahns/queue_epoll.rb +7 -13
- data/lib/yahns/queue_kqueue.rb +7 -8
- data/lib/yahns/rackup_handler.rb +0 -1
- data/lib/yahns/socket_helper.rb +2 -2
- data/lib/yahns/tee_input.rb +1 -1
- data/lib/yahns/tmpio.rb +6 -2
- data/lib/yahns/wbuf.rb +29 -13
- data/test/helper.rb +10 -0
- data/test/test_extras_proxy_pass.rb +3 -0
- data/test/test_input.rb +50 -1
- data/test/test_proxy_pass.rb +611 -0
- data/test/test_rack_hijack.rb +14 -10
- data/test/test_server.rb +3 -1
- data/test/test_ssl.rb +72 -0
- data/test/test_tmpio.rb +20 -0
- data/test/test_wbuf.rb +4 -3
- metadata +6 -2
data/test/helper.rb
CHANGED
@@ -133,6 +133,16 @@ def require_exec(cmd)
|
|
133
133
|
false
|
134
134
|
end
|
135
135
|
|
136
|
+
class DieIfUsed
|
137
|
+
def each
|
138
|
+
abort "body.each called after response hijack\n"
|
139
|
+
end
|
140
|
+
|
141
|
+
def close
|
142
|
+
abort "body.close called after response hijack\n"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
136
146
|
require 'yahns'
|
137
147
|
|
138
148
|
# needed for parallel (MT) tests)
|
@@ -39,7 +39,10 @@ class TestExtrasProxyPass < Testcase
|
|
39
39
|
host2, port2 = @srv2.addr[3], @srv2.addr[1]
|
40
40
|
pid = mkserver(cfg) do
|
41
41
|
$LOAD_PATH.unshift "#{Dir.pwd}/extras"
|
42
|
+
olderr = $stderr
|
43
|
+
$stderr = StringIO.new
|
42
44
|
require 'proxy_pass'
|
45
|
+
$stderr = olderr
|
43
46
|
@srv2.close
|
44
47
|
cfg.instance_eval do
|
45
48
|
app(:rack, ProxyPass.new("http://#{host2}:#{port2}/")) do
|
data/test/test_input.rb
CHANGED
@@ -11,13 +11,28 @@ class TestInput < Testcase
|
|
11
11
|
|
12
12
|
MD5 = lambda do |e|
|
13
13
|
input = e["rack.input"]
|
14
|
+
tmp = e["rack.tempfiles"]
|
15
|
+
case input
|
16
|
+
when StringIO, Yahns::StreamInput
|
17
|
+
abort "unexpected tempfiles" if tmp && tmp.include?(input)
|
18
|
+
when Yahns::TmpIO
|
19
|
+
abort "rack.tempfiles missing" unless tmp
|
20
|
+
abort "rack.tempfiles missing rack.input" unless tmp.include?(input)
|
21
|
+
else
|
22
|
+
abort "unrecognized input type: #{input.class}"
|
23
|
+
end
|
24
|
+
|
14
25
|
buf = ""
|
15
26
|
md5 = Digest::MD5.new
|
16
27
|
while input.read(16384, buf)
|
17
28
|
md5 << buf
|
18
29
|
end
|
19
30
|
body = md5.hexdigest
|
20
|
-
h = {
|
31
|
+
h = {
|
32
|
+
"Content-Length" => body.size.to_s,
|
33
|
+
"Content-Type" => 'text/plain',
|
34
|
+
"X-Input-Class" => input.class.to_s,
|
35
|
+
}
|
21
36
|
[ 200, h, [body] ]
|
22
37
|
end
|
23
38
|
|
@@ -63,6 +78,40 @@ class TestInput < Testcase
|
|
63
78
|
[ host, port, pid ]
|
64
79
|
end
|
65
80
|
|
81
|
+
def test_big_buffer_true
|
82
|
+
host, port, pid = input_server(MD5, true)
|
83
|
+
|
84
|
+
c = get_tcp_client(host, port)
|
85
|
+
buf = 'hello'
|
86
|
+
c.write "PUT / HTTP/1.0\r\nContent-Length: 5\r\n\r\n#{buf}"
|
87
|
+
head, body = c.read.split(/\r\n\r\n/)
|
88
|
+
assert_match %r{^X-Input-Class: StringIO\r\n}, head
|
89
|
+
assert_equal Digest::MD5.hexdigest(buf), body
|
90
|
+
c.close
|
91
|
+
|
92
|
+
c = get_tcp_client(host, port)
|
93
|
+
buf = 'hello' * 10000
|
94
|
+
c.write "PUT / HTTP/1.0\r\nContent-Length: 50000\r\n\r\n#{buf}"
|
95
|
+
head, body = c.read.split(/\r\n\r\n/)
|
96
|
+
|
97
|
+
# TODO: shouldn't need CapInput with known Content-Length...
|
98
|
+
assert_match %r{^X-Input-Class: Yahns::(CapInput|TmpIO)\r\n}, head
|
99
|
+
assert_equal Digest::MD5.hexdigest(buf), body
|
100
|
+
c.close
|
101
|
+
|
102
|
+
c = get_tcp_client(host, port)
|
103
|
+
c.write "PUT / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n"
|
104
|
+
c.write "Transfer-Encoding: chunked\r\n\r\n"
|
105
|
+
c.write "#{50000.to_s(16)}\r\n#{buf}\r\n0\r\n\r\n"
|
106
|
+
head, body = c.read.split(/\r\n\r\n/)
|
107
|
+
assert_match %r{^X-Input-Class: Yahns::CapInput\r\n}, head
|
108
|
+
assert_equal Digest::MD5.hexdigest(buf), body
|
109
|
+
c.close
|
110
|
+
|
111
|
+
ensure
|
112
|
+
quit_wait(pid)
|
113
|
+
end
|
114
|
+
|
66
115
|
def test_read_negative_lazy; _read_neg(:lazy); end
|
67
116
|
def test_read_negative_nobuffer; _read_neg(false); end
|
68
117
|
|
@@ -0,0 +1,611 @@
|
|
1
|
+
# Copyright (C) 2015 all contributors <yahns-public@yhbt.net>
|
2
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
3
|
+
require_relative 'server_helper'
|
4
|
+
require 'json'
|
5
|
+
require 'digest'
|
6
|
+
|
7
|
+
class TestProxyPass < Testcase
|
8
|
+
ENV["N"].to_i > 1 and parallelize_me!
|
9
|
+
include ServerHelper
|
10
|
+
OMFG = 'a' * (1024 * 1024 * 32)
|
11
|
+
TRUNCATE_BODY = "HTTP/1.1 200 OK\r\n" \
|
12
|
+
"Content-Length: 7\r\n" \
|
13
|
+
"Content-Type: text/PAIN\r\n\r\nshort".freeze
|
14
|
+
TRUNCATE_HEAD = "HTTP/1.1 200 OK\r\n" \
|
15
|
+
"Content-Length: 666\r\n".freeze
|
16
|
+
|
17
|
+
# not too big, or kcar will reject
|
18
|
+
BIG_HEADER = [%w(Content-Type text/plain), %W(Content-Length #{OMFG.size})]
|
19
|
+
3000.times { |i| BIG_HEADER << %W(X-#{i} BIG-HEADER!!!!!!!!!!!!!!) }
|
20
|
+
BIG_HEADER.freeze
|
21
|
+
STR4 = 'abcd' * (256 * 1024)
|
22
|
+
NCHUNK = 50
|
23
|
+
|
24
|
+
class ProxiedApp
|
25
|
+
def call(env)
|
26
|
+
h = [ %w(Content-Length 3), %w(Content-Type text/plain) ]
|
27
|
+
case env['REQUEST_METHOD']
|
28
|
+
when 'GET'
|
29
|
+
case env['PATH_INFO']
|
30
|
+
when '/giant-body'
|
31
|
+
h = [ %W(Content-Length #{OMFG.size}), %w(Content-Type text/plain) ]
|
32
|
+
[ 200, h, [ OMFG ] ]
|
33
|
+
when '/giant-chunky-body'
|
34
|
+
h = [ %w(content-type text/pain), %w(transfer-encoding chunked) ]
|
35
|
+
chunky = Object.new
|
36
|
+
def chunky.each
|
37
|
+
head = STR4.size.to_s(16) << "\r\n"
|
38
|
+
NCHUNK.times do
|
39
|
+
yield head
|
40
|
+
yield STR4
|
41
|
+
yield "\r\n".freeze
|
42
|
+
end
|
43
|
+
yield "0\r\n\r\n"
|
44
|
+
end
|
45
|
+
[ 200, h, chunky ]
|
46
|
+
when '/big-headers'
|
47
|
+
[ 200, BIG_HEADER, [ OMFG ] ]
|
48
|
+
when '/oversize-headers'
|
49
|
+
100000.times { |x| h << %W(X-TOOBIG-#{x} #{x}) }
|
50
|
+
[ 200, h, [ "big" ] ]
|
51
|
+
when %r{\A/slow-headers-(\d+(?:\.\d+)?)\z}
|
52
|
+
delay = $1.to_f
|
53
|
+
io = env['rack.hijack'].call
|
54
|
+
[ "HTTP/1.1 200 OK\r\n",
|
55
|
+
"Content-Length: 7\r\n",
|
56
|
+
"Content-Type: text/PAIN\r\n",
|
57
|
+
"connection: close\r\n\r\n",
|
58
|
+
"HIHIHI!"
|
59
|
+
].each do |l|
|
60
|
+
io.write(l)
|
61
|
+
sleep delay
|
62
|
+
end
|
63
|
+
io.close
|
64
|
+
when '/truncate-body'
|
65
|
+
io = env['rack.hijack'].call
|
66
|
+
io.write(TRUNCATE_BODY)
|
67
|
+
io.close
|
68
|
+
when '/eof-body-fast'
|
69
|
+
io = env['rack.hijack'].call
|
70
|
+
io.write("HTTP/1.0 200 OK\r\n\r\neof-body-fast")
|
71
|
+
io.close
|
72
|
+
when '/eof-body-slow'
|
73
|
+
io = env['rack.hijack'].call
|
74
|
+
io.write("HTTP/1.0 200 OK\r\n\r\n")
|
75
|
+
sleep 0.1
|
76
|
+
io.write("eof-body-slow")
|
77
|
+
io.close
|
78
|
+
when '/truncate-head'
|
79
|
+
io = env['rack.hijack'].call
|
80
|
+
io.write(TRUNCATE_HEAD)
|
81
|
+
io.close
|
82
|
+
when '/response-trailer'
|
83
|
+
h = [
|
84
|
+
%w(Content-Type text/pain),
|
85
|
+
%w(Transfer-Encoding chunked),
|
86
|
+
%w(Trailer Foo)
|
87
|
+
]
|
88
|
+
b = [ "3\r\n", "hi\n", "\r\n", "0\r\n", "Foo: bar", "\r\n", "\r\n" ]
|
89
|
+
case env['HTTP_X_TRAILER']
|
90
|
+
when 'fast'
|
91
|
+
b = [ b.join ]
|
92
|
+
when 'allslow'
|
93
|
+
def b.each
|
94
|
+
size.times do |i|
|
95
|
+
sleep 0.1
|
96
|
+
yield self[i]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
when /\Atlrslow(\d+)/
|
100
|
+
b.instance_variable_set(:@yahns_sleep_thresh, $1.to_i)
|
101
|
+
def b.each
|
102
|
+
size.times do |i|
|
103
|
+
sleep(0.1) if i > @yahns_sleep_thresh
|
104
|
+
yield self[i]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
[ 200, h, b ]
|
109
|
+
when '/immediate-EOF'
|
110
|
+
env['rack.hijack'].call.close
|
111
|
+
when %r{\A/chunky-slow-(\d+(?:\.\d+)?)\z}
|
112
|
+
delay = $1.to_f
|
113
|
+
chunky = Object.new
|
114
|
+
chunky.instance_variable_set(:@delay, delay)
|
115
|
+
if env['HTTP_VERSION'] == 'HTTP/1.0'
|
116
|
+
h = [ %w(Content-Type text/pain), %w(Content-Length 3) ]
|
117
|
+
def chunky.each
|
118
|
+
%w(H I !).each do |x|
|
119
|
+
sleep @delay
|
120
|
+
yield x
|
121
|
+
end
|
122
|
+
end
|
123
|
+
else
|
124
|
+
h = [ %w(Content-Type text/pain), %w(Transfer-Encoding chunked) ]
|
125
|
+
def chunky.each
|
126
|
+
sleep @delay
|
127
|
+
yield "3\r\nHI!\r\n"
|
128
|
+
sleep @delay
|
129
|
+
yield "0\r\n\r\n"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
[ 200, h, chunky ]
|
133
|
+
else
|
134
|
+
[ 200, h, [ "hi\n"] ]
|
135
|
+
end
|
136
|
+
when 'HEAD'
|
137
|
+
case env['PATH_INFO']
|
138
|
+
when '/big-headers'
|
139
|
+
[ 200, BIG_HEADER, [] ]
|
140
|
+
else
|
141
|
+
[ 200, h, [] ]
|
142
|
+
end
|
143
|
+
when 'PUT'
|
144
|
+
case env['PATH_INFO']
|
145
|
+
when '/forbidden-put'
|
146
|
+
# ignore rack.input
|
147
|
+
[ 403, [ %w(Content-Type text/html), %w(Content-Length 0) ], [] ]
|
148
|
+
when '/forbidden-put-abort'
|
149
|
+
env['rack.hijack'].call.close
|
150
|
+
# should not be seen:
|
151
|
+
[ 123, [ %w(Content-Type text/html), %w(Content-Length 0) ], [] ]
|
152
|
+
else
|
153
|
+
buf = env['rack.input'].read
|
154
|
+
[ 201, {
|
155
|
+
'Content-Length' => buf.bytesize.to_s,
|
156
|
+
'Content-Type' => 'text/plain',
|
157
|
+
}, [ buf ] ]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def setup
|
164
|
+
@srv2 = TCPServer.new(ENV["TEST_HOST"] || "127.0.0.1", 0)
|
165
|
+
server_helper_setup
|
166
|
+
end
|
167
|
+
|
168
|
+
def teardown
|
169
|
+
@srv2.close if defined?(@srv2) && !@srv2.closed?
|
170
|
+
server_helper_teardown
|
171
|
+
end
|
172
|
+
|
173
|
+
def test_unix_socket_no_path
|
174
|
+
tmpdir = Dir.mktmpdir
|
175
|
+
unix_path = "#{tmpdir}/proxy_pass.sock"
|
176
|
+
unix_srv = UNIXServer.new(unix_path)
|
177
|
+
err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
|
178
|
+
host2, port2 = @srv2.addr[3], @srv2.addr[1]
|
179
|
+
pid = mkserver(cfg) do
|
180
|
+
@srv.autoclose = @srv2.autoclose = false
|
181
|
+
ENV["YAHNS_FD"] = "#{@srv.fileno},#{@srv2.fileno}"
|
182
|
+
require 'yahns/proxy_pass'
|
183
|
+
cfg.instance_eval do
|
184
|
+
app(:rack, Yahns::ProxyPass.new("unix:#{unix_path}:/$fullpath")) do
|
185
|
+
listen "#{host}:#{port}"
|
186
|
+
end
|
187
|
+
app(:rack, Yahns::ProxyPass.new("unix:#{unix_path}:/foo$fullpath")) do
|
188
|
+
listen "#{host2}:#{port2}"
|
189
|
+
end
|
190
|
+
stderr_path err.path
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
pid2 = mkserver(cfg, unix_srv) do
|
195
|
+
@srv.close
|
196
|
+
@srv2.close
|
197
|
+
cfg.instance_eval do
|
198
|
+
rapp = lambda do |env|
|
199
|
+
body = env.to_json
|
200
|
+
hdr = {
|
201
|
+
'Content-Length' => body.bytesize.to_s,
|
202
|
+
'Content-Type' => 'application/json',
|
203
|
+
}
|
204
|
+
[ 200, hdr, [ body ] ]
|
205
|
+
end
|
206
|
+
app(:rack, rapp) { listen unix_path }
|
207
|
+
stderr_path err.path
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
Net::HTTP.start(host, port) do |http|
|
212
|
+
res = http.request(Net::HTTP::Get.new('/f00'))
|
213
|
+
assert_equal 200, res.code.to_i
|
214
|
+
body = JSON.parse(res.body)
|
215
|
+
assert_equal '/f00', body['PATH_INFO']
|
216
|
+
|
217
|
+
res = http.request(Net::HTTP::Get.new('/f00foo'))
|
218
|
+
assert_equal 200, res.code.to_i
|
219
|
+
body = JSON.parse(res.body)
|
220
|
+
assert_equal '/f00foo', body['PATH_INFO']
|
221
|
+
end
|
222
|
+
|
223
|
+
Net::HTTP.start(host2, port2) do |http|
|
224
|
+
res = http.request(Net::HTTP::Get.new('/Foo'))
|
225
|
+
assert_equal 200, res.code.to_i
|
226
|
+
body = JSON.parse(res.body)
|
227
|
+
assert_equal '/foo/Foo', body['PATH_INFO']
|
228
|
+
|
229
|
+
res = http.request(Net::HTTP::Get.new('/Foofoo'))
|
230
|
+
assert_equal 200, res.code.to_i
|
231
|
+
body = JSON.parse(res.body)
|
232
|
+
assert_equal '/foo/Foofoo', body['PATH_INFO']
|
233
|
+
end
|
234
|
+
ensure
|
235
|
+
quit_wait(pid)
|
236
|
+
quit_wait(pid2)
|
237
|
+
unix_srv.close if unix_srv
|
238
|
+
FileUtils.rm_rf(tmpdir) if tmpdir
|
239
|
+
end
|
240
|
+
|
241
|
+
def test_proxy_pass
|
242
|
+
err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
|
243
|
+
host2, port2 = @srv2.addr[3], @srv2.addr[1]
|
244
|
+
pid = mkserver(cfg) do
|
245
|
+
require 'yahns/proxy_pass'
|
246
|
+
@srv2.close
|
247
|
+
cfg.instance_eval do
|
248
|
+
app(:rack, Yahns::ProxyPass.new("http://#{host2}:#{port2}")) do
|
249
|
+
listen "#{host}:#{port}"
|
250
|
+
client_max_body_size nil
|
251
|
+
end
|
252
|
+
stderr_path err.path
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
pid2 = mkserver(cfg, @srv2) do
|
257
|
+
@srv.close
|
258
|
+
cfg.instance_eval do
|
259
|
+
app(:rack, ProxiedApp.new) do
|
260
|
+
listen "#{host2}:#{port2}"
|
261
|
+
client_max_body_size nil
|
262
|
+
input_buffering :lazy
|
263
|
+
end
|
264
|
+
stderr_path err.path
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
check_forbidden_put(host, port)
|
269
|
+
check_eof_body(host, port)
|
270
|
+
check_pipelining(host, port)
|
271
|
+
check_response_trailer(host, port)
|
272
|
+
|
273
|
+
gplv3 = File.open('COPYING')
|
274
|
+
|
275
|
+
Net::HTTP.start(host, port) do |http|
|
276
|
+
res = http.request(Net::HTTP::Get.new('/'))
|
277
|
+
assert_equal 200, res.code.to_i
|
278
|
+
n = res.body.bytesize
|
279
|
+
assert_operator n, :>, 1
|
280
|
+
res = http.request(Net::HTTP::Head.new('/'))
|
281
|
+
assert_equal 200, res.code.to_i
|
282
|
+
assert_equal n, res['Content-Length'].to_i
|
283
|
+
assert_nil res.body
|
284
|
+
|
285
|
+
# chunked encoding (PUT)
|
286
|
+
req = Net::HTTP::Put.new('/')
|
287
|
+
req.body_stream = gplv3
|
288
|
+
req.content_type = 'application/octet-stream'
|
289
|
+
req['Transfer-Encoding'] = 'chunked'
|
290
|
+
res = http.request(req)
|
291
|
+
gplv3.rewind
|
292
|
+
assert_equal gplv3.read, res.body
|
293
|
+
assert_equal 201, res.code.to_i
|
294
|
+
|
295
|
+
# chunked encoding (GET)
|
296
|
+
res = http.request(Net::HTTP::Get.new('/chunky-slow-0.1'))
|
297
|
+
assert_equal 200, res.code.to_i
|
298
|
+
assert_equal 'chunked', res['Transfer-encoding']
|
299
|
+
assert_equal "HI!", res.body
|
300
|
+
|
301
|
+
# slow headers (GET)
|
302
|
+
res = http.request(Net::HTTP::Get.new('/slow-headers-0.01'))
|
303
|
+
assert_equal 200, res.code.to_i
|
304
|
+
assert_equal 'text/PAIN', res['Content-Type']
|
305
|
+
assert_equal 'HIHIHI!', res.body
|
306
|
+
|
307
|
+
# normal content-length (PUT)
|
308
|
+
gplv3.rewind
|
309
|
+
req = Net::HTTP::Put.new('/')
|
310
|
+
req.body_stream = gplv3
|
311
|
+
req.content_type = 'application/octet-stream'
|
312
|
+
req.content_length = gplv3.size
|
313
|
+
res = http.request(req)
|
314
|
+
gplv3.rewind
|
315
|
+
assert_equal gplv3.read, res.body
|
316
|
+
assert_equal 201, res.code.to_i
|
317
|
+
|
318
|
+
# giant body
|
319
|
+
res = http.request(Net::HTTP::Get.new('/giant-body'))
|
320
|
+
assert_equal 200, res.code.to_i
|
321
|
+
assert_equal OMFG, res.body
|
322
|
+
|
323
|
+
# giant chunky body
|
324
|
+
sha1 = Digest::SHA1.new
|
325
|
+
http.request(Net::HTTP::Get.new('/giant-chunky-body')) do |response|
|
326
|
+
response.read_body do |chunk|
|
327
|
+
sha1.update(chunk)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
check = Digest::SHA1.new
|
331
|
+
NCHUNK.times { check.update(STR4) }
|
332
|
+
assert_equal check.hexdigest, sha1.hexdigest
|
333
|
+
|
334
|
+
# giant PUT content-length
|
335
|
+
req = Net::HTTP::Put.new('/')
|
336
|
+
req.body_stream = StringIO.new(OMFG)
|
337
|
+
req.content_type = 'application/octet-stream'
|
338
|
+
req.content_length = OMFG.size
|
339
|
+
res = http.request(req)
|
340
|
+
assert_equal OMFG, res.body
|
341
|
+
assert_equal 201, res.code.to_i
|
342
|
+
|
343
|
+
# giant PUT chunked encoding
|
344
|
+
req = Net::HTTP::Put.new('/')
|
345
|
+
req.body_stream = StringIO.new(OMFG)
|
346
|
+
req.content_type = 'application/octet-stream'
|
347
|
+
req['Transfer-Encoding'] = 'chunked'
|
348
|
+
res = http.request(req)
|
349
|
+
assert_equal OMFG, res.body
|
350
|
+
assert_equal 201, res.code.to_i
|
351
|
+
|
352
|
+
# sometimes upstream feeds kcar too much
|
353
|
+
req = Net::HTTP::Get.new('/oversize-headers')
|
354
|
+
res = http.request(req)
|
355
|
+
errs = File.readlines(@err.path).grep(/ERROR/)
|
356
|
+
assert_equal 1, errs.size
|
357
|
+
assert_match %r{upstream response error:}, errs[0]
|
358
|
+
@err.truncate(0)
|
359
|
+
end
|
360
|
+
|
361
|
+
# ensure we do not chunk responses back to an HTTP/1.0 client even if
|
362
|
+
# the proxy <-> upstream connection is chunky
|
363
|
+
%w(0 0.1).each do |delay|
|
364
|
+
begin
|
365
|
+
h10 = TCPSocket.new(host, port)
|
366
|
+
h10.write "GET /chunky-slow-#{delay} HTTP/1.0\r\n\r\n"
|
367
|
+
res = Timeout.timeout(60) { h10.read }
|
368
|
+
assert_match %r{^Connection: close\r\n}, res
|
369
|
+
assert_match %r{^Content-Type: text/pain\r\n}, res
|
370
|
+
assert_match %r{\r\n\r\nHI!\z}, res
|
371
|
+
refute_match %r{^Transfer-Encoding:}, res
|
372
|
+
refute_match %r{\r0\r\n}, res
|
373
|
+
ensure
|
374
|
+
h10.close
|
375
|
+
end
|
376
|
+
end
|
377
|
+
check_truncated_upstream(host, port)
|
378
|
+
check_slow_giant_body(host, port)
|
379
|
+
check_slow_read_headers(host, port)
|
380
|
+
ensure
|
381
|
+
gplv3.close if gplv3
|
382
|
+
quit_wait pid
|
383
|
+
quit_wait pid2
|
384
|
+
end
|
385
|
+
|
386
|
+
def check_pipelining(host, port)
|
387
|
+
pl = TCPSocket.new(host, port)
|
388
|
+
r1 = ''
|
389
|
+
r2 = ''
|
390
|
+
r3 = ''
|
391
|
+
Timeout.timeout(60) do
|
392
|
+
pl.write "GET / HTTP/1.1\r\nHost: example.com\r\n\r\nGET /"
|
393
|
+
until r1 =~ /hi\n/
|
394
|
+
r1 << pl.readpartial(666)
|
395
|
+
end
|
396
|
+
|
397
|
+
pl.write "chunky-slow-0.1 HTTP/1.1\r\nHost: example.com\r\n\r\nP"
|
398
|
+
until r2 =~ /\r\n3\r\nHI!\r\n0\r\n\r\n/
|
399
|
+
r2 << pl.readpartial(666)
|
400
|
+
end
|
401
|
+
|
402
|
+
pl.write "UT / HTTP/1.1\r\nHost: example.com\r\n"
|
403
|
+
pl.write "Transfer-Encoding: chunked\r\n\r\n"
|
404
|
+
pl.write "6\r\nchunky\r\n"
|
405
|
+
pl.write "0\r\n\r\n"
|
406
|
+
|
407
|
+
until r3 =~ /chunky/
|
408
|
+
r3 << pl.readpartial(666)
|
409
|
+
end
|
410
|
+
|
411
|
+
# ensure stuff still works after a chunked upload:
|
412
|
+
pl.write "GET / HTTP/1.1\r\nHost: example.com\r\n\r\nP"
|
413
|
+
after_up = ''
|
414
|
+
until after_up =~ /hi\n/
|
415
|
+
after_up << pl.readpartial(666)
|
416
|
+
end
|
417
|
+
re = /^Date:[^\r\n]+/
|
418
|
+
assert_equal after_up.sub(re, ''), r1.sub(re, '')
|
419
|
+
|
420
|
+
# another upload, this time without chunking
|
421
|
+
pl.write "UT / HTTP/1.1\r\nHost: example.com\r\n"
|
422
|
+
pl.write "Content-Length: 8\r\n\r\n"
|
423
|
+
pl.write "identity"
|
424
|
+
identity = ''
|
425
|
+
|
426
|
+
until identity =~ /identity/
|
427
|
+
identity << pl.readpartial(666)
|
428
|
+
end
|
429
|
+
assert_match %r{identity\z}, identity
|
430
|
+
assert_match %r{\AHTTP/1\.1 201\b}, identity
|
431
|
+
|
432
|
+
# ensure stuff still works after an identity upload:
|
433
|
+
pl.write "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
|
434
|
+
after_up = ''
|
435
|
+
until after_up =~ /hi\n/
|
436
|
+
after_up << pl.readpartial(666)
|
437
|
+
end
|
438
|
+
re = /^Date:[^\r\n]+/
|
439
|
+
assert_equal after_up.sub(re, ''), r1.sub(re, '')
|
440
|
+
|
441
|
+
pl.write "GET / HTTP/1.1\r\nHost: example.com"
|
442
|
+
sleep 0.1 # hope epoll wakes up and reads in this time
|
443
|
+
pl.write "\r\n\r\nGET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
|
444
|
+
burst = pl.readpartial(666)
|
445
|
+
until burst.scan(/^hi$/).size == 2
|
446
|
+
burst << pl.readpartial(666)
|
447
|
+
end
|
448
|
+
assert_equal 2, burst.scan(/^hi$/).size
|
449
|
+
assert_match %r{\r\n\r\nhi\n\z}, burst
|
450
|
+
end
|
451
|
+
r1 = r1.split("\r\n").reject { |x| x =~ /^Date: / }
|
452
|
+
r2 = r2.split("\r\n").reject { |x| x =~ /^Date: / }
|
453
|
+
assert_equal 'HTTP/1.1 200 OK', r1[0]
|
454
|
+
assert_equal 'HTTP/1.1 200 OK', r2[0]
|
455
|
+
assert_match %r{\r\n\r\nchunky\z}, r3
|
456
|
+
assert_match %r{\AHTTP/1\.1 201 Created\r\n}, r3
|
457
|
+
rescue => e
|
458
|
+
warn [ e.class, e.message ].inspect
|
459
|
+
warn e.backtrace.join("\n")
|
460
|
+
ensure
|
461
|
+
pl.close
|
462
|
+
end
|
463
|
+
|
464
|
+
def check_truncated_upstream(host, port)
|
465
|
+
# we want to make sure we show the truncated response without extra headers
|
466
|
+
s = TCPSocket.new(host, port)
|
467
|
+
check_err
|
468
|
+
res = Timeout.timeout(60) do
|
469
|
+
s.write "GET /truncate-body HTTP/1.1\r\nHost: example.com\r\n\r\n"
|
470
|
+
s.read
|
471
|
+
end
|
472
|
+
s.close
|
473
|
+
|
474
|
+
exp = "HTTP/1.1 200 OK\r\n" \
|
475
|
+
"Content-Length: 7\r\n" \
|
476
|
+
"Content-Type: text/PAIN\r\n" \
|
477
|
+
"Connection: keep-alive\r\n" \
|
478
|
+
"\r\nshort"
|
479
|
+
assert_equal exp, res
|
480
|
+
errs = File.readlines(@err.path).grep(/\bERROR\b/)
|
481
|
+
assert_equal 1, errs.size
|
482
|
+
assert_match(/premature upstream EOF/, errs[0])
|
483
|
+
@err.truncate(0)
|
484
|
+
|
485
|
+
# truncated headers or no response at all...
|
486
|
+
# Send a 502 error
|
487
|
+
%w(immediate-EOF truncate-head).each do |path|
|
488
|
+
s = TCPSocket.new(host, port)
|
489
|
+
check_err
|
490
|
+
res = Timeout.timeout(60) do
|
491
|
+
s.write "GET /#{path} HTTP/1.1\r\nHost: example.com\r\n\r\n"
|
492
|
+
s.read(1024)
|
493
|
+
end
|
494
|
+
assert_match %r{\AHTTP/1.1 502\s+}, res
|
495
|
+
s.close
|
496
|
+
errs = File.readlines(@err.path).grep(/\bERROR\b/)
|
497
|
+
assert_equal 1, errs.size
|
498
|
+
assert_match(/premature upstream EOF/, errs[0])
|
499
|
+
@err.truncate(0)
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
def check_slow_giant_body(host, port)
|
504
|
+
s = TCPSocket.new(host, port)
|
505
|
+
s.write "GET /giant-body HTTP/1.0\r\n\r\n"
|
506
|
+
sleep 0.1
|
507
|
+
str = ''
|
508
|
+
buf = ''
|
509
|
+
assert_raises(EOFError) { loop { str << s.readpartial(400, buf) } }
|
510
|
+
h, b = str.split(/\r\n\r\n/, 2)
|
511
|
+
assert_equal OMFG, b
|
512
|
+
assert_match %r{\AHTTP/1\.1 200\b}, h
|
513
|
+
ensure
|
514
|
+
s.close if s
|
515
|
+
end
|
516
|
+
|
517
|
+
def check_slow_read_headers(host, port)
|
518
|
+
s = TCPSocket.new(host, port)
|
519
|
+
s.write "GET /big-headers HTTP/1.1\r\nHost: example.com\r\n\r\n"
|
520
|
+
s.write "HEAD /big-headers HTTP/1.0\r\n\r\n"
|
521
|
+
buf = ''
|
522
|
+
res = ''
|
523
|
+
sleep 0.1
|
524
|
+
begin
|
525
|
+
res << s.readpartial(32786, buf)
|
526
|
+
rescue EOFError
|
527
|
+
break
|
528
|
+
end while true
|
529
|
+
# res = Timeout.timeout(60) { s.read }
|
530
|
+
assert_match %r{\r\n\r\n\z}, res
|
531
|
+
assert_match %r{\AHTTP/1\.1 200 OK}, res
|
532
|
+
ensure
|
533
|
+
s.close if s
|
534
|
+
end
|
535
|
+
|
536
|
+
def check_response_trailer(host, port)
|
537
|
+
thrs = [
|
538
|
+
"X-Trailer: fast\r\n",
|
539
|
+
"X-Trailer: allslow\r\n",
|
540
|
+
"X-Trailer: tlrslow1\r\n",
|
541
|
+
"X-Trailer: tlrslow2\r\n",
|
542
|
+
"X-Trailer: tlrslow3\r\n",
|
543
|
+
"X-Trailer: tlrslow4\r\n",
|
544
|
+
''
|
545
|
+
].map do |x|
|
546
|
+
Thread.new do
|
547
|
+
s = TCPSocket.new(host, port)
|
548
|
+
s.write "GET /response-trailer HTTP/1.1\r\n#{x}" \
|
549
|
+
"Host: example.com\r\n\r\n"
|
550
|
+
res = ''
|
551
|
+
buf = ''
|
552
|
+
Timeout.timeout(60) do
|
553
|
+
until res =~ /Foo: bar\r\n\r\n\z/
|
554
|
+
res << s.readpartial(16384, buf)
|
555
|
+
end
|
556
|
+
end
|
557
|
+
assert_match(%r{\r\n0\r\nFoo: bar\r\n\r\n\z}, res)
|
558
|
+
assert_match(%r{^Trailer: Foo\r\n}, res)
|
559
|
+
assert_match(%r{^Transfer-Encoding: chunked\r\n}, res)
|
560
|
+
assert_match(%r{\AHTTP/1\.1 200 OK\r\n}, res)
|
561
|
+
s.close
|
562
|
+
:OK
|
563
|
+
end
|
564
|
+
end
|
565
|
+
thrs.each { |t| assert_equal(:OK, t.value) }
|
566
|
+
end
|
567
|
+
|
568
|
+
def check_eof_body(host, port)
|
569
|
+
Timeout.timeout(60) do
|
570
|
+
s = TCPSocket.new(host, port)
|
571
|
+
s.write("GET /eof-body-fast HTTP/1.0\r\n\r\n")
|
572
|
+
res = s.read
|
573
|
+
assert_match %r{\AHTTP/1\.1 200 OK\r\n}, res
|
574
|
+
assert_match %r{\r\n\r\neof-body-fast\z}, res
|
575
|
+
s.close
|
576
|
+
|
577
|
+
s = TCPSocket.new(host, port)
|
578
|
+
s.write("GET /eof-body-slow HTTP/1.0\r\n\r\n")
|
579
|
+
res = s.read
|
580
|
+
assert_match %r{\AHTTP/1\.1 200 OK\r\n}, res
|
581
|
+
assert_match %r{\r\n\r\neof-body-slow\z}, res
|
582
|
+
s.close
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
def check_forbidden_put(host, port)
|
587
|
+
to_close = []
|
588
|
+
Timeout.timeout(60) do
|
589
|
+
s = TCPSocket.new(host, port)
|
590
|
+
to_close << s
|
591
|
+
s.write("PUT /forbidden-put HTTP/1.1\r\nHost: example.com\r\n" \
|
592
|
+
"Content-Length: #{OMFG.size}\r\n\r\n")
|
593
|
+
assert_equal OMFG.size, s.write(OMFG),
|
594
|
+
"proxy fully buffers, upstream does not"
|
595
|
+
assert_match %r{\AHTTP/1\.1 403 }, s.readpartial(1024)
|
596
|
+
assert_raises(EOFError) { s.readpartial(1) }
|
597
|
+
|
598
|
+
s = TCPSocket.new(host, port)
|
599
|
+
to_close << s
|
600
|
+
s.write("PUT /forbidden-put-abort HTTP/1.1\r\nHost: example.com\r\n" \
|
601
|
+
"Content-Length: #{OMFG.size}\r\n\r\n")
|
602
|
+
assert_equal OMFG.size, s.write(OMFG),
|
603
|
+
"proxy fully buffers, upstream does not"
|
604
|
+
assert_match %r{\AHTTP/1\.1 502 Bad Gateway}, s.readpartial(1024)
|
605
|
+
assert_raises(EOFError) { s.readpartial(1) }
|
606
|
+
@err.truncate(0)
|
607
|
+
end
|
608
|
+
ensure
|
609
|
+
to_close.each(&:close)
|
610
|
+
end
|
611
|
+
end
|