puma 8.0.2-java → 8.0.3-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/History.md +21 -0
- data/ext/puma_http11/mini_ssl.c +8 -1
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +44 -34
- data/ext/puma_http11/puma_http11.c +4 -3
- data/lib/puma/client.rb +103 -15
- data/lib/puma/configuration.rb +1 -0
- data/lib/puma/const.rb +4 -1
- data/lib/puma/control_cli.rb +14 -3
- data/lib/puma/dsl.rb +14 -1
- data/lib/puma/minissl.rb +101 -56
- data/lib/puma/puma_http11.jar +0 -0
- data/lib/puma/reactor.rb +35 -3
- data/lib/puma/response.rb +15 -1
- data/lib/puma/server.rb +86 -15
- data/lib/puma/util.rb +8 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9bb993a39d400591f128b4808ca9d7f735618d614f6e490f850838ac7121391c
|
|
4
|
+
data.tar.gz: aae15617fcd8cd7147701e1f594625554bc6178c991c5ee98e00ca5bc0d1346d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a47f4ca7106a37810eff02f38a0a7410904fa78bf3e9022176cc2d09eea261bb622f18b5d2a8f9e3330404850a2f5a2cd5178e5b65d2f78aab222c7faf1a467
|
|
7
|
+
data.tar.gz: db545b9e393c0236856a6db02e8b1953cd6eb19a41573aab67ff766f67fa65ba90e4e8d4984b9653481cfb834dc29d585aa94cc657adab31ce05d6a5d6ae5614
|
data/History.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## 8.0.3 / 2026-11-13
|
|
2
|
+
|
|
3
|
+
* Security
|
|
4
|
+
* Close connections whose reactor timeout passed even when another client keeps sending data. ([GHSA-m5xw-3hrw-qcmf](https://github.com/puma/puma/security/advisories/GHSA-m5xw-3hrw-qcmf))
|
|
5
|
+
* Don't block the reactor writing a `100 Continue` or `408` to a client that stops reading. ([GHSA-xw3h-pgr9-3vg7](https://github.com/puma/puma/security/advisories/GHSA-xw3h-pgr9-3vg7))
|
|
6
|
+
* Apply the write timeout to TLS responses and to file responses larger than 64 KB, and don't let a TLS client that stops reading keep its connection alive by sending TLS records with no request data. ([GHSA-x646-wp7c-h9mg](https://github.com/puma/puma/security/advisories/GHSA-x646-wp7c-h9mg))
|
|
7
|
+
* Bound how long a graceful shutdown or restart keeps reading an incomplete request, with the new `shutdown_body_timeout` option (default 25 seconds). ([GHSA-jm4r-w4xx-qxw7](https://github.com/puma/puma/security/advisories/GHSA-jm4r-w4xx-qxw7))
|
|
8
|
+
* Keep `+` literal in bind URL query values, and raise on an invalid `ssl_cipher_filter`. ([GHSA-r885-m39c-939g](https://github.com/puma/puma/security/advisories/GHSA-r885-m39c-939g))
|
|
9
|
+
* Verify the TLS certificate and hostname of `ssl://` control servers in `pumactl`, with the new `--control-ca-file` option for private CAs. ([GHSA-86xc-wrr9-xmmq](https://github.com/puma/puma/security/advisories/GHSA-86xc-wrr9-xmmq))
|
|
10
|
+
* Back off and rate-limit logging when `accept` runs out of file descriptors. ([GHSA-rf6g-xhmp-gm7v](https://github.com/puma/puma/security/advisories/GHSA-rf6g-xhmp-gm7v))
|
|
11
|
+
|
|
12
|
+
CVE IDs have been requested for these advisories and will be added here once assigned.
|
|
13
|
+
|
|
14
|
+
* Bugfixes
|
|
15
|
+
* Send the idle notification from a clustered worker only once per idle period, so the master no longer stops an active cluster or delays an idle shutdown ([#4101])
|
|
16
|
+
* Fix build errors on Windows arm64 with Ruby head ([#4071])
|
|
17
|
+
* Send TLS `close_notify` when closing an SSL connection on JRuby ([#4029])
|
|
18
|
+
|
|
1
19
|
## 8.0.2 / 2026-05-27
|
|
2
20
|
|
|
3
21
|
* Bugfixes
|
|
@@ -2341,6 +2359,9 @@ be added back in a future date when a java Puma::MiniSSL is added.
|
|
|
2341
2359
|
* Bugfixes
|
|
2342
2360
|
* Your bugfix goes here <Most recent on the top, like GitHub> (#Github Number)
|
|
2343
2361
|
|
|
2362
|
+
[#4101]:https://github.com/puma/puma/pull/4101 "PR by Nate Berkopec, merged 2026-11-13"
|
|
2363
|
+
[#4071]:https://github.com/puma/puma/pull/4071 "PR by Nate Berkopec, merged 2026-11-09"
|
|
2364
|
+
[#4029]:https://github.com/puma/puma/pull/4029 "PR by Islam Elsayed, merged 2026-09-28"
|
|
2344
2365
|
[#3944]:https://github.com/puma/puma/pull/3944 "PR by Nate Berkopec, merged 2026-05-26"
|
|
2345
2366
|
[#3929]:https://github.com/puma/puma/pull/3929 "PR by Joshua Young, merged 2026-04-26"
|
|
2346
2367
|
[#3928]:https://github.com/puma/puma/pull/3928 "PR by Nate Berkopec, merged 2026-04-16"
|
data/ext/puma_http11/mini_ssl.c
CHANGED
|
@@ -440,7 +440,14 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
|
|
|
440
440
|
|
|
441
441
|
if (!NIL_P(ssl_cipher_filter)) {
|
|
442
442
|
StringValue(ssl_cipher_filter);
|
|
443
|
-
|
|
443
|
+
ERR_clear_error();
|
|
444
|
+
if (SSL_CTX_set_cipher_list(ctx, RSTRING_PTR(ssl_cipher_filter)) != 1) {
|
|
445
|
+
if (ERR_GET_REASON(ERR_peek_last_error()) == SSL_R_NO_CIPHER_MATCH) {
|
|
446
|
+
ERR_clear_error();
|
|
447
|
+
} else {
|
|
448
|
+
raise_param_error("SSL_CTX_set_cipher_list", "ssl_cipher_filter");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
444
451
|
}
|
|
445
452
|
else {
|
|
446
453
|
SSL_CTX_set_cipher_list(ctx, "HIGH:!aNULL@STRENGTH");
|
|
@@ -383,36 +383,41 @@ public class MiniSSL extends RubyObject { // MiniSSL::Engine
|
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
MiniSSLBuffer inboundAppData = new MiniSSLBuffer(engine.getSession().getApplicationBufferSize());
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
386
|
+
int consumedTo;
|
|
387
|
+
do {
|
|
388
|
+
consumedTo = inboundNetData.position();
|
|
389
|
+
doOp(SSLOperation.UNWRAP, inboundNetData, inboundAppData);
|
|
390
|
+
|
|
391
|
+
HandshakeStatus handshakeStatus = engine.getHandshakeStatus();
|
|
392
|
+
boolean done = false;
|
|
393
|
+
while (!done) {
|
|
394
|
+
SSLEngineResult res;
|
|
395
|
+
switch (handshakeStatus) {
|
|
396
|
+
case NEED_WRAP:
|
|
397
|
+
res = doOp(SSLOperation.WRAP, inboundAppData, outboundNetData);
|
|
398
|
+
handshakeStatus = res.getHandshakeStatus();
|
|
399
|
+
break;
|
|
400
|
+
case NEED_UNWRAP:
|
|
401
|
+
res = doOp(SSLOperation.UNWRAP, inboundNetData, inboundAppData);
|
|
402
|
+
if (res.getStatus() == Status.BUFFER_UNDERFLOW) {
|
|
403
|
+
// need more data before we can shake more hands
|
|
404
|
+
done = true;
|
|
405
|
+
}
|
|
406
|
+
handshakeStatus = res.getHandshakeStatus();
|
|
407
|
+
break;
|
|
408
|
+
case NEED_TASK:
|
|
409
|
+
Runnable runnable;
|
|
410
|
+
while ((runnable = engine.getDelegatedTask()) != null) {
|
|
411
|
+
runnable.run();
|
|
412
|
+
}
|
|
413
|
+
handshakeStatus = engine.getHandshakeStatus();
|
|
414
|
+
break;
|
|
415
|
+
default:
|
|
401
416
|
done = true;
|
|
402
|
-
|
|
403
|
-
handshakeStatus = res.getHandshakeStatus();
|
|
404
|
-
break;
|
|
405
|
-
case NEED_TASK:
|
|
406
|
-
Runnable runnable;
|
|
407
|
-
while ((runnable = engine.getDelegatedTask()) != null) {
|
|
408
|
-
runnable.run();
|
|
409
|
-
}
|
|
410
|
-
handshakeStatus = engine.getHandshakeStatus();
|
|
411
|
-
break;
|
|
412
|
-
default:
|
|
413
|
-
done = true;
|
|
417
|
+
}
|
|
414
418
|
}
|
|
415
|
-
}
|
|
419
|
+
} while (inboundAppData.position() == 0 && !closed &&
|
|
420
|
+
inboundNetData.hasRemaining() && inboundNetData.position() > consumedTo);
|
|
416
421
|
|
|
417
422
|
if (inboundNetData.hasRemaining()) {
|
|
418
423
|
inboundNetData.compact();
|
|
@@ -482,14 +487,19 @@ public class MiniSSL extends RubyObject { // MiniSSL::Engine
|
|
|
482
487
|
|
|
483
488
|
@JRubyMethod
|
|
484
489
|
public IRubyObject shutdown() {
|
|
485
|
-
if
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
490
|
+
// The peer may be speaking plain HTTP if the handshake did not finish.
|
|
491
|
+
if (closed || !handshake || engine.isInboundDone() && engine.isOutboundDone()) {
|
|
492
|
+
return getRuntime().getTrue();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Generate close_notify for MiniSSL::Socket#close to send.
|
|
496
|
+
engine.closeOutbound();
|
|
497
|
+
try {
|
|
498
|
+
doOp(SSLOperation.WRAP, outboundAppData, outboundNetData);
|
|
499
|
+
} catch (SSLException e) {
|
|
489
500
|
return getRuntime().getTrue();
|
|
490
|
-
} else {
|
|
491
|
-
return getRuntime().getFalse();
|
|
492
501
|
}
|
|
502
|
+
return getRuntime().getFalse();
|
|
493
503
|
}
|
|
494
504
|
|
|
495
505
|
private static RubyClass getSSLError(Ruby runtime) {
|
|
@@ -385,7 +385,8 @@ static VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data, VALUE st
|
|
|
385
385
|
long dlen = 0;
|
|
386
386
|
|
|
387
387
|
from = FIX2INT(start);
|
|
388
|
-
|
|
388
|
+
dptr = RSTRING_PTR(data);
|
|
389
|
+
dlen = (long)RSTRING_LEN(data);
|
|
389
390
|
|
|
390
391
|
if(from >= dlen) {
|
|
391
392
|
rb_raise(eHttpParserError, "%s", "Requested start is after data buffer end.");
|
|
@@ -398,7 +399,7 @@ static VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data, VALUE st
|
|
|
398
399
|
if(puma_parser_has_error(http)) {
|
|
399
400
|
rb_raise(eHttpParserError, "%s", "Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?");
|
|
400
401
|
} else {
|
|
401
|
-
return
|
|
402
|
+
return SIZET2NUM(puma_parser_nread(http));
|
|
402
403
|
}
|
|
403
404
|
}
|
|
404
405
|
}
|
|
@@ -444,7 +445,7 @@ static VALUE HttpParser_nread(VALUE self)
|
|
|
444
445
|
{
|
|
445
446
|
puma_parser *http = HttpParser_unwrap(self);
|
|
446
447
|
|
|
447
|
-
return
|
|
448
|
+
return SIZET2NUM(http->nread);
|
|
448
449
|
}
|
|
449
450
|
|
|
450
451
|
/**
|
data/lib/puma/client.rb
CHANGED
|
@@ -54,6 +54,8 @@ module Puma
|
|
|
54
54
|
# chunk header.
|
|
55
55
|
MAX_CHUNK_HEADER_SIZE = 4096
|
|
56
56
|
|
|
57
|
+
SHUTDOWN_READ_GRACE = 1
|
|
58
|
+
|
|
57
59
|
# The maximum amount of excess data the client sends
|
|
58
60
|
# using chunk size extensions before we abort the connection.
|
|
59
61
|
MAX_CHUNK_EXCESS = 16 * 1024
|
|
@@ -74,11 +76,12 @@ module Puma
|
|
|
74
76
|
|
|
75
77
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
|
76
78
|
:tempfile, :io_buffer, :http_content_length_limit_exceeded,
|
|
77
|
-
:requests_served, :error_status_code
|
|
79
|
+
:requests_served, :error_status_code, :bytes_read
|
|
78
80
|
|
|
79
81
|
attr_writer :peerip, :http_content_length_limit, :supported_http_methods
|
|
80
82
|
|
|
81
|
-
attr_accessor :remote_addr_header, :listener, :env_set_http_version
|
|
83
|
+
attr_accessor :remote_addr_header, :listener, :env_set_http_version,
|
|
84
|
+
:reactor_error
|
|
82
85
|
|
|
83
86
|
def initialize(io, env=nil)
|
|
84
87
|
@io = io
|
|
@@ -106,6 +109,7 @@ module Puma
|
|
|
106
109
|
@http_content_length_limit = nil
|
|
107
110
|
@http_content_length_limit_exceeded = nil
|
|
108
111
|
@error_status_code = nil
|
|
112
|
+
@reactor_error = nil
|
|
109
113
|
|
|
110
114
|
@peerip = nil
|
|
111
115
|
@peer_family = nil
|
|
@@ -114,6 +118,8 @@ module Puma
|
|
|
114
118
|
@expect_proxy_proto = false
|
|
115
119
|
|
|
116
120
|
@body_remain = 0
|
|
121
|
+
@bytes_read = 0
|
|
122
|
+
@pending_write = nil
|
|
117
123
|
|
|
118
124
|
@in_last_chunk = false
|
|
119
125
|
|
|
@@ -248,8 +254,9 @@ module Puma
|
|
|
248
254
|
true
|
|
249
255
|
end
|
|
250
256
|
|
|
251
|
-
def try_to_finish
|
|
252
|
-
return
|
|
257
|
+
def try_to_finish(&deadline)
|
|
258
|
+
return false unless flush_pending_write
|
|
259
|
+
return read_body(&deadline) if in_data_phase
|
|
253
260
|
|
|
254
261
|
data = nil
|
|
255
262
|
begin
|
|
@@ -269,6 +276,7 @@ module Puma
|
|
|
269
276
|
raise EOFError
|
|
270
277
|
end
|
|
271
278
|
|
|
279
|
+
@bytes_read += data.bytesize
|
|
272
280
|
if @buffer
|
|
273
281
|
@buffer << data
|
|
274
282
|
else
|
|
@@ -282,19 +290,55 @@ module Puma
|
|
|
282
290
|
@parser.finished? ? process_env_body : false
|
|
283
291
|
end
|
|
284
292
|
|
|
285
|
-
def eagerly_finish
|
|
293
|
+
def eagerly_finish(&deadline)
|
|
286
294
|
return true if @ready
|
|
287
|
-
while @to_io.wait_readable(0)
|
|
288
|
-
|
|
295
|
+
while !write_pending? && @to_io.wait_readable(0)
|
|
296
|
+
read = @bytes_read
|
|
297
|
+
return true if try_to_finish(&deadline)
|
|
298
|
+
check_deadline(&deadline) if deadline
|
|
299
|
+
return false if @bytes_read == read
|
|
289
300
|
end
|
|
290
301
|
false
|
|
291
302
|
end
|
|
292
303
|
|
|
293
|
-
def finish(timeout)
|
|
304
|
+
def finish(timeout, &deadline)
|
|
294
305
|
return if @ready
|
|
295
|
-
|
|
306
|
+
set_timeout timeout
|
|
307
|
+
read = @bytes_read
|
|
308
|
+
until try_to_finish(&deadline)
|
|
309
|
+
if @bytes_read > read
|
|
310
|
+
set_timeout timeout
|
|
311
|
+
read = @bytes_read
|
|
312
|
+
elsif self.timeout == 0
|
|
313
|
+
timeout!
|
|
314
|
+
end
|
|
315
|
+
if deadline
|
|
316
|
+
check_deadline(&deadline)
|
|
317
|
+
limit = yield
|
|
318
|
+
wait = [self.timeout, limit ? limit - Process.clock_gettime(Process::CLOCK_MONOTONIC) : 0.1].min
|
|
319
|
+
wait_io(wait) if wait > 0
|
|
320
|
+
else
|
|
321
|
+
wait_io(self.timeout) || timeout!
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def write_pending?
|
|
327
|
+
!@pending_write.nil? || (@io.respond_to?(:write_pending?) && @io.write_pending?)
|
|
296
328
|
end
|
|
297
329
|
|
|
330
|
+
def take_pending_write
|
|
331
|
+
str = @pending_write
|
|
332
|
+
@pending_write = nil
|
|
333
|
+
str
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def wait_io(timeout)
|
|
337
|
+
return @to_io.wait_readable(timeout) unless write_pending?
|
|
338
|
+
@to_io.wait_writable(timeout)
|
|
339
|
+
end
|
|
340
|
+
private :wait_io
|
|
341
|
+
|
|
298
342
|
# Wraps `@parser.execute` and adds meaningful error messages
|
|
299
343
|
# @return [Integer] bytes of buffer read by parser
|
|
300
344
|
#
|
|
@@ -354,11 +398,41 @@ module Puma
|
|
|
354
398
|
|
|
355
399
|
def write_error(status_code)
|
|
356
400
|
begin
|
|
357
|
-
|
|
401
|
+
write_interim ERROR_RESPONSE[status_code]
|
|
358
402
|
rescue StandardError
|
|
359
403
|
end
|
|
360
404
|
end
|
|
361
405
|
|
|
406
|
+
def write_interim(str)
|
|
407
|
+
if @pending_write
|
|
408
|
+
@pending_write << str
|
|
409
|
+
return flush_pending_write
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
n = @io.write_nonblock(str, exception: false)
|
|
413
|
+
if n == :wait_writable
|
|
414
|
+
@pending_write = +str unless @io.respond_to?(:write_pending?) && @io.write_pending?
|
|
415
|
+
elsif n < str.bytesize
|
|
416
|
+
@pending_write = str.byteslice(n..-1)
|
|
417
|
+
end
|
|
418
|
+
rescue SystemCallError, IOError
|
|
419
|
+
raise ConnectionError, "Connection error detected during write"
|
|
420
|
+
end
|
|
421
|
+
private :write_interim
|
|
422
|
+
|
|
423
|
+
def flush_pending_write
|
|
424
|
+
return true unless @pending_write
|
|
425
|
+
|
|
426
|
+
n = @io.write_nonblock(@pending_write, exception: false)
|
|
427
|
+
return false if n == :wait_writable
|
|
428
|
+
|
|
429
|
+
@pending_write = n == @pending_write.bytesize ? nil : @pending_write.byteslice(n..-1)
|
|
430
|
+
@pending_write.nil?
|
|
431
|
+
rescue SystemCallError, IOError
|
|
432
|
+
raise ConnectionError, "Connection error detected during write"
|
|
433
|
+
end
|
|
434
|
+
private :flush_pending_write
|
|
435
|
+
|
|
362
436
|
def peerip
|
|
363
437
|
return @peerip if @peerip
|
|
364
438
|
|
|
@@ -426,8 +500,7 @@ module Puma
|
|
|
426
500
|
|
|
427
501
|
if @env[HTTP_EXPECT] == CONTINUE
|
|
428
502
|
# TODO allow a hook here to check the headers before going forward
|
|
429
|
-
|
|
430
|
-
@io.flush
|
|
503
|
+
write_interim HTTP_11_100
|
|
431
504
|
end
|
|
432
505
|
|
|
433
506
|
@read_header = false
|
|
@@ -522,9 +595,9 @@ module Puma
|
|
|
522
595
|
false
|
|
523
596
|
end
|
|
524
597
|
|
|
525
|
-
def read_body
|
|
598
|
+
def read_body(&deadline)
|
|
526
599
|
if @chunked_body
|
|
527
|
-
return read_chunked_body
|
|
600
|
+
return read_chunked_body(&deadline)
|
|
528
601
|
end
|
|
529
602
|
|
|
530
603
|
# Read an odd sized chunk so we can read even sized ones
|
|
@@ -549,6 +622,7 @@ module Puma
|
|
|
549
622
|
raise EOFError
|
|
550
623
|
end
|
|
551
624
|
|
|
625
|
+
@bytes_read += chunk.bytesize
|
|
552
626
|
remain -= @body.write(chunk)
|
|
553
627
|
end
|
|
554
628
|
|
|
@@ -563,7 +637,7 @@ module Puma
|
|
|
563
637
|
end
|
|
564
638
|
end
|
|
565
639
|
|
|
566
|
-
def read_chunked_body
|
|
640
|
+
def read_chunked_body(&deadline)
|
|
567
641
|
while true
|
|
568
642
|
begin
|
|
569
643
|
chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
|
|
@@ -581,13 +655,27 @@ module Puma
|
|
|
581
655
|
raise EOFError
|
|
582
656
|
end
|
|
583
657
|
|
|
658
|
+
@bytes_read += chunk.bytesize
|
|
584
659
|
if decode_chunk(chunk)
|
|
585
660
|
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
|
586
661
|
return true
|
|
587
662
|
end
|
|
663
|
+
|
|
664
|
+
check_deadline(&deadline) if deadline
|
|
588
665
|
end
|
|
589
666
|
end
|
|
590
667
|
|
|
668
|
+
def check_deadline
|
|
669
|
+
limit = yield
|
|
670
|
+
return unless limit
|
|
671
|
+
|
|
672
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
673
|
+
return if now < limit
|
|
674
|
+
return if now < limit + SHUTDOWN_READ_GRACE && @to_io.wait_readable(0)
|
|
675
|
+
|
|
676
|
+
timeout!
|
|
677
|
+
end
|
|
678
|
+
|
|
591
679
|
def setup_chunked_body(body)
|
|
592
680
|
@chunked_body = true
|
|
593
681
|
@partial_part_left = 0
|
data/lib/puma/configuration.rb
CHANGED
|
@@ -144,6 +144,7 @@ module Puma
|
|
|
144
144
|
first_data_timeout: 30,
|
|
145
145
|
force_shutdown_after: -1,
|
|
146
146
|
http_content_length_limit: nil,
|
|
147
|
+
shutdown_body_timeout: 25,
|
|
147
148
|
# Number of seconds to wait until the next request before shutting down.
|
|
148
149
|
idle_timeout: nil,
|
|
149
150
|
io_selector_backend: :auto,
|
data/lib/puma/const.rb
CHANGED
|
@@ -100,7 +100,7 @@ module Puma
|
|
|
100
100
|
# too taxing on performance.
|
|
101
101
|
module Const
|
|
102
102
|
|
|
103
|
-
PUMA_VERSION = VERSION = "8.0.
|
|
103
|
+
PUMA_VERSION = VERSION = "8.0.3"
|
|
104
104
|
CODE_NAME = "Into the Arena"
|
|
105
105
|
|
|
106
106
|
PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
|
|
@@ -109,6 +109,9 @@ module Puma
|
|
|
109
109
|
# sending data back
|
|
110
110
|
WRITE_TIMEOUT = 10
|
|
111
111
|
|
|
112
|
+
# How long the accept loop waits after running out of file descriptors
|
|
113
|
+
ACCEPT_ERROR_DELAY = 0.1
|
|
114
|
+
|
|
112
115
|
# The original URI requested by the client.
|
|
113
116
|
REQUEST_URI= "REQUEST_URI"
|
|
114
117
|
REQUEST_PATH = "REQUEST_PATH"
|
data/lib/puma/control_cli.rb
CHANGED
|
@@ -44,6 +44,7 @@ module Puma
|
|
|
44
44
|
@pid = nil
|
|
45
45
|
@control_url = nil
|
|
46
46
|
@control_auth_token = nil
|
|
47
|
+
@control_ca_file = nil
|
|
47
48
|
@config_file = nil
|
|
48
49
|
@command = nil
|
|
49
50
|
@environment = env['APP_ENV'] || env['RACK_ENV'] || env['RAILS_ENV']
|
|
@@ -80,6 +81,10 @@ module Puma
|
|
|
80
81
|
@control_auth_token = arg
|
|
81
82
|
end
|
|
82
83
|
|
|
84
|
+
o.on "--control-ca-file PATH", "CA certificate bundle for the TLS control server" do |arg|
|
|
85
|
+
@control_ca_file = arg
|
|
86
|
+
end
|
|
87
|
+
|
|
83
88
|
o.on "-F", "--config-file PATH", "Puma config script" do |arg|
|
|
84
89
|
@config_file = arg
|
|
85
90
|
end
|
|
@@ -179,11 +184,16 @@ module Puma
|
|
|
179
184
|
when 'ssl'
|
|
180
185
|
require 'openssl'
|
|
181
186
|
host = host[1..-2] if host&.start_with? '['
|
|
182
|
-
OpenSSL::SSL::
|
|
187
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
188
|
+
ctx.ca_file = @control_ca_file if @control_ca_file
|
|
189
|
+
ctx.set_params
|
|
190
|
+
ssl = OpenSSL::SSL::SSLSocket.new(
|
|
183
191
|
TCPSocket.new(host, uri.port),
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
ctx)
|
|
193
|
+
ssl.tap { |socket| socket.sync_close = true }
|
|
194
|
+
.tap { |socket| socket.hostname = host }
|
|
186
195
|
.tap(&:connect)
|
|
196
|
+
.tap { |socket| socket.post_connection_check host }
|
|
187
197
|
when 'tcp'
|
|
188
198
|
host = host[1..-2] if host&.start_with? '['
|
|
189
199
|
TCPSocket.new host, uri.port
|
|
@@ -230,6 +240,7 @@ module Puma
|
|
|
230
240
|
message response.last if PRINTABLE_COMMANDS.include?(@command)
|
|
231
241
|
end
|
|
232
242
|
ensure
|
|
243
|
+
server ||= ssl
|
|
233
244
|
if server
|
|
234
245
|
if uri.scheme == 'ssl'
|
|
235
246
|
server.sysclose
|
data/lib/puma/dsl.rb
CHANGED
|
@@ -399,7 +399,20 @@ module Puma
|
|
|
399
399
|
@options[:drain_on_shutdown] = which
|
|
400
400
|
end
|
|
401
401
|
|
|
402
|
-
#
|
|
402
|
+
# Define how long an incomplete request may continue to be read during shutdown.
|
|
403
|
+
#
|
|
404
|
+
# The default is 25 seconds.
|
|
405
|
+
#
|
|
406
|
+
# @example
|
|
407
|
+
# shutdown_body_timeout 10
|
|
408
|
+
#
|
|
409
|
+
# @see Puma::Server#graceful_shutdown
|
|
410
|
+
#
|
|
411
|
+
def shutdown_body_timeout(seconds)
|
|
412
|
+
@options[:shutdown_body_timeout] = Float(seconds)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Set the environment in which the Rack app will run. The value must be
|
|
403
416
|
# a string.
|
|
404
417
|
#
|
|
405
418
|
# The default is "development".
|
data/lib/puma/minissl.rb
CHANGED
|
@@ -25,6 +25,9 @@ module Puma
|
|
|
25
25
|
@engine = engine
|
|
26
26
|
@peercert = nil
|
|
27
27
|
@reuse = nil
|
|
28
|
+
@write_buffer = +''
|
|
29
|
+
@write_owed = 0
|
|
30
|
+
@write_owed_data = nil
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
# @!attribute [r] to_io
|
|
@@ -68,9 +71,8 @@ module Puma
|
|
|
68
71
|
|
|
69
72
|
return output if output
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
end
|
|
74
|
+
queue_ciphertext
|
|
75
|
+
flush_write_buffer
|
|
74
76
|
end
|
|
75
77
|
end
|
|
76
78
|
|
|
@@ -85,52 +87,60 @@ module Puma
|
|
|
85
87
|
def read_nonblock(size, *_)
|
|
86
88
|
# *_ is to deal with keyword args that were added
|
|
87
89
|
# at some point (and being used in the wild)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
90
|
+
output = engine_read_all
|
|
91
|
+
return output if output
|
|
92
|
+
|
|
93
|
+
raise IO::EAGAINWaitReadable unless flush_write_buffer_nonblock
|
|
94
|
+
|
|
95
|
+
data = @socket.read_nonblock(size, exception: false)
|
|
96
|
+
if data == :wait_readable || data == :wait_writable
|
|
97
|
+
# It would make more sense to let @socket.read_nonblock raise
|
|
98
|
+
# EAGAIN if necessary but it seems like it'll misbehave on Windows.
|
|
99
|
+
# I don't have a Windows machine to debug this so I can't explain
|
|
100
|
+
# exactly whats happening in that OS. Please let me know if you
|
|
101
|
+
# find out!
|
|
102
|
+
#
|
|
103
|
+
# In the meantime, we can emulate the correct behavior by
|
|
104
|
+
# capturing :wait_readable & :wait_writable and raising EAGAIN
|
|
105
|
+
# ourselves.
|
|
106
|
+
raise IO::EAGAINWaitReadable
|
|
107
|
+
elsif data.nil?
|
|
108
|
+
raise SSLError.exception "HTTP connection?" if bad_tlsv1_3?
|
|
109
|
+
return nil
|
|
110
|
+
end
|
|
108
111
|
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
@engine.inject(data)
|
|
113
|
+
output = engine_read_all
|
|
114
|
+
return output if output
|
|
111
115
|
|
|
112
|
-
|
|
116
|
+
queue_ciphertext
|
|
117
|
+
flush_write_buffer_nonblock
|
|
118
|
+
raise IO::EAGAINWaitReadable
|
|
119
|
+
end
|
|
113
120
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
end
|
|
117
|
-
end
|
|
121
|
+
def write_pending?
|
|
122
|
+
!@write_buffer.empty?
|
|
118
123
|
end
|
|
119
124
|
|
|
120
125
|
def write(data)
|
|
121
126
|
return 0 if data.empty?
|
|
122
127
|
|
|
123
128
|
data_size = data.bytesize
|
|
124
|
-
|
|
129
|
+
|
|
130
|
+
flush_write_buffer
|
|
131
|
+
if @write_owed_data && data.byteslice(0, @write_owed_data.bytesize).b == @write_owed_data
|
|
132
|
+
data = data.byteslice(@write_owed..-1)
|
|
133
|
+
@write_owed = 0
|
|
134
|
+
@write_owed_data = nil
|
|
135
|
+
return data_size if data.empty?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
need = data.bytesize
|
|
125
139
|
|
|
126
140
|
while true
|
|
127
141
|
wrote = @engine.write data
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
while (enc = @engine.extract)
|
|
131
|
-
enc_wr << enc
|
|
132
|
-
end
|
|
133
|
-
@socket.write enc_wr unless enc_wr.empty?
|
|
142
|
+
queue_ciphertext
|
|
143
|
+
flush_write_buffer
|
|
134
144
|
|
|
135
145
|
need -= wrote
|
|
136
146
|
|
|
@@ -143,34 +153,43 @@ module Puma
|
|
|
143
153
|
alias_method :syswrite, :write
|
|
144
154
|
alias_method :<<, :write
|
|
145
155
|
|
|
146
|
-
#
|
|
147
|
-
|
|
156
|
+
# As with SSL_write, retry with the same data after IO::EAGAINWaitWritable
|
|
157
|
+
def write_nonblock(data, exception: true)
|
|
158
|
+
return 0 if data.empty?
|
|
148
159
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
@write_owed = 0 if @write_owed_data && data.byteslice(0, @write_owed_data.bytesize).b != @write_owed_data
|
|
161
|
+
|
|
162
|
+
if @write_owed == 0
|
|
163
|
+
@write_owed = @engine.write data
|
|
164
|
+
@write_owed_data = nil
|
|
165
|
+
queue_ciphertext
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
unless flush_write_buffer_nonblock
|
|
169
|
+
@write_owed_data ||= data.byteslice(0, @write_owed).b
|
|
170
|
+
raise IO::EAGAINWaitWritable if exception
|
|
171
|
+
return :wait_writable
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
wrote = @write_owed
|
|
175
|
+
@write_owed = 0
|
|
176
|
+
@write_owed_data = nil
|
|
177
|
+
wrote
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def wait_writable(timeout = nil)
|
|
181
|
+
@socket.wait_writable timeout
|
|
161
182
|
end
|
|
162
183
|
|
|
163
184
|
def flush
|
|
185
|
+
flush_write_buffer
|
|
164
186
|
@socket.flush
|
|
165
187
|
end
|
|
166
188
|
|
|
167
189
|
def close
|
|
168
190
|
begin
|
|
169
|
-
unless @engine.shutdown
|
|
170
|
-
|
|
171
|
-
@socket.write alert_data
|
|
172
|
-
end
|
|
173
|
-
end
|
|
191
|
+
queue_ciphertext unless @engine.shutdown
|
|
192
|
+
flush_write_buffer_nonblock
|
|
174
193
|
rescue IOError, SystemCallError
|
|
175
194
|
# nothing
|
|
176
195
|
ensure
|
|
@@ -178,6 +197,32 @@ module Puma
|
|
|
178
197
|
end
|
|
179
198
|
end
|
|
180
199
|
|
|
200
|
+
def queue_ciphertext
|
|
201
|
+
while (enc = @engine.extract)
|
|
202
|
+
@write_buffer << enc
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
private :queue_ciphertext
|
|
206
|
+
|
|
207
|
+
def flush_write_buffer
|
|
208
|
+
return if @write_buffer.empty?
|
|
209
|
+
|
|
210
|
+
@socket.write @write_buffer
|
|
211
|
+
@write_buffer = +''
|
|
212
|
+
end
|
|
213
|
+
private :flush_write_buffer
|
|
214
|
+
|
|
215
|
+
def flush_write_buffer_nonblock
|
|
216
|
+
until @write_buffer.empty?
|
|
217
|
+
n = @socket.write_nonblock @write_buffer, exception: false
|
|
218
|
+
return false if n == :wait_writable
|
|
219
|
+
|
|
220
|
+
@write_buffer = @write_buffer.byteslice(n..-1)
|
|
221
|
+
end
|
|
222
|
+
true
|
|
223
|
+
end
|
|
224
|
+
private :flush_write_buffer_nonblock
|
|
225
|
+
|
|
181
226
|
# @!attribute [r] peeraddr
|
|
182
227
|
def peeraddr
|
|
183
228
|
@socket.peeraddr
|
data/lib/puma/puma_http11.jar
CHANGED
|
Binary file
|
data/lib/puma/reactor.rb
CHANGED
|
@@ -34,6 +34,7 @@ module Puma
|
|
|
34
34
|
@selector = ::NIO::Selector.new(NIO::Selector.backends.delete(backend))
|
|
35
35
|
@input = Queue.new
|
|
36
36
|
@timeouts = []
|
|
37
|
+
@timeout_keys = {}.compare_by_identity
|
|
37
38
|
@block = block
|
|
38
39
|
@reactor_size = 0
|
|
39
40
|
@reactor_max = 0
|
|
@@ -77,11 +78,13 @@ module Puma
|
|
|
77
78
|
def select_loop
|
|
78
79
|
begin
|
|
79
80
|
until @input.closed? && @input.empty?
|
|
81
|
+
requeue_moved_timeouts
|
|
82
|
+
|
|
80
83
|
# Wakeup any registered object that receives incoming data.
|
|
81
84
|
# Block until the earliest timeout or Selector#wakeup is called.
|
|
82
85
|
timeout = (earliest = @timeouts.first) && earliest.timeout
|
|
83
86
|
@selector.select(timeout) do |monitor|
|
|
84
|
-
wakeup!(monitor.value)
|
|
87
|
+
monitor.interests = interests(monitor.value) unless wakeup!(monitor.value)
|
|
85
88
|
end
|
|
86
89
|
|
|
87
90
|
# Wakeup all objects that timed out.
|
|
@@ -93,7 +96,7 @@ module Puma
|
|
|
93
96
|
client = @input.pop
|
|
94
97
|
register(client) if client.io_ok?
|
|
95
98
|
end
|
|
96
|
-
@timeouts.sort_by!
|
|
99
|
+
@timeouts.sort_by! { |client| @timeout_keys[client] }
|
|
97
100
|
end
|
|
98
101
|
end
|
|
99
102
|
rescue StandardError => e
|
|
@@ -110,14 +113,19 @@ module Puma
|
|
|
110
113
|
|
|
111
114
|
# Start monitoring the object.
|
|
112
115
|
def register(client)
|
|
113
|
-
@selector.register(client.to_io,
|
|
116
|
+
@selector.register(client.to_io, interests(client)).value = client
|
|
114
117
|
@reactor_size += 1
|
|
115
118
|
@reactor_max = @reactor_size if @reactor_max < @reactor_size
|
|
116
119
|
@timeouts << client
|
|
120
|
+
@timeout_keys[client] = client.timeout_at
|
|
117
121
|
rescue ArgumentError
|
|
118
122
|
# unreadable clients raise error when processed by NIO
|
|
119
123
|
end
|
|
120
124
|
|
|
125
|
+
def interests(client)
|
|
126
|
+
client.write_pending? ? :w : :r
|
|
127
|
+
end
|
|
128
|
+
|
|
121
129
|
# 'Wake up' a monitored object by calling the provided block.
|
|
122
130
|
# Stop monitoring the object if the block returns `true`.
|
|
123
131
|
def wakeup!(client)
|
|
@@ -125,7 +133,31 @@ module Puma
|
|
|
125
133
|
@selector.deregister client.to_io
|
|
126
134
|
@reactor_size -= 1
|
|
127
135
|
@timeouts.delete client
|
|
136
|
+
@timeout_keys.delete client
|
|
137
|
+
true
|
|
138
|
+
else
|
|
139
|
+
if client.timeout_at < @timeout_keys[client]
|
|
140
|
+
@timeouts.delete client
|
|
141
|
+
requeue client
|
|
142
|
+
end
|
|
143
|
+
false
|
|
128
144
|
end
|
|
129
145
|
end
|
|
146
|
+
|
|
147
|
+
# `@timeouts` is sorted by the timeout each client had when it was queued.
|
|
148
|
+
# Timeouts that moved earlier are requeued in `wakeup!`. Timeouts that moved
|
|
149
|
+
# later are requeued here, once the client reaches the front.
|
|
150
|
+
def requeue_moved_timeouts
|
|
151
|
+
while (client = @timeouts.first) && @timeout_keys[client] < client.timeout_at
|
|
152
|
+
@timeouts.shift
|
|
153
|
+
requeue client
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def requeue(client)
|
|
158
|
+
key = @timeout_keys[client] = client.timeout_at
|
|
159
|
+
index = @timeouts.bsearch_index { |c| @timeout_keys[c] > key } || @timeouts.size
|
|
160
|
+
@timeouts.insert index, client
|
|
161
|
+
end
|
|
130
162
|
end
|
|
131
163
|
end
|
data/lib/puma/response.rb
CHANGED
|
@@ -59,6 +59,8 @@ module Puma
|
|
|
59
59
|
|
|
60
60
|
return :close if closed_socket?(socket)
|
|
61
61
|
|
|
62
|
+
write_pending_interim socket, client
|
|
63
|
+
|
|
62
64
|
if @early_hints
|
|
63
65
|
env[EARLY_HINTS] = lambda { |headers|
|
|
64
66
|
begin
|
|
@@ -148,6 +150,8 @@ module Puma
|
|
|
148
150
|
|
|
149
151
|
return :close if closed_socket?(socket)
|
|
150
152
|
|
|
153
|
+
write_pending_interim socket, client
|
|
154
|
+
|
|
151
155
|
# Close the connection after a reasonable number of inline requests
|
|
152
156
|
force_keep_alive = @enable_keep_alives && client.requests_served < @max_keep_alive
|
|
153
157
|
|
|
@@ -248,6 +252,12 @@ module Puma
|
|
|
248
252
|
!shutting_down? && keep_alive ? :keep_alive : :close
|
|
249
253
|
end
|
|
250
254
|
|
|
255
|
+
def write_pending_interim(socket, client)
|
|
256
|
+
pending = client.take_pending_write
|
|
257
|
+
fast_write_str socket, pending if pending
|
|
258
|
+
end
|
|
259
|
+
private :write_pending_interim
|
|
260
|
+
|
|
251
261
|
# Used to write 'early hints', 'no body' responses, 'hijacked' responses,
|
|
252
262
|
# and body segments (called by `fast_write_response`).
|
|
253
263
|
# Writes a string to a socket (normally `Client#io`) using `write_nonblock`.
|
|
@@ -297,7 +307,11 @@ module Puma
|
|
|
297
307
|
fast_write_str socket, io_buffer.read_and_reset
|
|
298
308
|
else
|
|
299
309
|
fast_write_str socket, io_buffer.read_and_reset
|
|
300
|
-
|
|
310
|
+
remaining = content_length
|
|
311
|
+
while remaining > 0 && (chunk = body.read([remaining, BODY_LEN_MAX].min))
|
|
312
|
+
fast_write_str socket, chunk
|
|
313
|
+
remaining -= chunk.bytesize
|
|
314
|
+
end
|
|
301
315
|
end
|
|
302
316
|
end
|
|
303
317
|
elsif body.is_a?(::Array) && body.length == 1
|
data/lib/puma/server.rb
CHANGED
|
@@ -51,7 +51,7 @@ module Puma
|
|
|
51
51
|
# @todo the following may be deprecated in the future
|
|
52
52
|
attr_reader :auto_trim_time, :early_hints, :first_data_timeout,
|
|
53
53
|
:leak_stack_on_error,
|
|
54
|
-
:persistent_timeout, :reaping_time
|
|
54
|
+
:persistent_timeout, :reaping_time, :shutdown_body_timeout
|
|
55
55
|
|
|
56
56
|
attr_accessor :app
|
|
57
57
|
attr_accessor :binder
|
|
@@ -98,6 +98,8 @@ module Puma
|
|
|
98
98
|
@early_hints = @options[:early_hints]
|
|
99
99
|
@first_data_timeout = @options[:first_data_timeout]
|
|
100
100
|
@persistent_timeout = @options[:persistent_timeout]
|
|
101
|
+
@shutdown_body_timeout = @options[:shutdown_body_timeout]
|
|
102
|
+
@shutdown_body_deadline = nil
|
|
101
103
|
@idle_timeout = @options[:idle_timeout]
|
|
102
104
|
@min_threads = @options[:min_threads]
|
|
103
105
|
@max_threads = @options[:max_threads]
|
|
@@ -258,6 +260,7 @@ module Puma
|
|
|
258
260
|
@events.fire :state, :booting
|
|
259
261
|
|
|
260
262
|
@status = :run
|
|
263
|
+
@shutdown_body_deadline = nil
|
|
261
264
|
|
|
262
265
|
@thread_pool = ThreadPool.new(thread_name, options, server: self) do |processor, client|
|
|
263
266
|
process_client(processor, client)
|
|
@@ -312,6 +315,9 @@ module Puma
|
|
|
312
315
|
# If a client object times out, a 408 response is written, its connection is closed,
|
|
313
316
|
# and the object is removed from the reactor (return `true`).
|
|
314
317
|
#
|
|
318
|
+
# If the request cannot be read, the Client is passed to the ThreadPool with its
|
|
319
|
+
# `reactor_error` set, so that a processor thread writes the error response.
|
|
320
|
+
#
|
|
315
321
|
# If the Reactor is shutting down, all Clients are either timed out or passed to the
|
|
316
322
|
# ThreadPool, depending on their current state (#can_close?).
|
|
317
323
|
#
|
|
@@ -320,18 +326,33 @@ module Puma
|
|
|
320
326
|
# will wake up and again be checked to see if it's ready to be passed to the thread pool.
|
|
321
327
|
def reactor_wakeup(client)
|
|
322
328
|
shutdown = !@queue_requests
|
|
323
|
-
|
|
324
|
-
|
|
329
|
+
read = client.bytes_read
|
|
330
|
+
if client.try_to_finish { shutdown_body_limit } || (shutdown && !client.can_close?)
|
|
331
|
+
hand_to_thread_pool client
|
|
325
332
|
elsif shutdown || client.timeout == 0
|
|
326
333
|
client.timeout!
|
|
327
334
|
else
|
|
328
|
-
client.set_timeout(@first_data_timeout)
|
|
335
|
+
client.set_timeout(@first_data_timeout) if client.bytes_read > read
|
|
329
336
|
false
|
|
330
337
|
end
|
|
331
|
-
rescue
|
|
332
|
-
client_error(e, client)
|
|
338
|
+
rescue ConnectionError, EOFError
|
|
333
339
|
close_client_safely(client)
|
|
334
340
|
true
|
|
341
|
+
rescue StandardError => e
|
|
342
|
+
# Writing the error response may block on a client that is not reading,
|
|
343
|
+
# so leave it to a thread pool thread.
|
|
344
|
+
client.reactor_error = e
|
|
345
|
+
hand_to_thread_pool client
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def hand_to_thread_pool(client)
|
|
349
|
+
@thread_pool << client
|
|
350
|
+
true
|
|
351
|
+
rescue ThreadError
|
|
352
|
+
true
|
|
353
|
+
rescue StandardError
|
|
354
|
+
close_client_safely client
|
|
355
|
+
true
|
|
335
356
|
end
|
|
336
357
|
|
|
337
358
|
def handle_servers
|
|
@@ -361,13 +382,16 @@ module Puma
|
|
|
361
382
|
ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : @idle_timeout)
|
|
362
383
|
unless ios
|
|
363
384
|
unless shutting_down?
|
|
364
|
-
@idle_timeout_reached = true
|
|
365
|
-
|
|
366
385
|
if @clustered
|
|
367
|
-
@
|
|
386
|
+
unless @idle_timeout_reached
|
|
387
|
+
@idle_timeout_reached = true
|
|
388
|
+
@worker_write << "#{PipeRequest::PIPE_IDLE}#{Process.pid}\n" rescue nil
|
|
389
|
+
end
|
|
368
390
|
next
|
|
369
391
|
else
|
|
392
|
+
@idle_timeout_reached = true
|
|
370
393
|
@log_writer.log "- Idle timeout reached"
|
|
394
|
+
shutdown_body_deadline
|
|
371
395
|
@status = :stop
|
|
372
396
|
end
|
|
373
397
|
end
|
|
@@ -401,6 +425,9 @@ module Puma
|
|
|
401
425
|
sock.accept_nonblock
|
|
402
426
|
rescue IO::WaitReadable
|
|
403
427
|
next
|
|
428
|
+
rescue Errno::EMFILE, Errno::ENFILE, Errno::ENOBUFS, Errno::ENOMEM => e
|
|
429
|
+
accept_resource_error e
|
|
430
|
+
next
|
|
404
431
|
end
|
|
405
432
|
drain += 1 if shutting_down?
|
|
406
433
|
|
|
@@ -413,11 +440,17 @@ module Puma
|
|
|
413
440
|
# In the case that any of the sockets are unexpectedly close.
|
|
414
441
|
raise
|
|
415
442
|
rescue StandardError => e
|
|
416
|
-
|
|
443
|
+
begin
|
|
444
|
+
@log_writer.unknown_error e, nil, "Listen loop"
|
|
445
|
+
rescue StandardError
|
|
446
|
+
end
|
|
417
447
|
end
|
|
418
448
|
end
|
|
419
449
|
|
|
420
|
-
|
|
450
|
+
begin
|
|
451
|
+
@log_writer.debug { "Drained #{drain} additional connections." } if drain
|
|
452
|
+
rescue StandardError
|
|
453
|
+
end
|
|
421
454
|
@events.fire :state, @status
|
|
422
455
|
|
|
423
456
|
if queue_requests
|
|
@@ -427,7 +460,10 @@ module Puma
|
|
|
427
460
|
|
|
428
461
|
graceful_shutdown if @status == :stop || @status == :restart
|
|
429
462
|
rescue Exception => e
|
|
430
|
-
|
|
463
|
+
begin
|
|
464
|
+
@log_writer.unknown_error e, nil, "Exception handling servers"
|
|
465
|
+
rescue StandardError
|
|
466
|
+
end
|
|
431
467
|
ensure
|
|
432
468
|
# Errno::EBADF is infrequently raised
|
|
433
469
|
[@check, @notify].each do |io|
|
|
@@ -459,12 +495,14 @@ module Puma
|
|
|
459
495
|
|
|
460
496
|
case cmd
|
|
461
497
|
when STOP_COMMAND
|
|
498
|
+
shutdown_body_deadline
|
|
462
499
|
@status = :stop
|
|
463
500
|
return true
|
|
464
501
|
when HALT_COMMAND
|
|
465
502
|
@status = :halt
|
|
466
503
|
return true
|
|
467
504
|
when RESTART_COMMAND
|
|
505
|
+
shutdown_body_deadline
|
|
468
506
|
@status = :restart
|
|
469
507
|
return true
|
|
470
508
|
end
|
|
@@ -488,7 +526,9 @@ module Puma
|
|
|
488
526
|
requests = 0
|
|
489
527
|
|
|
490
528
|
begin
|
|
491
|
-
|
|
529
|
+
raise client.reactor_error if client.reactor_error
|
|
530
|
+
|
|
531
|
+
if @queue_requests && !client.eagerly_finish { shutdown_body_limit }
|
|
492
532
|
|
|
493
533
|
client.set_timeout(@first_data_timeout)
|
|
494
534
|
if @reactor.add client
|
|
@@ -498,7 +538,7 @@ module Puma
|
|
|
498
538
|
end
|
|
499
539
|
|
|
500
540
|
with_force_shutdown(client) do
|
|
501
|
-
client.finish(@first_data_timeout)
|
|
541
|
+
client.finish(@first_data_timeout) { shutdown_body_limit }
|
|
502
542
|
end
|
|
503
543
|
|
|
504
544
|
can_loop = true
|
|
@@ -519,7 +559,7 @@ module Puma
|
|
|
519
559
|
next_request_ready = if client.has_back_to_back_requests?
|
|
520
560
|
with_force_shutdown(client) { client.process_back_to_back_requests }
|
|
521
561
|
else
|
|
522
|
-
with_force_shutdown(client) { client.eagerly_finish }
|
|
562
|
+
with_force_shutdown(client) { client.eagerly_finish { shutdown_body_limit } }
|
|
523
563
|
end
|
|
524
564
|
|
|
525
565
|
if next_request_ready
|
|
@@ -551,6 +591,25 @@ module Puma
|
|
|
551
591
|
end
|
|
552
592
|
end
|
|
553
593
|
|
|
594
|
+
# Accepting fails while the process is out of file descriptors or memory.
|
|
595
|
+
# The connection stays in the backlog, so retrying at once would spin and
|
|
596
|
+
# log at full speed. Wait briefly and log at most once per second.
|
|
597
|
+
def accept_resource_error(error)
|
|
598
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
599
|
+
if @accept_error_logged_at.nil? || now - @accept_error_logged_at >= 1
|
|
600
|
+
suffix = @accept_errors_suppressed.to_i > 0 ? " (#{@accept_errors_suppressed} more suppressed)" : ""
|
|
601
|
+
@accept_error_logged_at = now
|
|
602
|
+
@accept_errors_suppressed = 0
|
|
603
|
+
begin
|
|
604
|
+
@log_writer.unknown_error error, nil, "Listen loop#{suffix}"
|
|
605
|
+
rescue StandardError
|
|
606
|
+
end
|
|
607
|
+
else
|
|
608
|
+
@accept_errors_suppressed += 1
|
|
609
|
+
end
|
|
610
|
+
sleep ACCEPT_ERROR_DELAY
|
|
611
|
+
end
|
|
612
|
+
|
|
554
613
|
# :nodoc:
|
|
555
614
|
def close_client_safely(client)
|
|
556
615
|
client.close
|
|
@@ -628,9 +687,19 @@ module Puma
|
|
|
628
687
|
end
|
|
629
688
|
private :response_to_error
|
|
630
689
|
|
|
690
|
+
def shutdown_body_deadline
|
|
691
|
+
@shutdown_body_deadline ||= Process.clock_gettime(Process::CLOCK_MONOTONIC) + @shutdown_body_timeout
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def shutdown_body_limit
|
|
695
|
+
@shutdown_body_deadline
|
|
696
|
+
end
|
|
697
|
+
|
|
631
698
|
# Wait for all outstanding requests to finish.
|
|
632
699
|
#
|
|
633
700
|
def graceful_shutdown
|
|
701
|
+
shutdown_body_deadline
|
|
702
|
+
|
|
634
703
|
if @status != :restart
|
|
635
704
|
@binder.close
|
|
636
705
|
end
|
|
@@ -656,6 +725,7 @@ module Puma
|
|
|
656
725
|
# off the request queue before finally exiting.
|
|
657
726
|
|
|
658
727
|
def stop(sync=false)
|
|
728
|
+
shutdown_body_deadline
|
|
659
729
|
notify_safely(STOP_COMMAND)
|
|
660
730
|
@thread.join if @thread && sync
|
|
661
731
|
end
|
|
@@ -666,6 +736,7 @@ module Puma
|
|
|
666
736
|
end
|
|
667
737
|
|
|
668
738
|
def begin_restart(sync=false)
|
|
739
|
+
shutdown_body_deadline
|
|
669
740
|
notify_safely(RESTART_COMMAND)
|
|
670
741
|
@thread.join if @thread && sync
|
|
671
742
|
end
|
data/lib/puma/util.rb
CHANGED
|
@@ -13,21 +13,25 @@ module Puma
|
|
|
13
13
|
# Escapes and unescapes a URI escaped string with
|
|
14
14
|
# +encoding+. +encoding+ will be the target encoding of the string
|
|
15
15
|
# returned, and it defaults to UTF-8
|
|
16
|
+
#
|
|
17
|
+
# Unlike form encoding, escape writes a space as '%20' and unescape
|
|
18
|
+
# keeps a literal '+', so values such as OpenSSL cipher strings
|
|
19
|
+
# ("AES+SHA") survive.
|
|
16
20
|
if defined?(::Encoding)
|
|
17
21
|
def escape(s, encoding = Encoding::UTF_8)
|
|
18
|
-
URI.encode_www_form_component(s, encoding)
|
|
22
|
+
URI.encode_www_form_component(s, encoding).gsub('+', '%20')
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
def unescape(s, encoding = Encoding::UTF_8)
|
|
22
|
-
URI.decode_www_form_component(s, encoding)
|
|
26
|
+
URI.decode_www_form_component(s.gsub('+', '%2B'), encoding)
|
|
23
27
|
end
|
|
24
28
|
else
|
|
25
29
|
def escape(s, encoding = nil)
|
|
26
|
-
URI.encode_www_form_component(s, encoding)
|
|
30
|
+
URI.encode_www_form_component(s, encoding).gsub('+', '%20')
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
def unescape(s, encoding = nil)
|
|
30
|
-
URI.decode_www_form_component(s, encoding)
|
|
34
|
+
URI.decode_www_form_component(s.gsub('+', '%2B'), encoding)
|
|
31
35
|
end
|
|
32
36
|
end
|
|
33
37
|
module_function :unescape, :escape
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: puma
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 8.0.
|
|
4
|
+
version: 8.0.3
|
|
5
5
|
platform: java
|
|
6
6
|
authors:
|
|
7
7
|
- Evan Phoenix
|
|
@@ -157,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
157
157
|
- !ruby/object:Gem::Version
|
|
158
158
|
version: '0'
|
|
159
159
|
requirements: []
|
|
160
|
-
rubygems_version: 4.0.
|
|
160
|
+
rubygems_version: 4.0.23
|
|
161
161
|
specification_version: 4
|
|
162
162
|
summary: A Ruby/Rack web server built for parallelism.
|
|
163
163
|
test_files: []
|