puma 5.6.5 → 6.4.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +338 -14
  3. data/LICENSE +0 -0
  4. data/README.md +79 -29
  5. data/bin/puma-wild +1 -1
  6. data/docs/architecture.md +0 -0
  7. data/docs/compile_options.md +34 -0
  8. data/docs/deployment.md +0 -0
  9. data/docs/fork_worker.md +1 -3
  10. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  11. data/docs/images/puma-connection-flow.png +0 -0
  12. data/docs/images/puma-general-arch.png +0 -0
  13. data/docs/jungle/README.md +0 -0
  14. data/docs/jungle/rc.d/README.md +0 -0
  15. data/docs/jungle/rc.d/puma.conf +0 -0
  16. data/docs/kubernetes.md +12 -0
  17. data/docs/nginx.md +1 -1
  18. data/docs/plugins.md +0 -0
  19. data/docs/rails_dev_mode.md +0 -0
  20. data/docs/restart.md +1 -0
  21. data/docs/signals.md +0 -0
  22. data/docs/stats.md +0 -0
  23. data/docs/systemd.md +3 -6
  24. data/docs/testing_benchmarks_local_files.md +150 -0
  25. data/docs/testing_test_rackup_ci_files.md +36 -0
  26. data/ext/puma_http11/PumaHttp11Service.java +0 -0
  27. data/ext/puma_http11/ext_help.h +0 -0
  28. data/ext/puma_http11/extconf.rb +16 -9
  29. data/ext/puma_http11/http11_parser.c +1 -1
  30. data/ext/puma_http11/http11_parser.h +1 -1
  31. data/ext/puma_http11/http11_parser.java.rl +2 -2
  32. data/ext/puma_http11/http11_parser.rl +2 -2
  33. data/ext/puma_http11/http11_parser_common.rl +2 -2
  34. data/ext/puma_http11/mini_ssl.c +127 -19
  35. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +0 -0
  36. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  37. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  38. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +157 -53
  39. data/ext/puma_http11/puma_http11.c +17 -9
  40. data/lib/puma/app/status.rb +4 -4
  41. data/lib/puma/binder.rb +50 -53
  42. data/lib/puma/cli.rb +16 -18
  43. data/lib/puma/client.rb +100 -26
  44. data/lib/puma/cluster/worker.rb +18 -11
  45. data/lib/puma/cluster/worker_handle.rb +4 -1
  46. data/lib/puma/cluster.rb +102 -40
  47. data/lib/puma/commonlogger.rb +21 -14
  48. data/lib/puma/configuration.rb +77 -59
  49. data/lib/puma/const.rb +129 -92
  50. data/lib/puma/control_cli.rb +15 -11
  51. data/lib/puma/detect.rb +7 -4
  52. data/lib/puma/dsl.rb +250 -56
  53. data/lib/puma/error_logger.rb +18 -9
  54. data/lib/puma/events.rb +6 -126
  55. data/lib/puma/io_buffer.rb +39 -4
  56. data/lib/puma/jruby_restart.rb +2 -1
  57. data/lib/puma/json_serialization.rb +0 -0
  58. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  59. data/lib/puma/launcher.rb +102 -175
  60. data/lib/puma/log_writer.rb +147 -0
  61. data/lib/puma/minissl/context_builder.rb +26 -12
  62. data/lib/puma/minissl.rb +104 -11
  63. data/lib/puma/null_io.rb +16 -2
  64. data/lib/puma/plugin/systemd.rb +90 -0
  65. data/lib/puma/plugin/tmp_restart.rb +1 -1
  66. data/lib/puma/plugin.rb +0 -0
  67. data/lib/puma/rack/builder.rb +6 -6
  68. data/lib/puma/rack/urlmap.rb +1 -1
  69. data/lib/puma/rack_default.rb +19 -4
  70. data/lib/puma/reactor.rb +19 -10
  71. data/lib/puma/request.rb +365 -170
  72. data/lib/puma/runner.rb +56 -20
  73. data/lib/puma/sd_notify.rb +149 -0
  74. data/lib/puma/server.rb +137 -89
  75. data/lib/puma/single.rb +13 -11
  76. data/lib/puma/state_file.rb +3 -6
  77. data/lib/puma/thread_pool.rb +57 -19
  78. data/lib/puma/util.rb +0 -11
  79. data/lib/puma.rb +12 -11
  80. data/lib/rack/handler/puma.rb +113 -86
  81. data/tools/Dockerfile +2 -2
  82. data/tools/trickletest.rb +0 -0
  83. metadata +11 -6
  84. data/lib/puma/queue_close.rb +0 -26
  85. data/lib/puma/systemd.rb +0 -46
