puma 6.4.1 → 7.2.1

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +407 -8
  3. data/README.md +109 -49
  4. data/docs/deployment.md +58 -23
  5. data/docs/fork_worker.md +11 -1
  6. data/docs/java_options.md +54 -0
  7. data/docs/jungle/README.md +1 -1
  8. data/docs/kubernetes.md +11 -16
  9. data/docs/plugins.md +6 -2
  10. data/docs/restart.md +2 -2
  11. data/docs/signals.md +21 -21
  12. data/docs/stats.md +11 -5
  13. data/docs/systemd.md +14 -5
  14. data/ext/puma_http11/extconf.rb +20 -32
  15. data/ext/puma_http11/mini_ssl.c +29 -9
  16. data/ext/puma_http11/org/jruby/puma/Http11.java +40 -9
  17. data/ext/puma_http11/puma_http11.c +125 -118
  18. data/lib/puma/app/status.rb +11 -3
  19. data/lib/puma/binder.rb +21 -11
  20. data/lib/puma/cli.rb +10 -8
  21. data/lib/puma/client.rb +183 -83
  22. data/lib/puma/cluster/worker.rb +24 -21
  23. data/lib/puma/cluster/worker_handle.rb +38 -8
  24. data/lib/puma/cluster.rb +73 -47
  25. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  26. data/lib/puma/commonlogger.rb +3 -3
  27. data/lib/puma/configuration.rb +131 -60
  28. data/lib/puma/const.rb +31 -12
  29. data/lib/puma/control_cli.rb +10 -6
  30. data/lib/puma/detect.rb +2 -0
  31. data/lib/puma/dsl.rb +411 -121
  32. data/lib/puma/error_logger.rb +7 -5
  33. data/lib/puma/events.rb +25 -10
  34. data/lib/puma/io_buffer.rb +8 -4
  35. data/lib/puma/jruby_restart.rb +0 -16
  36. data/lib/puma/launcher/bundle_pruner.rb +1 -1
  37. data/lib/puma/launcher.rb +73 -55
  38. data/lib/puma/log_writer.rb +9 -9
  39. data/lib/puma/minissl/context_builder.rb +1 -0
  40. data/lib/puma/minissl.rb +1 -1
  41. data/lib/puma/null_io.rb +26 -0
  42. data/lib/puma/plugin/systemd.rb +3 -3
  43. data/lib/puma/rack/urlmap.rb +1 -1
  44. data/lib/puma/reactor.rb +19 -13
  45. data/lib/puma/request.rb +71 -39
  46. data/lib/puma/runner.rb +15 -17
  47. data/lib/puma/sd_notify.rb +1 -4
  48. data/lib/puma/server.rb +134 -73
  49. data/lib/puma/single.rb +7 -4
  50. data/lib/puma/state_file.rb +3 -2
  51. data/lib/puma/thread_pool.rb +57 -80
  52. data/lib/puma/util.rb +0 -7
  53. data/lib/puma.rb +10 -0
  54. data/lib/rack/handler/puma.rb +10 -7
  55. data/tools/Dockerfile +15 -5
  56. metadata +14 -15
  57. data/ext/puma_http11/ext_help.h +0 -15
data/docs/signals.md CHANGED
@@ -17,13 +17,13 @@ $ ps aux | grep tail
17
17
  schneems 87152 0.0 0.0 2432772 492 s032 S+ 12:46PM 0:00.00 tail -f my.log
18
18
  ```
19
19
 
20
- You can send a signal in Ruby using the [Process module](https://www.ruby-doc.org/core-2.1.1/Process.html#kill-method):
20
+ You can send a signal in Ruby using the [Process module](https://docs.ruby-lang.org/en/master/Process.html#method-c-kill):
21
21
 
22
22
  ```
23
23
  $ irb
24
24
  > puts pid
25
25
  => 87152
26
- Process.detach(pid) # https://ruby-doc.org/core-2.1.1/Process.html#method-c-detach
26
+ Process.detach(pid) # https://docs.ruby-lang.org/en/master/Process.html#method-c-detach
27
27
  Process.kill("TERM", pid)
