unicorn 5.5.2 → 5.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.
@@ -45,8 +45,9 @@ def call(env)
45
45
 
46
46
  COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP)
47
47
 
48
+ HEAVY_WORKERS = 2
48
49
  HEAVY_CFG = <<-EOS
49
- worker_processes 4
50
+ worker_processes #{HEAVY_WORKERS}
50
51
  timeout 30
51
52
  logger Logger.new('#{COMMON_TMP.path}')
52
53
  before_fork do |server, worker|
@@ -606,6 +607,7 @@ def test_unicorn_config_listen_augments_cli
606
607
  def test_weird_config_settings
607
608
  File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
608
609
  ucfg = Tempfile.new('unicorn_test_config')
610
+ proc_total = HEAVY_WORKERS + 1 # + 1 for master
609
611
  ucfg.syswrite(HEAVY_CFG)
610
612
  pid = xfork do
611
613
  redirect_test_io do
@@ -616,9 +618,9 @@ def test_weird_config_settings
616
618
  results = retry_hit(["http://#{@addr}:#{@port}/"])
617
619
  assert_equal String, results[0].class
618
620
  wait_master_ready(COMMON_TMP.path)
619
- wait_workers_ready(COMMON_TMP.path, 4)
621
+ wait_workers_ready(COMMON_TMP.path, HEAVY_WORKERS)
620
622
  bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
621
- assert_equal 4, bf.size
623
+ assert_equal HEAVY_WORKERS, bf.size
622
624
  rotate = Tempfile.new('unicorn_rotate')
623
625
 
624
626
  File.rename(COMMON_TMP.path, rotate.path)
@@ -630,20 +632,20 @@ def test_weird_config_settings
630
632
  tries = DEFAULT_TRIES
631
633
  log = File.readlines(rotate.path)
632
634
  while (tries -= 1) > 0 &&
633
- log.grep(/reopening logs\.\.\./).size < 5
635
+ log.grep(/reopening logs\.\.\./).size < proc_total
634
636
  sleep DEFAULT_RES
635
637
  log = File.readlines(rotate.path)
636
638
  end
637
- assert_equal 5, log.grep(/reopening logs\.\.\./).size
639
+ assert_equal proc_total, log.grep(/reopening logs\.\.\./).size
638
640
  assert_equal 0, log.grep(/done reopening logs/).size
639
641
 
640
642
  tries = DEFAULT_TRIES
641
643
  log = File.readlines(COMMON_TMP.path)
642
- while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5
644
+ while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < proc_total
643
645
  sleep DEFAULT_RES
644
646
  log = File.readlines(COMMON_TMP.path)
645
647
  end
646
- assert_equal 5, log.grep(/done reopening logs/).size
648
+ assert_equal proc_total, log.grep(/done reopening logs/).size
647
649
  assert_equal 0, log.grep(/reopening logs\.\.\./).size
648
650
 
649
651
  Process.kill(:QUIT, pid)
@@ -34,16 +34,33 @@
34
34
  Debugger.start
35
35
  end
36
36
 
37
+ unless RUBY_VERSION < '3.1'
38
+ warn "Unicorn was only tested against MRI up to 3.0.\n" \
39
+ "It might not properly work with #{RUBY_VERSION}"
40
+ end
41
+
37
42
  def redirect_test_io
38
43
  orig_err = STDERR.dup
39
44
  orig_out = STDOUT.dup
40
- STDERR.reopen("test_stderr.#{$$}.log", "a")
41
- STDOUT.reopen("test_stdout.#{$$}.log", "a")
45
+ new_out = File.open("test_stdout.#$$.log", "a")
46
+ new_err = File.open("test_stderr.#$$.log", "a")
47
+ new_out.sync = new_err.sync = true
48
+
49
+ if tail = ENV['TAIL'] # "tail -F" if GNU, "tail -f" otherwise
50
+ require 'shellwords'
51
+ cmd = tail.shellsplit
52
+ cmd << new_out.path
53
+ cmd << new_err.path
54
+ pid = Process.spawn(*cmd, { 1 => 2, :pgroup => true })
55
+ sleep 0.1 # wait for tail(1) to startup
56
+ end
57
+ STDERR.reopen(new_err)
58
+ STDOUT.reopen(new_out)
42
59
  STDERR.sync = STDOUT.sync = true
