puma 5.6.7 → 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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +327 -16
  3. data/README.md +79 -29
  4. data/bin/puma-wild +1 -1
  5. data/docs/compile_options.md +34 -0
  6. data/docs/fork_worker.md +1 -3
  7. data/docs/kubernetes.md +12 -0
  8. data/docs/nginx.md +1 -1
  9. data/docs/restart.md +1 -0
  10. data/docs/systemd.md +3 -6
  11. data/docs/testing_benchmarks_local_files.md +150 -0
  12. data/docs/testing_test_rackup_ci_files.md +36 -0
  13. data/ext/puma_http11/extconf.rb +16 -9
  14. data/ext/puma_http11/http11_parser.c +1 -1
  15. data/ext/puma_http11/http11_parser.h +1 -1
  16. data/ext/puma_http11/http11_parser.java.rl +2 -2
  17. data/ext/puma_http11/http11_parser.rl +2 -2
  18. data/ext/puma_http11/http11_parser_common.rl +2 -2
  19. data/ext/puma_http11/mini_ssl.c +127 -19
  20. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  21. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  22. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +157 -53
  23. data/ext/puma_http11/puma_http11.c +17 -9
  24. data/lib/puma/app/status.rb +4 -4
  25. data/lib/puma/binder.rb +50 -53
  26. data/lib/puma/cli.rb +16 -18
  27. data/lib/puma/client.rb +86 -19
  28. data/lib/puma/cluster/worker.rb +18 -11
  29. data/lib/puma/cluster/worker_handle.rb +4 -1
  30. data/lib/puma/cluster.rb +102 -40
  31. data/lib/puma/commonlogger.rb +21 -14
  32. data/lib/puma/configuration.rb +77 -59
  33. data/lib/puma/const.rb +129 -92
  34. data/lib/puma/control_cli.rb +15 -11
  35. data/lib/puma/detect.rb +7 -4
  36. data/lib/puma/dsl.rb +250 -56
  37. data/lib/puma/error_logger.rb +18 -9
  38. data/lib/puma/events.rb +6 -126
  39. data/lib/puma/io_buffer.rb +39 -4
  40. data/lib/puma/jruby_restart.rb +2 -1
  41. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  42. data/lib/puma/launcher.rb +102 -175
  43. data/lib/puma/log_writer.rb +147 -0
  44. data/lib/puma/minissl/context_builder.rb +26 -12
  45. data/lib/puma/minissl.rb +104 -11
  46. data/lib/puma/null_io.rb +16 -2
  47. data/lib/puma/plugin/systemd.rb +90 -0
  48. data/lib/puma/plugin/tmp_restart.rb +1 -1
  49. data/lib/puma/rack/builder.rb +6 -6
  50. data/lib/puma/rack/urlmap.rb +1 -1
  51. data/lib/puma/rack_default.rb +19 -4
  52. data/lib/puma/reactor.rb +19 -10
  53. data/lib/puma/request.rb +365 -170
  54. data/lib/puma/runner.rb +56 -20
  55. data/lib/puma/sd_notify.rb +149 -0
  56. data/lib/puma/server.rb +137 -89
  57. data/lib/puma/single.rb +13 -11
  58. data/lib/puma/state_file.rb +3 -6
  59. data/lib/puma/thread_pool.rb +57 -19
  60. data/lib/puma/util.rb +0 -11
  61. data/lib/puma.rb +9 -10
  62. data/lib/rack/handler/puma.rb +113 -86
  63. data/tools/Dockerfile +2 -2
  64. metadata +11 -7
  65. data/lib/puma/queue_close.rb +0 -26
  66. data/lib/puma/systemd.rb +0 -46
  67. data/lib/rack/version_restriction.rb +0 -15
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,7 +41,7 @@ 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
@@ -48,6 +51,14 @@ module Puma
48
51
  CHUNK_VALID_ENDING = Const::LINE_END
49
52
  CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
50
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
61
+
51
62
  # Content-Length header value validation
52
63
  CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
53
64
 
@@ -58,17 +69,13 @@ module Puma
58
69
  EmptyBody = NullIO.new
59
70
 
60
71
  include Puma::Const
61
- extend Forwardable
62
72
 
63
73
  def initialize(io, env=nil)
64
74
  @io = io
65
75
  @to_io = io.to_io
76
+ @io_buffer = IOBuffer.new
66
77
  @proto_env = env
67
- if !env
68
- @env = nil
69
- else
70
- @env = env.dup
71
- end
78
+ @env = env&.dup
72
79
 
73
80
  @parser = HttpParser.new
74
81
  @parsed_bytes = 0
@@ -86,7 +93,11 @@ module Puma
86
93
  @requests_served = 0
87
94
  @hijacked = false
88
95
 
96
+ @http_content_length_limit = nil
97
+ @http_content_length_limit_exceeded = false
98
+
89
99
  @peerip = nil
100
+ @peer_family = nil
90
101
  @listener = nil
91
102
  @remote_addr_header = nil
92
103
  @expect_proxy_proto = false
@@ -94,16 +105,22 @@ module Puma
94
105
  @body_remain = 0
95
106
 
96
107
  @in_last_chunk = false
108
+
109
+ # need unfrozen ASCII-8BIT, +'' is UTF-8
110
+ @read_buffer = String.new # rubocop: disable Performance/UnfreezeString
97
111
  end
