puma 8.0.3-java → 8.1.0-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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +115 -0
  3. data/README.md +20 -5
  4. data/docs/6.0-Upgrade.md +1 -1
  5. data/docs/8.0-Upgrade.md +2 -2
  6. data/docs/deployment.md +3 -3
  7. data/docs/fork_worker.md +34 -4
  8. data/docs/kubernetes.md +3 -3
  9. data/docs/plugins.md +2 -2
  10. data/docs/puma_support.md +78 -0
  11. data/docs/rails_dev_mode.md +2 -2
  12. data/docs/restart.md +4 -3
  13. data/docs/signals.md +2 -2
  14. data/docs/stats.md +5 -5
  15. data/docs/systemd.md +4 -2
  16. data/docs/testing_benchmarks_local_files.md +1 -1
  17. data/docs/testing_test_rackup_ci_files.md +2 -2
  18. data/ext/puma_http11/extconf.rb +2 -0
  19. data/ext/puma_http11/http11_parser.c +6 -2
  20. data/ext/puma_http11/http11_parser_common.rl +4 -1
  21. data/ext/puma_http11/mini_ssl.c +8 -4
  22. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +36 -36
  23. data/ext/puma_http11/puma_http11.c +24 -0
  24. data/lib/puma/binder.rb +29 -0
  25. data/lib/puma/cli.rb +6 -0
  26. data/lib/puma/client.rb +75 -29
  27. data/lib/puma/client_env.rb +32 -18
  28. data/lib/puma/cluster/worker.rb +109 -33
  29. data/lib/puma/cluster/worker_handle.rb +16 -0
  30. data/lib/puma/cluster.rb +173 -44
  31. data/lib/puma/commonlogger.rb +3 -3
  32. data/lib/puma/configuration.rb +22 -3
  33. data/lib/puma/const.rb +11 -9
  34. data/lib/puma/dsl.rb +265 -91
  35. data/lib/puma/launcher.rb +6 -4
  36. data/lib/puma/minissl.rb +1 -1
  37. data/lib/puma/plugin/systemd.rb +1 -1
  38. data/lib/puma/puma_http11.jar +0 -0
  39. data/lib/puma/response.rb +13 -5
  40. data/lib/puma/runner.rb +5 -1
  41. data/lib/puma/sd_notify.rb +4 -1
  42. data/lib/puma/server.rb +45 -20
  43. data/lib/puma/thread_pool.rb +12 -1
  44. data/lib/puma.rb +1 -1
  45. metadata +3 -2
@@ -7,7 +7,7 @@ and by the type of object used for the body.
7
7
 
8
8
  Five rackup files are located in 'test/rackup' that can be used. All have their
9
9
  request body size (in kB) set via `Body-Conf` header or with `ENV['CI_BODY_CONF']`.
10
- Additionally, the ci_select.ru file can have it's body type set via a starting
10
+ Additionally, the ci_select.ru file can have its body type set via a starting
11
11
  character.
12
12
 
13
13
  * **ci_array.ru** - body is an `Array` of 1kB strings. `Content-Length` is not set.
@@ -21,7 +21,7 @@ contain 1kB items.
21
21
 
22
22
  All can be delayed by a float value (seconds) specified by the `Dly` header
23
23
 
24
- Note that rhe `Body-Conf` header takes precedence, and `ENV['CI_BODY_CONF']` is
24
+ Note that the `Body-Conf` header takes precedence, and `ENV['CI_BODY_CONF']` is
25
25
  only read on load.
26
26
 
27
27
  ## ci_select.ru
@@ -8,6 +8,8 @@ if $mingw
8
8
  have_library 'ssp'
9
9
  end
10
10
 
11
+ have_func "prctl(PR_SET_CHILD_SUBREAPER, 1)", "sys/prctl.h"
12
+
11
13
  unless ENV["PUMA_DISABLE_SSL"]
12
14
  # don't use pkg_config('openssl') if '--with-openssl-dir' is used
13
15
  has_openssl_dir = dir_config('openssl').any? ||
@@ -592,6 +592,10 @@ case 24:
592
592
  case 60: goto st0;
593
593
  case 62: goto st0;