28
28
  ```
29
29
 
@@ -33,16 +33,16 @@ Now you will see via `ps` that there is no more `tail` process. Sometimes when r
33
33
 
34
34
  Puma cluster responds to these signals:
35
35
 
36
- - `TTIN` increment the worker count by 1
37
- - `TTOU` decrement the worker count by 1
38
- - `TERM` send `TERM` to worker. The worker will attempt to finish then exit.
39
- - `USR2` restart workers. This also reloads the Puma configuration file, if there is one.
40
- - `USR1` restart workers in phases, a rolling restart. This will not reload the configuration file.
41
- - `HUP ` reopen log files defined in stdout_redirect configuration parameter. If there is no stdout_redirect option provided, it will behave like `INT`
42
- - `INT ` equivalent of sending Ctrl-C to cluster. Puma will attempt to finish then exit.
43
- - `CHLD`
44
- - `URG ` refork workers in phases from worker 0 if `fork_workers` option is enabled.
45
- - `INFO` print backtraces of all puma threads
36
+ - `TTIN`: Increment the worker count by 1.
37
+ - `TTOU`: Decrement the worker count by 1.
38
+ - `TERM`: Send `TERM` to worker. The worker will attempt to finish then exit.
39
+ - `USR2`: Restart workers. This also reloads the Puma configuration file, if there is one.
40
+ - `USR1`: Restart workers in phases, a rolling restart. This will not reload the configuration file.
41
+ - `HUP`: Reopen log files defined in `stdout_redirect` configuration parameter. If there is no `stdout_redirect` option provided, it will behave like `INT`.
42
+ - `INT`: Equivalent of sending Ctrl-C to cluster. Puma will attempt to finish then exit.
43
+ - `CHLD`: Reap zombie child processes and wake event loop in `fork_worker` mode.
44
+ - `URG`: Refork workers in phases from worker 0 if `fork_worker` option is enabled.
45
+ - `INFO`: Print backtraces of all Puma threads.
46
46
 
47
47
  ## Callbacks order in case of different signals
48
48
 
@@ -54,12 +54,12 @@ puma configuration file reloaded, if there is one
54
54
  puma configuration file reloaded, if there is one
55
55
 
56
56
  before_fork
57
- on_worker_fork
57
+ before_worker_fork
58
58
  after_worker_fork
59
59
 
60
60
  Gemfile in context
61
61
 
62
- on_worker_boot
62
+ before_worker_boot
63
63
 
64
64
  Code of the app is loaded and running
65
65
  ```
@@ -67,18 +67,18 @@ Code of the app is loaded and running
67
67
  ### Send USR2
68
68
 
69
69
  ```
70
- on_worker_shutdown
71
- on_restart
70
+ before_worker_shutdown
71
+ before_restart
72
72
 
73
73
  puma configuration file reloaded, if there is one
74
74
 
75
75
  before_fork
76
- on_worker_fork
76
+ before_worker_fork
77
77
  after_worker_fork
78
78
 
79
79
  Gemfile in context
80
80
 
81
- on_worker_boot
81
+ before_worker_boot
82
82
 
83
83
  Code of the app is loaded and running
84
84
  ```
@@ -86,13 +86,13 @@ Code of the app is loaded and running
86
86
  ### Send USR1
87
87
 
88
88
  ```
89
- on_worker_shutdown
90
- on_worker_fork
89
+ before_worker_shutdown
90
+ before_worker_fork
91
91
  after_worker_fork
92
92
 
93
93
  Gemfile in context
94
94
 
95
- on_worker_boot
95
+ before_worker_boot
96
96
 
97
97
  Code of the app is loaded and running
98
98
  ```
data/docs/stats.md CHANGED
@@ -55,16 +55,22 @@ end
55
55
 
56
56
  When Puma runs in single mode, these stats are available at the top level. When Puma runs in cluster mode, these stats are available within the `worker_status` array in a hash labeled `last_status`, in an array of hashes where one hash represents each worker.
57
57
 
