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.
- checksums.yaml +4 -4
- data/History.md +338 -14
- data/LICENSE +0 -0
- data/README.md +79 -29
- data/bin/puma-wild +1 -1
- data/docs/architecture.md +0 -0
- data/docs/compile_options.md +34 -0
- data/docs/deployment.md +0 -0
- data/docs/fork_worker.md +1 -3
- data/docs/images/puma-connection-flow-no-reactor.png +0 -0
- data/docs/images/puma-connection-flow.png +0 -0
- data/docs/images/puma-general-arch.png +0 -0
- data/docs/jungle/README.md +0 -0
- data/docs/jungle/rc.d/README.md +0 -0
- data/docs/jungle/rc.d/puma.conf +0 -0
- data/docs/kubernetes.md +12 -0
- data/docs/nginx.md +1 -1
- data/docs/plugins.md +0 -0
- data/docs/rails_dev_mode.md +0 -0
- data/docs/restart.md +1 -0
- data/docs/signals.md +0 -0
- data/docs/stats.md +0 -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/PumaHttp11Service.java +0 -0
- data/ext/puma_http11/ext_help.h +0 -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/no_ssl/PumaHttp11Service.java +0 -0
- 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 +100 -26
- 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/json_serialization.rb +0 -0
- 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/plugin.rb +0 -0
- 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 +12 -11
- data/lib/rack/handler/puma.rb +113 -86
- data/tools/Dockerfile +2 -2
- data/tools/trickletest.rb +0 -0
- metadata +11 -6
- data/lib/puma/queue_close.rb +0 -26
- 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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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] ||
|
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, :
|
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
|
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?(
|
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
|
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
|
-
|
525
|
-
if rest.bytesize < last_crlf_size
|
570
|
+
if rest.bytesize < CHUNK_VALID_ENDING_SIZE
|
526
571
|
@buffer = nil
|
527
|
-
@partial_part_left =
|
572
|
+
@partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
|
528
573
|
return false
|
529
574
|
else
|
530
|
-
|
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, :
|
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
|
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
|