594
594
  case 63: goto tr39;
595
+ case 92: goto st0;
596
+ case 96: goto st0;
597
+ case 123: goto st0;
598
+ case 125: goto st0;
595
599
  case 127: goto st0;
596
600
  }
597
601
  if ( 0 <= (*p) && (*p) <= 31 )
@@ -607,7 +611,7 @@ st25:
607
611
  if ( ++p == pe )
608
612
  goto _test_eof25;
609
613
  case 25:
610
- #line 611 "ext/puma_http11/http11_parser.c"
614
+ #line 615 "ext/puma_http11/http11_parser.c"
611
615
  switch( (*p) ) {
612
616
  case 32: goto tr41;
613
617
  case 34: goto st0;
@@ -627,7 +631,7 @@ st26:
627
631
  if ( ++p == pe )
628
632
  goto _test_eof26;
629
633
  case 26:
630
- #line 631 "ext/puma_http11/http11_parser.c"
634
+ #line 635 "ext/puma_http11/http11_parser.c"
631
635
  switch( (*p) ) {
632
636
  case 32: goto tr44;
633
637
  case 34: goto st0;
@@ -17,6 +17,9 @@
17
17
  escape = ("%" xdigit xdigit);
18
18
  uchar = (unreserved | escape | "%");
19
19
  pchar = (uchar | ":" | "@" | "&" | "=" | "+" | ";");
20
+ # RFC 3986 disallows these in paths; browsers never send them unescaped there.
21
+ path_unwise = ("{" | "}" | "`" | "\\");
22
+ path_char = pchar -- path_unwise;
20
23
  tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t");
21
24
 
22
25
  # elements
@@ -26,7 +29,7 @@
26
29
  scheme = ( alpha | digit | "+" | "-" | "." )* ;
27
30
  absolute_uri = (scheme ":" (uchar | reserved )*);
28
31
 
29
- path = ( pchar+ ( "/" pchar* )* ) ;
32
+ path = ( path_char+ ( "/" path_char* )* ) ;
30
33
  query = ( uchar | reserved )* %query_string ;
31
34
  param = ( pchar | "/" )* ;
32
35
  params = ( param ( ";" param )* ) ;
@@ -228,7 +228,7 @@ sslctx_alloc(VALUE klass) {
228
228
  VALUE
229
229
  sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
230
230
  SSL_CTX* ctx;
231
- int ssl_options;
231
+ int ssl_options, private_key_result;
232
232
  VALUE key, cert, ca, verify_mode, ssl_cipher_filter, ssl_ciphersuites, no_tlsv1, no_tlsv1_1,
233
233
  verification_flags, session_id_bytes, cert_pem, key_pem, key_password_command, key_password;
234
234
  BIO *bio;
@@ -292,15 +292,19 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
292
292
  StringValue(key_password);
293
293
  password_cb = password_callback;
294
294
  password = RSTRING_PTR(key_password);
295
- SSL_CTX_set_default_passwd_cb(ctx, password_cb);
296
- SSL_CTX_set_default_passwd_cb_userdata(ctx, (void *) password);
297
295
  }
298
296
  }
299
297
 
300
298
  if (!NIL_P(key)) {
301
299
  StringValue(key);
302
300
 
303
- if (SSL_CTX_use_PrivateKey_file(ctx, RSTRING_PTR(key), SSL_FILETYPE_PEM) != 1) {
301
+ SSL_CTX_set_default_passwd_cb(ctx, password_cb);
302
+ SSL_CTX_set_default_passwd_cb_userdata(ctx, (void *) password);
303
+ private_key_result = SSL_CTX_use_PrivateKey_file(ctx, RSTRING_PTR(key), SSL_FILETYPE_PEM);
304
+ SSL_CTX_set_default_passwd_cb(ctx, NULL);
305
+ SSL_CTX_set_default_passwd_cb_userdata(ctx, NULL);
306
+
307
+ if (private_key_result != 1) {
304
308
  raise_file_error("SSL_CTX_use_PrivateKey_file", RSTRING_PTR(key));
305
309
  }
306
310
  }
@@ -36,8 +36,8 @@ private static short[] init__puma_parser_key_offsets_0()
36
36
  return new short [] {
37
37
  0, 0, 8, 17, 27, 29, 30, 31, 32, 33, 34, 36,
38
38
  39, 41, 44, 45, 61, 62, 78, 85, 91, 99, 107, 117,
39
- 125, 134, 142, 150, 159, 168, 177, 186, 195, 204, 213, 222,
40
- 231, 240, 249, 258, 267, 276, 285, 294, 303, 312, 313
39
+ 125, 138, 146, 154, 163, 172, 181, 190, 199, 208, 217, 226,
40
+ 235, 244, 253, 262, 271, 280, 289, 298, 307, 316, 317
41
41
  };
42
42
  }
43
43
 
@@ -57,23 +57,23 @@ private static char[] init__puma_parser_trans_keys_0()
57
57
  31, 13, 127, 0, 8, 10, 31, 32, 60, 62, 127, 0,
58
58
  31, 34, 35, 32, 60, 62, 127, 0, 31, 34, 35, 43,
59
59
  58, 45, 46, 48, 57, 65, 90, 97, 122, 32, 34, 35,
60
- 60, 62, 127, 0, 31, 32, 34, 35, 60, 62, 63, 127,
61
- 0, 31, 32, 34, 35, 60, 62, 127, 0, 31, 32, 34,
62
- 35, 60, 62, 127, 0, 31, 32, 36, 95, 45, 46, 48,
63
- 57, 65, 90, 32, 36, 95, 45, 46, 48, 57, 65, 90,
64
- 32, 36, 95, 45, 46, 48, 57, 65, 90, 32, 36, 95,
65
- 45, 46, 48, 57, 65, 90, 32, 36, 95, 45, 46, 48,
66
- 57, 65, 90, 32, 36, 95, 45, 46, 48, 57, 65, 90,
67
- 32, 36, 95, 45, 46, 48, 57, 65, 90, 32, 36, 95,
68
- 45, 46, 48, 57, 65, 90, 32, 36, 95, 45, 46, 48,
69
- 57, 65, 90, 32, 36, 95, 45, 46, 48, 57, 65, 90,
70
- 32, 36, 95, 45, 46, 48, 57, 65, 90, 32, 36, 95,
71
- 45, 46, 48, 57, 65, 90, 32, 36, 95, 45, 46, 48,
72
- 57, 65, 90, 32, 36, 95, 45, 46, 48, 57, 65, 90,
73
- 32, 36, 95, 45, 46, 48, 57, 65, 90, 32, 36, 95,
74
- 45, 46, 48, 57, 65, 90, 32, 36, 95, 45, 46, 48,
75
- 57, 65, 90, 32, 36, 95, 45, 46, 48, 57, 65, 90,
76
- 32, 0
60
+ 60, 62, 127, 0, 31, 32, 34, 35, 60, 62, 63, 92,
61
+ 96, 123, 125, 127, 0, 31, 32, 34, 35, 60, 62, 127,
62
+ 0, 31, 32, 34, 35, 60, 62, 127, 0, 31, 32, 36,
63
+ 95, 45, 46, 48, 57, 65, 90, 32, 36, 95, 45, 46,
64
+ 48, 57, 65, 90, 32, 36, 95, 45, 46, 48, 57, 65,
65
+ 90, 32, 36, 95, 45, 46, 48, 57, 65, 90, 32, 36,
66
+ 95, 45, 46, 48, 57, 65, 90, 32, 36, 95, 45, 46,
67
+ 48, 57, 65, 90, 32, 36, 95, 45, 46, 48, 57, 65,
68
+ 90, 32, 36, 95, 45, 46, 48, 57, 65, 90, 32, 36,
69
+ 95, 45, 46, 48, 57, 65, 90, 32, 36, 95, 45, 46,
70
+ 48, 57, 65, 90, 32, 36, 95, 45, 46, 48, 57, 65,
71
+ 90, 32, 36, 95, 45, 46, 48, 57, 65, 90, 32, 36,
72
+ 95, 45, 46, 48, 57, 65, 90, 32, 36, 95, 45, 46,
73
+ 48, 57, 65, 90, 32, 36, 95, 45, 46, 48, 57, 65,
74
+ 90, 32, 36, 95, 45, 46, 48, 57, 65, 90, 32, 36,
75
+ 95, 45, 46, 48, 57, 65, 90, 32, 36, 95, 45, 46,
76
+ 48, 57, 65, 90, 32, 0
77
77
  };