data/lib/puma/client.rb CHANGED
@@ -8,9 +8,9 @@ class IO
8
8
  end
9
9
  end
10
10
 
11
- require 'puma/detect'
11
+ require_relative 'detect'
12
+ require_relative 'io_buffer'
12
13
  require 'tempfile'
13
- require 'forwardable'
14
14
 
15
15
  if Puma::IS_JRUBY
16
16
  # We have to work around some OpenSSL buffer/io-readiness bugs
@@ -25,6 +25,9 @@ module Puma
25
25
 
26
26
  class HttpParserError501 < IOError; end
27
27
 
28
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
29
+
30
+
28
31
  # An instance of this class represents a unique request from a client.
29
32
  # For example, this could be a web request from a browser or from CURL.
30
33
  #
@@ -38,14 +41,23 @@ module Puma
38
41
  # the header and body are fully buffered via the `try_to_finish` method.
39
42
  # They can be used to "time out" a response via the `timeout_at` reader.
40
43
  #
41
- class Client
44
+ class Client # :nodoc:
42
45
 
43
46
  # this tests all values but the last, which must be chunked
44
47
  ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
45
48
 
46
49
  # chunked body validation
47
50
  CHUNK_SIZE_INVALID = /[^\h]/.freeze
48
- CHUNK_VALID_ENDING = "\r\n".freeze
51
+ CHUNK_VALID_ENDING = Const::LINE_END
52
+ CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
53
+
54
+ # The maximum number of bytes we'll buffer looking for a valid
55
+ # chunk header.
56
+ MAX_CHUNK_HEADER_SIZE = 4096
57
+
58
+ # The maximum amount of excess data the client sends
59
+ # using chunk size extensions before we abort the connection.
60
+ MAX_CHUNK_EXCESS = 16 * 1024
49
61
 
50
62
  # Content-Length header value validation
51
63
  CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
@@ -57,17 +69,13 @@ module Puma
57
69
  EmptyBody = NullIO.new
58
70
 
59
71
  include Puma::Const
60
- extend Forwardable
61
72
 
62
73
  def initialize(io, env=nil)
63
74
  @io = io
64
75
  @to_io = io.to_io
76
+ @io_buffer = IOBuffer.new
65
77
  @proto_env = env
66
- if !env
67
- @env = nil
68
- else
69
- @env = env.dup
70
- end
78
+ @env = env&.dup
71
79
 
72
80
  @parser = HttpParser.new
73
81
  @parsed_bytes = 0
@@ -85,7 +93,11 @@ module Puma
85
93
  @requests_served = 0
86
94
  @hijacked = false
87
95
 
96
+ @http_content_length_limit = nil
97
+ @http_content_length_limit_exceeded = false
98
+
88
99
  @peerip = nil
100
+ @peer_family = nil
89
101
  @listener = nil
90
102
  @remote_addr_header = nil
91
103
  @expect_proxy_proto = false
@@ -93,16 +105,22 @@ module Puma
93
105
  @body_remain = 0
94
106
 
95
107
  @in_last_chunk = false
108
+
109
+ # need unfrozen ASCII-8BIT, +'' is UTF-8
110
+ @read_buffer = String.new # rubocop: disable Performance/UnfreezeString
96
111
  end
97
112
 
98
113
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
99
- :tempfile
114
+ :tempfile, :io_buffer, :http_content_length_limit_exceeded
100
115
 
101
- attr_writer :peerip
116
+ attr_writer :peerip, :http_content_length_limit
102
117
 
103
118
  attr_accessor :remote_addr_header, :listener
104
119
 
