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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ccf5cdd3f952f6ea55feda29b8899a36cc64ce8ecb5c79bb981f0450b7a74962
4
- data.tar.gz: c0094d7ac6edb28c93ad7354b719fbd337278ff743f7b7ea7a4268736e12e70d
3
+ metadata.gz: 9bb993a39d400591f128b4808ca9d7f735618d614f6e490f850838ac7121391c
4
+ data.tar.gz: aae15617fcd8cd7147701e1f594625554bc6178c991c5ee98e00ca5bc0d1346d
5
5
  SHA512:
6
- metadata.gz: 6e371849168d7a0f5ba1d88c12083a7aec0c8530e518251da1d077bc90af3cccdd1fbf43c7166881a8e8a87e5b5ffdef9782cf863907bf9c2dae5ba4c6010d52
7
- data.tar.gz: e85bb20d08fbdf474558b0abb8cead8de3e4a26373134c19872eb9eb8850ee92fd42c09987777c535cf6bcd50f1f2f7622282277ef36aaab6cce7d792a354e71
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"
@@ -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
@@ -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 read_body if in_data_phase
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) # rubocop: disable Style/WhileUntilModifier
288
- return true if try_to_finish
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
- @to_io.wait_readable(timeout) || timeout! until try_to_finish
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
- @io << ERROR_RESPONSE[status_code]
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
- @io << HTTP_11_100
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
@@ -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.2"
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"
@@ -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
@@ -399,7 +399,20 @@ module Puma
399
399
  @options[:drain_on_shutdown] = which
400
400
  end
401
401
 
402
- # Set the environment in which the rack's app will run. The value must be
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
- 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 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
- @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
+ 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
- IO.copy_stream body, socket
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
- if client.try_to_finish || (shutdown && !client.can_close?)
324
- @thread_pool << client
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 StandardError => e
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
- @worker_write << "#{PipeRequest::PIPE_IDLE}#{Process.pid}\n" rescue nil
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
- @log_writer.unknown_error e, nil, "Listen loop"
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
- @log_writer.debug { "Drained #{drain} additional connections." } if drain
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
- @log_writer.unknown_error e, nil, "Exception handling servers"
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
- if @queue_requests && !client.eagerly_finish
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.2
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.3
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: []