puma 3.12.2 → 4.3.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.

Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +122 -2
  3. data/README.md +76 -48
  4. data/docs/architecture.md +1 -0
  5. data/docs/deployment.md +24 -4
  6. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  7. data/docs/images/puma-connection-flow.png +0 -0
  8. data/docs/images/puma-general-arch.png +0 -0
  9. data/docs/plugins.md +20 -10
  10. data/docs/restart.md +4 -2
  11. data/docs/systemd.md +27 -9
  12. data/docs/tcp_mode.md +96 -0
  13. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  14. data/ext/puma_http11/extconf.rb +13 -0
  15. data/ext/puma_http11/http11_parser.c +37 -62
  16. data/ext/puma_http11/http11_parser.java.rl +21 -37
  17. data/ext/puma_http11/http11_parser_common.rl +3 -3
  18. data/ext/puma_http11/mini_ssl.c +78 -8
  19. data/ext/puma_http11/org/jruby/puma/Http11.java +106 -114
  20. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +91 -106
  21. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  22. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +15 -4
  23. data/ext/puma_http11/puma_http11.c +2 -0
  24. data/lib/puma.rb +8 -0
  25. data/lib/puma/accept_nonblock.rb +7 -1
  26. data/lib/puma/app/status.rb +35 -29
  27. data/lib/puma/binder.rb +38 -60
  28. data/lib/puma/cli.rb +4 -0
  29. data/lib/puma/client.rb +221 -199
  30. data/lib/puma/cluster.rb +53 -30
  31. data/lib/puma/configuration.rb +4 -3
  32. data/lib/puma/const.rb +22 -18
  33. data/lib/puma/control_cli.rb +30 -5
  34. data/lib/puma/dsl.rb +299 -75
  35. data/lib/puma/events.rb +4 -1
  36. data/lib/puma/io_buffer.rb +1 -6
  37. data/lib/puma/launcher.rb +95 -53
  38. data/lib/puma/minissl.rb +35 -17
  39. data/lib/puma/minissl/context_builder.rb +76 -0
  40. data/lib/puma/plugin.rb +5 -2
  41. data/lib/puma/plugin/tmp_restart.rb +2 -0
  42. data/lib/puma/rack/builder.rb +2 -0
  43. data/lib/puma/rack/urlmap.rb +2 -0
  44. data/lib/puma/rack_default.rb +2 -0
  45. data/lib/puma/reactor.rb +110 -57
  46. data/lib/puma/runner.rb +11 -3
  47. data/lib/puma/server.rb +58 -47
  48. data/lib/puma/single.rb +3 -3
  49. data/lib/puma/thread_pool.rb +15 -33
  50. data/lib/puma/util.rb +1 -6
  51. data/lib/rack/handler/puma.rb +3 -3
  52. data/tools/docker/Dockerfile +16 -0
  53. data/tools/jungle/init.d/puma +6 -6
  54. data/tools/trickletest.rb +0 -1
  55. metadata +21 -8
  56. data/lib/puma/compat.rb +0 -14
  57. data/lib/puma/convenient.rb +0 -25
  58. data/lib/puma/daemon_ext.rb +0 -33
  59. data/lib/puma/delegation.rb +0 -13
  60. data/lib/puma/java_io_buffer.rb +0 -47
  61. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
@@ -200,6 +200,8 @@ void http_field(puma_parser* hp, const char *field, size_t flen,
200
200
  f = rb_str_new(hp->buf, new_size);
201
201
  }
202
202
 
203
+ while (vlen > 0 && isspace(value[vlen - 1])) vlen--;
204
+
203
205
  /* check for duplicate header */
204
206
  v = rb_hash_aref(hp->request, f);
205
207
 
@@ -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,26 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
1
5
  module Puma
2
6
  module App
7
+ # Check out {#call}'s source code to see what actions this web application
8
+ # can respond to.
3
9
  class Status
4
- def initialize(cli)
5
- @cli = cli
6
- @auth_token = nil
7
- end
8
10
  OK_STATUS = '{ "status": "ok" }'.freeze
9
11
 