78
78
  }
79
79
 
@@ -85,7 +85,7 @@ private static byte[] init__puma_parser_single_lengths_0()
85
85
  return new byte [] {
86
86
  0, 2, 3, 4, 2, 1, 1, 1, 1, 1, 0, 1,
87
87
  0, 1, 1, 4, 1, 4, 3, 2, 4, 4, 2, 6,
88
- 7, 6, 6, 3, 3, 3, 3, 3, 3, 3, 3, 3,
88
+ 11, 6, 6, 3, 3, 3, 3, 3, 3, 3, 3, 3,
89
89
  3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 0
90
90
  };
91
91
  }
@@ -111,8 +111,8 @@ private static short[] init__puma_parser_index_offsets_0()
111
111
  return new short [] {
112
112
  0, 0, 6, 13, 21, 24, 26, 28, 30, 32, 34, 36,
113
113
  39, 41, 44, 46, 57, 59, 70, 76, 81, 88, 95, 102,
114
- 110, 119, 127, 135, 142, 149, 156, 163, 170, 177, 184, 191,
115
- 198, 205, 212, 219, 226, 233, 240, 247, 254, 261, 263
114
+ 110, 123, 131, 139, 146, 153, 160, 167, 174, 181, 188, 195,
115
+ 202, 209, 216, 223, 230, 237, 244, 251, 258, 265, 267
116
116
  };