43
60
 
44
61
  at_exit do
45
- File.unlink("test_stderr.#{$$}.log") rescue nil
46
- File.unlink("test_stdout.#{$$}.log") rescue nil
62
+ File.unlink(new_out.path) rescue nil
63
+ File.unlink(new_err.path) rescue nil
47
64
  end
48
65
 
49
66
  begin
@@ -51,6 +68,7 @@ def redirect_test_io
51
68
  ensure
52
69
  STDERR.reopen(orig_err)
53
70
  STDOUT.reopen(orig_out)
71
+ Process.kill(:TERM, pid) if pid
54
72
  end
55
73
  end
56
74
 
@@ -265,32 +283,6 @@ def wait_for_death(pid)
265
283
  raise "PID:#{pid} never died!"
266
284
  end
267
285
 
268
- # executes +cmd+ and chunks its STDOUT
269
- def chunked_spawn(stdout, *cmd)
270
- fork {
271
- crd, cwr = IO.pipe
272
- crd.binmode
273
- cwr.binmode
274
- crd.sync = cwr.sync = true
275
-
276
- pid = fork {
277
- STDOUT.reopen(cwr)
278
- crd.close
279
- cwr.close
280
- exec(*cmd)
281
- }
282
- cwr.close
283
- begin
284
- buf = crd.readpartial(16384)
285
- stdout.write("#{'%x' % buf.size}\r\n#{buf}")
286
- rescue EOFError
287
- stdout.write("0\r\n")
288
- pid, status = Process.waitpid(pid)
289
- exit status.exitstatus
290
- end while true
291
- }
292
- end
293
-
294
286
  def reset_sig_handlers
295
287
  %w(WINCH QUIT INT TERM USR1 USR2 HUP TTIN TTOU CHLD).each do |sig|
296
288
  trap(sig, "DEFAULT")
@@ -11,6 +11,20 @@ def setup
11
11
  @parser = HttpParser.new
12
12
  end
13
13
 
14
+ # RFC 7230 allows gzip/deflate/compress Transfer-Encoding,
15
+ # but "chunked" must be last if used
16
+ def test_is_chunked
17
+ [ 'chunked,chunked', 'chunked,gzip', 'chunked,gzip,chunked' ].each do |x|
18
+ assert_raise(HttpParserError) { HttpParser.is_chunked?(x) }
19
+ end
20
+ [ 'gzip, chunked', 'gzip,chunked', 'gzip ,chunked' ].each do |x|
21
+ assert HttpParser.is_chunked?(x)
22
+ end
23
+ [ 'gzip', 'xhunked', 'xchunked' ].each do |x|
24
+ assert !HttpParser.is_chunked?(x)
25
+ end
26
+ end
27
+
14
28
  def test_parser_max_len
15
29
  assert_raises(RangeError) do
16
30
  HttpParser.max_header_len = 0xffffffff + 1
@@ -566,6 +580,73 @@ def test_invalid_content_length
566
580
  end
567
581
  end
568
582
 
