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.
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 = { "Content-Length" => body.size.to_s, "Content-Type" => 'text/plain' }
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