117
117
  }
118
118
 
@@ -131,20 +131,20 @@ private static byte[] init__puma_parser_indicies_0()
131
131
  1, 1, 1, 25, 29, 1, 1, 1, 28, 30, 1, 1,
132
132
  1, 1, 1, 31, 32, 1, 1, 1, 1, 1, 33, 34,
133
133
  35, 34, 34, 34, 34, 1, 8, 1, 9, 1, 1, 1,
134
- 1, 35, 36, 1, 38, 1, 1, 39, 1, 1, 37, 40,
135
- 1, 42, 1, 1, 1, 1, 41, 43, 1, 45, 1, 1,
136
- 1, 1, 44, 2, 46, 46, 46, 46, 46, 1, 2, 47,
137
- 47, 47, 47, 47, 1, 2, 48, 48, 48, 48, 48, 1,
138
- 2, 49, 49, 49, 49, 49, 1, 2, 50, 50, 50, 50,
139
- 50, 1, 2, 51, 51, 51, 51, 51, 1, 2, 52, 52,
140
- 52, 52, 52, 1, 2, 53, 53, 53, 53, 53, 1, 2,
141
- 54, 54, 54, 54, 54, 1, 2, 55, 55, 55, 55, 55,
142
- 1, 2, 56, 56, 56, 56, 56, 1, 2, 57, 57, 57,
143
- 57, 57, 1, 2, 58, 58, 58, 58, 58, 1, 2, 59,
144
- 59, 59, 59, 59, 1, 2, 60, 60, 60, 60, 60, 1,
145
- 2, 61, 61, 61, 61, 61, 1, 2, 62, 62, 62, 62,
146
- 62, 1, 2, 63, 63, 63, 63, 63, 1, 2, 1, 1,
147
- 0
134
+ 1, 35, 36, 1, 38, 1, 1, 39, 1, 1, 1, 1,
135
+ 1, 1, 37, 40, 1, 42, 1, 1, 1, 1, 41, 43,
136
+ 1, 45, 1, 1, 1, 1, 44, 2, 46, 46, 46, 46,
137
+ 46, 1, 2, 47, 47, 47, 47, 47, 1, 2, 48, 48,
138
+ 48, 48, 48, 1, 2, 49, 49, 49, 49, 49, 1, 2,
139
+ 50, 50, 50, 50, 50, 1, 2, 51, 51, 51, 51, 51,
140
+ 1, 2, 52, 52, 52, 52, 52, 1, 2, 53, 53, 53,
141
+ 53, 53, 1, 2, 54, 54, 54, 54, 54, 1, 2, 55,
142
+ 55, 55, 55, 55, 1, 2, 56, 56, 56, 56, 56, 1,
143
+ 2, 57, 57, 57, 57, 57, 1, 2, 58, 58, 58, 58,
144
+ 58, 1, 2, 59, 59, 59, 59, 59, 1, 2, 60, 60,
145
+ 60, 60, 60, 1, 2, 61, 61, 61, 61, 61, 1, 2,
146
+ 62, 62, 62, 62, 62, 1, 2, 63, 63, 63, 63, 63,
147
+ 1, 2, 1, 1, 0
148
148
  };
