puma 4.0.1 → 4.2.1

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.

@@ -1,5 +1,5 @@
1
1
  %%{
2
-
2
+
3
3
  machine puma_parser_common;
4
4
 
5
5
  #### HTTP PROTOCOL GRAMMAR
@@ -16,7 +16,7 @@
16
16
  unreserved = (alpha | digit | safe | extra | national);
17
17
  escape = ("%" xdigit xdigit);
18
18
  uchar = (unreserved | escape | "%");
19
- pchar = (uchar | ":" | "@" | "&" | "=" | "+");
19
+ pchar = (uchar | ":" | "@" | "&" | "=" | "+" | ";");
20
20
  tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t");
21
21
 
22
22
  # elements
@@ -30,7 +30,7 @@
30
30
  query = ( uchar | reserved )* %query_string ;
31
31
  param = ( pchar | "/" )* ;
32
32
  params = ( param ( ";" param )* ) ;
33
- rel_path = ( path? %request_path (";" params)? ) ("?" %start_query query)?;
33
+ rel_path = ( path? %request_path ) ("?" %start_query query)?;
34
34
  absolute_path = ( "/"+ rel_path );
35
35
 
36
36
  Request_URI = ( "*" | absolute_uri | absolute_path ) >mark %request_uri;
@@ -142,7 +142,7 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
142
142
  VALUE obj;
143
143
  SSL_CTX* ctx;
144
144
  SSL* ssl;
145
- int ssl_options;
145
+ int min, ssl_options;
146
146
 
147
147
  ms_conn* conn = engine_alloc(self, &obj);
148
148
 
@@ -168,6 +168,9 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
168
168
  ID sym_no_tlsv1 = rb_intern("no_tlsv1");
169
169
  VALUE no_tlsv1 = rb_funcall(mini_ssl_ctx, sym_no_tlsv1, 0);
170
170
 
171
+ ID sym_no_tlsv1_1 = rb_intern("no_tlsv1_1");
172
+ VALUE no_tlsv1_1 = rb_funcall(mini_ssl_ctx, sym_no_tlsv1_1, 0);
173
+
171
174
  #ifdef HAVE_TLS_SERVER_METHOD
172
175
  ctx = SSL_CTX_new(TLS_server_method());
173
176
  #else
@@ -183,12 +186,36 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
183
186
  SSL_CTX_load_verify_locations(ctx, RSTRING_PTR(ca), NULL);
184
187
  }
185
188
 
186
- ssl_options = SSL_OP_CIPHER_SERVER_PREFERENCE | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_SINGLE_DH_USE | SSL_OP_SINGLE_ECDH_USE | SSL_OP_NO_COMPRESSION;
189
+ ssl_options = SSL_OP_CIPHER_SERVER_PREFERENCE | SSL_OP_SINGLE_ECDH_USE | SSL_OP_NO_COMPRESSION;
187
190
 
188
- if(RTEST(no_tlsv1)) {
191
+ #ifdef HAVE_SSL_CTX_SET_MIN_PROTO_VERSION
192
+ if (RTEST(no_tlsv1_1)) {
193
+ min = TLS1_2_VERSION;
194
+ }
195
+ else if (RTEST(no_tlsv1)) {
196
+ min = TLS1_1_VERSION;
197
+ }
198
+ else {
199
+ min = TLS1_VERSION;
200
+ }
201
+
202
+ SSL_CTX_set_min_proto_version(ctx, min);
203
+
204
+ SSL_CTX_set_options(ctx, ssl_options);
205
+
206
+ #else
207
+ /* As of 1.0.2f, SSL_OP_SINGLE_DH_USE key use is always on */
208
+ ssl_options |= SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_SINGLE_DH_USE;
209
+
210
+ if (RTEST(no_tlsv1)) {
189
211
  ssl_options |= SSL_OP_NO_TLSv1;
190
212
  }
213
+ if(RTEST(no_tlsv1_1)) {
214
+ ssl_options |= SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1;
215
+ }
191
216
  SSL_CTX_set_options(ctx, ssl_options);
217
+ #endif
218
+
192
219
  SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF);
193
220
 