10
- attr_accessor :auth_token
11
-
12
- def authenticate(env)
13
- return true unless @auth_token
14
- env['QUERY_STRING'].to_s.split(/&;/).include?("token=#{@auth_token}")
15
- end
16
-
17
- def rack_response(status, body, content_type='application/json')
18
- headers = {
19
- 'Content-Type' => content_type,
20
- 'Content-Length' => body.bytesize.to_s
21
- }
22
-
23
- [status, headers, [body]]
12
+ def initialize(cli, token = nil)
13
+ @cli = cli
14
+ @auth_token = token
24
15
  end
25
16
 
26
17
  def call(env)
@@ -31,44 +22,59 @@ module Puma
31
22
  case env['PATH_INFO']
32
23
  when /\/stop$/
33
24
  @cli.stop
34
- return rack_response(200, OK_STATUS)
25
+ rack_response(200, OK_STATUS)
35
26
 
36
27
  when /\/halt$/
37
28
  @cli.halt
38
- return rack_response(200, OK_STATUS)
29
+ rack_response(200, OK_STATUS)
39
30
 
40
31
  when /\/restart$/
41
32
  @cli.restart
42
- return rack_response(200, OK_STATUS)
33
+ rack_response(200, OK_STATUS)
43
34
 
44
35
  when /\/phased-restart$/
45
36
  if !@cli.phased_restart
46
- return rack_response(404, '{ "error": "phased restart not available" }')
37
+ rack_response(404, '{ "error": "phased restart not available" }')
47
38
  else
48
- return rack_response(200, OK_STATUS)
39
+ rack_response(200, OK_STATUS)
49
40
  end
50
41
 
51
42
  when /\/reload-worker-directory$/
52
43
  if !@cli.send(:reload_worker_directory)
53
- return rack_response(404, '{ "error": "reload_worker_directory not available" }')
44
+ rack_response(404, '{ "error": "reload_worker_directory not available" }')
54
45
  else
55
- return rack_response(200, OK_STATUS)
46
+ rack_response(200, OK_STATUS)
56
47
  end
57
48
 
58
49
  when /\/gc$/
59
50
  GC.start
60
- return rack_response(200, OK_STATUS)
51
+ rack_response(200, OK_STATUS)
61
52
 
62
53
  when /\/gc-stats$/
63
- json = "{" + GC.stat.map { |k, v| "\"#{k}\": #{v}" }.join(",") + "}"
64
- return rack_response(200, json)
54
+ rack_response(200, GC.stat.to_json)
65
55
 
66
56
  when /\/stats$/
67
- return rack_response(200, @cli.stats)
57
+ rack_response(200, @cli.stats)
68
58
  else
69
59
  rack_response 404, "Unsupported action", 'text/plain'
70
60
  end
71
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
72
78
  end
73
79
  end
74
80
  end
@@ -5,6 +5,7 @@ require 'socket'
5
5
 
6
6
  require 'puma/const'
7
7
  require 'puma/util'
8
+ require 'puma/minissl/context_builder'
8
9
 
9
10
  module Puma
10
11
  class Binder
@@ -42,7 +43,7 @@ module Puma
42
43
  @ios = []
43
44
  end
44
45
 
45
- attr_reader :listeners, :ios
46
+ attr_reader :ios
46
47
 
47
48
  def env(sock)
48
49
  @envs.fetch(sock, @proto_env)
@@ -50,7 +51,6 @@ module Puma
50
51
 
51
52
  def close
52
53
  @ios.each { |i| i.close }
53
- @unix_paths.each { |i| File.unlink i }
54
54
  end
55
55
 
56
56
  def import_from_env
@@ -104,7 +104,17 @@ module Puma
104
104
  bak = params.fetch('backlog', 1024).to_i
105
105
 
106
106
  io = add_tcp_listener uri.host, uri.port, opt, bak
107
- logger.log "* Listening on #{str}"
107
+
108
+ @ios.each do |i|
109
+ next unless TCPServer === i
110
+ addr = if i.local_address.ipv6?
111
+ "[#{i.local_address.ip_unpack[0]}]:#{i.local_address.ip_unpack[1]}"
112
+ else
113
+ i.local_address.ip_unpack.join(':')
114
+ end
115
+
116
+ logger.log "* Listening on tcp://#{addr}"
117
+ end
108
118
  end
109
119
 
110
120
  @listeners << [str, io] if io
@@ -145,61 +155,7 @@ module Puma
145
155
  @listeners << [str, io]
146
156
  when "ssl"
147
157
  params = Util.parse_query uri.query
