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.
- 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
|