puma 6.3.0 → 6.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of puma might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/History.md +47 -2
- data/README.md +18 -9
- data/docs/kubernetes.md +12 -0
- data/ext/puma_http11/mini_ssl.c +61 -6
- data/lib/puma/binder.rb +2 -2
- data/lib/puma/cli.rb +4 -0
- data/lib/puma/client.rb +19 -11
- data/lib/puma/configuration.rb +3 -1
- data/lib/puma/const.rb +2 -2
- data/lib/puma/control_cli.rb +12 -5
- data/lib/puma/dsl.rb +51 -4
- data/lib/puma/rack/urlmap.rb +1 -1
- data/lib/puma/runner.rb +5 -1
- data/lib/puma/server.rb +52 -23
- data/lib/puma/thread_pool.rb +34 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e58946300296f4d215a5859fcadb101b4fcec6d9012b38fd0af10c5fdb3ce0c2
|
4
|
+
data.tar.gz: 397fa109025cbc87d7466be97ed91e44789d522db2c4b89d1ef5f4e40db9f772
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a0c6a4e6c10522de9a9c7d5eed57d5205a6d4acfe18e9d7c310044d5793a2a67a266c3b69f8ed4c5385559149dc858dbcf1b4307c86459b9723ea49303de5685
|
7
|
+
data.tar.gz: cf7a60331c499a1387414ca2d6955edbc239242a62ea06941bf912714ee4dcb9295a5a1bd9c3561fd396c54a2e04e5a0aa2194ef17b37accb78e049e4cb1ca6b
|
data/History.md
CHANGED
@@ -1,3 +1,26 @@
|
|
1
|
+
## 6.4.0 / 2023-09-21
|
2
|
+
|
3
|
+
* Features
|
4
|
+
* on_thread_exit hook ([#2920])
|
5
|
+
* on_thread_start_hook ([#3195])
|
6
|
+
* Shutdown on idle ([#3209], [#2580])
|
7
|
+
* New error message when control server port taken ([#3204])
|
8
|
+
|
9
|
+
* Refactor
|
10
|
+
* Remove `Forwardable` dependency ([#3191], #3190)
|
11
|
+
* Update URLMap Regexp usage for Ruby v3.3 ([#3165])
|
12
|
+
|
13
|
+
* Bugfixes
|
14
|
+
* Bring the cert_pem: parameter into parity with the cert: parameter to ssl_bind. ([#3174])
|
15
|
+
* Fix using control server with IPv6 host ([#3181])
|
16
|
+
* control_cli.rb - add require_relative 'log_writer' ([#3187])
|
17
|
+
* Fix cases where fallback Rack response wasn't sent to the client ([#3094])
|
18
|
+
|
19
|
+
## 6.3.1 / 2023-08-18
|
20
|
+
|
21
|
+
* Security
|
22
|
+
* Address HTTP request smuggling vulnerabilities with zero-length Content Length header and trailer fields ([GHSA-68xg-gqqm-vgj8](https://github.com/puma/puma/security/advisories/GHSA-68xg-gqqm-vgj8))
|
23
|
+
|
1
24
|
## 6.3.0 / 2023-05-31
|
2
25
|
|
3
26
|
* Features
|
@@ -10,7 +33,7 @@
|
|
10
33
|
* Handle malformed request path ([#3155], [#3148])
|
11
34
|
* Misc lib file fixes - trapping additional errors, CI helper ([#3129])
|
12
35
|
* Fixup req form data file upload with "r\n" line endings ([#3137])
|
13
|
-
* Restore rack 1.6 compatibility
|
36
|
+
* Restore rack 1.6 compatibility ([#3156])
|
14
37
|
|
15
38
|
* Refactor
|
16
39
|
* const.rb - Update Puma::HTTP_STATUS_CODES ([#3162])
|
@@ -121,6 +144,16 @@
|
|
121
144
|
* Ruby 3.2 will have native IO#wait_* methods, don't require io/wait ([#2903])
|
122
145
|
* Various internal API refactorings ([#2942], [#2921], [#2922], [#2955])
|
123
146
|
|
147
|
+
## 5.6.7 / 2023-08-18
|
148
|
+
|
149
|
+
* Security
|
150
|
+
* Address HTTP request smuggling vulnerabilities with zero-length Content Length header and trailer fields ([GHSA-68xg-gqqm-vgj8](https://github.com/puma/puma/security/advisories/GHSA-68xg-gqqm-vgj8))
|
151
|
+
|
152
|
+
## 5.6.6 / 2023-06-21
|
153
|
+
|
154
|
+
* Bugfix
|
155
|
+
* Prevent loading with rack 3 ([#3166])
|
156
|
+
|
124
157
|
## 5.6.5 / 2022-08-23
|
125
158
|
|
126
159
|
* Feature
|
@@ -1996,6 +2029,17 @@ be added back in a future date when a java Puma::MiniSSL is added.
|
|
1996
2029
|
* Bugfixes
|
1997
2030
|
* Your bugfix goes here <Most recent on the top, like GitHub> (#Github Number)
|
1998
2031
|
|
2032
|
+
[#2920]:https://github.com/puma/puma/pull/2920 "PR by @biinari, merged 2023-07-11"
|
2033
|
+
[#3195]:https://github.com/puma/puma/pull/3195 "PR by @binarygit, merged 2023-08-15"
|
2034
|
+
[#3209]:https://github.com/puma/puma/pull/3209 "PR by @joshuay03, merged 2023-09-04"
|
2035
|
+
[#2580]:https://github.com/puma/puma/issues/2580 "Issue by @schuetzm, closed 2023-09-04"
|
2036
|
+
[#3204]:https://github.com/puma/puma/pull/3204 "PR by @dhavalsingh, merged 2023-08-25"
|
2037
|
+
[#3191]:https://github.com/puma/puma/pull/3191 "PR by @MSP-Greg, merged 2023-08-31"
|
2038
|
+
[#3165]:https://github.com/puma/puma/pull/3165 "PR by @fallwith, merged 2023-06-06"
|
2039
|
+
[#3174]:https://github.com/puma/puma/pull/3174 "PR by @copiousfreetime, merged 2023-06-11"
|
2040
|
+
[#3181]:https://github.com/puma/puma/pull/3181 "PR by @MSP-Greg, merged 2023-06-23"
|
2041
|
+
[#3187]:https://github.com/puma/puma/pull/3187 "PR by @MSP-Greg, merged 2023-06-30"
|
2042
|
+
[#3094]:https://github.com/puma/puma/pull/3094 "PR by @Vuta, merged 2023-07-23"
|
1999
2043
|
[#3106]:https://github.com/puma/puma/pull/3106 "PR by @MSP-Greg, merged 2023-05-29"
|
2000
2044
|
[#3014]:https://github.com/puma/puma/issues/3014 "Issue by @kyledrake, closed 2023-05-29"
|
2001
2045
|
[#3161]:https://github.com/puma/puma/pull/3161 "PR by @MSP-Greg, merged 2023-05-27"
|
@@ -2094,6 +2138,7 @@ be added back in a future date when a java Puma::MiniSSL is added.
|
|
2094
2138
|
[#2921]:https://github.com/puma/puma/issues/2921 "Issue by @MSP-Greg, closed 2022-09-15"
|
2095
2139
|
[#2922]:https://github.com/puma/puma/issues/2922 "Issue by @MSP-Greg, closed 2022-09-10"
|
2096
2140
|
[#2955]:https://github.com/puma/puma/pull/2955 "PR by @cafedomancer, merged 2022-09-15"
|
2141
|
+
[#3166]:https://github.com/puma/puma/pull/3166 "PR by @JoeDupuis, merged 2023-06-08"
|
2097
2142
|
[#2868]:https://github.com/puma/puma/pull/2868 "PR by @MSP-Greg, merged 2022-06-02"
|
2098
2143
|
[#2866]:https://github.com/puma/puma/issues/2866 "Issue by @slondr, closed 2022-06-02"
|
2099
2144
|
[#2883]:https://github.com/puma/puma/pull/2883 "PR by @MSP-Greg, merged 2022-06-02"
|
@@ -2120,7 +2165,7 @@ be added back in a future date when a java Puma::MiniSSL is added.
|
|
2120
2165
|
[#2794]:https://github.com/puma/puma/pull/2794 "PR by @johnnyshields, merged 2022-01-10"
|
2121
2166
|
[#2759]:https://github.com/puma/puma/pull/2759 "PR by @ob-stripe, merged 2021-12-11"
|
2122
2167
|
[#2731]:https://github.com/puma/puma/pull/2731 "PR by @baelter, merged 2021-11-02"
|
2123
|
-
[#2341]:https://github.com/puma/puma/issues/2341 "Issue by @cjlarose,
|
2168
|
+
[#2341]:https://github.com/puma/puma/issues/2341 "Issue by @cjlarose, closed 2023-07-23"
|
2124
2169
|
[#2728]:https://github.com/puma/puma/pull/2728 "PR by @dalibor, merged 2021-10-31"
|
2125
2170
|
[#2733]:https://github.com/puma/puma/pull/2733 "PR by @ob-stripe, merged 2021-12-12"
|
2126
2171
|
[#2807]:https://github.com/puma/puma/pull/2807 "PR by @MSP-Greg, merged 2022-01-25"
|
data/README.md
CHANGED
@@ -12,11 +12,15 @@ Puma is a **simple, fast, multi-threaded, and highly parallel HTTP 1.1 server fo
|
|
12
12
|
|
13
13
|
## Built For Speed & Parallelism
|
14
14
|
|
15
|
-
Puma
|
15
|
+
Puma is a server for [Rack](https://github.com/rack/rack)-powered HTTP applications written in Ruby. It is:
|
16
|
+
* **Multi-threaded**. Each request is served in a separate thread. This helps you serve more requests per second with less memory use.
|
17
|
+
* **Multi-process**. "Pre-forks" in cluster mode, using less memory per-process thanks to copy-on-write memory.
|
18
|
+
* **Standalone**. With SSL support, zero-downtime rolling restarts and a built-in request bufferer, you can deploy Puma without any reverse proxy.
|
19
|
+
* **Battle-tested**. Our HTTP parser is inherited from Mongrel and has over 15 years of production use. Puma is currently the most popular Ruby webserver, and is the default server for Ruby on Rails.
|
16
20
|
|
17
21
|
Originally designed as a server for [Rubinius](https://github.com/rubinius/rubinius), Puma also works well with Ruby (MRI) and JRuby.
|
18
22
|
|
19
|
-
On MRI, there is a Global VM Lock (GVL) that ensures only one thread can run Ruby code at a time. But if you're doing a lot of blocking IO (such as HTTP calls to external APIs like Twitter), Puma still improves MRI's throughput by allowing IO waiting to be done in parallel.
|
23
|
+
On MRI, there is a Global VM Lock (GVL) that ensures only one thread can run Ruby code at a time. But if you're doing a lot of blocking IO (such as HTTP calls to external APIs like Twitter), Puma still improves MRI's throughput by allowing IO waiting to be done in parallel. Truly parallel Ruby implementations (TruffleRuby, JRuby) don't have this limitation.
|
20
24
|
|
21
25
|
## Quick Start
|
22
26
|
|
@@ -114,6 +118,8 @@ $ WEB_CONCURRENCY=3 puma -t 8:32
|
|
114
118
|
|
115
119
|
Note that threads are still used in clustered mode, and the `-t` thread flag setting is per worker, so `-w 2 -t 16:16` will spawn 32 threads in total, with 16 in each worker process.
|
116
120
|
|
121
|
+
For an in-depth discussion of the tradeoffs of thread and process count settings, [see our docs](https://github.com/puma/puma/blob/9282a8efa5a0c48e39c60d22ca70051a25df9f55/docs/kubernetes.md#workers-per-pod-and-other-config-issues).
|
122
|
+
|
117
123
|
In clustered mode, Puma can "preload" your application. This loads all the application code *prior* to forking. Preloading reduces total memory usage of your application via an operating system feature called [copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write).
|
118
124
|
|
119
125
|
If the `WEB_CONCURRENCY` environment variable is set to a value > 1 (and `--prune-bundler` has not been specified), preloading will be enabled by default. Otherwise, you can use the `--preload` flag from the command line:
|
@@ -211,29 +217,32 @@ Puma supports the [`localhost`] gem for self-signed certificates. This is partic
|
|
211
217
|
|
212
218
|
Puma automatically configures SSL when the [`localhost`] gem is loaded in a `development` environment:
|
213
219
|
|
220
|
+
Add the gem to your Gemfile:
|
214
221
|
```ruby
|
215
|
-
# Add the gem to your Gemfile
|
216
222
|
group(:development) do
|
217
223
|
gem 'localhost'
|
218
224
|
end
|
225
|
+
```
|
219
226
|
|
220
|
-
|
227
|
+
And require it implicitly using bundler:
|
228
|
+
```ruby
|
221
229
|
require "bundler"
|
222
230
|
Bundler.require(:default, ENV["RACK_ENV"].to_sym)
|
231
|
+
```
|
223
232
|
|
224
|
-
|
225
|
-
|
233
|
+
Alternatively, you can require the gem in your configuration file, either `config/puma/development.rb`, `config/puma.rb`, or set via the `-C` cli option:
|
234
|
+
```ruby
|
226
235
|
require 'localhost'
|
227
|
-
|
236
|
+
# configuration methods (from Puma::DSL) as needed
|
228
237
|
```
|
229
238
|
|
230
239
|
Additionally, Puma must be listening to an SSL socket:
|
231
240
|
|
232
241
|
```shell
|
233
|
-
$ puma -b 'ssl://localhost:9292' config.
|
242
|
+
$ puma -b 'ssl://localhost:9292' -C config/use_local_host.rb
|
234
243
|
|
235
244
|
# The following options allow you to reach Puma over HTTP as well:
|
236
|
-
$ puma -b ssl://localhost:9292 -b tcp://localhost:9393 config.
|
245
|
+
$ puma -b ssl://localhost:9292 -b tcp://localhost:9393 -C config/use_local_host.rb
|
237
246
|
```
|
238
247
|
|
239
248
|
[`localhost`]: https://github.com/socketry/localhost
|
data/docs/kubernetes.md
CHANGED
@@ -64,3 +64,15 @@ There is a subtle race condition between step 2 and 3: The replication controlle
|
|
64
64
|
The way Kubernetes works this way, rather than handling step 2 synchronously, is due to the CAP theorem: in a distributed system there is no way to guarantee that any message will arrive promptly. In particular, waiting for all Service controllers to report back might get stuck for an indefinite time if one of them has already been terminated or if there has been a net split. A way to work around this is to add a sleep to the pre-stop hook of the same time as the `terminationGracePeriodSeconds` time. This will allow the Puma process to keep serving new requests during the entire grace period, although it will no longer receive new requests after all Service controllers have propagated the removal of the pod from their endpoint lists. Then, after `terminationGracePeriodSeconds`, the pod receives `SIGKILL` and closes down. If your process can't handle SIGKILL properly, for example because it needs to release locks in different services, you can also sleep for a shorter period (and/or increase `terminationGracePeriodSeconds`) as long as the time slept is longer than the time that your Service controllers take to propagate the pod removal. The downside of this workaround is that all pods will take at minimum the amount of time slept to shut down and this will increase the time required for your rolling deploy.
|
65
65
|
|
66
66
|
More discussions and links to relevant articles can be found in https://github.com/puma/puma/issues/2343.
|
67
|
+
|
68
|
+
## Workers Per Pod, and Other Config Issues
|
69
|
+
|
70
|
+
With containerization, you will have to make a decision about how "big" to make each pod. Should you run 2 pods with 50 workers each? 25 pods, each with 4 workers? 100 pods, with each Puma running in single mode? Each scenario represents the same total amount of capacity (100 Puma processes that can respond to requests), but there are tradeoffs to make.
|
71
|
+
|
72
|
+
* Worker counts should be somewhere between 4 and 32 in most cases. You want more than 4 in order to minimize time spent in request queueing for a free Puma worker, but probably less than ~32 because otherwise autoscaling is working in too large of an increment or they probably won't fit very well into your nodes.
|
73
|
+
* Unless you have a very I/O-heavy application (50%+ time spent waiting on IO), use the default thread count (5 for MRI). Using higher numbers of threads with low I/O wait (<50%) will lead to additional request queueing time (latency!) and additional memory usage.
|
74
|
+
* More processes per pod reduces memory usage per process, because of copy-on-write memory and because the cost of the single master process is "amortized" over more child processes.
|
75
|
+
* Don't run less than 4 processes per pod if you can. Low numbers of processes per pod will lead to high request queueing, which means you will have to run more pods.
|
76
|
+
* If multithreaded, allocate 1 CPU per worker. If single threaded, allocate 0.75 cpus per worker. Most web applications spend about 25% of their time in I/O - but when you're running multi-threaded, your Puma process will have higher CPU usage and should be able to fully saturate a CPU core.
|
77
|
+
* Most Puma processes will use about ~512MB-1GB per worker, and about 1GB for the master process. However, you probably shouldn't bother with setting memory limits lower than around 2GB per process, because most places you are deploying will have 2GB of RAM per CPU. A sensible memory limit for a Puma configuration of 4 child workers might be something like 8 GB (1 GB for the master, 7GB for the 4 children).
|
78
|
+
|
data/ext/puma_http11/mini_ssl.c
CHANGED
@@ -36,6 +36,12 @@ void raise_file_error(const char* caller, const char *filename) {
|
|
36
36
|
rb_raise(eError, "%s: error in file '%s': %s", caller, filename, ERR_error_string(ERR_get_error(), NULL));
|
37
37
|
}
|
38
38
|
|
39
|
+
NORETURN(void raise_param_error(const char* caller, const char *param));
|
40
|
+
|
41
|
+
void raise_param_error(const char* caller, const char *param) {
|
42
|
+
rb_raise(eError, "%s: error with parameter '%s': %s", caller, param, ERR_error_string(ERR_get_error(), NULL));
|
43
|
+
}
|
44
|
+
|
39
45
|
void engine_free(void *ptr) {
|
40
46
|
ms_conn *conn = ptr;
|
41
47
|
ms_cert_buf* cert_buf = (ms_cert_buf*)SSL_get_app_data(conn->ssl);
|
@@ -226,7 +232,7 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
|
|
226
232
|
VALUE key, cert, ca, verify_mode, ssl_cipher_filter, no_tlsv1, no_tlsv1_1,
|
227
233
|
verification_flags, session_id_bytes, cert_pem, key_pem, key_password_command, key_password;
|
228
234
|
BIO *bio;
|
229
|
-
X509 *x509;
|
235
|
+
X509 *x509 = NULL;
|
230
236
|
EVP_PKEY *pkey;
|
231
237
|
pem_password_cb *password_cb = NULL;
|
232
238
|
const char *password = NULL;
|
@@ -298,16 +304,65 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
|
|
298
304
|
}
|
299
305
|
|
300
306
|
if (!NIL_P(cert_pem)) {
|
307
|
+
X509 *ca = NULL;
|
308
|
+
unsigned long err;
|
309
|
+
|
301
310
|
bio = BIO_new(BIO_s_mem());
|
302
311
|
BIO_puts(bio, RSTRING_PTR(cert_pem));
|
312
|
+
|
313
|
+
/**
|
314
|
+
* Much of this pulled as a simplified version of the `use_certificate_chain_file` method
|
315
|
+
* from openssl's `ssl_rsa.c` file.
|
316
|
+
*/
|
317
|
+
|
318
|
+
/* first read the cert as the first item in the pem file */
|
303
319
|
x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
320
|
+
if (NULL == x509) {
|
321
|
+
BIO_free_all(bio);
|
322
|
+
raise_param_error("PEM_read_bio_X509", "cert_pem");
|
323
|
+
}
|
304
324
|
|
305
|
-
|
306
|
-
|
307
|
-
|
325
|
+
/* Add the cert to the context */
|
326
|
+
/* 1 is success - otherwise check the error codes */
|
327
|
+
if (1 != SSL_CTX_use_certificate(ctx, x509)) {
|
328
|
+
BIO_free_all(bio);
|
329
|
+
raise_param_error("SSL_CTX_use_certificate", "cert_pem");
|
330
|
+
}
|
331
|
+
|
332
|
+
X509_free(x509); /* no longer need our reference */
|
333
|
+
|
334
|
+
/* Now lets load up the rest of the certificate chain */
|
335
|
+
/* 1 is success 0 is error */
|
336
|
+
if (0 == SSL_CTX_clear_chain_certs(ctx)) {
|
337
|
+
BIO_free_all(bio);
|
338
|
+
raise_param_error("SSL_CTX_clear_chain_certs","cert_pem");
|
339
|
+
}
|
340
|
+
|
341
|
+
while (1) {
|
342
|
+
ca = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
343
|
+
|
344
|
+
if (NULL == ca) {
|
345
|
+
break;
|
346
|
+
}
|
347
|
+
|
348
|
+
if (0 == SSL_CTX_add0_chain_cert(ctx, ca)) {
|
349
|
+
BIO_free_all(bio);
|
350
|
+
raise_param_error("SSL_CTX_add0_chain_cert","cert_pem");
|
351
|
+
}
|
352
|
+
/* don't free ca - its now owned by the context */
|
353
|
+
}
|
354
|
+
|
355
|
+
/* ca is NULL - so its either the end of the file or an error */
|
356
|
+
err = ERR_peek_last_error();
|
357
|
+
|
358
|
+
/* If its the end of the file - then we are done, in any case free the bio */
|
359
|
+
BIO_free_all(bio);
|
360
|
+
|
361
|
+
if ((ERR_GET_LIB(err) == ERR_LIB_PEM) && (ERR_GET_REASON(err) == PEM_R_NO_START_LINE)) {
|
362
|
+
ERR_clear_error();
|
363
|
+
} else {
|
364
|
+
raise_param_error("PEM_read_bio_X509","cert_pem");
|
308
365
|
}
|
309
|
-
X509_free(x509);
|
310
|
-
BIO_free(bio);
|
311
366
|
}
|
312
367
|
|
313
368
|
if (!NIL_P(key_pem)) {
|
data/lib/puma/binder.rb
CHANGED
@@ -330,7 +330,7 @@ module Puma
|
|
330
330
|
return
|
331
331
|
end
|
332
332
|
|
333
|
-
host = host[1..-2] if host
|
333
|
+
host = host[1..-2] if host&.start_with? '['
|
334
334
|
tcp_server = TCPServer.new(host, port)
|
335
335
|
|
336
336
|
if optimize_for_latency
|
@@ -364,7 +364,7 @@ module Puma
|
|
364
364
|
return
|
365
365
|
end
|
366
366
|
|
367
|
-
host = host[1..-2] if host
|
367
|
+
host = host[1..-2] if host&.start_with? '['
|
368
368
|
s = TCPServer.new(host, port)
|
369
369
|
if optimize_for_latency
|
370
370
|
s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
data/lib/puma/cli.rb
CHANGED
@@ -144,6 +144,10 @@ module Puma
|
|
144
144
|
$LOAD_PATH.unshift(*arg.split(':'))
|
145
145
|
end
|
146
146
|
|
147
|
+
o.on "--idle-timeout SECONDS", "Number of seconds until the next request before automatic shutdown" do |arg|
|
148
|
+
user_config.idle_timeout arg
|
149
|
+
end
|
150
|
+
|
147
151
|
o.on "-p", "--port PORT", "Define the TCP port to bind to",
|
148
152
|
"Use -b for more advanced options" do |arg|
|
149
153
|
user_config.bind "tcp://#{Configuration::DEFAULTS[:tcp_host]}:#{arg}"
|
data/lib/puma/client.rb
CHANGED
@@ -11,7 +11,6 @@ end
|
|
11
11
|
require_relative 'detect'
|
12
12
|
require_relative 'io_buffer'
|
13
13
|
require 'tempfile'
|
14
|
-
require 'forwardable'
|
15
14
|
|
16
15
|
if Puma::IS_JRUBY
|
17
16
|
# We have to work around some OpenSSL buffer/io-readiness bugs
|
@@ -49,7 +48,8 @@ module Puma
|
|
49
48
|
|
50
49
|
# chunked body validation
|
51
50
|
CHUNK_SIZE_INVALID = /[^\h]/.freeze
|
52
|
-
CHUNK_VALID_ENDING =
|
51
|
+
CHUNK_VALID_ENDING = Const::LINE_END
|
52
|
+
CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
|
53
53
|
|
54
54
|
# Content-Length header value validation
|
55
55
|
CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
|
@@ -61,7 +61,6 @@ module Puma
|
|
61
61
|
EmptyBody = NullIO.new
|
62
62
|
|
63
63
|
include Puma::Const
|
64
|
-
extend Forwardable
|
65
64
|
|
66
65
|
def initialize(io, env=nil)
|
67
66
|
@io = io
|
@@ -110,7 +109,10 @@ module Puma
|
|
110
109
|
|
111
110
|
attr_accessor :remote_addr_header, :listener
|
112
111
|
|
113
|
-
|
112
|
+
# Remove in Puma 7?
|
113
|
+
def closed?
|
114
|
+
@to_io.closed?
|
115
|
+
end
|
114
116
|
|
115
117
|
# Test to see if io meets a bare minimum of functioning, @to_io needs to be
|
116
118
|
# used for MiniSSL::Socket
|
@@ -382,8 +384,8 @@ module Puma
|
|
382
384
|
cl = @env[CONTENT_LENGTH]
|
383
385
|
|
384
386
|
if cl
|
385
|
-
# cannot contain characters that are not \d
|
386
|
-
if CONTENT_LENGTH_VALUE_INVALID.match? cl
|
387
|
+
# cannot contain characters that are not \d, or be empty
|
388
|
+
if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
|
387
389
|
raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
|
388
390
|
end
|
389
391
|
else
|
@@ -544,7 +546,7 @@ module Puma
|
|
544
546
|
|
545
547
|
while !io.eof?
|
546
548
|
line = io.gets
|
547
|
-
if line.end_with?(
|
549
|
+
if line.end_with?(CHUNK_VALID_ENDING)
|
548
550
|
# Puma doesn't process chunk extensions, but should parse if they're
|
549
551
|
# present, which is the reason for the semicolon regex
|
550
552
|
chunk_hex = line.strip[/\A[^;]+/]
|
@@ -556,13 +558,19 @@ module Puma
|
|
556
558
|
@in_last_chunk = true
|
557
559
|
@body.rewind
|
558
560
|
rest = io.read
|
559
|
-
|
560
|
-
if rest.bytesize < last_crlf_size
|
561
|
+
if rest.bytesize < CHUNK_VALID_ENDING_SIZE
|
561
562
|
@buffer = nil
|
562
|
-
@partial_part_left =
|
563
|
+
@partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
|
563
564
|
return false
|
564
565
|
else
|
565
|
-
|
566
|
+
# if the next character is a CRLF, set buffer to everything after that CRLF
|
567
|
+
start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
|
568
|
+
CHUNK_VALID_ENDING_SIZE
|
569
|
+
else # we have started a trailer section, which we do not support. skip it!
|
570
|
+
rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
|
571
|
+
end
|
572
|
+
|
573
|
+
@buffer = rest[start_of_rest..-1]
|
566
574
|
@buffer = nil if @buffer.empty?
|
567
575
|
set_ready
|
568
576
|
return true
|
data/lib/puma/configuration.rb
CHANGED
@@ -133,8 +133,10 @@ module Puma
|
|
133
133
|
debug: false,
|
134
134
|
early_hints: nil,
|
135
135
|
environment: 'development'.freeze,
|
136
|
-
# Number of seconds to wait until we get the first data for the request
|
136
|
+
# Number of seconds to wait until we get the first data for the request.
|
137
137
|
first_data_timeout: 30,
|
138
|
+
# Number of seconds to wait until the next request before shutting down.
|
139
|
+
idle_timeout: nil,
|
138
140
|
io_selector_backend: :auto,
|
139
141
|
log_requests: false,
|
140
142
|
logger: STDOUT,
|
data/lib/puma/const.rb
CHANGED
@@ -100,8 +100,8 @@ module Puma
|
|
100
100
|
# too taxing on performance.
|
101
101
|
module Const
|
102
102
|
|
103
|
-
PUMA_VERSION = VERSION = "6.
|
104
|
-
CODE_NAME = "
|
103
|
+
PUMA_VERSION = VERSION = "6.4.0"
|
104
|
+
CODE_NAME = "The Eagle of Durango"
|
105
105
|
|
106
106
|
PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
|
107
107
|
|
data/lib/puma/control_cli.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'optparse'
|
4
|
-
require_relative 'state_file'
|
5
4
|
require_relative 'const'
|
6
5
|
require_relative 'detect'
|
7
|
-
require_relative 'configuration'
|
8
6
|
require 'uri'
|
9
7
|
require 'socket'
|
10
8
|
|
@@ -126,6 +124,9 @@ module Puma
|
|
126
124
|
end
|
127
125
|
|
128
126
|
if @config_file
|
127
|
+
require_relative 'configuration'
|
128
|
+
require_relative 'log_writer'
|
129
|
+
|
129
130
|
config = Puma::Configuration.new({ config_files: [@config_file] }, {})
|
130
131
|
config.load
|
131
132
|
@state ||= config.options[:state]
|
@@ -149,6 +150,8 @@ module Puma
|
|
149
150
|
raise "State file not found: #{@state}"
|
150
151
|
end
|
151
152
|
|
153
|
+
require_relative 'state_file'
|
154
|
+
|
152
155
|
sf = Puma::StateFile.new
|
153
156
|
sf.load @state
|
154
157
|
|
@@ -164,22 +167,26 @@ module Puma
|
|
164
167
|
def send_request
|
165
168
|
uri = URI.parse @control_url
|
166
169
|
|
170
|
+
host = uri.host
|
171
|
+
|
167
172
|
# create server object by scheme
|
168
173
|
server =
|
169
174
|
case uri.scheme
|
170
175
|
when 'ssl'
|
171
176
|
require 'openssl'
|
177
|
+
host = host[1..-2] if host&.start_with? '['
|
172
178
|
OpenSSL::SSL::SSLSocket.new(
|
173
|
-
TCPSocket.new(
|
179
|
+
TCPSocket.new(host, uri.port),
|
174
180
|
OpenSSL::SSL::SSLContext.new)
|
175
181
|
.tap { |ssl| ssl.sync_close = true } # default is false
|
176
182
|
.tap(&:connect)
|
177
183
|
when 'tcp'
|
178
|
-
|
184
|
+
host = host[1..-2] if host&.start_with? '['
|
185
|
+
TCPSocket.new host, uri.port
|
179
186
|
when 'unix'
|
180
187
|
# check for abstract UNIXSocket
|
181
188
|
UNIXSocket.new(@control_url.start_with?('unix://@') ?
|
182
|
-
"\0#{
|
189
|
+
"\0#{host}#{uri.path}" : "#{host}#{uri.path}")
|
183
190
|
else
|
184
191
|
raise "Invalid scheme: #{uri.scheme}"
|
185
192
|
end
|
data/lib/puma/dsl.rb
CHANGED
@@ -315,16 +315,22 @@ module Puma
|
|
315
315
|
bind URI::Generic.build(scheme: 'tcp', host: host, port: Integer(port)).to_s
|
316
316
|
end
|
317
317
|
|
318
|
+
# Define how long the tcp socket stays open, if no data has been received.
|
319
|
+
# @see Puma::Server.new
|
320
|
+
def first_data_timeout(seconds)
|
321
|
+
@options[:first_data_timeout] = Integer(seconds)
|
322
|
+
end
|
323
|
+
|
318
324
|
# Define how long persistent connections can be idle before Puma closes them.
|
319
325
|
# @see Puma::Server.new
|
320
326
|
def persistent_timeout(seconds)
|
321
327
|
@options[:persistent_timeout] = Integer(seconds)
|
322
328
|
end
|
323
329
|
|
324
|
-
#
|
330
|
+
# If a new request is not received within this number of seconds, begin shutting down.
|
325
331
|
# @see Puma::Server.new
|
326
|
-
def
|
327
|
-
@options[:
|
332
|
+
def idle_timeout(seconds)
|
333
|
+
@options[:idle_timeout] = Integer(seconds)
|
328
334
|
end
|
329
335
|
|
330
336
|
# Work around leaky apps that leave garbage in Thread locals
|
@@ -510,6 +516,12 @@ module Puma
|
|
510
516
|
# `true`, which sets reuse 'on' with default values, or a hash, with `:size`
|
511
517
|
# and/or `:timeout` keys, each with integer values.
|
512
518
|
#
|
519
|
+
# The `cert:` options hash parameter can be the path to a certificate
|
520
|
+
# file including all intermediate certificates in PEM format.
|
521
|
+
#
|
522
|
+
# The `cert_pem:` options hash parameter can be String containing the
|
523
|
+
# cerificate and all intermediate certificates in PEM format.
|
524
|
+
#
|
513
525
|
# @example
|
514
526
|
# ssl_bind '127.0.0.1', '9292', {
|
515
527
|
# cert: path_to_cert,
|
@@ -717,6 +729,40 @@ module Puma
|
|
717
729
|
process_hook :before_refork, key, block, 'on_refork'
|
718
730
|
end
|
719
731
|
|
732
|
+
# Code to run immediately before a thread starts. The worker does not
|
733
|
+
# start new threads until this code finishes.
|
734
|
+
#
|
735
|
+
# This hook is useful for doing something when a thread
|
736
|
+
# starts.
|
737
|
+
#
|
738
|
+
# This can be called multiple times to add several hooks.
|
739
|
+
#
|
740
|
+
# @example
|
741
|
+
# on_thread_start do
|
742
|
+
# puts 'On thread start...'
|
743
|
+
# end
|
744
|
+
def on_thread_start(&block)
|
745
|
+
@options[:before_thread_start] ||= []
|
746
|
+
@options[:before_thread_start] << block
|
747
|
+
end
|
748
|
+
|
749
|
+
# Code to run immediately before a thread exits. The worker does not
|
750
|
+
# accept new requests until this code finishes.
|
751
|
+
#
|
752
|
+
# This hook is useful for cleaning up thread local resources when a thread
|
753
|
+
# is trimmed.
|
754
|
+
#
|
755
|
+
# This can be called multiple times to add several hooks.
|
756
|
+
#
|
757
|
+
# @example
|
758
|
+
# on_thread_exit do
|
759
|
+
# puts 'On thread exit...'
|
760
|
+
# end
|
761
|
+
def on_thread_exit(&block)
|
762
|
+
@options[:before_thread_exit] ||= []
|
763
|
+
@options[:before_thread_exit] << block
|
764
|
+
end
|
765
|
+
|
720
766
|
# Code to run out-of-band when the worker is idle.
|
721
767
|
# These hooks run immediately after a request has finished
|
722
768
|
# processing and there are no busy threads on the worker.
|
@@ -848,7 +894,8 @@ module Puma
|
|
848
894
|
# not a request timeout, it is to protect against a hung or dead process.
|
849
895
|
# Setting this value will not protect against slow requests.
|
850
896
|
#
|
851
|
-
#
|
897
|
+
# This value must be greater than worker_check_interval.
|
898
|
+
# The default value is 60 seconds.
|
852
899
|
#
|
853
900
|
# @note Cluster mode only.
|
854
901
|
# @example
|
data/lib/puma/rack/urlmap.rb
CHANGED
@@ -34,7 +34,7 @@ module Puma::Rack
|
|
34
34
|
end
|
35
35
|
|
36
36
|
location = location.chomp('/')
|
37
|
-
match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)",
|
37
|
+
match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", Regexp::NOENCODING)
|
38
38
|
|
39
39
|
[host, location, match, app]
|
40
40
|
}.sort_by do |(host, location, _, _)|
|
data/lib/puma/runner.rb
CHANGED
@@ -75,7 +75,11 @@ module Puma
|
|
75
75
|
control = Puma::Server.new app, nil,
|
76
76
|
{ min_threads: 0, max_threads: 1, queue_requests: false, log_writer: @log_writer }
|
77
77
|
|
78
|
-
|
78
|
+
begin
|
79
|
+
control.binder.parse [str], nil, 'Starting control server'
|
80
|
+
rescue Errno::EADDRINUSE, Errno::EACCES => e
|
81
|
+
raise e, "Error: Control server address '#{str}' is already in use. Original error: #{e.message}"
|
82
|
+
end
|
79
83
|
|
80
84
|
control.run thread_name: 'ctl'
|
81
85
|
@control = control
|
data/lib/puma/server.rb
CHANGED
@@ -15,7 +15,6 @@ require_relative 'request'
|
|
15
15
|
|
16
16
|
require 'socket'
|
17
17
|
require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
|
18
|
-
require 'forwardable'
|
19
18
|
|
20
19
|
module Puma
|
21
20
|
|
@@ -32,7 +31,6 @@ module Puma
|
|
32
31
|
class Server
|
33
32
|
include Puma::Const
|
34
33
|
include Request
|
35
|
-
extend Forwardable
|
36
34
|
|
37
35
|
attr_reader :thread
|
38
36
|
attr_reader :log_writer
|
@@ -48,9 +46,6 @@ module Puma
|
|
48
46
|
attr_accessor :app
|
49
47
|
attr_accessor :binder
|
50
48
|
|
51
|
-
def_delegators :@binder, :add_tcp_listener, :add_ssl_listener,
|
52
|
-
:add_unix_listener, :connected_ports
|
53
|
-
|
54
49
|
THREAD_LOCAL_KEY = :puma_server
|
55
50
|
|
56
51
|
# Create a server for the rack app +app+.
|
@@ -86,15 +81,16 @@ module Puma
|
|
86
81
|
UserFileDefaultOptions.new(options, Configuration::DEFAULTS)
|
87
82
|
end
|
88
83
|
|
89
|
-
@log_writer
|
90
|
-
@early_hints
|
91
|
-
@first_data_timeout
|
92
|
-
@
|
93
|
-
@
|
94
|
-
@
|
95
|
-
@
|
96
|
-
@
|
97
|
-
@
|
84
|
+
@log_writer = @options.fetch :log_writer, LogWriter.stdio
|
85
|
+
@early_hints = @options[:early_hints]
|
86
|
+
@first_data_timeout = @options[:first_data_timeout]
|
87
|
+
@persistent_timeout = @options[:persistent_timeout]
|
88
|
+
@idle_timeout = @options[:idle_timeout]
|
89
|
+
@min_threads = @options[:min_threads]
|
90
|
+
@max_threads = @options[:max_threads]
|
91
|
+
@queue_requests = @options[:queue_requests]
|
92
|
+
@max_fast_inline = @options[:max_fast_inline]
|
93
|
+
@io_selector_backend = @options[:io_selector_backend]
|
98
94
|
@http_content_length_limit = @options[:http_content_length_limit]
|
99
95
|
|
100
96
|
# make this a hash, since we prefer `key?` over `include?`
|
@@ -330,8 +326,12 @@ module Puma
|
|
330
326
|
|
331
327
|
while @status == :run || (drain && shutting_down?)
|
332
328
|
begin
|
333
|
-
ios = IO.select sockets, nil, nil, (shutting_down? ? 0 :
|
334
|
-
|
329
|
+
ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : @idle_timeout)
|
330
|
+
unless ios
|
331
|
+
@status = :stop unless shutting_down?
|
332
|
+
break
|
333
|
+
end
|
334
|
+
|
335
335
|
ios.first.each do |sock|
|
336
336
|
if sock == check
|
337
337
|
break if handle_check
|
@@ -476,7 +476,7 @@ module Puma
|
|
476
476
|
end
|
477
477
|
true
|
478
478
|
rescue StandardError => e
|
479
|
-
client_error(e, client)
|
479
|
+
client_error(e, client, requests)
|
480
480
|
# The ensure tries to close +client+ down
|
481
481
|
requests > 0
|
482
482
|
ensure
|
@@ -504,22 +504,22 @@ module Puma
|
|
504
504
|
# :nocov:
|
505
505
|
|
506
506
|
# Handle various error types thrown by Client I/O operations.
|
507
|
-
def client_error(e, client)
|
507
|
+
def client_error(e, client, requests = 1)
|
508
508
|
# Swallow, do not log
|
509
509
|
return if [ConnectionError, EOFError].include?(e.class)
|
510
510
|
|
511
|
-
lowlevel_error(e, client.env)
|
512
511
|
case e
|
513
512
|
when MiniSSL::SSLError
|
513
|
+
lowlevel_error(e, client.env)
|
514
514
|
@log_writer.ssl_error e, client.io
|
515
515
|
when HttpParserError
|
516
|
-
client
|
516
|
+
response_to_error(client, requests, e, 400)
|
517
517
|
@log_writer.parse_error e, client
|
518
518
|
when HttpParserError501
|
519
|
-
client
|
519
|
+
response_to_error(client, requests, e, 501)
|
520
520
|
@log_writer.parse_error e, client
|
521
521
|
else
|
522
|
-
client
|
522
|
+
response_to_error(client, requests, e, 500)
|
523
523
|
@log_writer.unknown_error e, nil, "Read"
|
524
524
|
end
|
525
525
|
end
|
@@ -541,10 +541,17 @@ module Puma
|
|
541
541
|
backtrace = e.backtrace.nil? ? '<no backtrace available>' : e.backtrace.join("\n")
|
542
542
|
[status, {}, ["Puma caught this error: #{e.message} (#{e.class})\n#{backtrace}"]]
|
543
543
|
else
|
544
|
-
[status, {}, ["
|
544
|
+
[status, {}, [""]]
|
545
545
|
end
|
546
546
|
end
|
547
547
|
|
548
|
+
def response_to_error(client, requests, err, status_code)
|
549
|
+
status, headers, res_body = lowlevel_error(err, client.env, status_code)
|
550
|
+
prepare_response(status, headers, res_body, requests, client)
|
551
|
+
client.write_error(status_code)
|
552
|
+
end
|
553
|
+
private :response_to_error
|
554
|
+
|
548
555
|
# Wait for all outstanding requests to finish.
|
549
556
|
#
|
550
557
|
def graceful_shutdown
|
@@ -623,5 +630,27 @@ module Puma
|
|
623
630
|
def stats
|
624
631
|
STAT_METHODS.map {|name| [name, send(name) || 0]}.to_h
|
625
632
|
end
|
633
|
+
|
634
|
+
# below are 'delegations' to binder
|
635
|
+
# remove in Puma 7?
|
636
|
+
|
637
|
+
|
638
|
+
def add_tcp_listener(host, port, optimize_for_latency = true, backlog = 1024)
|
639
|
+
@binder.add_tcp_listener host, port, optimize_for_latency, backlog
|
640
|
+
end
|
641
|
+
|
642
|
+
def add_ssl_listener(host, port, ctx, optimize_for_latency = true,
|
643
|
+
backlog = 1024)
|
644
|
+
@binder.add_ssl_listener host, port, ctx, optimize_for_latency, backlog
|
645
|
+
end
|
646
|
+
|
647
|
+
def add_unix_listener(path, umask = nil, mode = nil, backlog = 1024)
|
648
|
+
@binder.add_unix_listener path, umask, mode, backlog
|
649
|
+
end
|
650
|
+
|
651
|
+
# @!attribute [r] connected_ports
|
652
|
+
def connected_ports
|
653
|
+
@binder.connected_ports
|
654
|
+
end
|
626
655
|
end
|
627
656
|
end
|
data/lib/puma/thread_pool.rb
CHANGED
@@ -51,6 +51,8 @@ module Puma
|
|
51
51
|
@block = block
|
52
52
|
@out_of_band = options[:out_of_band]
|
53
53
|
@clean_thread_locals = options[:clean_thread_locals]
|
54
|
+
@before_thread_start = options[:before_thread_start]
|
55
|
+
@before_thread_exit = options[:before_thread_exit]
|
54
56
|
@reaping_time = options[:reaping_time]
|
55
57
|
@auto_trim_time = options[:auto_trim_time]
|
56
58
|
|
@@ -107,6 +109,7 @@ module Puma
|
|
107
109
|
def spawn_thread
|
108
110
|
@spawned += 1
|
109
111
|
|
112
|
+
trigger_before_thread_start_hooks
|
110
113
|
th = Thread.new(@spawned) do |spawned|
|
111
114
|
Puma.set_thread_name '%s tp %03i' % [@name, spawned]
|
112
115
|
todo = @todo
|
@@ -125,6 +128,7 @@ module Puma
|
|
125
128
|
@spawned -= 1
|
126
129
|
@workers.delete th
|
127
130
|
not_full.signal
|
131
|
+
trigger_before_thread_exit_hooks
|
128
132
|
Thread.exit
|
129
133
|
end
|
130
134
|
|
@@ -162,6 +166,36 @@ module Puma
|
|
162
166
|
|
163
167
|
private :spawn_thread
|
164
168
|
|
169
|
+
def trigger_before_thread_start_hooks
|
170
|
+
return unless @before_thread_start&.any?
|
171
|
+
|
172
|
+
@before_thread_start.each do |b|
|
173
|
+
begin
|
174
|
+
b.call
|
175
|
+
rescue Exception => e
|
176
|
+
STDERR.puts "WARNING before_thread_start hook failed with exception (#{e.class}) #{e.message}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
nil
|
180
|
+
end
|
181
|
+
|
182
|
+
private :trigger_before_thread_start_hooks
|
183
|
+
|
184
|
+
def trigger_before_thread_exit_hooks
|
185
|
+
return unless @before_thread_exit&.any?
|
186
|
+
|
187
|
+
@before_thread_exit.each do |b|
|
188
|
+
begin
|
189
|
+
b.call
|
190
|
+
rescue Exception => e
|
191
|
+
STDERR.puts "WARNING before_thread_exit hook failed with exception (#{e.class}) #{e.message}"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
nil
|
195
|
+
end
|
196
|
+
|
197
|
+
private :trigger_before_thread_exit_hooks
|
198
|
+
|
165
199
|
# @version 5.0.0
|
166
200
|
def trigger_out_of_band_hook
|
167
201
|
return false unless @out_of_band&.any?
|