58
- * backlog: requests that are waiting for an available thread to be available. if this is above 0, you need more capacity [always true?]
59
- * running: how many threads are running
60
- * pool_capacity: the number of requests that the server is capable of taking right now. For example, if the number is 5, then it means there are 5 threads sitting idle ready to take a request. If one request comes in, then the value would be 4 until it finishes processing. If the minimum threads allowed is zero, this number will still have a maximum value of the maximum threads allowed.
58
+ * backlog: requests that are waiting for an available thread to be available. if this is frequently above 0, you need more capacity.
59
+ * running: how many threads are spawned. A spawned thread may be busy processing a request or waiting for a new request. If `min_threads` and `max_threads` are set to the same number,
60
+ this will be a never-changing number (other than rare cases when a thread dies, etc).
61
+ * busy_threads: `running` - `how many threads are waiting to receive work` + `how many requests are waiting for a thread to pick them up`.
62
+ this is a "wholistic" stat reflecting the overall current state of work to be done and the capacity to do it.
63
+ * pool_capacity: `how many threads are waiting to receive work` + `max_threads` - `running`. In a typical configuration where `min_threads`
64
+ and `max_threads` are configured to the same number, this is simply `how many threads are waiting to receive work`. This number exists only as a stat
65
+ and is not used for any internal decisions, unlike `busy_threads`, which is usually a more useful stat.
61
66
  * max_threads: the maximum number of threads Puma is configured to spool per worker
62
67
  * requests_count: the number of requests this worker has served since starting
63
-
68
+ * reactor_max: the maximum observed number of requests held in Puma's "reactor" which is used for asyncronously buffering request bodies. This stat is reset on every call, so it's the maximum value observed since the last stat call.
69
+ * backlog_max: the maximum number of requests that have been fully buffered by the reactor and placed in a ready queue, but have not yet been picked up by a server thread. This stat is reset on every call, so it's the maximum value observed since the last stat call.
64
70
 
65
71
  ### cluster mode
66
72
 
