puma 5.6.7 → 6.4.2

Sign up to get free protection for your applications and to get access to all the features.
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