149
149
  }
150
150
 
@@ -13,6 +13,10 @@
13
13
  #include <ctype.h>
14
14
  #include "http11_parser.h"
15
15
 
16
+ #ifdef HAVE_PRCTL
17
+ #include <sys/prctl.h>
18
+ #endif
19
+
16
20
  #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
17
21
 
18
22
  static VALUE eHttpParserError;
@@ -460,6 +464,24 @@ static VALUE HttpParser_body(VALUE self) {
460
464
  return http->body;
461
465
  }
462
466
 
467
+ /**
468
+ * call-seq:
469
+ * Puma.enable_child_subreaper -> true or false
470
+ *
471
+ * Marks the calling process as a child subreaper so that orphaned descendants
472
+ * are reparented to it. Returns false where this is unsupported.
473
+ */
474
+ static VALUE Puma_enable_child_subreaper(VALUE self) {
475
+ #ifdef HAVE_PRCTL
476
+ if (prctl(PR_SET_CHILD_SUBREAPER, 1) < 0) {
477
+ rb_sys_fail("prctl(PR_SET_CHILD_SUBREAPER)");
478
+ }
479
+ return Qtrue;
480
+ #else
481
+ return Qfalse;
482
+ #endif
483
+ }
484
+
463
485
  #ifdef HAVE_OPENSSL_BIO_H
464
486
  void Init_mini_ssl(VALUE mod);
465
487
  #endif
@@ -483,6 +505,8 @@ RUBY_FUNC_EXPORTED void Init_puma_http11(void)
483
505
  rb_global_variable(&eHttpParserError);
484
506
  eHttpParserError = rb_define_class_under(mPuma, "HttpParserError", rb_eStandardError);
485
507
 
508
+ rb_define_singleton_method(mPuma, "enable_child_subreaper", Puma_enable_child_subreaper, 0);
509
+
486
510
  rb_define_alloc_func(cHttpParser, HttpParser_alloc);
487
511
  rb_define_method(cHttpParser, "initialize", HttpParser_init, 0);
488
512
  rb_define_method(cHttpParser, "reset", HttpParser_reset, 0);
data/lib/puma/binder.rb CHANGED
@@ -309,6 +309,13 @@ module Puma
309
309
  end
310
310
  end
311
311
 
312
+ # Removes and returns the activated socket for +url+ as <tt>[key, socket]</tt>,
313
+ # or +nil+ if none was activated for it.
314
+ def take_activated_socket(url)
315
+ key = activated_keys_for(url).find { |k| @activated_sockets.key?(k) }
316
+ [key, @activated_sockets.delete(key)] if key
317
+ end
318
+
312
319
  def localhost_authority
313
320
  @localhost_authority ||= Localhost::Authority.fetch if defined?(Localhost::Authority) && !Puma::IS_JRUBY
314
321
  end
@@ -486,6 +493,28 @@ module Puma
486
493
 
487
494
  private
488
495
 
496
+ def activated_keys_for(url)
497
+ uri = URI.parse url
498
+ case uri.scheme
499
+ when "tcp", "ssl"
500
+ [[:tcp, uri.host, uri.port]]
501
+ when "unix"
502
+ path = "#{uri.host}#{uri.path}".gsub("%20", " ")
503
+ return [] if path.start_with?("@")
504
+
505
+ real_path = begin
506
+ File.realdirpath(path)
507
+ rescue SystemCallError
508
+ path
509
+ end
510
+ [[:unix, path], [:unix, real_path]].uniq
511
+ else
512
+ []
513
+ end
514
+ rescue URI::InvalidURIError
515
+ []
516
+ end
517
+
489
518
  # @!attribute [r] loopback_addresses
