puma 5.6.7 → 6.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/History.md +327 -16
- data/README.md +79 -29
- data/bin/puma-wild +1 -1
- data/docs/compile_options.md +34 -0
- data/docs/fork_worker.md +1 -3
- data/docs/kubernetes.md +12 -0
- data/docs/nginx.md +1 -1
- data/docs/restart.md +1 -0
- data/docs/systemd.md +3 -6
- data/docs/testing_benchmarks_local_files.md +150 -0
- data/docs/testing_test_rackup_ci_files.md +36 -0
- data/ext/puma_http11/extconf.rb +16 -9
- data/ext/puma_http11/http11_parser.c +1 -1
- data/ext/puma_http11/http11_parser.h +1 -1
- data/ext/puma_http11/http11_parser.java.rl +2 -2
- data/ext/puma_http11/http11_parser.rl +2 -2
- data/ext/puma_http11/http11_parser_common.rl +2 -2
- data/ext/puma_http11/mini_ssl.c +127 -19
- data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +157 -53
- data/ext/puma_http11/puma_http11.c +17 -9
- data/lib/puma/app/status.rb +4 -4
- data/lib/puma/binder.rb +50 -53
- data/lib/puma/cli.rb +16 -18
- data/lib/puma/client.rb +86 -19
- data/lib/puma/cluster/worker.rb +18 -11
- data/lib/puma/cluster/worker_handle.rb +4 -1
- data/lib/puma/cluster.rb +102 -40
- data/lib/puma/commonlogger.rb +21 -14
- data/lib/puma/configuration.rb +77 -59
- data/lib/puma/const.rb +129 -92
- data/lib/puma/control_cli.rb +15 -11
- data/lib/puma/detect.rb +7 -4
- data/lib/puma/dsl.rb +250 -56
- data/lib/puma/error_logger.rb +18 -9
- data/lib/puma/events.rb +6 -126
- data/lib/puma/io_buffer.rb +39 -4
- data/lib/puma/jruby_restart.rb +2 -1
- data/lib/puma/launcher/bundle_pruner.rb +104 -0
- data/lib/puma/launcher.rb +102 -175
- data/lib/puma/log_writer.rb +147 -0
- data/lib/puma/minissl/context_builder.rb +26 -12
- data/lib/puma/minissl.rb +104 -11
- data/lib/puma/null_io.rb +16 -2
- data/lib/puma/plugin/systemd.rb +90 -0
- data/lib/puma/plugin/tmp_restart.rb +1 -1
- data/lib/puma/rack/builder.rb +6 -6
- data/lib/puma/rack/urlmap.rb +1 -1
- data/lib/puma/rack_default.rb +19 -4
- data/lib/puma/reactor.rb +19 -10
- data/lib/puma/request.rb +365 -170
- data/lib/puma/runner.rb +56 -20
- data/lib/puma/sd_notify.rb +149 -0
- data/lib/puma/server.rb +137 -89
- data/lib/puma/single.rb +13 -11
- data/lib/puma/state_file.rb +3 -6
- data/lib/puma/thread_pool.rb +57 -19
- data/lib/puma/util.rb +0 -11
- data/lib/puma.rb +9 -10
- data/lib/rack/handler/puma.rb +113 -86
- data/tools/Dockerfile +2 -2
- metadata +11 -7
- data/lib/puma/queue_close.rb +0 -26
- data/lib/puma/systemd.rb +0 -46
- 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
|
-
|
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
|
-
|
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
|
-
|
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] ||
|
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, :
|
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
|
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
|
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, :
|
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
|
data/lib/puma/cluster/worker.rb
CHANGED
@@ -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
|
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
|
-
@
|
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
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|