583
+ def test_duplicate_content_length
584
+ str = "PUT / HTTP/1.1\r\n" \
585
+ "Content-Length: 1\r\n" \
586
+ "Content-Length: 9\r\n" \
587
+ "\r\n"
588
+ assert_raises(HttpParserError) { @parser.headers({}, str) }
589
+ end
590
+
591
+ def test_chunked_overrides_content_length
592
+ order = [ 'Transfer-Encoding: chunked', 'Content-Length: 666' ]
593
+ %w(a b).each do |x|
594
+ str = "PUT /#{x} HTTP/1.1\r\n" \
595
+ "#{order.join("\r\n")}" \
596
+ "\r\n\r\na\r\nhelloworld\r\n0\r\n\r\n"
597
+ order.reverse!
598
+ env = @parser.headers({}, str)
599
+ assert_nil @parser.content_length
600
+ assert_equal 'chunked', env['HTTP_TRANSFER_ENCODING']
601
+ assert_equal '666', env['CONTENT_LENGTH'],
602
+ 'Content-Length logged so the app can log a possible client bug/attack'
603
+ @parser.filter_body(dst = '', str)
604
+ assert_equal 'helloworld', dst
605
+ @parser.parse # handle the non-existent trailer
606
+ assert @parser.next?
607
+ end
608
+ end
609
+
610
+ def test_chunked_order_good
611
+ str = "PUT /x HTTP/1.1\r\n" \
612
+ "Transfer-Encoding: gzip\r\n" \
613
+ "Transfer-Encoding: chunked\r\n" \
614
+ "\r\n"
615
+ env = @parser.headers({}, str)
616
+ assert_equal 'gzip,chunked', env['HTTP_TRANSFER_ENCODING']
617
+ assert_nil @parser.content_length
618
+
619
+ @parser.clear
620
+ str = "PUT /x HTTP/1.1\r\n" \
621
+ "Transfer-Encoding: gzip, chunked\r\n" \
622
+ "\r\n"
623
+ env = @parser.headers({}, str)
624
+ assert_equal 'gzip, chunked', env['HTTP_TRANSFER_ENCODING']
625
+ assert_nil @parser.content_length
626
+ end
627
+
628
+ def test_chunked_order_bad
629
+ str = "PUT /x HTTP/1.1\r\n" \
630
+ "Transfer-Encoding: chunked\r\n" \
631
+ "Transfer-Encoding: gzip\r\n" \
632
+ "\r\n"
633
+ assert_raise(HttpParserError) { @parser.headers({}, str) }
634
+ end
635
+
636
+ def test_double_chunked
637
+ str = "PUT /x HTTP/1.1\r\n" \
638
+ "Transfer-Encoding: chunked\r\n" \
639
+ "Transfer-Encoding: chunked\r\n" \
640
+ "\r\n"
641
+ assert_raise(HttpParserError) { @parser.headers({}, str) }
642
+
643
+ @parser.clear
644
+ str = "PUT /x HTTP/1.1\r\n" \
645
+ "Transfer-Encoding: chunked,chunked\r\n" \
646
+ "\r\n"
647
+ assert_raise(HttpParserError) { @parser.headers({}, str) }
648
+ end
649
+
569
650
  def test_backtrace_is_empty
570
651
  begin
571
652
  @parser.headers({}, "AAADFSFDSFD\r\n\r\n")
@@ -23,6 +23,16 @@ def call(env)
23
23
  end
24
24
  end
25
25
 
26
+ class TestEarlyHintsHandler
27
+ def call(env)
28
+ while env['rack.input'].read(4096)
29
+ end
30
+ env['rack.early_hints'].call(
31
+ "Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload"
32
+ )
33
+ [200, { 'Content-Type' => 'text/plain' }, ['hello!\n']]
34
+ end
35
+ end
26
36
 
27
37
  class WebServerTest < Test::Unit::TestCase
28
38
 
@@ -84,6 +94,26 @@ def test_preload_app_config
84
94
  tmp.close!
85
95
  end
86
96
 
97
+ def test_early_hints
98
+ teardown
99
+ redirect_test_io do
100
+ @server = HttpServer.new(TestEarlyHintsHandler.new,
101
+ :listeners => [ "127.0.0.1:#@port"],
102
+ :early_hints => true)
103
+ @server.start
104
+ end
105
+
106
+ sock = TCPSocket.new('127.0.0.1', @port)
107
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
108
+
109
+ responses = sock.read(4096)
110
+ assert_match %r{\AHTTP/1.[01] 103\b}, responses
111
+ assert_match %r{^Link: </style\.css>}, responses
112
+ assert_match %r{^Link: </script\.js>}, responses
113
+
114
+ assert_match %r{^HTTP/1.[01] 200\b}, responses
115
+ end
116
+
87
117
  def test_broken_app
88
118
  teardown
89
119
  app = lambda { |env| raise RuntimeError, "hello" }
@@ -236,15 +236,10 @@ def test_chunked_upload_via_curl
236
236
  resp = Tempfile.new('resp')