105
- def_delegators :@io, :closed?
120
+ # Remove in Puma 7?
121
+ def closed?
122
+ @to_io.closed?
123
+ end
106
124
 
107
125
  # Test to see if io meets a bare minimum of functioning, @to_io needs to be
108
126
  # used for MiniSSL::Socket
@@ -138,6 +156,7 @@ module Puma
138
156
 
139
157
  def reset(fast_check=true)
140
158
  @parser.reset
159
+ @io_buffer.reset
141
160
  @read_header = true
142
161
  @read_proxy = !!@expect_proxy_proto
143
162
  @env = @proto_env.dup
@@ -148,6 +167,7 @@ module Puma
148
167
  @body_remain = 0
149
168
  @peerip = nil if @remote_addr_header
150
169
  @in_last_chunk = false
170
+ @http_content_length_limit_exceeded = false
151
171
 
152
172
  if @buffer
153
173
  return false unless try_to_parse_proxy_protocol
@@ -207,6 +227,17 @@ module Puma
207
227
  end
208
228
 
209
229
  def try_to_finish
230
+ if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
231
+ @http_content_length_limit_exceeded = true
232
+ end
233
+
234
+ if @http_content_length_limit_exceeded
235
+ @buffer = nil
236
+ @body = EmptyBody
237
+ set_ready
238
+ return true
239
+ end
240
+
210
241
  return read_body if in_data_phase
211
242
 
212
243
  begin
@@ -236,6 +267,10 @@ module Puma
236
267
 
237
268
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
238
269
 
270
+ if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
271
+ @http_content_length_limit_exceeded = true
272
+ end
273
+
239
274
  if @parser.finished?
240
275
  return setup_body
241
276
  elsif @parsed_bytes >= MAX_HEADER
@@ -273,7 +308,7 @@ module Puma
273
308
  return @peerip if @peerip
274
309
 
275
310
  if @remote_addr_header
276
- hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
311
+ hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
277
312
  @peerip = hdr
278
313
  return hdr
279
314
  end
@@ -281,6 +316,16 @@ module Puma
281
316
  @peerip ||= @io.peeraddr.last
282
317
  end
283
318
 
319
+ def peer_family
320
+ return @peer_family if @peer_family
321
+
322
+ @peer_family ||= begin
323
+ @io.local_address.afamily
324
+ rescue
325
+ Socket::AF_INET
326
+ end
327
+ end
328
+
284
329
  # Returns true if the persistent connection can be closed immediately
285
330
  # without waiting for the configured idle/shutdown timeout.
286
331
  # @version 5.0.0
@@ -304,7 +349,7 @@ module Puma
304
349
  private
305
350
 
306
351
  def setup_body
307
- @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
352
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
308
353
 
309
354
  if @env[HTTP_EXPECT] == CONTINUE
310
355
  # TODO allow a hook here to check the headers before
@@ -347,8 +392,8 @@ module Puma
347
392
  cl = @env[CONTENT_LENGTH]
348
393
 
349
394
  if cl
350
- # cannot contain characters that are not \d
351
- if cl =~ CONTENT_LENGTH_VALUE_INVALID
395
+ # cannot contain characters that are not \d, or be empty
396
+ if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
352
397
  raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
353
398
  end
354
399
  else
@@ -401,7 +446,7 @@ module Puma
401
446
  end
402
447
 
403
448
  begin
404
- chunk = @io.read_nonblock(want)
449
+ chunk = @io.read_nonblock(want, @read_buffer)
405
450
  rescue IO::WaitReadable
406
451
  return false
407
452
  rescue SystemCallError, IOError
@@ -433,7 +478,7 @@ module Puma
433
478
  def read_chunked_body
434
479
  while true
435
480
  begin
436
- chunk = @io.read_nonblock(4096)
481
+ chunk = @io.read_nonblock(4096, @read_buffer)
437
482
  rescue IO::WaitReadable
438
483
  return false
439
484
  rescue SystemCallError, IOError
@@ -459,6 +504,7 @@ module Puma
459
504
  @chunked_body = true
460
505
  @partial_part_left = 0