148
- require 'puma/minissl'
149
-
150
- MiniSSL.check
151
-
152
- ctx = MiniSSL::Context.new
153
-
154
- if defined?(JRUBY_VERSION)
155
- unless params['keystore']
156
- @events.error "Please specify the Java keystore via 'keystore='"
157
- end
158
-
159
- ctx.keystore = params['keystore']
160
-
161
- unless params['keystore-pass']
162
- @events.error "Please specify the Java keystore password via 'keystore-pass='"
163
- end
164
-
165
- ctx.keystore_pass = params['keystore-pass']
166
- ctx.ssl_cipher_list = params['ssl_cipher_list'] if params['ssl_cipher_list']
167
- else
168
- unless params['key']
169
- @events.error "Please specify the SSL key via 'key='"
170
- end
171
-
172
- ctx.key = params['key']
173
-
174
- unless params['cert']
175
- @events.error "Please specify the SSL cert via 'cert='"
176
- end
177
-
178
- ctx.cert = params['cert']
179
-
180
- if ['peer', 'force_peer'].include?(params['verify_mode'])
181
- unless params['ca']
182
- @events.error "Please specify the SSL ca via 'ca='"
183
- end
184
- end
185
-
186
- ctx.ca = params['ca'] if params['ca']
187
- ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
188
- end
189
-
190
- if params['verify_mode']
191
- ctx.verify_mode = case params['verify_mode']
192
- when "peer"
193
- MiniSSL::VERIFY_PEER
194
- when "force_peer"
195
- MiniSSL::VERIFY_PEER | MiniSSL::VERIFY_FAIL_IF_NO_PEER_CERT
196
- when "none"
197
- MiniSSL::VERIFY_NONE
198
- else
199
- @events.error "Please specify a valid verify_mode="
200
- MiniSSL::VERIFY_NONE
201
- end
202
- end
158
+ ctx = MiniSSL::ContextBuilder.new(params, @events).context
203
159
 
204
160
  if fd = @inherited_fds.delete(str)
205
161
  logger.log "* Inherited #{str}"
@@ -349,7 +305,7 @@ module Puma
349
305
  # Tell the server to listen on +path+ as a UNIX domain socket.
350
306
  #
351
307
  def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
352
- @unix_paths << path
308
+ @unix_paths << path unless File.exist? path
353
309
 
354
310
  # Let anyone connect by default
355
311
  umask ||= 0
@@ -387,7 +343,7 @@ module Puma
387
343
  end
388
344
 
389
345
  def inherit_unix_listener(path, fd)
390
- @unix_paths << path
346
+ @unix_paths << path unless File.exist? path
391
347
 
392
348
  if fd.kind_of? TCPServer
393
349
  s = fd
@@ -403,5 +359,27 @@ module Puma
403
359
  s
404
360
  end
405
361
 
362
+ def close_listeners
363
+ @listeners.each do |l, io|
364
+ io.close
365
+ uri = URI.parse(l)
366
+ next unless uri.scheme == 'unix'
367
+ unix_path = "#{uri.host}#{uri.path}"
368
+ File.unlink unix_path if @unix_paths.include? unix_path
369
+ end
370
+ end
371
+
372
+ def close_unix_paths
373
+ @unix_paths.each { |up| File.unlink(up) if File.exist? up }
374
+ end
375
+
376
+ def redirects_for_restart
377
+ redirects = {:close_others => true}
378
+ @listeners.each_with_index do |(l, io), i|
379
+ ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
380
+ redirects[io.to_i] = io.to_i
381
+ end
382
+ redirects
383
+ end
406
384
  end
407
385
  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
+ user_config.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,19 +24,24 @@ 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
- # for example it is passed into `IO.select` inside of the `Puma::Reactor`.
31
- # This is accomplished by the `to_io` method which gets called on any
32
- # non-IO objects being used with the IO api such as `IO.select.
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
+ # `IO::try_convert` (which may call `#to_io`) when a new socket is
33
+ # registered.
33
34
  #
34
35
  # Instances of this class are responsible for knowing if
35
36
  # the header and body are fully buffered via the `try_to_finish` method.
36
37
  # They can be used to "time out" a response via the `timeout_at` reader.
37
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
+
38
43
  include Puma::Const
39
- extend Puma::Delegation
44
+ extend Forwardable
40
45
 