67
- * phase: which phase of restart the process is in, during [phased restart](https://github.com/puma/puma/blob/master/docs/restart.md)
73
+ * phase: which phase of restart the process is in, during [phased restart](https://github.com/puma/puma/blob/main/docs/restart.md)
68
74
  * workers: ??
69
75
  * booted_workers: how many workers currently running?
70
76
  * old_workers: ??
data/docs/systemd.md CHANGED
@@ -72,7 +72,7 @@ systemd and Puma also support socket activation, where systemd opens the
72
72
  listening socket(s) in advance and provides them to the Puma master process on
73
73
  startup. Among other advantages, this keeps listening sockets open across puma
74
74
  restarts and achieves graceful restarts, including when upgraded Puma, and is
75
- compatible with both clustered mode and application preload.
75
+ compatible with both cluster mode and application preload.
76
76
 
77
77
  **Note:** Any wrapper scripts which `exec`, or other indirections in `ExecStart`
78
78
  may result in activated socket file descriptors being closed before reaching the
@@ -99,9 +99,11 @@ ListenStream=0.0.0.0:9293
99
99
  # ListenStream=/run/puma.sock
100
100
 
101
101
  # Socket options matching Puma defaults
102
- NoDelay=true
103
102
  ReusePort=true
104
103
  Backlog=1024
104
+ # Enable this if you're using Puma with the "low_latency" option, read more in Puma DSL docs and systemd docs:
105
+ # https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#NoDelay=
106
+ # NoDelay=true
105
107
 
106
108
  [Install]
107
109
  WantedBy=sockets.target
@@ -117,8 +119,8 @@ or cluster mode.
117
119
  ### Sockets and symlinks
118
120
 
119
121
  When using releases folders, you should set the socket path using the shared
120
- folder path (ex. `/srv/projet/shared/tmp/puma.sock`), not the release folder
121
- path (`/srv/projet/releases/1234/tmp/puma.sock`).
122
+ folder path (ex. `/srv/project/shared/tmp/puma.sock`), not the release folder
123
+ path (`/srv/project/releases/1234/tmp/puma.sock`).
122
124
 
123
125
  Puma will detect the release path socket as different than the one provided by
124
126
  systemd and attempt to bind it again, resulting in the exception `There is
@@ -137,7 +139,7 @@ automatically for any activated socket. When systemd socket activation is not
137
139
  enabled, this option does nothing.
138
140
 
139
141
  This also accepts an optional argument `only` (DSL: `'only'`) to discard any
140
- binds that's not socket activated.
142
+ binds that are not socket activated.
141
143
 
142
144
  ## Usage
143
145
 
@@ -239,6 +241,13 @@ cap $stage puma:start --dry-run
239
241
  cap $stage puma:stop --dry-run
240
242
  ~~~~
241
243
 
244
+ ### Disabling Puma Systemd Integration
245
+
246
+ If you would like to disable Puma's systemd integration, for example if you handle it elsewhere
247
+ in your code yourself, simply set the the environment variable `PUMA_SKIP_SYSTEMD` to any value.
248
+
249
+
250
+
242
251
  [Restart]: https://www.freedesktop.org/software/systemd/man/systemd.service.html#Restart=
243
252
  [#1367]: https://github.com/puma/puma/issues/1367
244
253
  [#1499]: https://github.com/puma/puma/issues/1499
@@ -10,15 +10,13 @@ end
10
10
 
11
11
  unless ENV["PUMA_DISABLE_SSL"]
12
12
  # don't use pkg_config('openssl') if '--with-openssl-dir' is used
13
- # also looks within the Ruby build for directory info
14
13
  has_openssl_dir = dir_config('openssl').any? ||
15
- RbConfig::CONFIG['configure_args']&.include?('openssl') ||
16
- Dir.exist?("#{RbConfig::TOPDIR}/src/main/c/openssl") # TruffleRuby
14
+ RbConfig::CONFIG['configure_args']&.include?('openssl')
17
15
 
18
16
  found_pkg_config = !has_openssl_dir && pkg_config('openssl')
19
17
 
20
18
  found_ssl = if !$mingw && found_pkg_config
21
- puts 'using OpenSSL pkgconfig (openssl.pc)'
19
+ puts '──── Using OpenSSL pkgconfig (openssl.pc) ────'
22
20
  true
23
21
  elsif have_library('libcrypto', 'BIO_read') && have_library('libssl', 'SSL_CTX_new')
24
22
  true
@@ -33,45 +31,35 @@ unless ENV["PUMA_DISABLE_SSL"]
33
31
  if found_ssl
34
32
  have_header "openssl/bio.h"
35
33
 
36
- # below is yes for 1.0.2 & later
37
- have_func "DTLS_method" , "openssl/ssl.h"
38
- have_func "SSL_CTX_set_session_cache_mode(NULL, 0)", "openssl/ssl.h"
34
+ ssl_h = "openssl/ssl.h".freeze
39
35
 
40
- # below are yes for 1.1.0 & later
41
- have_func "TLS_server_method" , "openssl/ssl.h"
42
- have_func "SSL_CTX_set_min_proto_version(NULL, 0)" , "openssl/ssl.h"
36
+ puts "\n──── Below are yes for 1.0.2 & later ────"
37
+ have_func "DTLS_method" , ssl_h
38
+ have_func "SSL_CTX_set_session_cache_mode(NULL, 0)", ssl_h
43
39
 
44
- have_func "X509_STORE_up_ref"
45
- have_func "SSL_CTX_set_ecdh_auto(NULL, 0)" , "openssl/ssl.h"
40
+ puts "\n──── Below are yes for 1.1.0 & later ────"
41
+ have_func "TLS_server_method" , ssl_h
42
+ have_func "SSL_CTX_set_min_proto_version(NULL, 0)" , ssl_h
46
43
 
47
- # below exists in 1.1.0 and later, but isn't documented until 3.0.0
48
- have_func "SSL_CTX_set_dh_auto(NULL, 0)" , "openssl/ssl.h"
44
+ puts "\n──── Below is yes for 1.1.0 and later, but isn't documented until 3.0.0 ────"
45
+ # https://github.com/openssl/openssl/blob/OpenSSL_1_1_0/include/openssl/ssl.h#L1159
46
+ have_func "SSL_CTX_set_dh_auto(NULL, 0)" , ssl_h
49
47
 
50
- # below is yes for 3.0.0 & later
51
- have_func "SSL_get1_peer_certificate" , "openssl/ssl.h"
48
+ puts "\n──── Below is yes for 1.1.1 & later ────"
49
+ have_func "SSL_CTX_set_ciphersuites(NULL, \"\")" , ssl_h
52
50
 
53
- # Random.bytes available in Ruby 2.5 and later, Random::DEFAULT deprecated in 3.0
54
- if Random.respond_to?(:bytes)
55
- $defs.push "-DHAVE_RANDOM_BYTES"
56
- puts "checking for Random.bytes... yes"
57
- else
58
- puts "checking for Random.bytes... no"
59
- end
51
+ puts "\n──── Below is yes for 3.0.0 & later ────"
52
+ have_func "SSL_get1_peer_certificate" , ssl_h
53
+
54
+ puts ''
60
55
  end
61
56
  end
62
57
 
63
58
  if ENV["PUMA_MAKE_WARNINGS_INTO_ERRORS"]
64
59
  # Make all warnings into errors
65
60
  # Except `implicit-fallthrough` since most failures comes from ragel state machine generated code
66
- if respond_to?(:append_cflags, true) # Ruby 2.5 and later
67
- append_cflags(config_string('WERRORFLAG') || '-Werror')
68
- append_cflags '-Wno-implicit-fallthrough'
69
- else
70
- # flag may not exist on some platforms, -Werror may not be defined on some platforms, but
71
- # works with all in current CI
72
- $CFLAGS << " #{config_string('WERRORFLAG') || '-Werror'}"
73
- $CFLAGS << ' -Wno-implicit-fallthrough'
74
- end
61
+ append_cflags(config_string('WERRORFLAG') || '-Werror')
62
+ append_cflags '-Wno-implicit-fallthrough'
75
63
  end
76
64
 
77
65
  create_makefile("puma/puma_http11")
@@ -229,7 +229,7 @@ VALUE
229
229
  sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
230
230
  SSL_CTX* ctx;
231
231
  int ssl_options;
232
- VALUE key, cert, ca, verify_mode, ssl_cipher_filter, no_tlsv1, no_tlsv1_1,
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;
235
235
  X509 *x509 = NULL;
@@ -269,6 +269,8 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
269
269
 
270
270
  ssl_cipher_filter = rb_funcall(mini_ssl_ctx, rb_intern_const("ssl_cipher_filter"), 0);
271
271
 
272
+ ssl_ciphersuites = rb_funcall(mini_ssl_ctx, rb_intern_const("ssl_ciphersuites"), 0);
273
+
272
274
  no_tlsv1 = rb_funcall(mini_ssl_ctx, rb_intern_const("no_tlsv1"), 0);
273
275
 
274
276
  no_tlsv1_1 = rb_funcall(mini_ssl_ctx, rb_intern_const("no_tlsv1_1"), 0);
@@ -444,6 +446,14 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
444
446
  SSL_CTX_set_cipher_list(ctx, "HIGH:!aNULL@STRENGTH");
445
447
  }
446
448
 
449
+ #if HAVE_SSL_CTX_SET_CIPHERSUITES
450
+ // Only override OpenSSL default ciphersuites if config option is supplied.
451
+ if (!NIL_P(ssl_ciphersuites)) {
452
+ StringValue(ssl_ciphersuites);
453
+ SSL_CTX_set_ciphersuites(ctx, RSTRING_PTR(ssl_ciphersuites));
454
+ }
455
+ #endif
456
+
447
457
  #if OPENSSL_VERSION_NUMBER < 0x10002000L
448
458
  // Remove this case if OpenSSL 1.0.1 (now EOL) support is no longer needed.
449
459
  ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
@@ -461,13 +471,8 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
461
471
  SSL_CTX_set_verify(ctx, NUM2INT(verify_mode), engine_verify_callback);
462
472
  }
463
473
 
464
- // Random.bytes available in Ruby 2.5 and later, Random::DEFAULT deprecated in 3.0
465
474
  session_id_bytes = rb_funcall(
466
- #ifdef HAVE_RANDOM_BYTES
467
475
  rb_cRandom,
468
- #else
469
- rb_const_get(rb_cRandom, rb_intern_const("DEFAULT")),
470
- #endif
471
476
  rb_intern_const("bytes"),
472
477
  1, ULL2NUM(SSL_MAX_SSL_SESSION_ID_LENGTH));
473
478
 
@@ -655,14 +660,29 @@ VALUE engine_shutdown(VALUE self) {
655
660
 
656
661
  TypedData_Get_Struct(self, ms_conn, &engine_data_type, conn);
657
662
 
663
+ if (SSL_in_init(conn->ssl)) {
664
+ // Avoid "shutdown while in init" error
665
+ // See https://github.com/openssl/openssl/blob/openssl-3.5.2/ssl/ssl_lib.c#L2827-L2828
666
+ return Qtrue;
667
+ }
668
+
658
669
  ERR_clear_error();
659
670
 
660
671
  ok = SSL_shutdown(conn->ssl);
661
- if (ok == 0) {
662
- return Qfalse;
672
+ // See https://github.com/openssl/openssl/blob/openssl-3.5.2/ssl/ssl_lib.c#L2792-L2797
673
+ // for description of SSL_shutdown return values.
674
+ switch (ok) {
675
+ case 0:
676
+ // "close notify" alert is sent by us.
677
+ return Qfalse;
678
+ case 1:
679
+ // "close notify" alert was received from peer.
680
+ return Qtrue;
681
+ default:
682
+ raise_error(conn->ssl, ok);
663
683
  }
664
684
 
665
- return Qtrue;
685
+ return Qnil;
666
686
  }
667
687
 
668
688
  VALUE engine_init(VALUE self) {
@@ -26,14 +26,14 @@ public class Http11 extends RubyObject {
26
26
  public final static String MAX_FIELD_NAME_LENGTH_ERR = "HTTP element FIELD_NAME is longer than the 256 allowed length.";
27
27
  public final static int MAX_FIELD_VALUE_LENGTH = 80 * 1024;
28
28
  public final static String MAX_FIELD_VALUE_LENGTH_ERR = "HTTP element FIELD_VALUE is longer than the 81920 allowed length.";
29
- public final static int MAX_REQUEST_URI_LENGTH = 1024 * 12;
30
- public final static String MAX_REQUEST_URI_LENGTH_ERR = "HTTP element REQUEST_URI is longer than the 12288 allowed length.";
29
+ public final static int MAX_REQUEST_URI_LENGTH = getConstLength("PUMA_REQUEST_URI_MAX_LENGTH", 1024 * 12);
30
+ public final static String MAX_REQUEST_URI_LENGTH_ERR = "HTTP element REQUEST_URI is longer than the " + MAX_REQUEST_URI_LENGTH + " allowed length.";
31
31
  public final static int MAX_FRAGMENT_LENGTH = 1024;
32
- public final static String MAX_FRAGMENT_LENGTH_ERR = "HTTP element REQUEST_PATH is longer than the 1024 allowed length.";
33
- public final static int MAX_REQUEST_PATH_LENGTH = 8192;
34
- public final static String MAX_REQUEST_PATH_LENGTH_ERR = "HTTP element REQUEST_PATH is longer than the 8192 allowed length.";
35
- public final static int MAX_QUERY_STRING_LENGTH = 1024 * 10;
36
- public final static String MAX_QUERY_STRING_LENGTH_ERR = "HTTP element QUERY_STRING is longer than the 10240 allowed length.";
32
+ public final static String MAX_FRAGMENT_LENGTH_ERR = "HTTP element FRAGMENT is longer than the 1024 allowed length.";
33
+ public final static int MAX_REQUEST_PATH_LENGTH = getConstLength("PUMA_REQUEST_PATH_MAX_LENGTH", 8192);
34
+ public final static String MAX_REQUEST_PATH_LENGTH_ERR = "HTTP element REQUEST_PATH is longer than the " + MAX_REQUEST_PATH_LENGTH + " allowed length.";
35
+ public final static int MAX_QUERY_STRING_LENGTH = getConstLength("PUMA_QUERY_STRING_MAX_LENGTH", 10 * 1024);
36
+ public final static String MAX_QUERY_STRING_LENGTH_ERR = "HTTP element QUERY_STRING is longer than the " + MAX_QUERY_STRING_LENGTH +" allowed length.";
37
37
  public final static int MAX_HEADER_LENGTH = 1024 * (80 + 32);
38
38
  public final static String MAX_HEADER_LENGTH_ERR = "HTTP element HEADER is longer than the 114688 allowed length.";
39
39
 
@@ -48,6 +48,27 @@ public class Http11 extends RubyObject {
48
48
  public static final ByteList QUERY_STRING_BYTELIST = new ByteList(ByteList.plain("QUERY_STRING"));
49
49
  public static final ByteList SERVER_PROTOCOL_BYTELIST = new ByteList(ByteList.plain("SERVER_PROTOCOL"));
50
50
 
51
+ public static String getEnvOrProperty(String name) {
52
+ String envValue = System.getenv(name);
53
+ return (envValue != null) ? envValue : System.getProperty(name);
54
+ }
55
+
56
+ public static int getConstLength(String name, Integer defaultValue) {
57
+ String stringValue = getEnvOrProperty(name);
58
+ if (stringValue == null || stringValue.isEmpty()) return defaultValue;
59
+
60
+ try {
61
+ int value = Integer.parseUnsignedInt(stringValue);
62
+ if (value <= 0) {
63
+ throw new NumberFormatException("The number is not positive.");
64
+ }
65
+ return value;
66
+ } catch (NumberFormatException e) {
67
+ System.err.println(String.format("The value %s for %s is invalid. Using default value %d instead.", stringValue, name, defaultValue));
68
+ return defaultValue;
69
+ }
70
+ }
71
+
51
72
  private static ObjectAllocator ALLOCATOR = new ObjectAllocator() {
52
73
  public IRubyObject allocate(Ruby runtime, RubyClass klass) {
53
74
  return new Http11(runtime, klass);
@@ -56,7 +77,7 @@ public class Http11 extends RubyObject {
56
77
 
57
78
  public static void createHttp11(Ruby runtime) {
58
79
  RubyModule mPuma = runtime.defineModule("Puma");
59
- mPuma.defineClassUnder("HttpParserError",runtime.getClass("IOError"),runtime.getClass("IOError").getAllocator());
80
+ mPuma.defineClassUnder("HttpParserError",runtime.getClass("StandardError"),runtime.getClass("StandardError").getAllocator());
60
81
 
61
82
  RubyClass cHttpParser = mPuma.defineClassUnder("HttpParser",runtime.getObject(),ALLOCATOR);
62
83
  cHttpParser.defineAnnotatedMethods(Http11.class);
@@ -88,6 +109,10 @@ public class Http11 extends RubyObject {
88
109
  return (RubyClass)runtime.getModule("Puma").getConstant("HttpParserError");
89
110
  }
90
111
 
112
+ private static boolean is_ows(int c) {
113
+ return c == ' ' || c == '\t';
114
+ }
115
+
91
116
  public static void http_field(Ruby runtime, RubyHash req, ByteList buffer, int field, int flen, int value, int vlen) {
92
117
  RubyString f;
93
118
  IRubyObject v;
@@ -99,12 +124,18 @@ public class Http11 extends RubyObject {
99
124
  int bite = b.get(i) & 0xFF;
100
125
  if(bite == '-') {
101
126
  b.set(i, (byte)'_');
127
+ } else if(bite == '_') {
128
+ b.set(i, (byte)',');
102
129
  } else {
103
130
  b.set(i, (byte)Character.toUpperCase(bite));
104
131
  }
105
132
  }
106
133
 
107
- while (vlen > 0 && Character.isWhitespace(buffer.get(value + vlen - 1))) vlen--;
134
+ while (vlen > 0 && is_ows(buffer.get(value + vlen - 1))) vlen--;
135
+ while (vlen > 0 && is_ows(buffer.get(value))) {
136
+ vlen--;
137
+ value++;
138
+ }
108
139
 
109
140
  if (b.equals(CONTENT_LENGTH_BYTELIST) || b.equals(CONTENT_TYPE_BYTELIST)) {
110
141
  f = RubyString.newString(runtime, b);