461
506
  @prev_chunk = ""
507
+ @excess_cr = 0
462
508
 
463
509
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
464
510
  @body.unlink
@@ -509,11 +555,11 @@ module Puma
509
555
 
510
556
  while !io.eof?
511
557
  line = io.gets
512
- if line.end_with?("\r\n")
558
+ if line.end_with?(CHUNK_VALID_ENDING)
513
559
  # Puma doesn't process chunk extensions, but should parse if they're
514
560
  # present, which is the reason for the semicolon regex
515
561
  chunk_hex = line.strip[/\A[^;]+/]
516
- if chunk_hex =~ CHUNK_SIZE_INVALID
562
+ if CHUNK_SIZE_INVALID.match? chunk_hex
517
563
  raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
518
564
  end
519
565
  len = chunk_hex.to_i(16)
@@ -521,19 +567,39 @@ module Puma
521
567
  @in_last_chunk = true
522
568
  @body.rewind
523
569
  rest = io.read
524
- last_crlf_size = "\r\n".bytesize
525
- if rest.bytesize < last_crlf_size
570
+ if rest.bytesize < CHUNK_VALID_ENDING_SIZE
526
571
  @buffer = nil
527
- @partial_part_left = last_crlf_size - rest.bytesize
572
+ @partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
528
573
  return false
529
574
  else
530
- @buffer = rest[last_crlf_size..-1]
575
+ # if the next character is a CRLF, set buffer to everything after that CRLF
576
+ start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
577
+ CHUNK_VALID_ENDING_SIZE
578
+ else # we have started a trailer section, which we do not support. skip it!
579
+ rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
580
+ end
581
+
582
+ @buffer = rest[start_of_rest..-1]
531
583
  @buffer = nil if @buffer.empty?
532
584
  set_ready
533
585
  return true
534
586
  end
535
587
  end
536
588
 
589
+ # Track the excess as a function of the size of the
590
+ # header vs the size of the actual data. Excess can
591
+ # go negative (and is expected to) when the body is
592
+ # significant.
593
+ # The additional of chunk_hex.size and 2 compensates
594
+ # for a client sending 1 byte in a chunked body over
595
+ # a long period of time, making sure that that client
596
+ # isn't accidentally eventually punished.
597
+ @excess_cr += (line.size - len - chunk_hex.size - 2)
598
+
599
+ if @excess_cr >= MAX_CHUNK_EXCESS
600
+ raise HttpParserError, "Maximum chunk excess detected"
601
+ end
602
+
537
603
  len += 2
538
604
 
539
605
  part = io.read(len)
@@ -561,6 +627,10 @@ module Puma
561
627
  @partial_part_left = len - part.size
562
628
  end
563
629
  else
630
+ if @prev_chunk.size + chunk.size >= MAX_CHUNK_HEADER_SIZE
631
+ raise HttpParserError, "maximum size of chunk header exceeded"
632
+ end
633
+
564
634
  @prev_chunk = line
565
635
  return false
566
636
  end
@@ -576,10 +646,14 @@ module Puma
576
646
 
577
647
  def set_ready
578
648
  if @body_read_start
579
- @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
649
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
580
650
  end
581
651
  @requests_served += 1
582
652
  @ready = true
583
653
  end
654
+
655
+ def above_http_content_limit(value)
656
+ @http_content_length_limit&.< value
657
+ end
584
658
  end
585
659
  end
@@ -2,27 +2,29 @@
2
2
 
3
3
  module Puma
4
4
  class Cluster < Puma::Runner
5
+ #—————————————————————— DO NOT USE — this class is for internal use only ———
6
+
7
+
5
8
  # This class is instantiated by the `Puma::Cluster` and represents a single
6
9
  # worker process.
7
10
  #
8
11
  # At the core of this class is running an instance of `Puma::Server` which
9
12
  # gets created via the `start_server` method from the `Puma::Runner` class
10
13
  # that this inherits from.
11
- class Worker < Puma::Runner
14
+ class Worker < Puma::Runner # :nodoc:
12
15
  attr_reader :index, :master
13
16
 
14
17
  def initialize(index:, master:, launcher:, pipes:, server: nil)
