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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33227b4a818b08ed8c3890e145bd9770b624c26fb389461edb906b8d93988816
4
- data.tar.gz: 85c7054c6080eda102fb1633a7289be8c36e35c2fab48c7df729e0cdcf200aa3
3
+ metadata.gz: 44db10ef8822503bf3f707c746f6bfded7e59250cd12a7721e8652cc43cc7c35
4
+ data.tar.gz: 00cfe4cae3673a99be3edea74213023841d090997f7e3c70907ba5ce99bcdff1
5
5
  SHA512:
6
- metadata.gz: b0a374ea779f3178705bcafc36b054fdd8e850eaafdf3878410501d3d130d5f7c32c0350eb5cd1e4a47056f3eed8b0a5ac5e08cc63f560b25bde2cb469713642
7
- data.tar.gz: 9a26f84cbb51453040b611b276440cf5b4b99fec3e5c1562e207eb27e08e8c0fe059419208b93001b7a8fee70d83422f4d778c9499a05ab2b7526eaf9e79ec9f
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"
@@ -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
- SSL_CTX_set_cipher_list(ctx, RSTRING_PTR(ssl_cipher_filter));
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
- doOp(SSLOperation.UNWRAP, inboundNetData, inboundAppData);
387
-
388
- HandshakeStatus handshakeStatus = engine.getHandshakeStatus();
389
- boolean done = false;
390
- while (!done) {
391
- SSLEngineResult res;
392
- switch (handshakeStatus) {
393
- case NEED_WRAP:
394
- res = doOp(SSLOperation.WRAP, inboundAppData, outboundNetData);
395
- handshakeStatus = res.getHandshakeStatus();
396
- break;
397
- case NEED_UNWRAP:
398
- res = doOp(SSLOperation.UNWRAP, inboundNetData, inboundAppData);
399
- if (res.getStatus() == Status.BUFFER_UNDERFLOW) {
400
- // need more data before we can shake more hands
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 (closed || engine.isInboundDone() && engine.isOutboundDone()) {
486
- if (engine.isOutboundDone()) {
487
- engine.closeOutbound();
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
- RSTRING_GETMEM(data, dptr, dlen);
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 INT2FIX(puma_parser_nread(http));
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 INT2FIX(http->nread);
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) # rubocop: disable Style/WhileUntilModifier
308
- return true if try_to_finish
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
- @to_io.wait_readable(timeout) || timeout! until try_to_finish
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
- @io << ERROR_RESPONSE[status_code]
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
- @io << HTTP_11_100
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
@@ -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.1"
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"
@@ -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::SSLSocket.new(
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
- OpenSSL::SSL::SSLContext.new)
185
- .tap { |ssl| ssl.sync_close = true } # default is false
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
- # Set the environment in which the rack's app will run. The value must be
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
- while neg_data = @engine.extract
72
- @socket.write neg_data
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
- while true
89
- output = engine_read_all
90
- return output if output
91
-
92
- data = @socket.read_nonblock(size, exception: false)
93
- if data == :wait_readable || data == :wait_writable
94
- # It would make more sense to let @socket.read_nonblock raise
95
- # EAGAIN if necessary but it seems like it'll misbehave on Windows.
96
- # I don't have a Windows machine to debug this so I can't explain
97
- # exactly whats happening in that OS. Please let me know if you
98
- # find out!
99
- #
100
- # In the meantime, we can emulate the correct behavior by
101
- # capturing :wait_readable & :wait_writable and raising EAGAIN
102
- # ourselves.
103
- raise IO::EAGAINWaitReadable
104
- elsif data.nil?
105
- raise SSLError.exception "HTTP connection?" if bad_tlsv1_3?
106
- return nil
107
- end
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
- @engine.inject(data)
110
- output = engine_read_all
112
+ @engine.inject(data)
113
+ output = engine_read_all
114
+ return output if output
111
115
 
112
- return output if output
116
+ queue_ciphertext
117
+ flush_write_buffer_nonblock
118
+ raise IO::EAGAINWaitReadable
119
+ end
113
120
 
114
- while neg_data = @engine.extract
115
- @socket.write neg_data
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
- need = data_size
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
- enc_wr = +''
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
- # This is a temporary fix to deal with websockets code using
147
- # write_nonblock.
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
- # The problem with implementing it properly
150
- # is that it means we'd have to have the ability to rewind
151
- # an engine because after we write+extract, the socket
152
- # write_nonblock call might raise an exception and later
153
- # code would pass the same data in, but the engine would think
154
- # it had already written the data in.
155
- #
156
- # So for the time being (and since write blocking is quite rare),
157
- # go ahead and actually block in write_nonblock.
158
- #
159
- def write_nonblock(data, *_)
160
- write data
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
- while alert_data = @engine.extract
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
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!(&:timeout_at)
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, :r).value = client
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
- IO.copy_stream body, socket
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
- if client.try_to_finish || (shutdown && !client.can_close?)
322
- @thread_pool << client
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 StandardError => e
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
- @worker_write << "#{PipeRequest::PIPE_IDLE}#{Process.pid}\n" rescue nil
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
- @log_writer.unknown_error e, nil, "Listen loop"
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
- @log_writer.debug "Drained #{drain} additional connections." if drain
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
- @log_writer.unknown_error e, nil, "Exception handling servers"
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
- if @queue_requests && !client.eagerly_finish
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.1
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.3
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: []