41
46
  def initialize(io, env=nil)
42
47
  @io = io
@@ -54,6 +59,7 @@ module Puma
54
59
  @ready = false
55
60
 
56
61
  @body = nil
62
+ @body_read_start = nil
57
63
  @buffer = nil
58
64
  @tempfile = nil
59
65
 
@@ -64,6 +70,10 @@ module Puma
64
70
 
65
71
  @peerip = nil
66
72
  @remote_addr_header = nil
73
+
74
+ @body_remain = 0
75
+
76
+ @in_last_chunk = false
67
77
  end
68
78
 
69
79
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
@@ -73,7 +83,7 @@ module Puma
73
83
 
74
84
  attr_accessor :remote_addr_header
75
85
 
76
- forward :closed?, :@io
86
+ def_delegators :@io, :closed?
77
87
 
78
88
  def inspect
79
89
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
@@ -102,6 +112,9 @@ module Puma
102
112
  @tempfile = nil
103
113
  @parsed_bytes = 0
104
114
  @ready = false
115
+ @body_remain = 0
116
+ @peerip = nil
117
+ @in_last_chunk = false
105
118
 
106
119
  if @buffer
107
120
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
@@ -114,9 +127,16 @@ module Puma
114
127
  end
115
128
 
116
129
  return false
117
- elsif fast_check &&
118
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
119
- return try_to_finish
130
+ else
131
+ begin
132
+ if fast_check &&
133
+ IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
134
+ return try_to_finish
135
+ end
136
+ rescue IOError
137
+ # swallow it
138
+ end
139
+
120
140
  end
121
141
  end
122
142
 
@@ -128,165 +148,6 @@ module Puma
128
148
  end
129
149
  end
130
150
 
131
- # The object used for a request with no body. All requests with
132
- # no body share this one object since it has no state.
133
- EmptyBody = NullIO.new
134
-
135
- def setup_chunked_body(body)
136
- @chunked_body = true
137
- @partial_part_left = 0
138
- @prev_chunk = ""
139
-
140
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
141
- @body.binmode
142
- @tempfile = @body
143
-
144
- return decode_chunk(body)
145
- end
146
-
147
- def decode_chunk(chunk)
148
- if @partial_part_left > 0
149
- if @partial_part_left <= chunk.size
150
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
151
- chunk = chunk[@partial_part_left..-1]
152
- else
153
- @body << chunk
154
- @partial_part_left -= chunk.size
155
- return false
156
- end
157
- end
158
-
159
- if @prev_chunk.empty?
160
- io = StringIO.new(chunk)
161
- else
162
- io = StringIO.new(@prev_chunk+chunk)
163
- @prev_chunk = ""
164
- end
165
-
166
- while !io.eof?
167
- line = io.gets
168
- if line.end_with?("\r\n")
169
- len = line.strip.to_i(16)
170
- if len == 0
171
- @body.rewind
172
- rest = io.read
173
- rest = rest[2..-1] if rest.start_with?("\r\n")
174
- @buffer = rest.empty? ? nil : rest
175
- @requests_served += 1
176
- @ready = true
177
- return true
178
- end
179
-
180
- len += 2
181
-
182
- part = io.read(len)
183
-
184
- unless part
185
- @partial_part_left = len
186
- next
187
- end
188
-
189
- got = part.size
190
-
191
- case
192
- when got == len
193
- @body << part[0..-3] # to skip the ending \r\n
194
- when got <= len - 2
195
- @body << part
196
- @partial_part_left = len - part.size
197
- when got == len - 1 # edge where we get just \r but not \n
198
- @body << part[0..-2]
199
- @partial_part_left = len - part.size
200
- end
201
- else
202
- @prev_chunk = line
203
- return false
204
- end
205
- end
206
-
207
- return false
208
- end
209
-
210
- def read_chunked_body
211
- while true
212
- begin
213
- chunk = @io.read_nonblock(4096)
214
- rescue Errno::EAGAIN
215
- return false
216
- rescue SystemCallError, IOError
217
- raise ConnectionError, "Connection error detected during read"
218
- end
219
-
220
- # No chunk means a closed socket
221
- unless chunk
222
- @body.close
223
- @buffer = nil
224
- @requests_served += 1
225
- @ready = true
226
- raise EOFError
227
- end
228
-
229
- return true if decode_chunk(chunk)
230
- end
231
- end
232
-
233
- def setup_body
234
- if @env[HTTP_EXPECT] == CONTINUE
235
- # TODO allow a hook here to check the headers before
236
- # going forward
237
- @io << HTTP_11_100
238
- @io.flush
239
- end
240
-
241
- @read_header = false
242
-
243
- body = @parser.body
244
-
245
- te = @env[TRANSFER_ENCODING2]
246
-
247
- if te && CHUNKED.casecmp(te) == 0
248
- return setup_chunked_body(body)
249
- end
250
-
251
- @chunked_body = false
252
-
253
- cl = @env[CONTENT_LENGTH]
254
-
255
- unless cl
256
- @buffer = body.empty? ? nil : body
257
- @body = EmptyBody
258
- @requests_served += 1
259
- @ready = true
260
- return true
261
- end
262
-
263
- remain = cl.to_i - body.bytesize
264
-
265
- if remain <= 0
266
- @body = StringIO.new(body)
267
- @buffer = nil
268
- @requests_served += 1
269
- @ready = true
270
- return true
271
- end
272
-
273
- if remain > MAX_BODY
274
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
275
- @body.binmode
276
- @tempfile = @body
277
- else
278
- # The body[0,0] trick is to get an empty string in the same
279
- # encoding as body.
280
- @body = StringIO.new body[0,0]
281
- end
282
-
283
- @body.write body
284
-
285
- @body_remain = remain
286
-
287
- return false
288
- end
289
-
290
151
  def try_to_finish