15
- super launcher, launcher.events
18
+ super(launcher)
16
19
 
17
20
  @index = index
18
21
  @master = master
19
- @launcher = launcher
20
- @options = launcher.options
21
22
  @check_pipe = pipes[:check_pipe]
22
23
  @worker_write = pipes[:worker_write]
23
24
  @fork_pipe = pipes[:fork_pipe]
24
25
  @wakeup = pipes[:wakeup]
25
26
  @server = server
27
+ @hook_data = {}
26
28
  end
27
29
 
28
30
  def run
@@ -52,13 +54,14 @@ module Puma
52
54
 
53
55
  # Invoke any worker boot hooks so they can get
54
56
  # things in shape before booting the app.
55
- @launcher.config.run_hooks :before_worker_boot, index, @launcher.events
57
+ @config.run_hooks(:before_worker_boot, index, @log_writer, @hook_data)
56
58
 
57
59
  begin
58
60
  server = @server ||= start_server
59
61
  rescue Exception => e
60
62
  log "! Unable to start worker"
61
- log e.backtrace[0]
63
+ log e
64
+ log e.backtrace.join("\n ")
62
65
  exit 1
63
66
  end
64
67
 
@@ -83,8 +86,7 @@ module Puma
83
86
  if restart_server.length > 0
84
87
  restart_server.clear
85
88
  server.begin_restart(true)
86
- @launcher.config.run_hooks :before_refork, nil, @launcher.events
87
- Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
89
+ @config.run_hooks(:before_refork, nil, @log_writer, @hook_data)
88
90
  end
89
91
  elsif idx == 0 # restart server
90
92
  restart_server << true << false
@@ -113,6 +115,11 @@ module Puma
113
115
 
114
116
  while restart_server.pop
115
117
  server_thread = server.run
118
+
119
+ if @log_writer.debug? && index == 0
120
+ debug_loaded_extensions "Loaded Extensions - worker 0:"
121
+ end
122
+
116
123
  stat_thread ||= Thread.new(@worker_write) do |io|
117
124
  Puma.set_thread_name "stat pld"
118
125
  base_payload = "p#{Process.pid}"
@@ -138,7 +145,7 @@ module Puma
138
145
 
139
146
  # Invoke any worker shutdown hooks so they can prevent the worker
140
147
  # exiting until any background operations are completed
141
- @launcher.config.run_hooks :before_worker_shutdown, index, @launcher.events
148
+ @config.run_hooks(:before_worker_shutdown, index, @log_writer, @hook_data)
142
149
  ensure
143
150
  @worker_write << "t#{Process.pid}\n" rescue nil
144
151
  @worker_write.close
@@ -147,7 +154,7 @@ module Puma
147
154
  private
148
155
 
149
156
  def spawn_worker(idx)
150
- @launcher.config.run_hooks :before_worker_fork, idx, @launcher.events
157
+ @config.run_hooks(:before_worker_fork, idx, @log_writer, @hook_data)
151
158
 
152
159
  pid = fork do
153
160
  new_worker = Worker.new index: idx,
@@ -165,7 +172,7 @@ module Puma
165
172
  exit! 1
166
173
  end
167
174
 
168
- @launcher.config.run_hooks :after_worker_fork, idx, @launcher.events
175
+ @config.run_hooks(:after_worker_fork, idx, @log_writer, @hook_data)
169
176
  pid
170
177
  end
171
178
  end
@@ -2,12 +2,15 @@
2
2
 
3
3
  module Puma
4
4
  class Cluster < Runner
5
+ #—————————————————————— DO NOT USE — this class is for internal use only ———
6
+
7
+
5
8
  # This class represents a worker process from the perspective of the puma
6
9
  # master process. It contains information about the process and its health
7
10
  # and it exposes methods to control the process via IPC. It does not
8
11
  # include the actual logic executed by the worker process itself. For that,
9
12
  # see Puma::Cluster::Worker.
10
- class WorkerHandle
13
+ class WorkerHandle # :nodoc:
11
14
  def initialize(idx, pid, phase, options)
12
15
  @index = idx
13
16
  @pid = pid