puma 7.2.1-java → 7.2.2-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 +105 -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/request.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: 44db10ef8822503bf3f707c746f6bfded7e59250cd12a7721e8652cc43cc7c35
|
|
4
|
+
data.tar.gz: 00cfe4cae3673a99be3edea74213023841d090997f7e3c70907ba5ce99bcdff1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15823260017d91b72eafc5d23f220769537898946c170f40cafc7213118c7b78f937770bf535823b1933c5bfd718054a7976498b443128db743e16c8e8082879
|
|
7
|
+
data.tar.gz: 7780664361ab405eea483cf534ea0c418be6a3737d713986b093dd36ddaa5ec500dd0027003c0aba4e03227c4bb58d43c3e68254aba2b1e485b9d46ce862f24a
|
data/History.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## 7.2.2 / 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
|
## 7.2.1 / 2026-05-27
|
|
2
20
|
|
|
3
21
|
* Bugfixes
|
|
@@ -2300,6 +2318,9 @@ be added back in a future date when a java Puma::MiniSSL is added.
|
|
|
2300
2318
|
* Bugfixes
|
|
2301
2319
|
* Your bugfix goes here <Most recent on the top, like GitHub> (#Github Number)
|
|
2302
2320
|
|
|
2321
|
+
[#4101]:https://github.com/puma/puma/pull/4101 "PR by Nate Berkopec, merged 2026-11-13"
|
|
2322
|
+
[#4071]:https://github.com/puma/puma/pull/4071 "PR by Nate Berkopec, merged 2026-11-09"
|
|
2323
|
+
[#4029]:https://github.com/puma/puma/pull/4029 "PR by Islam Elsayed, merged 2026-09-28"
|
|
2303
2324
|
[#3947]:https://github.com/puma/puma/pull/3947 "PR by Nate Berkopec, merged 2026-05-26"
|
|
2304
2325
|
[#3863]:https://github.com/puma/puma/pull/3863 "PR by Nate Berkopec, merged 2026-01-20"
|
|
2305
2326
|
[#3861]:https://github.com/puma/puma/pull/3861 "PR by MSP-Greg, merged 2026-01-20"
|
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
|
@@ -47,6 +47,8 @@ module Puma
|
|
|
47
47
|
# chunk header.
|
|
48
48
|
MAX_CHUNK_HEADER_SIZE = 4096
|
|
49
49
|
|
|
50
|
+
SHUTDOWN_READ_GRACE = 1
|
|
51
|
+
|
|
50
52
|
# The maximum amount of excess data the client sends
|
|
51
53
|
# using chunk size extensions before we abort the connection.
|
|
52
54
|
MAX_CHUNK_EXCESS = 16 * 1024
|
|
@@ -93,6 +95,8 @@ module Puma
|
|
|
93
95
|
@http_content_length_limit = nil
|
|
94
96
|
@http_content_length_limit_exceeded = false
|
|
95
97
|
|
|
98
|
+
@reactor_error = nil
|
|
99
|
+
|
|
96
100
|
@peerip = nil
|
|
97
101
|
@peer_family = nil
|
|
98
102
|
@listener = nil
|
|
@@ -100,6 +104,8 @@ module Puma
|
|
|
100
104
|
@expect_proxy_proto = false
|
|
101
105
|
|
|
102
106
|
@body_remain = 0
|
|
107
|
+
@bytes_read = 0
|
|
108
|
+
@pending_write = nil
|
|
103
109
|
|
|
104
110
|
@in_last_chunk = false
|
|
105
111
|
|
|
@@ -109,11 +115,12 @@ module Puma
|
|
|
109
115
|
|
|
110
116
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
|
111
117
|
:tempfile, :io_buffer, :http_content_length_limit_exceeded,
|
|
112
|
-
:requests_served
|
|
118
|
+
:requests_served, :bytes_read
|
|
113
119
|
|
|
114
120
|
attr_writer :peerip, :http_content_length_limit
|
|
115
121
|
|
|
116
|
-
attr_accessor :remote_addr_header, :listener
|
|
122
|
+
attr_accessor :remote_addr_header, :listener,
|
|
123
|
+
:reactor_error
|
|
117
124
|
|
|
118
125
|
# Remove in Puma 7?
|
|
119
126
|
def closed?
|
|
@@ -246,7 +253,9 @@ module Puma
|
|
|
246
253
|
true
|
|
247
254
|
end
|
|
248
255
|
|
|
249
|
-
def try_to_finish
|
|
256
|
+
def try_to_finish(&deadline)
|
|
257
|
+
return false unless flush_pending_write
|
|
258
|
+
|
|
250
259
|
if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
|
|
251
260
|
@http_content_length_limit_exceeded = true
|
|
252
261
|
end
|
|
@@ -258,7 +267,7 @@ module Puma
|
|
|
258
267
|
return true
|
|
259
268
|
end
|
|
260
269
|
|
|
261
|
-
return read_body if in_data_phase
|
|
270
|
+
return read_body(&deadline) if in_data_phase
|
|
262
271
|
|
|
263
272
|
data = nil
|
|
264
273
|
begin
|
|
@@ -278,6 +287,7 @@ module Puma
|
|
|
278
287
|
raise EOFError
|
|
279
288
|
end
|
|
280
289
|
|
|
290
|
+
@bytes_read += data.bytesize
|
|
281
291
|
if @buffer
|
|
282
292
|
@buffer << data
|
|
283
293
|
else
|
|
@@ -302,19 +312,55 @@ module Puma
|
|
|
302
312
|
end
|
|
303
313
|
end
|
|
304
314
|
|
|
305
|
-
def eagerly_finish
|
|
315
|
+
def eagerly_finish(&deadline)
|
|
306
316
|
return true if @ready
|
|
307
|
-
while @to_io.wait_readable(0)
|
|
308
|
-
|
|
317
|
+
while !write_pending? && @to_io.wait_readable(0)
|
|
318
|
+
read = @bytes_read
|
|
319
|
+
return true if try_to_finish(&deadline)
|
|
320
|
+
check_deadline(&deadline) if deadline
|
|
321
|
+
return false if @bytes_read == read
|
|
309
322
|
end
|
|
310
323
|
false
|
|
311
324
|
end
|
|
312
325
|
|
|
313
|
-
def finish(timeout)
|
|
326
|
+
def finish(timeout, &deadline)
|
|
314
327
|
return if @ready
|
|
315
|
-
|
|
328
|
+
set_timeout timeout
|
|
329
|
+
read = @bytes_read
|
|
330
|
+
until try_to_finish(&deadline)
|
|
331
|
+
if @bytes_read > read
|
|
332
|
+
set_timeout timeout
|
|
333
|
+
read = @bytes_read
|
|
334
|
+
elsif self.timeout == 0
|
|
335
|
+
timeout!
|
|
336
|
+
end
|
|
337
|
+
if deadline
|
|
338
|
+
check_deadline(&deadline)
|
|
339
|
+
limit = yield
|
|
340
|
+
wait = [self.timeout, limit ? limit - Process.clock_gettime(Process::CLOCK_MONOTONIC) : 0.1].min
|
|
341
|
+
wait_io(wait) if wait > 0
|
|
342
|
+
else
|
|
343
|
+
wait_io(self.timeout) || timeout!
|
|
344
|
+
end
|
|
345
|
+
end
|
|
316
346
|
end
|
|
317
347
|
|
|
348
|
+
def write_pending?
|
|
349
|
+
!@pending_write.nil? || (@io.respond_to?(:write_pending?) && @io.write_pending?)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def take_pending_write
|
|
353
|
+
str = @pending_write
|
|
354
|
+
@pending_write = nil
|
|
355
|
+
str
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def wait_io(timeout)
|
|
359
|
+
return @to_io.wait_readable(timeout) unless write_pending?
|
|
360
|
+
@to_io.wait_writable(timeout)
|
|
361
|
+
end
|
|
362
|
+
private :wait_io
|
|
363
|
+
|
|
318
364
|
# Wraps `@parser.execute` and adds meaningful error messages
|
|
319
365
|
# @return [Integer] bytes of buffer read by parser
|
|
320
366
|
#
|
|
@@ -360,11 +406,41 @@ module Puma
|
|
|
360
406
|
|
|
361
407
|
def write_error(status_code)
|
|
362
408
|
begin
|
|
363
|
-
|
|
409
|
+
write_interim ERROR_RESPONSE[status_code]
|
|
364
410
|
rescue StandardError
|
|
365
411
|
end
|
|
366
412
|
end
|
|
367
413
|
|
|
414
|
+
def write_interim(str)
|
|
415
|
+
if @pending_write
|
|
416
|
+
@pending_write << str
|
|
417
|
+
return flush_pending_write
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
n = @io.write_nonblock(str, exception: false)
|
|
421
|
+
if n == :wait_writable
|
|
422
|
+
@pending_write = +str unless @io.respond_to?(:write_pending?) && @io.write_pending?
|
|
423
|
+
elsif n < str.bytesize
|
|
424
|
+
@pending_write = str.byteslice(n..-1)
|
|
425
|
+
end
|
|
426
|
+
rescue SystemCallError, IOError
|
|
427
|
+
raise ConnectionError, "Connection error detected during write"
|
|
428
|
+
end
|
|
429
|
+
private :write_interim
|
|
430
|
+
|
|
431
|
+
def flush_pending_write
|
|
432
|
+
return true unless @pending_write
|
|
433
|
+
|
|
434
|
+
n = @io.write_nonblock(@pending_write, exception: false)
|
|
435
|
+
return false if n == :wait_writable
|
|
436
|
+
|
|
437
|
+
@pending_write = n == @pending_write.bytesize ? nil : @pending_write.byteslice(n..-1)
|
|
438
|
+
@pending_write.nil?
|
|
439
|
+
rescue SystemCallError, IOError
|
|
440
|
+
raise ConnectionError, "Connection error detected during write"
|
|
441
|
+
end
|
|
442
|
+
private :flush_pending_write
|
|
443
|
+
|
|
368
444
|
def peerip
|
|
369
445
|
return @peerip if @peerip
|
|
370
446
|
|
|
@@ -415,8 +491,7 @@ module Puma
|
|
|
415
491
|
if @env[HTTP_EXPECT] == CONTINUE
|
|
416
492
|
# TODO allow a hook here to check the headers before
|
|
417
493
|
# going forward
|
|
418
|
-
|
|
419
|
-
@io.flush
|
|
494
|
+
write_interim HTTP_11_100
|
|
420
495
|
end
|
|
421
496
|
|
|
422
497
|
@read_header = false
|
|
@@ -507,9 +582,9 @@ module Puma
|
|
|
507
582
|
false
|
|
508
583
|
end
|
|
509
584
|
|
|
510
|
-
def read_body
|
|
585
|
+
def read_body(&deadline)
|
|
511
586
|
if @chunked_body
|
|
512
|
-
return read_chunked_body
|
|
587
|
+
return read_chunked_body(&deadline)
|
|
513
588
|
end
|
|
514
589
|
|
|
515
590
|
# Read an odd sized chunk so we can read even sized ones
|
|
@@ -534,6 +609,7 @@ module Puma
|
|
|
534
609
|
raise EOFError
|
|
535
610
|
end
|
|
536
611
|
|
|
612
|
+
@bytes_read += chunk.bytesize
|
|
537
613
|
remain -= @body.write(chunk)
|
|
538
614
|
end
|
|
539
615
|
|
|
@@ -548,7 +624,7 @@ module Puma
|
|
|
548
624
|
end
|
|
549
625
|
end
|
|
550
626
|
|
|
551
|
-
def read_chunked_body
|
|
627
|
+
def read_chunked_body(&deadline)
|
|
552
628
|
while true
|
|
553
629
|
begin
|
|
554
630
|
chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
|
|
@@ -566,13 +642,27 @@ module Puma
|
|
|
566
642
|
raise EOFError
|
|
567
643
|
end
|
|
568
644
|
|
|
645
|
+
@bytes_read += chunk.bytesize
|
|
569
646
|
if decode_chunk(chunk)
|
|
570
647
|
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
|
571
648
|
return true
|
|
572
649
|
end
|
|
650
|
+
|
|
651
|
+
check_deadline(&deadline) if deadline
|
|
573
652
|
end
|
|
574
653
|
end
|
|
575
654
|
|
|
655
|
+
def check_deadline
|
|
656
|
+
limit = yield
|
|
657
|
+
return unless limit
|
|
658
|
+
|
|
659
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
660
|
+
return if now < limit
|
|
661
|
+
return if now < limit + SHUTDOWN_READ_GRACE && @to_io.wait_readable(0)
|
|
662
|
+
|
|
663
|
+
timeout!
|
|
664
|
+
end
|
|
665
|
+
|
|
576
666
|
def setup_chunked_body(body)
|
|
577
667
|
@chunked_body = true
|
|
578
668
|
@partial_part_left = 0
|
data/lib/puma/configuration.rb
CHANGED
|
@@ -139,6 +139,7 @@ module Puma
|
|
|
139
139
|
environment: 'development'.freeze,
|
|
140
140
|
# Number of seconds to wait until we get the first data for the request.
|
|
141
141
|
first_data_timeout: 30,
|
|
142
|
+
shutdown_body_timeout: 25,
|
|
142
143
|
# Number of seconds to wait until the next request before shutting down.
|
|
143
144
|
idle_timeout: nil,
|
|
144
145
|
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 = "7.2.
|
|
103
|
+
PUMA_VERSION = VERSION = "7.2.2"
|
|
104
104
|
CODE_NAME = "On The Corner"
|
|
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
|
@@ -398,7 +398,20 @@ module Puma
|
|
|
398
398
|
@options[:drain_on_shutdown] = which
|
|
399
399
|
end
|
|
400
400
|
|
|
401
|
-
#
|
|
401
|
+
# Define how long an incomplete request may continue to be read during shutdown.
|
|
402
|
+
#
|
|
403
|
+
# The default is 25 seconds.
|
|
404
|
+
#
|
|
405
|
+
# @example
|
|
406
|
+
# shutdown_body_timeout 10
|
|
407
|
+
#
|
|
408
|
+
# @see Puma::Server#graceful_shutdown
|
|
409
|
+
#
|
|
410
|
+
def shutdown_body_timeout(seconds)
|
|
411
|
+
@options[:shutdown_body_timeout] = Float(seconds)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Set the environment in which the Rack app will run. The value must be
|
|
402
415
|
# a string.
|
|
403
416
|
#
|
|
404
417
|
# 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 what's 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
|
+
elsif client.timeout_at < @timeout_keys[client]
|
|
139
|
+
@timeouts.delete client
|
|
140
|
+
requeue client
|
|
141
|
+
false
|
|
142
|
+
else
|
|
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/request.rb
CHANGED
|
@@ -58,6 +58,8 @@ module Puma
|
|
|
58
58
|
|
|
59
59
|
return :close if closed_socket?(socket)
|
|
60
60
|
|
|
61
|
+
write_pending_interim socket, client
|
|
62
|
+
|
|
61
63
|
if client.http_content_length_limit_exceeded
|
|
62
64
|
return prepare_response(413, {}, ["Payload Too Large"], requests, client)
|
|
63
65
|
end
|
|
@@ -177,6 +179,8 @@ module Puma
|
|
|
177
179
|
|
|
178
180
|
return :close if closed_socket?(socket)
|
|
179
181
|
|
|
182
|
+
write_pending_interim socket, client
|
|
183
|
+
|
|
180
184
|
# Close the connection after a reasonable number of inline requests
|
|
181
185
|
force_keep_alive = @enable_keep_alives && client.requests_served < @max_keep_alive
|
|
182
186
|
|
|
@@ -292,6 +296,12 @@ module Puma
|
|
|
292
296
|
end
|
|
293
297
|
end
|
|
294
298
|
|
|
299
|
+
def write_pending_interim(socket, client)
|
|
300
|
+
pending = client.take_pending_write
|
|
301
|
+
fast_write_str socket, pending if pending
|
|
302
|
+
end
|
|
303
|
+
private :write_pending_interim
|
|
304
|
+
|
|
295
305
|
# Used to write 'early hints', 'no body' responses, 'hijacked' responses,
|
|
296
306
|
# and body segments (called by `fast_write_response`).
|
|
297
307
|
# Writes a string to a socket (normally `Client#io`) using `write_nonblock`.
|
|
@@ -341,7 +351,11 @@ module Puma
|
|
|
341
351
|
fast_write_str socket, io_buffer.read_and_reset
|
|
342
352
|
else
|
|
343
353
|
fast_write_str socket, io_buffer.read_and_reset
|
|
344
|
-
|
|
354
|
+
remaining = content_length
|
|
355
|
+
while remaining > 0 && (chunk = body.read([remaining, BODY_LEN_MAX].min))
|
|
356
|
+
fast_write_str socket, chunk
|
|
357
|
+
remaining -= chunk.bytesize
|
|
358
|
+
end
|
|
345
359
|
end
|
|
346
360
|
end
|
|
347
361
|
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) { |client| process_client client }
|
|
263
266
|
|
|
@@ -310,6 +313,9 @@ module Puma
|
|
|
310
313
|
# If a client object times out, a 408 response is written, its connection is closed,
|
|
311
314
|
# and the object is removed from the reactor (return `true`).
|
|
312
315
|
#
|
|
316
|
+
# If the request cannot be read, the Client is passed to the ThreadPool with its
|
|
317
|
+
# `reactor_error` set, so that a processor thread writes the error response.
|
|
318
|
+
#
|
|
313
319
|
# If the Reactor is shutting down, all Clients are either timed out or passed to the
|
|
314
320
|
# ThreadPool, depending on their current state (#can_close?).
|
|
315
321
|
#
|
|
@@ -318,18 +324,33 @@ module Puma
|
|
|
318
324
|
# will wake up and again be checked to see if it's ready to be passed to the thread pool.
|
|
319
325
|
def reactor_wakeup(client)
|
|
320
326
|
shutdown = !@queue_requests
|
|
321
|
-
|
|
322
|
-
|
|
327
|
+
read = client.bytes_read
|
|
328
|
+
if client.try_to_finish { shutdown_body_limit } || (shutdown && !client.can_close?)
|
|
329
|
+
hand_to_thread_pool client
|
|
323
330
|
elsif shutdown || client.timeout == 0
|
|
324
331
|
client.timeout!
|
|
325
332
|
else
|
|
326
|
-
client.set_timeout(@first_data_timeout)
|
|
333
|
+
client.set_timeout(@first_data_timeout) if client.bytes_read > read
|
|
327
334
|
false
|
|
328
335
|
end
|
|
329
|
-
rescue
|
|
330
|
-
client_error(e, client)
|
|
336
|
+
rescue ConnectionError, EOFError
|
|
331
337
|
close_client_safely(client)
|
|
332
338
|
true
|
|
339
|
+
rescue StandardError => e
|
|
340
|
+
# Writing the error response may block on a client that is not reading,
|
|
341
|
+
# so leave it to a thread pool thread.
|
|
342
|
+
client.reactor_error = e
|
|
343
|
+
hand_to_thread_pool client
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def hand_to_thread_pool(client)
|
|
347
|
+
@thread_pool << client
|
|
348
|
+
true
|
|
349
|
+
rescue ThreadError
|
|
350
|
+
true
|
|
351
|
+
rescue StandardError
|
|
352
|
+
close_client_safely client
|
|
353
|
+
true
|
|
333
354
|
end
|
|
334
355
|
|
|
335
356
|
def handle_servers
|
|
@@ -359,13 +380,16 @@ module Puma
|
|
|
359
380
|
ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : @idle_timeout)
|
|
360
381
|
unless ios
|
|
361
382
|
unless shutting_down?
|
|
362
|
-
@idle_timeout_reached = true
|
|
363
|
-
|
|
364
383
|
if @clustered
|
|
365
|
-
@
|
|
384
|
+
unless @idle_timeout_reached
|
|
385
|
+
@idle_timeout_reached = true
|
|
386
|
+
@worker_write << "#{PipeRequest::PIPE_IDLE}#{Process.pid}\n" rescue nil
|
|
387
|
+
end
|
|
366
388
|
next
|
|
367
389
|
else
|
|
390
|
+
@idle_timeout_reached = true
|
|
368
391
|
@log_writer.log "- Idle timeout reached"
|
|
392
|
+
shutdown_body_deadline
|
|
369
393
|
@status = :stop
|
|
370
394
|
end
|
|
371
395
|
end
|
|
@@ -399,6 +423,9 @@ module Puma
|
|
|
399
423
|
sock.accept_nonblock
|
|
400
424
|
rescue IO::WaitReadable
|
|
401
425
|
next
|
|
426
|
+
rescue Errno::EMFILE, Errno::ENFILE, Errno::ENOBUFS, Errno::ENOMEM => e
|
|
427
|
+
accept_resource_error e
|
|
428
|
+
next
|
|
402
429
|
end
|
|
403
430
|
drain += 1 if shutting_down?
|
|
404
431
|
client = new_client(io, sock)
|
|
@@ -410,11 +437,17 @@ module Puma
|
|
|
410
437
|
# In the case that any of the sockets are unexpectedly close.
|
|
411
438
|
raise
|
|
412
439
|
rescue StandardError => e
|
|
413
|
-
|
|
440
|
+
begin
|
|
441
|
+
@log_writer.unknown_error e, nil, "Listen loop"
|
|
442
|
+
rescue StandardError
|
|
443
|
+
end
|
|
414
444
|
end
|
|
415
445
|
end
|
|
416
446
|
|
|
417
|
-
|
|
447
|
+
begin
|
|
448
|
+
@log_writer.debug "Drained #{drain} additional connections." if drain
|
|
449
|
+
rescue StandardError
|
|
450
|
+
end
|
|
418
451
|
@events.fire :state, @status
|
|
419
452
|
|
|
420
453
|
if queue_requests
|
|
@@ -424,7 +457,10 @@ module Puma
|
|
|
424
457
|
|
|
425
458
|
graceful_shutdown if @status == :stop || @status == :restart
|
|
426
459
|
rescue Exception => e
|
|
427
|
-
|
|
460
|
+
begin
|
|
461
|
+
@log_writer.unknown_error e, nil, "Exception handling servers"
|
|
462
|
+
rescue StandardError
|
|
463
|
+
end
|
|
428
464
|
ensure
|
|
429
465
|
# Errno::EBADF is infrequently raised
|
|
430
466
|
[@check, @notify].each do |io|
|
|
@@ -454,12 +490,14 @@ module Puma
|
|
|
454
490
|
|
|
455
491
|
case cmd
|
|
456
492
|
when STOP_COMMAND
|
|
493
|
+
shutdown_body_deadline
|
|
457
494
|
@status = :stop
|
|
458
495
|
return true
|
|
459
496
|
when HALT_COMMAND
|
|
460
497
|
@status = :halt
|
|
461
498
|
return true
|
|
462
499
|
when RESTART_COMMAND
|
|
500
|
+
shutdown_body_deadline
|
|
463
501
|
@status = :restart
|
|
464
502
|
return true
|
|
465
503
|
end
|
|
@@ -483,7 +521,9 @@ module Puma
|
|
|
483
521
|
requests = 0
|
|
484
522
|
|
|
485
523
|
begin
|
|
486
|
-
|
|
524
|
+
raise client.reactor_error if client.reactor_error
|
|
525
|
+
|
|
526
|
+
if @queue_requests && !client.eagerly_finish { shutdown_body_limit }
|
|
487
527
|
|
|
488
528
|
client.set_timeout(@first_data_timeout)
|
|
489
529
|
if @reactor.add client
|
|
@@ -493,7 +533,7 @@ module Puma
|
|
|
493
533
|
end
|
|
494
534
|
|
|
495
535
|
with_force_shutdown(client) do
|
|
496
|
-
client.finish(@first_data_timeout)
|
|
536
|
+
client.finish(@first_data_timeout) { shutdown_body_limit }
|
|
497
537
|
end
|
|
498
538
|
|
|
499
539
|
can_loop = true
|
|
@@ -514,7 +554,7 @@ module Puma
|
|
|
514
554
|
next_request_ready = if client.has_back_to_back_requests?
|
|
515
555
|
with_force_shutdown(client) { client.process_back_to_back_requests }
|
|
516
556
|
else
|
|
517
|
-
with_force_shutdown(client) { client.eagerly_finish }
|
|
557
|
+
with_force_shutdown(client) { client.eagerly_finish { shutdown_body_limit } }
|
|
518
558
|
end
|
|
519
559
|
|
|
520
560
|
if next_request_ready
|
|
@@ -546,6 +586,25 @@ module Puma
|
|
|
546
586
|
end
|
|
547
587
|
end
|
|
548
588
|
|
|
589
|
+
# Accepting fails while the process is out of file descriptors or memory.
|
|
590
|
+
# The connection stays in the backlog, so retrying at once would spin and
|
|
591
|
+
# log at full speed. Wait briefly and log at most once per second.
|
|
592
|
+
def accept_resource_error(error)
|
|
593
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
594
|
+
if @accept_error_logged_at.nil? || now - @accept_error_logged_at >= 1
|
|
595
|
+
suffix = @accept_errors_suppressed.to_i > 0 ? " (#{@accept_errors_suppressed} more suppressed)" : ""
|
|
596
|
+
@accept_error_logged_at = now
|
|
597
|
+
@accept_errors_suppressed = 0
|
|
598
|
+
begin
|
|
599
|
+
@log_writer.unknown_error error, nil, "Listen loop#{suffix}"
|
|
600
|
+
rescue StandardError
|
|
601
|
+
end
|
|
602
|
+
else
|
|
603
|
+
@accept_errors_suppressed += 1
|
|
604
|
+
end
|
|
605
|
+
sleep ACCEPT_ERROR_DELAY
|
|
606
|
+
end
|
|
607
|
+
|
|
549
608
|
# :nodoc:
|
|
550
609
|
def close_client_safely(client)
|
|
551
610
|
client.close
|
|
@@ -615,9 +674,19 @@ module Puma
|
|
|
615
674
|
end
|
|
616
675
|
private :response_to_error
|
|
617
676
|
|
|
677
|
+
def shutdown_body_deadline
|
|
678
|
+
@shutdown_body_deadline ||= Process.clock_gettime(Process::CLOCK_MONOTONIC) + @shutdown_body_timeout
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def shutdown_body_limit
|
|
682
|
+
@shutdown_body_deadline
|
|
683
|
+
end
|
|
684
|
+
|
|
618
685
|
# Wait for all outstanding requests to finish.
|
|
619
686
|
#
|
|
620
687
|
def graceful_shutdown
|
|
688
|
+
shutdown_body_deadline
|
|
689
|
+
|
|
621
690
|
if options[:shutdown_debug]
|
|
622
691
|
threads = Thread.list
|
|
623
692
|
total = threads.size
|
|
@@ -664,6 +733,7 @@ module Puma
|
|
|
664
733
|
# off the request queue before finally exiting.
|
|
665
734
|
|
|
666
735
|
def stop(sync=false)
|
|
736
|
+
shutdown_body_deadline
|
|
667
737
|
notify_safely(STOP_COMMAND)
|
|
668
738
|
@thread.join if @thread && sync
|
|
669
739
|
end
|
|
@@ -674,6 +744,7 @@ module Puma
|
|
|
674
744
|
end
|
|
675
745
|
|
|
676
746
|
def begin_restart(sync=false)
|
|
747
|
+
shutdown_body_deadline
|
|
677
748
|
notify_safely(RESTART_COMMAND)
|
|
678
749
|
@thread.join if @thread && sync
|
|
679
750
|
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: 7.2.
|
|
4
|
+
version: 7.2.2
|
|
5
5
|
platform: java
|
|
6
6
|
authors:
|
|
7
7
|
- Evan Phoenix
|
|
@@ -146,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
146
146
|
- !ruby/object:Gem::Version
|
|
147
147
|
version: '0'
|
|
148
148
|
requirements: []
|
|
149
|
-
rubygems_version: 4.0.
|
|
149
|
+
rubygems_version: 4.0.23
|
|
150
150
|
specification_version: 4
|
|
151
151
|
summary: A Ruby/Rack web server built for parallelism.
|
|
152
152
|
test_files: []
|