291
152
  return read_body unless @read_header
292
153
 
@@ -294,15 +155,14 @@ module Puma
294
155
  data = @io.read_nonblock(CHUNK_SIZE)
295
156
  rescue Errno::EAGAIN
296
157
  return false
297
- rescue SystemCallError, IOError
158
+ rescue SystemCallError, IOError, EOFError
298
159
  raise ConnectionError, "Connection error detected during read"
299
160
  end
300
161
 
301
162
  # No data means a closed socket
302
163
  unless data
303
164
  @buffer = nil
304
- @requests_served += 1
305
- @ready = true
165
+ set_ready
306
166
  raise EOFError
307
167
  end
308
168
 
@@ -338,8 +198,7 @@ module Puma
338
198
  # No data means a closed socket
339
199
  unless data
340
200
  @buffer = nil
341
- @requests_served += 1
342
- @ready = true
201
+ set_ready
343
202
  raise EOFError
344
203
  end
345
204
 
@@ -389,6 +248,84 @@ module Puma
389
248
  true
390
249
  end
391
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
+
392
329
  def read_body
393
330
  if @chunked_body
394
331
  return read_chunked_body
@@ -416,8 +353,7 @@ module Puma
416
353
  unless chunk
417
354
  @body.close
418
355
  @buffer = nil
419
- @requests_served += 1
420
- @ready = true
356
+ set_ready
421
357
  raise EOFError
422
358
  end
423
359
 
@@ -426,8 +362,7 @@ module Puma
426
362
  if remain <= 0
427
363
  @body.rewind
428
364
  @buffer = nil
429
- @requests_served += 1
430
- @ready = true
365
+ set_ready
431
366
  return true
432
367
  end
433
368
 
@@ -436,37 +371,124 @@ module Puma
436
371
  false
437
372
  end
438
373
 
439
- def write_400
440
- begin
441
- @io << ERROR_400_RESPONSE
442
- rescue StandardError
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)
443
393
  end
444
394
  end
445
395
 
446
- def write_408
447
- begin
448
- @io << ERROR_408_RESPONSE
449
- rescue StandardError
450
- 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)
451
406
  end
452
407
 
453
- def write_500
454
- begin
455
- @io << ERROR_500_RESPONSE
456
- 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
457
421
  end
458
- end
459
422
 
460
- def peerip
461
- return @peerip if @peerip
423
+ if @prev_chunk.empty?
424
+ io = StringIO.new(chunk)
425
+ else
426
+ io = StringIO.new(@prev_chunk+chunk)
427
+ @prev_chunk = ""
428
+ end
462
429
 
463
- if @remote_addr_header
464
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
465
- @peerip = hdr
466
- return hdr
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
450
+
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
467
476
  end
468
477
 
469
- @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
470
492
  end
471
493
  end
472
494
  end