237
237
  resp.sync = true
238
238
 
239
- rd, wr = IO.pipe
240
- wr.sync = rd.sync = true
241
- pid = fork {
242
- STDIN.reopen(rd)
243
- rd.close
244
- wr.close
245
- STDOUT.reopen(resp)
246
- exec cmd
247
- }
239
+ rd, wr = IO.pipe.each do |io|
240
+ io.sync = io.close_on_exec = true
241
+ end
242
+ pid = spawn(*cmd, { 0 => rd, 1 => resp })
248
243
  rd.close
249
244
 
250
245
  tmp.rewind
@@ -11,24 +11,25 @@
11
11
 
12
12
  Gem::Specification.new do |s|
13
13
  s.name = %q{unicorn}
14
- s.version = (ENV['VERSION'] || '5.5.0').dup
14
+ s.version = (ENV['VERSION'] || '5.7.0').dup
15
15
  s.authors = ['unicorn hackers']
16
16
  s.summary = 'Rack HTTP server for fast clients and Unix'
17
17
  s.description = File.read('README').split("\n\n")[1]
18
- s.email = %q{unicorn-public@bogomips.org}
18
+ s.email = %q{unicorn-public@yhbt.net}
19
19
  s.executables = %w(unicorn unicorn_rails)
20
20
  s.extensions = %w(ext/unicorn_http/extconf.rb)
21
21
  s.extra_rdoc_files = IO.readlines('.document').map!(&:chomp!).keep_if do |f|
22
22
  File.exist?(f)
23
23
  end
24
24
  s.files = manifest
25
- s.homepage = 'https://bogomips.org/unicorn/'
25
+ s.homepage = 'https://yhbt.net/unicorn/'
26
26
  s.test_files = test_files
27
27
 
28
- # technically we need ">= 1.9.3", too, but avoid the array here since
29
- # old rubygems versions (1.8.23.2 at least) do not support multiple
30
- # version requirements here.
31
- s.required_ruby_version = '< 3.0'
28
+ # 1.9.3 is the minumum supported version. We don't specify
29
+ # a maximum version to make it easier to test pre-releases,
30
+ # but we do warn users if they install unicorn on an untested
31
+ # version in extconf.rb
32
+ s.required_ruby_version = ">= 1.9.3"
32
33
 
33
34
  # We do not have a hard dependency on rack, it's possible to load
34
35
  # things which respond to #call. HTTP status lines in responses
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unicorn
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.5.2
4
+ version: 5.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - unicorn hackers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-12-20 00:00:00.000000000 Z
11
+ date: 2020-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -72,7 +72,7 @@ description: |-
72
72
  advantage of features in Unix/Unix-like kernels. Slow clients should
73
73
  only be served by placing a reverse proxy capable of fully buffering
74
74
  both the the request and response in between unicorn and slow clients.
75
- email: unicorn-public@bogomips.org
75
+ email: unicorn-public@yhbt.net
76
76
  executables:
77
77
  - unicorn
78
78
  - unicorn_rails
@@ -271,7 +271,7 @@ files:
271
271
  - unicorn.gemspec
272
272
  - unicorn_1
273
273
  - unicorn_rails_1
274
- homepage: https://bogomips.org/unicorn/
274
+ homepage: https://yhbt.net/unicorn/
275
275
  licenses:
276
276
  - GPL-2.0+
277
277
  - Ruby-1.8
@@ -282,9 +282,9 @@ require_paths:
282
282
  - lib
283
283
  required_ruby_version: !ruby/object:Gem::Requirement
284
284
  requirements:
285
- - - "<"
285
+ - - ">="
286
286
  - !ruby/object:Gem::Version
287
- version: '3.0'
287
+ version: 1.9.3
288
288
  required_rubygems_version: !ruby/object:Gem::Requirement
289
289
  requirements:
290
290
  - - ">="
@@ -301,4 +301,5 @@ test_files:
301
301
  - test/unit/test_http_parser_ng.rb
302
302
  - test/unit/test_request.rb
303
303
  - test/unit/test_server.rb
304
+ - test/unit/test_upload.rb
304
305
  - test/unit/test_util.rb