490
519
  def loopback_addresses
491
520
  t = Socket.ip_address_list.select do |addrinfo|
data/lib/puma/cli.rb CHANGED
@@ -138,6 +138,12 @@ module Puma
138
138
  user_config.fork_worker(*args.compact)
139
139
  end
140
140
 
141
+ o.on "--mold-worker=[REQUESTS]", Array,
142
+ "Fork new workers from a mold promoted from an existing worker. Cluster mode only",
143
+ "Promote a new mold at each comma-separated REQUESTS threshold (default 1000)" do |thresholds|
144
+ user_config.mold_worker(*thresholds)
145
+ end
146
+
141
147
  o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
142
148
  $LOAD_PATH.unshift(*arg.split(':'))
143
149
  end
data/lib/puma/client.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative 'detect'
4
4
  require_relative 'io_buffer'
5
5
  require_relative 'client_env'
6
+ require_relative 'null_io'
6
7
  require 'tempfile'
7
8
 
8
9
  if Puma::IS_JRUBY
@@ -18,6 +19,17 @@ module Puma
18
19
 
19
20
  class HttpParserError501 < IOError; end
20
21
 
22
+ # Raised when a +continue_callback+ answers an <tt>Expect: 100-continue</tt>
23
+ # request itself, so the request body is never read.
24
+ class ContinueRejected < StandardError
25
+ attr_reader :response
26
+
27
+ def initialize(response)
28
+ super("Expect: 100-continue answered by continue_callback")
29
+ @response = response
30
+ end
31
+ end
32
+
21
33
  #———————————————————————— DO NOT USE — this class is for internal use only ———
22
34
 
23
35
 
@@ -46,12 +58,12 @@ module Puma
46
58
  ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
47
59
 
48
60
  # chunked body validation
49
- CHUNK_SIZE_INVALID = /[^\h]/.freeze
61
+ CHUNK_SIZE_VALID = /\A\h+\z/.freeze
50
62
  CHUNK_VALID_ENDING = Const::LINE_END
51
63
  CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
52
64
 
53
65
  # The maximum number of bytes we'll buffer looking for a valid
54
- # chunk header.
66
+ # chunk header, and the maximum size of a trailer section.
55
67
  MAX_CHUNK_HEADER_SIZE = 4096
56
68
 
57
69
  SHUTDOWN_READ_GRACE = 1
@@ -78,7 +90,8 @@ module Puma
78
90
  :tempfile, :io_buffer, :http_content_length_limit_exceeded,
79
91
  :requests_served, :error_status_code, :bytes_read
80
92
 
81
- attr_writer :peerip, :http_content_length_limit, :supported_http_methods
93
+ attr_writer :peerip, :http_content_length_limit, :supported_http_methods,
94
+ :allow_underscore_headers, :continue_callback
82
95
 
83
96
  attr_accessor :remote_addr_header, :listener, :env_set_http_version,
84
97
  :reactor_error
@@ -108,6 +121,8 @@ module Puma
108
121
 
109
122
  @http_content_length_limit = nil
110
123
  @http_content_length_limit_exceeded = nil
124
+ @allow_underscore_headers = true
125
+ @continue_callback = nil
111
126
  @error_status_code = nil
112
127
  @reactor_error = nil
113
128
 
@@ -354,6 +369,8 @@ module Puma
354
369
  raise e unless HttpParserError === e && e.message.include?('non-SSL')
355
370
 
356
371
  req, _ = @buffer.split "\r\n\r\n"
372
+ raise e if req.nil? || req.empty?
373
+
357
374
  request_line, headers = req.split "\r\n", 2
358
375
 
359
376
  # below checks for request issues and changes error message accordingly
@@ -384,6 +401,7 @@ module Puma
384
401
 
385
402
  # processes the `env` and the request body
386
403
  def process_env_body
404
+ validate_env
387
405
  temp = setup_body
388
406
  normalize_env
389
407
  req_env_post_parse