194
221
  if (!NIL_P(ssl_cipher_filter)) {
@@ -458,14 +485,35 @@ void Init_mini_ssl(VALUE puma) {
458
485
  // OpenSSL Build / Runtime/Load versions
459
486
 
460
487
  /* Version of OpenSSL that Puma was compiled with */
461
- rb_define_const(mod, "OPENSSL_VERSION", rb_str_new2(OPENSSL_VERSION_TEXT));
488
+ rb_define_const(mod, "OPENSSL_VERSION", rb_str_new2(OPENSSL_VERSION_TEXT));
462
489
 
463
490
  #if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x10100000
464
- /* Version of OpenSSL that Puma loaded with */
465
- rb_define_const(mod, "OPENSSL_LIBRARY_VERSION", rb_str_new2(OpenSSL_version(OPENSSL_VERSION)));
491
+ /* Version of OpenSSL that Puma loaded with */
492
+ rb_define_const(mod, "OPENSSL_LIBRARY_VERSION", rb_str_new2(OpenSSL_version(OPENSSL_VERSION)));
466
493
  #else
467
- rb_define_const(mod, "OPENSSL_LIBRARY_VERSION", rb_str_new2(SSLeay_version(SSLEAY_VERSION)));
494
+ rb_define_const(mod, "OPENSSL_LIBRARY_VERSION", rb_str_new2(SSLeay_version(SSLEAY_VERSION)));
468
495
  #endif
496
+
497
+ #if defined(OPENSSL_NO_SSL3) || defined(OPENSSL_NO_SSL3_METHOD)
498
+ /* True if SSL3 is not available */
499
+ rb_define_const(mod, "OPENSSL_NO_SSL3", Qtrue);
500
+ #else
501
+ rb_define_const(mod, "OPENSSL_NO_SSL3", Qfalse);
502
+ #endif
503
+
504
+ #if defined(OPENSSL_NO_TLS1) || defined(OPENSSL_NO_TLS1_METHOD)
505
+ /* True if TLS1 is not available */
506
+ rb_define_const(mod, "OPENSSL_NO_TLS1", Qtrue);
507
+ #else
508
+ rb_define_const(mod, "OPENSSL_NO_TLS1", Qfalse);
509
+ #endif
510
+
511
+ #if defined(OPENSSL_NO_TLS1_1) || defined(OPENSSL_NO_TLS1_1_METHOD)
512
+ /* True if TLS1_1 is not available */
513
+ rb_define_const(mod, "OPENSSL_NO_TLS1_1", Qtrue);
514
+ #else
515
+ rb_define_const(mod, "OPENSSL_NO_TLS1_1", Qfalse);
516
+ #endif
469
517
 
470
518
  rb_define_singleton_method(mod, "check", noop, 0);
471
519
 
@@ -166,6 +166,10 @@ public class MiniSSL extends RubyObject {
166
166
  protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" };
167
167
  }
168
168
 
169
+ if(miniSSLContext.callMethod(threadContext, "no_tlsv1_1").isTrue()) {
170
+ protocols = new String[] { "TLSv1.2" };
171
+ }
172
+
169
173
  engine.setEnabledProtocols(protocols);
170
174
  engine.setUseClientMode(false);
171
175
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Standard libraries
2
4
  require 'socket'
3
5
  require 'tempfile'
@@ -20,4 +22,10 @@ module Puma
20
22
  def self.stats
21
23
  @get_stats.stats
22
24
  end
25
+
26
+ # Thread name is new in Ruby 2.3
27
+ def self.set_thread_name(name)
28
+ return unless Thread.current.respond_to?(:name=)
29
+ Thread.current.name = "puma #{name}"
30
+ end
23
31
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'openssl'
2
4
 
3
5
  module OpenSSL
@@ -13,7 +15,11 @@ module OpenSSL
13
15
  ssl.accept if @start_immediately
14
16
  ssl
15
17
  rescue SSLError => ex
16
- sock.close
18
+ if ssl
19
+ ssl.close
20
+ else
21
+ sock.close
22
+ end
17
23
  raise ex
18
24
  end
19
25
  end
@@ -1,28 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
 
3
5
  module Puma
4
6
  module App
7
+ # Check out {#call}'s source code to see what actions this web application
8
+ # can respond to.
5
9
  class Status
6
- def initialize(cli)
7
- @cli = cli
8
- @auth_token = nil
9
- end
10
10
  OK_STATUS = '{ "status": "ok" }'.freeze
11
11
 
12
- attr_accessor :auth_token
13
-
14
- def authenticate(env)
15
- return true unless @auth_token
16
- env['QUERY_STRING'].to_s.split(/&;/).include?("token=#{@auth_token}")
17
- end
18
-
19
- def rack_response(status, body, content_type='application/json')
20
- headers = {
21
- 'Content-Type' => content_type,
22
- 'Content-Length' => body.bytesize.to_s
23
- }
24
-
25
- [status, headers, [body]]
12
+ def initialize(cli, token = nil)
13
+ @cli = cli
14
+ @auth_token = token
26
15
  end
27
16
 
28
17
  def call(env)
@@ -33,43 +22,59 @@ module Puma
33
22
  case env['PATH_INFO']
34
23
  when /\/stop$/
35
24
  @cli.stop
36
- return rack_response(200, OK_STATUS)
25
+ rack_response(200, OK_STATUS)
37
26
 
38
27
  when /\/halt$/
39
28
  @cli.halt
40
- return rack_response(200, OK_STATUS)
29
+ rack_response(200, OK_STATUS)
41
30
 
42
31
  when /\/restart$/
43
32
  @cli.restart
44
- return rack_response(200, OK_STATUS)
33
+ rack_response(200, OK_STATUS)
45
34
 
46
35
  when /\/phased-restart$/
47
36
  if !@cli.phased_restart
48
- return rack_response(404, '{ "error": "phased restart not available" }')
37
+ rack_response(404, '{ "error": "phased restart not available" }')
49
38
  else
50
- return rack_response(200, OK_STATUS)
39
+ rack_response(200, OK_STATUS)
51
40
  end
52
41
 
53
42
  when /\/reload-worker-directory$/
54
43
  if !@cli.send(:reload_worker_directory)
55
- return rack_response(404, '{ "error": "reload_worker_directory not available" }')
44
+ rack_response(404, '{ "error": "reload_worker_directory not available" }')
56
45
  else
57
- return rack_response(200, OK_STATUS)
46
+ rack_response(200, OK_STATUS)
58
47
  end
59
48
 
60
49
  when /\/gc$/
61
50
  GC.start
62
- return rack_response(200, OK_STATUS)
51
+ rack_response(200, OK_STATUS)
63
52
 
64
53
  when /\/gc-stats$/
65
- return rack_response(200, GC.stat.to_json)
54
+ rack_response(200, GC.stat.to_json)
66
55
 
67
56
  when /\/stats$/
68
- return rack_response(200, @cli.stats)
57
+ rack_response(200, @cli.stats)
69
58
  else
70
59
  rack_response 404, "Unsupported action", 'text/plain'
71
60
  end
72
61
  end
62
+
63
+ private
64
+
65
+ def authenticate(env)
66
+ return true unless @auth_token
67
+ env['QUERY_STRING'].to_s.split(/&;/).include?("token=#{@auth_token}")
68
+ end
69
+
70
+ def rack_response(status, body, content_type='application/json')
71
+ headers = {
72
+ 'Content-Type' => content_type,
73
+ 'Content-Length' => body.bytesize.to_s
74
+ }
75
+
76
+ [status, headers, [body]]
77
+ end
73
78
  end
74
79
  end
75
80
  end
@@ -42,7 +42,7 @@ module Puma
42
42
  @ios = []
43
43
  end
44
44
 
45
- attr_reader :listeners, :ios
45
+ attr_reader :ios
46
46
 
47
47
  def env(sock)
48
48
  @envs.fetch(sock, @proto_env)
@@ -50,14 +50,6 @@ module Puma
50
50
 
51
51
  def close
52
52
  @ios.each { |i| i.close }
53
- @unix_paths.each do |i|
54
- # Errno::ENOENT is intermittently raised
55
- begin
56
- unix_socket = UNIXSocket.new i
57
- unix_socket.close
58
- rescue Errno::ENOENT
59
- end
60
- end
61
53
  end
62
54
 
63
55
  def import_from_env
@@ -111,7 +103,17 @@ module Puma
111
103
  bak = params.fetch('backlog', 1024).to_i
112
104
 
113
105
  io = add_tcp_listener uri.host, uri.port, opt, bak
114
- logger.log "* Listening on #{str}"
106
+
107
+ @ios.each do |i|
108
+ next unless TCPServer === i
109
+ addr = if i.local_address.ipv6?
110
+ "[#{i.local_address.ip_unpack[0]}]:#{i.local_address.ip_unpack[1]}"
111
+ else
112
+ i.local_address.ip_unpack.join(':')
113
+ end
114
+
115
+ logger.log "* Listening on tcp://#{addr}"
116
+ end
115
117
  end
116
118
 
117
119
  @listeners << [str, io] if io
@@ -195,6 +197,7 @@ module Puma
195
197
  end
196
198
 
197
199
  ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
200
+ ctx.no_tlsv1_1 = true if params['no_tlsv1_1'] == 'true'
198
201
 
199
202
  if params['verify_mode']
200
203
  ctx.verify_mode = case params['verify_mode']
@@ -358,7 +361,7 @@ module Puma
358
361
  # Tell the server to listen on +path+ as a UNIX domain socket.
359
362
  #
360
363
  def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
361
- @unix_paths << path
364
+ @unix_paths << path unless File.exist? path
362
365
 
363
366
  # Let anyone connect by default
364
367
  umask ||= 0
@@ -396,7 +399,7 @@ module Puma
396
399
  end
397
400
 
398
401
  def inherit_unix_listener(path, fd)
399
- @unix_paths << path
402
+ @unix_paths << path unless File.exist? path
400
403
 
401
404
  if fd.kind_of? TCPServer
402
405
  s = fd
@@ -412,5 +415,27 @@ module Puma
412
415
  s
413
416
  end
414
417
 
418
+ def close_listeners
419
+ @listeners.each do |l, io|
420
+ io.close
421
+ uri = URI.parse(l)
422
+ next unless uri.scheme == 'unix'
423
+ unix_path = "#{uri.host}#{uri.path}"
424
+ File.unlink unix_path if @unix_paths.include? unix_path
425
+ end
426
+ end
427
+
428
+ def close_unix_paths
429
+ @unix_paths.each { |up| File.unlink(up) if File.exist? up }
430
+ end
431
+
432
+ def redirects_for_restart
433
+ redirects = {:close_others => true}
434
+ @listeners.each_with_index do |(l, io), i|
435
+ ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
436
+ redirects[io.to_i] = io.to_i
437
+ end
438
+ redirects
439
+ end
415
440
  end
416
441
  end
@@ -161,6 +161,10 @@ module Puma
161
161
  user_config.prune_bundler
162
162
  end
163
163
 
164
+ o.on "--extra-runtime-dependencies GEM1,GEM2", "Defines any extra needed gems when using --prune-bundler" do |arg|
165
+ c.extra_runtime_dependencies arg.split(',')
166
+ end
167
+
164
168
  o.on "-q", "--quiet", "Do not log requests internally (default true)" do
165
169
  user_config.quiet
166
170
  end
@@ -9,8 +9,8 @@ class IO
9
9
  end
10
10
 
11
11
  require 'puma/detect'
12
- require 'puma/delegation'
13
12
  require 'tempfile'
13
+ require 'forwardable'
14
14
 
15
15
  if Puma::IS_JRUBY
16
16
  # We have to work around some OpenSSL buffer/io-readiness bugs
@@ -24,11 +24,11 @@ module Puma
24
24
  class ConnectionError < RuntimeError; end
25
25
 
26
26
  # An instance of this class represents a unique request from a client.
27
- # For example a web request from a browser or from CURL. This
27
+ # For example, this could be a web request from a browser or from CURL.
28
28
  #
29
29
  # An instance of `Puma::Client` can be used as if it were an IO object
30
- # by the reactor, that's because the latter is expected to call `#to_io`
31
- # on any non-IO objects it polls. For example nio4r internally calls
30
+ # by the reactor. The reactor is expected to call `#to_io`
31
+ # on any non-IO objects it polls. For example, nio4r internally calls
32
32
  # `IO::try_convert` (which may call `#to_io`) when a new socket is
33
33
  # registered.
34
34
  #
@@ -36,8 +36,12 @@ module Puma
36
36
  # the header and body are fully buffered via the `try_to_finish` method.
37
37
  # They can be used to "time out" a response via the `timeout_at` reader.
38
38
  class Client
39
+ # The object used for a request with no body. All requests with
40
+ # no body share this one object since it has no state.
41
+ EmptyBody = NullIO.new
42
+
39
43
  include Puma::Const
40
- extend Puma::Delegation
44
+ extend Forwardable
41
45
 
42
46
  def initialize(io, env=nil)
43
47
  @io = io
@@ -68,6 +72,8 @@ module Puma
68
72
  @remote_addr_header = nil
69
73
 
70
74
  @body_remain = 0
75
+
76
+ @in_last_chunk = false
71
77
  end
72
78
 
73
79
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
@@ -77,7 +83,7 @@ module Puma
77
83
 
78
84
  attr_accessor :remote_addr_header
79
85
 
80
- forward :closed?, :@io
86
+ def_delegators :@io, :closed?
81
87
 
82
88
  def inspect
83
89
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
@@ -108,6 +114,7 @@ module Puma
108
114
  @ready = false
109
115
  @body_remain = 0
110
116
  @peerip = nil
117
+ @in_last_chunk = false
111
118
 
112
119
  if @buffer
113
120
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
@@ -141,166 +148,6 @@ module Puma
141
148
  end
142
149
  end
143
150
 
144
- # The object used for a request with no body. All requests with
145
- # no body share this one object since it has no state.
146
- EmptyBody = NullIO.new
147
-
148
- def setup_chunked_body(body)
149
- @chunked_body = true
150
- @partial_part_left = 0
151
- @prev_chunk = ""
152
-
153
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
154
- @body.binmode
155
- @tempfile = @body
156
-
157
- return decode_chunk(body)
158
- end
159
-
160
- def decode_chunk(chunk)
161
- if @partial_part_left > 0
162
- if @partial_part_left <= chunk.size
163
- if @partial_part_left > 2
164
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
165
- end
166
- chunk = chunk[@partial_part_left..-1]
167
- @partial_part_left = 0
168
- else
169
- @body << chunk
170
- @partial_part_left -= chunk.size
171
- return false
172
- end
173
- end
174
-
175
- if @prev_chunk.empty?
176
- io = StringIO.new(chunk)
177
- else
178
- io = StringIO.new(@prev_chunk+chunk)
179
- @prev_chunk = ""
180
- end
181
-
182
- while !io.eof?
183
- line = io.gets
184
- if line.end_with?("\r\n")
185
- len = line.strip.to_i(16)
186
- if len == 0
187
- @body.rewind
188
- rest = io.read
189
- rest = rest[2..-1] if rest.start_with?("\r\n")
190
- @buffer = rest.empty? ? nil : rest
191
- set_ready
192
- return true
193
- end
194
-
195
- len += 2
196
-
197
- part = io.read(len)
198
-
199
- unless part
200
- @partial_part_left = len
201
- next
202
- end
203
-
204
- got = part.size
205
-
206
- case
207
- when got == len
208
- @body << part[0..-3] # to skip the ending \r\n
209
- when got <= len - 2
210
- @body << part
211
- @partial_part_left = len - part.size
212
- when got == len - 1 # edge where we get just \r but not \n
213
- @body << part[0..-2]
214
- @partial_part_left = len - part.size
215
- end
216
- else
217
- @prev_chunk = line
218
- return false
219
- end
220
- end
221
-
222
- return false
223
- end
224
-
225
- def read_chunked_body
226
- while true
227
- begin
228
- chunk = @io.read_nonblock(4096)
229
- rescue IO::WaitReadable
230
- return false
231
- rescue SystemCallError, IOError
232
- raise ConnectionError, "Connection error detected during read"
233
- end
234
-
235
- # No chunk means a closed socket
236
- unless chunk
237
- @body.close
238
- @buffer = nil
239
- set_ready
240
- raise EOFError
241
- end
242
-
243
- return true if decode_chunk(chunk)
244
- end
245
- end
246
-
247
- def setup_body
248
- @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
249
-
250
- if @env[HTTP_EXPECT] == CONTINUE
251
- # TODO allow a hook here to check the headers before
252
- # going forward
253
- @io << HTTP_11_100
254
- @io.flush
255
- end
256
-
257
- @read_header = false
258
-
259
- body = @parser.body
260
-
261
- te = @env[TRANSFER_ENCODING2]
262
-
263
- if te && CHUNKED.casecmp(te) == 0
264
- return setup_chunked_body(body)
265
- end
266
-
267
- @chunked_body = false
268
-
269
- cl = @env[CONTENT_LENGTH]
270
-
271
- unless cl
272
- @buffer = body.empty? ? nil : body
273
- @body = EmptyBody
274
- set_ready
275
- return true
276
- end
277
-
278
- remain = cl.to_i - body.bytesize
279
-
280
- if remain <= 0
281
- @body = StringIO.new(body)
282
- @buffer = nil
283
- set_ready
284
- return true
285
- end
286
-
287
- if remain > MAX_BODY
288
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
289
- @body.binmode
290
- @tempfile = @body
291
- else
292
- # The body[0,0] trick is to get an empty string in the same
293
- # encoding as body.
294
- @body = StringIO.new body[0,0]
295
- end
296
-
297
- @body.write body
298
-
299
- @body_remain = remain
300
-
301
- return false
302
- end
303
-
304
151
  def try_to_finish
305
152
  return read_body unless @read_header
306
153
 
@@ -401,6 +248,84 @@ module Puma
401
248
  true
402
249
  end
403
250
 
251
+ def write_error(status_code)
252
+ begin
253
+ @io << ERROR_RESPONSE[status_code]
254
+ rescue StandardError
255
+ end
256
+ end
257
+
258
+ def peerip
259
+ return @peerip if @peerip
260
+
261
+ if @remote_addr_header
262
+ hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
263
+ @peerip = hdr
264
+ return hdr
265
+ end
266
+
267
+ @peerip ||= @io.peeraddr.last
268
+ end
269
+
270
+ private
271
+
272
+ def setup_body
273
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
274
+
275
+ if @env[HTTP_EXPECT] == CONTINUE
276
+ # TODO allow a hook here to check the headers before
277
+ # going forward
278
+ @io << HTTP_11_100
279
+ @io.flush
280
+ end
281
+
282
+ @read_header = false
283
+
284
+ body = @parser.body
285
+
286
+ te = @env[TRANSFER_ENCODING2]
287
+
288
+ if te && CHUNKED.casecmp(te) == 0
289
+ return setup_chunked_body(body)
290
+ end
291
+
292
+ @chunked_body = false
293
+
294
+ cl = @env[CONTENT_LENGTH]
295
+
296
+ unless cl
297
+ @buffer = body.empty? ? nil : body
298
+ @body = EmptyBody
299
+ set_ready
300
+ return true
301
+ end
302
+
303
+ remain = cl.to_i - body.bytesize
304
+
305
+ if remain <= 0
306
+ @body = StringIO.new(body)
307
+ @buffer = nil
308
+ set_ready
309
+ return true
310
+ end
311
+
312
+ if remain > MAX_BODY
313
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
314
+ @body.binmode
315
+ @tempfile = @body
316
+ else
317
+ # The body[0,0] trick is to get an empty string in the same
318
+ # encoding as body.
319
+ @body = StringIO.new body[0,0]
320
+ end
321
+
322
+ @body.write body
323
+
324
+ @body_remain = remain
325
+
326
+ return false
327
+ end
328
+
404
329
  def read_body
405
330
  if @chunked_body
406
331
  return read_chunked_body
@@ -446,45 +371,124 @@ module Puma
446
371
  false
447
372
  end
448
373
 
449
- def set_ready
450
- if @body_read_start
451
- @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
374
+ def read_chunked_body
375
+ while true
376
+ begin
377
+ chunk = @io.read_nonblock(4096)
378
+ rescue IO::WaitReadable
379
+ return false
380
+ rescue SystemCallError, IOError
381
+ raise ConnectionError, "Connection error detected during read"
382
+ end
383
+
384
+ # No chunk means a closed socket
385
+ unless chunk
386
+ @body.close
387
+ @buffer = nil
388
+ set_ready
389
+ raise EOFError
390
+ end
391
+
392
+ return true if decode_chunk(chunk)
452
393
  end
453
- @requests_served += 1
454
- @ready = true
455
394
  end
456
395
 
457
- def write_400
458
- begin
459
- @io << ERROR_400_RESPONSE
460
- rescue StandardError
461
- end
396
+ def setup_chunked_body(body)
397
+ @chunked_body = true
398
+ @partial_part_left = 0
399
+ @prev_chunk = ""
400
+
401
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
402
+ @body.binmode
403
+ @tempfile = @body
404
+
405
+ return decode_chunk(body)
462
406
  end
463
407
 
464
- def write_408
465
- begin
466
- @io << ERROR_408_RESPONSE
467
- rescue StandardError
408
+ def decode_chunk(chunk)
409
+ if @partial_part_left > 0
410
+ if @partial_part_left <= chunk.size
411
+ if @partial_part_left > 2
412
+ @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
413
+ end
414
+ chunk = chunk[@partial_part_left..-1]
415
+ @partial_part_left = 0
416
+ else
417
+ @body << chunk if @partial_part_left > 2 # don't include the last \r\n
418
+ @partial_part_left -= chunk.size
419
+ return false
420
+ end
468
421
  end
469
- end
470
422
 
471
- def write_500
472
- begin
473
- @io << ERROR_500_RESPONSE
474
- rescue StandardError
423
+ if @prev_chunk.empty?
424
+ io = StringIO.new(chunk)
425
+ else
426
+ io = StringIO.new(@prev_chunk+chunk)
427
+ @prev_chunk = ""
475
428
  end
476
- end
477
429
 
478
- def peerip
479
- return @peerip if @peerip
430
+ while !io.eof?
431
+ line = io.gets
432
+ if line.end_with?("\r\n")
433
+ len = line.strip.to_i(16)
434
+ if len == 0
435
+ @in_last_chunk = true
436
+ @body.rewind
437
+ rest = io.read
438
+ last_crlf_size = "\r\n".bytesize
439
+ if rest.bytesize < last_crlf_size
440
+ @buffer = nil
441
+ @partial_part_left = last_crlf_size - rest.bytesize
442
+ return false
443
+ else
444
+ @buffer = rest[last_crlf_size..-1]
445
+ @buffer = nil if @buffer.empty?
446
+ set_ready
447
+ return true
448
+ end
449
+ end
480
450
 
481
- if @remote_addr_header
482
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
483
- @peerip = hdr
484
- return hdr
451
+ len += 2
452
+
453
+ part = io.read(len)
454
+
455
+ unless part
456
+ @partial_part_left = len
457
+ next
458
+ end
459
+
460
+ got = part.size
461
+
462
+ case
463
+ when got == len
464
+ @body << part[0..-3] # to skip the ending \r\n
465
+ when got <= len - 2
466
+ @body << part
467
+ @partial_part_left = len - part.size
468
+ when got == len - 1 # edge where we get just \r but not \n
469
+ @body << part[0..-2]
470
+ @partial_part_left = len - part.size
471
+ end
472
+ else
473
+ @prev_chunk = line
474
+ return false
475
+ end
485
476
  end
486
477
 
487
- @peerip ||= @io.peeraddr.last
478
+ if @in_last_chunk
479
+ set_ready
480
+ true
481
+ else
482
+ false
483
+ end
484
+ end
485
+
486
+ def set_ready
487
+ if @body_read_start
488
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
489
+ end
490
+ @requests_served += 1
491
+ @ready = true
488
492
  end
489
493
  end
490
494
  end