98
112
 
99
113
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
100
- :tempfile
114
+ :tempfile, :io_buffer, :http_content_length_limit_exceeded
101
115
 
102
- attr_writer :peerip
116
+ attr_writer :peerip, :http_content_length_limit
103
117
 
104
118
  attr_accessor :remote_addr_header, :listener
105
119
 
106
- def_delegators :@io, :closed?
120
+ # Remove in Puma 7?
121
+ def closed?
122
+ @to_io.closed?
123
+ end
107
124
 
108
125
  # Test to see if io meets a bare minimum of functioning, @to_io needs to be
109
126
  # used for MiniSSL::Socket
@@ -139,6 +156,7 @@ module Puma
139
156
 
140
157
  def reset(fast_check=true)
141
158
  @parser.reset
159
+ @io_buffer.reset
142
160
  @read_header = true
143
161
  @read_proxy = !!@expect_proxy_proto
144
162
  @env = @proto_env.dup
@@ -149,6 +167,7 @@ module Puma
149
167
  @body_remain = 0
150
168
  @peerip = nil if @remote_addr_header
151
169
  @in_last_chunk = false
170
+ @http_content_length_limit_exceeded = false
152
171
 
153
172
  if @buffer
154
173
  return false unless try_to_parse_proxy_protocol
@@ -208,6 +227,17 @@ module Puma
208
227
  end
209
228
 
210
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
+
211
241
  return read_body if in_data_phase
212
242
 
213
243
  begin
@@ -237,6 +267,10 @@ module Puma
237
267
 
238
268
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
239
269
 
270
+ if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
271
+ @http_content_length_limit_exceeded = true
272
+ end
273
+
240
274
  if @parser.finished?
241
275
  return setup_body
242
276
  elsif @parsed_bytes >= MAX_HEADER
@@ -274,7 +308,7 @@ module Puma
274
308
  return @peerip if @peerip
275
309
 
276
310
  if @remote_addr_header
277
- hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
311
+ hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
278
312
  @peerip = hdr
279
313
  return hdr
280
314
  end
@@ -282,6 +316,16 @@ module Puma
282
316
  @peerip ||= @io.peeraddr.last
283
317
  end
284
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
+
285
329
  # Returns true if the persistent connection can be closed immediately
286
330
  # without waiting for the configured idle/shutdown timeout.
287
331
  # @version 5.0.0
@@ -305,7 +349,7 @@ module Puma
305
349
  private
306
350
 
307
351
  def setup_body
308
- @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
352
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
309
353
 
310
354
  if @env[HTTP_EXPECT] == CONTINUE
311
355
  # TODO allow a hook here to check the headers before
@@ -349,7 +393,7 @@ module Puma
349
393
 
350
394
  if cl
351
395
  # cannot contain characters that are not \d, or be empty
352
- if cl =~ CONTENT_LENGTH_VALUE_INVALID || cl.empty?
396
+ if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
353
397
  raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
354
398
  end
355
399
  else
@@ -402,7 +446,7 @@ module Puma
402
446
  end
403
447
 
404
448
  begin
405
- chunk = @io.read_nonblock(want)
449
+ chunk = @io.read_nonblock(want, @read_buffer)
406
450
  rescue IO::WaitReadable
407
451
  return false
408
452
  rescue SystemCallError, IOError
@@ -434,7 +478,7 @@ module Puma
434
478
  def read_chunked_body
435
479
  while true
436
480
  begin
437
- chunk = @io.read_nonblock(4096)
481
+ chunk = @io.read_nonblock(4096, @read_buffer)
438
482
  rescue IO::WaitReadable
439
483
  return false
440
484
  rescue SystemCallError, IOError
@@ -460,6 +504,7 @@ module Puma
460
504
  @chunked_body = true
461
505
  @partial_part_left = 0
462
506
  @prev_chunk = ""
507
+ @excess_cr = 0
463
508
 
464
509
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
465
510
  @body.unlink
@@ -514,7 +559,7 @@ module Puma
514
559
  # Puma doesn't process chunk extensions, but should parse if they're
515
560
  # present, which is the reason for the semicolon regex
516
561
  chunk_hex = line.strip[/\A[^;]+/]
517
- if chunk_hex =~ CHUNK_SIZE_INVALID
562
+ if CHUNK_SIZE_INVALID.match? chunk_hex
518
563
  raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
519
564
  end
520
565
  len = chunk_hex.to_i(16)
@@ -541,6 +586,20 @@ module Puma
541
586
  end
542
587
  end
543
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
+
544
603
  len += 2
545
604
 
546
605
  part = io.read(len)
@@ -568,6 +627,10 @@ module Puma
568
627
  @partial_part_left = len - part.size
569
628
  end
570
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
+
571
634
  @prev_chunk = line
572
635
  return false
573
636
  end
@@ -583,10 +646,14 @@ module Puma
583
646
 
584
647
  def set_ready
585
648
  if @body_read_start
586
- @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
587
650
  end
588
651
  @requests_served += 1
589
652
  @ready = true
590
653
  end
654
+
655
+ def above_http_content_limit(value)
656
+ @http_content_length_limit&.< value
657
+ end
591
658
  end
592
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