@@ -496,10 +514,21 @@ module Puma
496
514
  # @return [Boolean] true if the body can be completely read, false otherwise
497
515
  #
498
516
  def setup_body
517
+ # The parsers preserve underscores as commas to distinguish these aliases
518
+ # from real framing headers. Reject them before 100 Continue or body reads,
519
+ # regardless of allow_underscore_headers, to avoid proxy disagreements.
520
+ if @env.key?('HTTP_CONTENT,LENGTH') || @env.key?('HTTP_TRANSFER,ENCODING')
521
+ @error_status_code = 400
522
+ @env[HTTP_CONNECTION] = CLOSE
523
+ raise HttpParserError, 'Underscores are not allowed in HTTP framing headers'
524
+ end
525
+
499
526
  @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
500
527
 
501
528
  if @env[HTTP_EXPECT] == CONTINUE
502
- # TODO allow a hook here to check the headers before going forward
529
+ if @continue_callback && (response = @continue_callback.call(@env))
530
+ reject_continue response
531
+ end
503
532
  write_interim HTTP_11_100
504
533
  end
505
534
 
@@ -704,6 +733,8 @@ module Puma
704
733
  end
705
734
 
706
735
  def decode_chunk(chunk)
736
+ return finish_last_chunk(@prev_chunk + chunk) if @in_last_chunk
737
+
707
738
  if @partial_part_left > 0
708
739
  if @partial_part_left <= chunk.size
709
740
  if @partial_part_left > 2
@@ -739,31 +770,18 @@ module Puma
739
770
  # Puma doesn't process chunk extensions, but should parse if they're
740
771
  # present, which is the reason for the semicolon regex
741
772
  chunk_hex = line.strip[/\A[^;]+/]
742
- if CHUNK_SIZE_INVALID.match? chunk_hex
743
- raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
773
+ unless CHUNK_SIZE_VALID.match? chunk_hex
774
+ err_msg = chunk_hex && !chunk_hex.empty? ?
775
+ "Invalid chunk size: '#{chunk_hex}'" :
776
+ "Chunk size cannot be empty or nil"
777
+ raise HttpParserError, err_msg
744
778
  end
779
+
745
780
  len = chunk_hex.to_i(16)
746
781
  if len == 0
747
782
  @in_last_chunk = true
748
783
  @body.rewind
749
- rest = io.read
750
- if rest.bytesize < CHUNK_VALID_ENDING_SIZE
751
- @buffer = nil
752
- @partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
753
- return false
754
- else
755
- # if the next character is a CRLF, set buffer to everything after that CRLF
756
- start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
757
- CHUNK_VALID_ENDING_SIZE
758
- else # we have started a trailer section, which we do not support. skip it!
759
- rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
760
- end
761
-
762
- @buffer = rest[start_of_rest..-1]
763
- @buffer = nil if @buffer.empty?
764
- set_ready
765
- return true
766
- end
784
+ return finish_last_chunk(io.read)
767
785
  end
768
786
 
769
787
  # Track the excess as a function of the size of the
@@ -816,12 +834,33 @@ module Puma
816
834
  end
817
835
  end
818
836
 
819
- if @in_last_chunk
820
- set_ready
821
- true
822
- else
823
- false
837
+ false
838
+ end
839
+
840
+ # Consume the terminator that ends a chunked body. Per RFC 9112 §7.1.2
841
+ # this is either a bare CRLF or an optional trailer section ending in
842
+ # CRLF*2. Puma does not surface trailers, so we skip past them. Bytes
843
+ # past the terminator belong to the next pipelined request.
844
+ def finish_last_chunk(rest)
845
+ if rest.start_with?(CHUNK_VALID_ENDING)
846
+ trailer_end = CHUNK_VALID_ENDING_SIZE
847
+ elsif idx = rest.index(CHUNK_VALID_ENDING * 2)
848
+ trailer_end = idx + CHUNK_VALID_ENDING_SIZE * 2
849
+ elsif rest.bytesize < MAX_CHUNK_HEADER_SIZE
850
+ @prev_chunk = rest
851
+ return false
852
+ end
853
+
854
+ if trailer_end.nil? || trailer_end > MAX_CHUNK_HEADER_SIZE
855
+ @env[HTTP_CONNECTION] = CLOSE
856
+ raise HttpParserError, "maximum size of chunk trailer exceeded"
824
857
  end
