puma 5.0.0.beta1 → 5.0.0.beta2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d624c4973e123e4b085bd0080a85d76c996f26743dde5d8e55ca4f3971e6d77a
4
- data.tar.gz: 8f3ebe99584b845456708a73ae5305b012058c09af0a5e75bead0322c1c8501c
3
+ metadata.gz: b265505e1f4f00fe8c7de1c1d66103664bdd5920c2aed54b66e3303ded01c881
4
+ data.tar.gz: 3c921f609fd679b3b8cfbbc807678c16d09b03f43a8104913f0e0dd54e7e93f1
5
5
  SHA512:
6
- metadata.gz: 1dbaeeb104fa56b185b1b647fc865a3b25eb621dba523dba15d056bef66137279fbb57545330ab3f486e610a88ed487f3ccf2e6f0b9c75de12522dc9db9b0029
7
- data.tar.gz: 1b2f86bb5275f3764b891da8c3bc3643e1544d7209385aae8d87b5aa572945ceb627ef70df23060bc27abc9714b16377b8fa371b3ac3a10738e76cbd69f88841
6
+ metadata.gz: 74dce09aab280163c048a0c3ae3a81cd16b2ac32c7641fd43640d65797a61edf6e25a11f5111324111cd0068a98fc2ec13fcb3ac44154cef39d4bb3eb839a392
7
+ data.tar.gz: c6cf2c5d6d29867130be2fa572d5aea12a45a50052841a2de26e0a4600004f6fffae80922a9c641e8a155c8967ea8349d759b62a7ae9c2f6cbc84433fd092d13
data/History.md CHANGED
@@ -6,11 +6,12 @@
6
6
  * EXPERIMENTAL: Added `nakayoshi_fork` option. Reduce memory usage in preloaded cluster-mode apps by GCing before fork and compacting, where available. (#2093, #2256)
7
7
  * Added pumactl `thread-backtraces` command to print thread backtraces (#2054)
8
8
  * Added incrementing `requests_count` to `Puma.stats`. (#2106)
9
- * Increased maximum URI path length from 2048 to 8196 bytes (#2167)
9
+ * Increased maximum URI path length from 2048 to 8192 bytes (#2167, #2344)
10
10
  * `lowlevel_error_handler` is now called during a forced threadpool shutdown, and if a callable with 3 arguments is set, we now also pass the status code (#2203)
11
11
  * Faster phased restart and worker timeout (#2220)
12
12
  * Added `state_permission` to config DSL to set state file permissions (#2238)
13
13
  * Added `Puma.stats_hash`, which returns a stats in Hash instead of a JSON string (#2086, #2253)
14
+ * `rack.multithread` and `rack.multiprocess` now dynamically resolved by `max_thread` and `workers` respectively (#2288)
14
15
 
15
16
  * Deprecations, Removals and Breaking API Changes
16
17
  * `--control` has been removed. Use `--control-url` (#1487)
@@ -24,8 +25,12 @@
24
25
  * Daemonization has been removed without replacement. (#2170)
25
26
  * Changed #connected_port to #connected_ports (#2076)
26
27
  * Configuration: `environment` is read from `RAILS_ENV`, if `RACK_ENV` can't be found (#2022)
28
+ * Log binding on http:// for TCP bindings to make it clickable
27
29
 
28
30
  * Bugfixes
31
+ * Fix JSON loading issues on phased-restarts (#2269)
32
+ * Improve shutdown reliability (#2312, #2338)
33
+ * Close client http connections made to an ssl server with TLSv1.3 (#2116)
29
34
  * Do not set user_config to quiet by default to allow for file config (#2074)
30
35
  * Always close SSL connection in Puma::ControlCLI (#2211)
31
36
  * Windows update extconf.rb for use with ssp and varied Ruby/MSYS2 combinations (#2069)
@@ -44,6 +49,15 @@
44
49
  * Fix `UserFileDefaultOptions#fetch` to properly use `default` (#2233)
45
50
  * Improvements to `out_of_band` hook (#2234)
46
51
  * Prefer the rackup file specified by the CLI (#2225)
52
+ * Fix for spawning subprocesses with fork_worker option (#2267)
53
+ * Set `CONTENT_LENGTH` for chunked requests (#2287)
54
+ * JRuby - Add Puma::MiniSSL::Engine#init? and #teardown methods, run all SSL tests (#2317)
55
+ * Improve shutdown reliability (#2312)
56
+ * Resolve issue with threadpool waiting counter decrement when thread is killed
57
+ * Constrain rake-compiler version to 0.9.4 to fix `ClassNotFound` exception when using MiniSSL with Java8.
58
+ * Fix recursive `prune_bundler` (#2319).
59
+ * Ensure that TCP_CORK is usable
60
+ * Fix corner case when request body is chunked (#2326)
47
61
 
48
62
  * Refactor
49
63
  * Remove unused loader argument from Plugin initializer (#2095)
@@ -54,6 +68,14 @@
54
68
  * ThreadPool concurrency refactoring (#2220)
55
69
  * JSON parse cluster worker stats instead of regex (#2124)
56
70
  * Support parallel tests in verbose progress reporting (#2223)
71
+ * Refactor error handling in server accept loop (#2239)
72
+
73
+ ## 4.3.4/4.3.5 and 3.12.5/3.12.6 / 2020-05-22
74
+
75
+ Each patchlevel release contains a separate security fix. We recommend simply upgrading to 4.3.5/3.12.6.
76
+
77
+ * Security
78
+ * Fix: Fixed two separate HTTP smuggling vulnerabilities that used the Transfer-Encoding header. CVE-2020-11076 and CVE-2020-11077.
57
79
 
58
80
  ## 4.3.3 and 3.12.4 / 2020-02-28
59
81
 
data/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  [![Code Climate](https://codeclimate.com/github/puma/puma.svg)](https://codeclimate.com/github/puma/puma)
10
10
  [![SemVer](https://api.dependabot.com/badges/compatibility_score?dependency-name=puma&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=puma&package-manager=bundler&version-scheme=semver)
11
- [![StackOverflow](http://img.shields.io/badge/stackoverflow-Puma-blue.svg)]( http://stackoverflow.com/questions/tagged/puma )
11
+ [![StackOverflow](https://img.shields.io/badge/stackoverflow-Puma-blue.svg)]( https://stackoverflow.com/questions/tagged/puma )
12
12
 
13
13
  Puma is a **simple, fast, multi-threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications**.
14
14
 
@@ -27,7 +27,7 @@ $ gem install puma
27
27
  $ puma
28
28
  ```
29
29
 
30
- Without arguments, puma will look for a rackup (.ru) file in
30
+ Without arguments, puma will look for a rackup (.ru) file in
31
31
  working directory called `config.ru`.
32
32
 
33
33
  ## Frameworks
@@ -135,7 +135,7 @@ Preloading can’t be used with phased restart, since phased restart kills and r
135
135
  If puma encounters an error outside of the context of your application, it will respond with a 500 and a simple
136
136
  textual error message (see `lowlevel_error` in [this file](https://github.com/puma/puma/blob/master/lib/puma/server.rb)).
137
137
  You can specify custom behavior for this scenario. For example, you can report the error to your third-party
138
- error-tracking service (in this example, [rollbar](http://rollbar.com)):
138
+ error-tracking service (in this example, [rollbar](https://rollbar.com)):
139
139
 
140
140
  ```ruby
141
141
  lowlevel_error_handler do |e|
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- ![http://bit.ly/2iJuFky](images/puma-general-arch.png)
5
+ ![https://bit.ly/2iJuFky](images/puma-general-arch.png)
6
6
 
7
7
  Puma is a threaded web server, processing requests across a TCP or UNIX socket.
8
8
 
@@ -12,7 +12,7 @@ Clustered mode is shown/discussed here. Single mode is analogous to having a sin
12
12
 
13
13
  ## Connection pipeline
14
14
 
15
- ![http://bit.ly/2zwzhEK](images/puma-connection-flow.png)
15
+ ![https://bit.ly/2zwzhEK](images/puma-connection-flow.png)
16
16
 
17
17
  * Upon startup, Puma listens on a TCP or UNIX socket.
18
18
  * The backlog of this socket is configured (with a default of 1024), determining how many established but unaccepted connections can exist concurrently.
@@ -29,7 +29,7 @@ Clustered mode is shown/discussed here. Single mode is analogous to having a sin
29
29
 
30
30
  ### Disabling `queue_requests`
31
31
 
32
- ![http://bit.ly/2zxCJ1Z](images/puma-connection-flow-no-reactor.png)
32
+ ![https://bit.ly/2zxCJ1Z](images/puma-connection-flow-no-reactor.png)
33
33
 
34
34
  The `queue_requests` option is `true` by default, enabling the separate thread used to buffer requests as described above.
35
35
 
@@ -20,7 +20,10 @@ Welcome back!
20
20
  Puma was originally conceived as a thread-only webserver, but grew the ability to
21
21
  also use processes in version 2.
22
22
 
23
- Here are some rules of thumb:
23
+ To run puma in single mode (e.g. for a development environment) you will need to
24
+ set the number of workers to 0, anything above will run in cluster mode.
25
+
26
+ Here are some rules of thumb for cluster mode:
24
27
 
25
28
  ### MRI
26
29
 
@@ -66,7 +69,8 @@ thread to become available.
66
69
 
67
70
  * Have your upstream proxy set a header with the time it received the request:
68
71
  * nginx: `proxy_set_header X-Request-Start "${msec}";`
69
- * haproxy: `http-request set-header X-Request-Start "%t";`
72
+ * haproxy >= 1.9: `http-request set-header X-Request-Start t=%[date()]%[date_us()]`
73
+ * haproxy < 1.9: `http-request set-header X-Request-Start t=%[date()]`
70
74
  * In your Rack middleware, determine the amount of time elapsed since `X-Request-Start`.
71
75
  * To improve accuracy, you will want to subtract time spent waiting for slow clients:
72
76
  * `env['puma.request_body_wait']` contains the number of milliseconds Puma spent
@@ -1,8 +1,8 @@
1
- The [unix signal](http://en.wikipedia.org/wiki/Unix_signal) is a method of sending messages between [processes](http://en.wikipedia.org/wiki/Process_(computing)). When a signal is sent, the operating system interrupts the target process's normal flow of execution. There are standard signals that are used to stop a process but there are also custom signals that can be used for other purposes. This document is an attempt to list all supported signals that Puma will respond to. In general, signals need only be sent to the master process of a cluster.
1
+ The [unix signal](https://en.wikipedia.org/wiki/Unix_signal) is a method of sending messages between [processes](https://en.wikipedia.org/wiki/Process_(computing)). When a signal is sent, the operating system interrupts the target process's normal flow of execution. There are standard signals that are used to stop a process but there are also custom signals that can be used for other purposes. This document is an attempt to list all supported signals that Puma will respond to. In general, signals need only be sent to the master process of a cluster.
2
2
 
3
3
  ## Sending Signals
4
4
 
5
- If you are new to signals it can be useful to see how they can be used. When a process is created in a *nix like operating system it will have a [PID - or process identifier](http://en.wikipedia.org/wiki/Process_identifier) that can be used to send signals to the process. For demonstration we will create an infinitely running process by tailing a file:
5
+ If you are new to signals it can be useful to see how they can be used. When a process is created in a *nix like operating system it will have a [PID - or process identifier](https://en.wikipedia.org/wiki/Process_identifier) that can be used to send signals to the process. For demonstration we will create an infinitely running process by tailing a file:
6
6
 
7
7
  ```sh
8
8
  $ echo "foo" >> my.log
@@ -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](http://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://www.ruby-doc.org/core-2.1.1/Process.html#kill-method):
21
21
 
22
22
  ```
23
23
  $ irb
24
24
  > puts pid
25
25
  => 87152
26
- Process.detach(pid) # http://ruby-doc.org/core-2.1.1/Process.html#method-c-detach
26
+ Process.detach(pid) # https://ruby-doc.org/core-2.1.1/Process.html#method-c-detach
27
27
  Process.kill("TERM", pid)
28
28
  ```
29
29
 
@@ -14,12 +14,14 @@
14
14
 
15
15
  /*
16
16
  * capitalizes all lower-case ASCII characters,
17
- * converts dashes to underscores.
17
+ * converts dashes to underscores, and underscores to commas.
18
18
  */
19
19
  static void snake_upcase_char(char *c)
20
20
  {
21
21
  if (*c >= 'a' && *c <= 'z')
22
22
  *c &= ~0x20;
23
+ else if (*c == '_')
24
+ *c = ',';
23
25
  else if (*c == '-')
24
26
  *c = '_';
25
27
  }
@@ -12,12 +12,14 @@
12
12
 
13
13
  /*
14
14
  * capitalizes all lower-case ASCII characters,
15
- * converts dashes to underscores.
15
+ * converts dashes to underscores, and underscores to commas.
16
16
  */
17
17
  static void snake_upcase_char(char *c)
18
18
  {
19
19
  if (*c >= 'a' && *c <= 'z')
20
20
  *c &= ~0x20;
21
+ else if (*c == '_')
22
+ *c = ',';
21
23
  else if (*c == '-')
22
24
  *c = '_';
23
25
  }
@@ -301,6 +301,7 @@ void raise_error(SSL* ssl, int result) {
301
301
  char msg[512];
302
302
  const char* err_str;
303
303
  int err = errno;
304
+ int mask = 4095;
304
305
  int ssl_err = SSL_get_error(ssl, result);
305
306
  int verify_err = (int) SSL_get_verify_result(ssl);
306
307
 
@@ -317,8 +318,8 @@ void raise_error(SSL* ssl, int result) {
317
318
  } else {
318
319
  err = (int) ERR_get_error();
319
320
  ERR_error_string_n(err, buf, sizeof(buf));
320
- snprintf(msg, sizeof(msg), "OpenSSL error: %s - %d", buf, err);
321
-
321
+ int errexp = err & mask;
322
+ snprintf(msg, sizeof(msg), "OpenSSL error: %s - %d", buf, errexp);
322
323
  }
323
324
  } else {
324
325
  snprintf(msg, sizeof(msg), "Unknown OpenSSL error: %d", ssl_err);
@@ -462,6 +463,13 @@ VALUE engine_peercert(VALUE self) {
462
463
  return rb_cert_buf;
463
464
  }
464
465
 
466
+ static VALUE
467
+ engine_ssl_vers_st(VALUE self) {
468
+ ms_conn* conn;
469
+ Data_Get_Struct(self, ms_conn, conn);
470
+ return rb_ary_new3(2, rb_str_new2(SSL_get_version(conn->ssl)), rb_str_new2(SSL_state_string(conn->ssl)));
471
+ }
472
+
465
473
  VALUE noop(VALUE self) {
466
474
  return Qnil;
467
475
  }
@@ -533,6 +541,8 @@ void Init_mini_ssl(VALUE puma) {
533
541
  rb_define_method(eng, "init?", engine_init, 0);
534
542
 
535
543
  rb_define_method(eng, "peercert", engine_peercert, 0);
544
+
545
+ rb_define_method(eng, "ssl_vers_st", engine_ssl_vers_st, 0);
536
546
  }
537
547
 
538
548
  #else
@@ -120,6 +120,8 @@ public class MiniSSL extends RubyObject {
120
120
  }
121
121
 
122
122
  private SSLEngine engine;
123
+ private boolean closed;
124
+ private boolean handshake;
123
125
  private MiniSSLBuffer inboundNetData;
124
126
  private MiniSSLBuffer outboundAppData;
125
127
  private MiniSSLBuffer outboundNetData;
@@ -157,6 +159,8 @@ public class MiniSSL extends RubyObject {
157
159
  SSLContext sslCtx = SSLContext.getInstance("TLS");
158
160
 
159
161
  sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
162
+ closed = false;
163
+ handshake = false;
160
164
  engine = sslCtx.createSSLEngine();
161
165
 
162
166
  String[] protocols;
@@ -173,7 +177,7 @@ public class MiniSSL extends RubyObject {
173
177
  engine.setEnabledProtocols(protocols);
174
178
  engine.setUseClientMode(false);
175
179
 
176
- long verify_mode = miniSSLContext.callMethod(threadContext, "verify_mode").convertToInteger().getLongValue();
180
+ long verify_mode = miniSSLContext.callMethod(threadContext, "verify_mode").convertToInteger("to_i").getLongValue();
177
181
  if ((verify_mode & 0x1) != 0) { // 'peer'
178
182
  engine.setWantClientAuth(true);
179
183
  }
@@ -240,14 +244,21 @@ public class MiniSSL extends RubyObject {
240
244
  // need to wait for more data to come in before we retry
241
245
  retryOp = false;
242
246
  break;
247
+ case CLOSED:
248
+ closed = true;
249
+ retryOp = false;
250
+ break;
243
251
  default:
244
- // other cases are OK and CLOSED. We're done here.
252
+ // other case is OK. We're done here.
245
253
  retryOp = false;
246
254
  }
255
+ if (res.getHandshakeStatus() == HandshakeStatus.FINISHED) {
256
+ handshake = true;
257
+ }
247
258
  }
248
259
 
249
260
  // after each op, run any delegated tasks if needed
250
- if(engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
261
+ if(res.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
251
262
  Runnable runnable;
252
263
  while ((runnable = engine.getDelegatedTask()) != null) {
253
264
  runnable.run();
@@ -271,13 +282,14 @@ public class MiniSSL extends RubyObject {
271
282
 
272
283
  HandshakeStatus handshakeStatus = engine.getHandshakeStatus();
273
284
  boolean done = false;
285
+ SSLEngineResult res = null;
274
286
  while (!done) {
275
287
  switch (handshakeStatus) {
276
288
  case NEED_WRAP:
277
- doOp(SSLOperation.WRAP, inboundAppData, outboundNetData);
289
+ res = doOp(SSLOperation.WRAP, inboundAppData, outboundNetData);
278
290
  break;
279
291
  case NEED_UNWRAP:
280
- SSLEngineResult res = doOp(SSLOperation.UNWRAP, inboundNetData, inboundAppData);
292
+ res = doOp(SSLOperation.UNWRAP, inboundNetData, inboundAppData);
281
293
  if (res.getStatus() == Status.BUFFER_UNDERFLOW) {
282
294
  // need more data before we can shake more hands
283
295
  done = true;
@@ -286,7 +298,9 @@ public class MiniSSL extends RubyObject {
286
298
  default:
287
299
  done = true;
288
300
  }
289
- handshakeStatus = engine.getHandshakeStatus();
301
+ if (!done) {
302
+ handshakeStatus = res.getHandshakeStatus();
303
+ }
290
304
  }
291
305
 
292
306
  if (inboundNetData.hasRemaining()) {
@@ -360,4 +374,21 @@ public class MiniSSL extends RubyObject {
360
374
  return getRuntime().getNil();
361
375
  }
362
376
  }
377
+
378
+ @JRubyMethod(name = "init?")
379
+ public IRubyObject isInit(ThreadContext context) {
380
+ return handshake ? getRuntime().getFalse() : getRuntime().getTrue();
381
+ }
382
+
383
+ @JRubyMethod
384
+ public IRubyObject shutdown() {
385
+ if (closed || engine.isInboundDone() && engine.isOutboundDone()) {
386
+ if (engine.isOutboundDone()) {
387
+ engine.closeOutbound();
388
+ }
389
+ return getRuntime().getTrue();
390
+ } else {
391
+ return getRuntime().getFalse();
392
+ }
393
+ }
363
394
  }
@@ -54,7 +54,7 @@ DEF_MAX_LENGTH(FIELD_NAME, 256);
54
54
  DEF_MAX_LENGTH(FIELD_VALUE, 80 * 1024);
55
55
  DEF_MAX_LENGTH(REQUEST_URI, 1024 * 12);
56
56
  DEF_MAX_LENGTH(FRAGMENT, 1024); /* Don't know if this length is specified somewhere or not */
57
- DEF_MAX_LENGTH(REQUEST_PATH, 8196);
57
+ DEF_MAX_LENGTH(REQUEST_PATH, 8192);
58
58
  DEF_MAX_LENGTH(QUERY_STRING, (1024 * 10));
59
59
  DEF_MAX_LENGTH(HEADER, (1024 * (80 + 32)));
60
60
 
@@ -20,6 +20,7 @@ module Puma
20
20
  end
21
21
 
22
22
  def self.stats
23
+ require 'json'
23
24
  @get_stats.stats.to_json
24
25
  end
25
26
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
-
5
3
  module Puma
6
4
  module App
7
5
  # Check out {#call}'s source code to see what actions this web application
@@ -19,6 +17,10 @@ module Puma
19
17
  return rack_response(403, 'Invalid auth token', 'text/plain')
20
18
  end
21
19
 
20
+ if env['PATH_INFO'] =~ /\/(gc-stats|stats|thread-backtraces)$/
21
+ require 'json'
22
+ end
23
+
22
24
  case env['PATH_INFO']
23
25
  when /\/stop$/
24
26
  @cli.stop
@@ -6,6 +6,7 @@ require 'socket'
6
6
  require 'puma/const'
7
7
  require 'puma/util'
8
8
  require 'puma/minissl/context_builder'
9
+ require 'puma/configuration'
9
10
 
10
11
  module Puma
11
12
  class Binder
@@ -13,7 +14,7 @@ module Puma
13
14
 
14
15
  RACK_VERSION = [1,6].freeze
15
16
 
16
- def initialize(events)
17
+ def initialize(events, conf = Configuration.new)
17
18
  @events = events
18
19
  @listeners = []
19
20
  @inherited_fds = {}
@@ -23,8 +24,8 @@ module Puma
23
24
  @proto_env = {
24
25
  "rack.version".freeze => RACK_VERSION,
25
26
  "rack.errors".freeze => events.stderr,
26
- "rack.multithread".freeze => true,
27
- "rack.multiprocess".freeze => false,
27
+ "rack.multithread".freeze => conf.options[:max_threads] > 1,
28
+ "rack.multiprocess".freeze => conf.options[:workers] >= 1,
28
29
  "rack.run_once".freeze => false,
29
30
  "SCRIPT_NAME".freeze => ENV['SCRIPT_NAME'] || "",
30
31
 
@@ -113,7 +114,7 @@ module Puma
113
114
  i.local_address.ip_unpack.join(':')
114
115
  end
115
116
 
116
- logger.log "* #{log_msg} on tcp://#{addr}"
117
+ logger.log "* #{log_msg} on http://#{addr}"
117
118
  end
118
119
  end
119
120
 
@@ -308,8 +308,16 @@ module Puma
308
308
 
309
309
  te = @env[TRANSFER_ENCODING2]
310
310
 
311
- if te && CHUNKED.casecmp(te) == 0
312
- return setup_chunked_body(body)
311
+ if te
312
+ if te.include?(",")
313
+ te.split(",").each do |part|
314
+ if CHUNKED.casecmp(part.strip) == 0
315
+ return setup_chunked_body(body)
316
+ end
317
+ end
318
+ elsif CHUNKED.casecmp(te) == 0
319
+ return setup_chunked_body(body)
320
+ end
313
321
  end
314
322
 
315
323
  @chunked_body = false
@@ -412,7 +420,10 @@ module Puma
412
420
  raise EOFError
413
421
  end
414
422
 
415
- return true if decode_chunk(chunk)
423
+ if decode_chunk(chunk)
424
+ @env[CONTENT_LENGTH] = @chunked_content_length
425
+ return true
426
+ end
416
427
  end
417
428
  end
418
429
 
@@ -424,20 +435,36 @@ module Puma
424
435
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
425
436
  @body.binmode
426
437
  @tempfile = @body
438
+ @chunked_content_length = 0
439
+
440
+ if decode_chunk(body)
441
+ @env[CONTENT_LENGTH] = @chunked_content_length
442
+ return true
443
+ end
444
+ end
427
445
 
428
- return decode_chunk(body)
446
+ def write_chunk(str)
447
+ @chunked_content_length += @body.write(str)
429
448
  end
430
449
 
431
450
  def decode_chunk(chunk)
432
451
  if @partial_part_left > 0
433
452
  if @partial_part_left <= chunk.size
434
453
  if @partial_part_left > 2
435
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
454
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
436
455
  end
437
456
  chunk = chunk[@partial_part_left..-1]
438
457
  @partial_part_left = 0
439
458
  else
440
- @body << chunk if @partial_part_left > 2 # don't include the last \r\n
459
+ if @partial_part_left > 2
460
+ if @partial_part_left == chunk.size + 1
461
+ # Don't include the last \r
462
+ write_chunk(chunk[0..(@partial_part_left-3)])
463
+ else
464
+ # don't include the last \r\n
465
+ write_chunk(chunk)
466
+ end
467
+ end
441
468
  @partial_part_left -= chunk.size
442
469
  return false
443
470
  end
@@ -484,12 +511,12 @@ module Puma
484
511
 
485
512
  case
486
513
  when got == len
487
- @body << part[0..-3] # to skip the ending \r\n
514
+ write_chunk(part[0..-3]) # to skip the ending \r\n
488
515
  when got <= len - 2
489
- @body << part
516
+ write_chunk(part)
490
517
  @partial_part_left = len - part.size
491
518
  when got == len - 1 # edge where we get just \r but not \n
492
- @body << part[0..-2]
519
+ write_chunk(part[0..-2])
493
520
  @partial_part_left = len - part.size
494
521
  end
495
522
  else