858
+
859
+ @prev_chunk = ""
860
+ @buffer = rest[trailer_end..-1]
861
+ @buffer = nil if @buffer.empty?
862
+ set_ready
863
+ true
825
864
  end
826
865
 
827
866
  def set_ready
@@ -832,6 +871,13 @@ module Puma
832
871
  @ready = true
833
872
  end
834
873
 
874
+ def reject_continue(response)
875
+ @buffer = nil
876
+ @body = EmptyBody
877
+ @env[HTTP_CONNECTION] = 'close'
878
+ raise ContinueRejected, response
879
+ end
880
+
835
881
  def raise_above_http_content_limit
836
882
  @http_content_length_limit_exceeded = true
837
883
  @buffer = nil
@@ -12,6 +12,22 @@ module Puma
12
12
 
13
13
  include Puma::Const
14
14
 
15
+ def validate_env
16
+ return unless @env[SERVER_PROTOCOL] == HTTP_11
17
+
18
+ unless @env.key? HTTP_HOST
19
+ @error_status_code = 400
20
+ @env[HTTP_CONNECTION] = CLOSE
21
+ raise HttpParserError, "Missing Host header"
22
+ end
23
+
24
+ if @env[HTTP_HOST].empty?
25
+ @error_status_code = 400
26
+ @env[HTTP_CONNECTION] = CLOSE
27
+ raise HttpParserError, "Invalid Host header"
28
+ end
29
+ end
30
+
15
31
  # Given a Hash +env+ for the request read from +client+, add
16
32
  # and fixup keys to comply with Rack's env guidelines.
17
33
  # @param env [Hash] see Puma::Client#env, from request
@@ -105,9 +121,10 @@ module Puma
105
121
  # avoid allocation in the common case (ie there are no headers
106
122
  # with `,` in their names), that's why it has the extra conditionals.
107
123
  #
108
- # @note If a normalized version of a `,` header already exists, we ignore
109
- # the `,` version. This prevents clobbering headers managed by proxies
110
- # but not by clients (Like X-Forwarded-For).
124
+ # @note If underscore headers are disallowed, `,` headers are discarded.
125
+ # Otherwise, if a normalized version of a `,` header already exists, we
126
+ # ignore the `,` version. This prevents clobbering headers managed by
127
+ # proxies but not by clients (Like X-Forwarded-For).
111
128
  #
112
129
  # @param env [Hash] see Puma::Client#env, from request, modifies in place
113
130
  # @version 5.0.3
@@ -115,28 +132,25 @@ module Puma
115
132
  def req_env_post_parse
116
133
  to_delete = nil
117
134
  to_add = nil
135
+ underscore_headers = nil
118
136
 
119
137
  @env.each do |k,v|
120
- if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k)
121
- if to_delete
122
- to_delete << k
123
- else
124
- to_delete = [k]
125
- end
138
+ next unless k.start_with?("HTTP_") && k.include?(",")
126
139
 
127
- new_k = k.tr(",", "_")
128
- if @env.key?(new_k)
129
- next
130
- end
140
+ (underscore_headers ||= []) << k.delete_prefix("HTTP_").tr("_,", "-_")
141
+ next if @allow_underscore_headers && UNMASKABLE_HEADERS.key?(k)
131
142
 
132
- unless to_add
133
- to_add = {}
134
- end
143
+ (to_delete ||= []) << k
144
+ next unless @allow_underscore_headers
135
145
 
136
- to_add[new_k] = v
137
- end
146
+ new_k = k.tr(",", "_")
147
+ next if @env.key?(new_k)
148
+
149
+ (to_add ||= {})[new_k] = v
138
150
  end
139
151
 
152
+ @env[PUMA_UNDERSCORE_HEADERS] = underscore_headers if underscore_headers
153
+
140
154
  if to_delete # rubocop:disable Style/SafeNavigation
141
155
  to_delete.each { |k| env.delete(k) }
142
156
  end