puma 3.0.0.rc1 → 5.0.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

Files changed (91) hide show
  1. checksums.yaml +5 -5
  2. data/{History.txt → History.md} +703 -70
  3. data/LICENSE +23 -20
  4. data/README.md +173 -163
  5. data/docs/architecture.md +37 -0
  6. data/{DEPLOYMENT.md → docs/deployment.md} +28 -6
  7. data/docs/fork_worker.md +31 -0
  8. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  9. data/docs/images/puma-connection-flow.png +0 -0
  10. data/docs/images/puma-general-arch.png +0 -0
  11. data/docs/jungle/README.md +13 -0
  12. data/docs/jungle/rc.d/README.md +74 -0
  13. data/docs/jungle/rc.d/puma +61 -0
  14. data/docs/jungle/rc.d/puma.conf +10 -0
  15. data/{tools → docs}/jungle/upstart/README.md +0 -0
  16. data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
  17. data/{tools → docs}/jungle/upstart/puma.conf +1 -1
  18. data/docs/nginx.md +2 -2
  19. data/docs/plugins.md +38 -0
  20. data/docs/restart.md +41 -0
  21. data/docs/signals.md +57 -3
  22. data/docs/systemd.md +228 -0
  23. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  24. data/ext/puma_http11/extconf.rb +16 -0
  25. data/ext/puma_http11/http11_parser.c +287 -468
  26. data/ext/puma_http11/http11_parser.h +1 -0
  27. data/ext/puma_http11/http11_parser.java.rl +21 -37
  28. data/ext/puma_http11/http11_parser.rl +10 -9
  29. data/ext/puma_http11/http11_parser_common.rl +4 -4
  30. data/ext/puma_http11/mini_ssl.c +159 -10
  31. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  32. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +99 -132
  33. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +30 -6
  34. data/ext/puma_http11/puma_http11.c +6 -38
  35. data/lib/puma.rb +25 -5
  36. data/lib/puma/accept_nonblock.rb +7 -1
  37. data/lib/puma/app/status.rb +53 -26
  38. data/lib/puma/binder.rb +150 -119
  39. data/lib/puma/cli.rb +56 -38
  40. data/lib/puma/client.rb +277 -80
  41. data/lib/puma/cluster.rb +326 -130
  42. data/lib/puma/commonlogger.rb +21 -20
  43. data/lib/puma/configuration.rb +160 -161
  44. data/lib/puma/const.rb +50 -47
  45. data/lib/puma/control_cli.rb +104 -63
  46. data/lib/puma/detect.rb +13 -1
  47. data/lib/puma/dsl.rb +463 -114
  48. data/lib/puma/events.rb +22 -13
  49. data/lib/puma/io_buffer.rb +9 -5
  50. data/lib/puma/jruby_restart.rb +2 -59
  51. data/lib/puma/launcher.rb +195 -105
  52. data/lib/puma/minissl.rb +110 -4
  53. data/lib/puma/minissl/context_builder.rb +76 -0
  54. data/lib/puma/null_io.rb +9 -14
  55. data/lib/puma/plugin.rb +32 -12
  56. data/lib/puma/plugin/tmp_restart.rb +19 -6
  57. data/lib/puma/rack/builder.rb +7 -5
  58. data/lib/puma/rack/urlmap.rb +11 -8
  59. data/lib/puma/rack_default.rb +2 -0
  60. data/lib/puma/reactor.rb +242 -32
  61. data/lib/puma/runner.rb +41 -30
  62. data/lib/puma/server.rb +265 -183
  63. data/lib/puma/single.rb +22 -63
  64. data/lib/puma/state_file.rb +9 -2
  65. data/lib/puma/thread_pool.rb +179 -68
  66. data/lib/puma/util.rb +3 -11
  67. data/lib/rack/handler/puma.rb +60 -11
  68. data/tools/Dockerfile +16 -0
  69. data/tools/trickletest.rb +1 -2
  70. metadata +35 -99
  71. data/COPYING +0 -55
  72. data/Gemfile +0 -13
  73. data/Manifest.txt +0 -79
  74. data/Rakefile +0 -158
  75. data/docs/config.md +0 -0
  76. data/ext/puma_http11/io_buffer.c +0 -155
  77. data/lib/puma/capistrano.rb +0 -94
  78. data/lib/puma/compat.rb +0 -18
  79. data/lib/puma/convenient.rb +0 -23
  80. data/lib/puma/daemon_ext.rb +0 -31
  81. data/lib/puma/delegation.rb +0 -11
  82. data/lib/puma/java_io_buffer.rb +0 -45
  83. data/lib/puma/rack/backports/uri/common_18.rb +0 -56
  84. data/lib/puma/rack/backports/uri/common_192.rb +0 -52
  85. data/lib/puma/rack/backports/uri/common_193.rb +0 -29
  86. data/lib/puma/tcp_logger.rb +0 -32
  87. data/puma.gemspec +0 -52
  88. data/tools/jungle/README.md +0 -9
  89. data/tools/jungle/init.d/README.md +0 -54
  90. data/tools/jungle/init.d/puma +0 -394
  91. data/tools/jungle/init.d/run-puma +0 -3
@@ -1,7 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'optparse'
2
4
  require 'uri'
3
5
 
6
+ require 'puma'
7
+ require 'puma/configuration'
4
8
  require 'puma/launcher'
9
+ require 'puma/const'
10
+ require 'puma/events'
5
11
 
6
12
  module Puma
7
13
  class << self
@@ -44,21 +50,21 @@ module Puma
44
50
  @parser.parse! @argv
45
51
 
46
52
  if file = @argv.shift
47
- @conf.configure do |c|
48
- c.rackup file
53
+ @conf.configure do |user_config, file_config|
54
+ file_config.rackup file
49
55
  end
50
56
  end
51
57
  rescue UnsupportedOption
52
58
  exit 1
53
59
  end
54
60
 
55
- @conf.configure do |c|
61
+ @conf.configure do |user_config, file_config|
56
62
  if @stdout || @stderr
57
- c.stdout_redirect @stdout, @stderr, @append
63
+ user_config.stdout_redirect @stdout, @stderr, @append
58
64
  end
59
65
 
60
66
  if @control_url
61
- c.activate_control_app @control_url, @control_options
67
+ user_config.activate_control_app @control_url, @control_options
62
68
  end
63
69
  end
64
70
 
@@ -74,33 +80,36 @@ module Puma
74
80
  @launcher.run
75
81
  end
76
82
 
77
- private
83
+ private
78
84
  def unsupported(str)
79
85
  @events.error(str)
80
86
  raise UnsupportedOption
81
87
  end
82
88
 
89
+ def configure_control_url(command_line_arg)
90
+ if command_line_arg
91
+ @control_url = command_line_arg
92
+ elsif Puma.jruby?
93
+ unsupported "No default url available on JRuby"
94
+ end
95
+ end
96
+
83
97
  # Build the OptionParser object to handle the available options.
84
98
  #
85
99
 
86
100
  def setup_options
87
- @conf = Configuration.new do |c|
101
+ @conf = Configuration.new do |user_config, file_config|
88
102
  @parser = OptionParser.new do |o|
89
103
  o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg|
90
- c.bind arg
104
+ user_config.bind arg
91
105
  end
92
106
 
93
107
  o.on "-C", "--config PATH", "Load PATH as a config file" do |arg|
94
- c.load arg
108
+ file_config.load arg
95
109
  end
96
110
 
97
- o.on "--control URL", "The bind url to use for the control server",
98
- "Use 'auto' to use temp unix server" do |arg|
99
- if arg
100
- @control_url = arg
101
- elsif Puma.jruby?
102
- unsupported "No default url available on JRuby"
103
- end
111
+ o.on "--control-url URL", "The bind url to use for the control server. Use 'auto' to use temp unix server" do |arg|
112
+ configure_control_url(arg)
104
113
  end
105
114
 
106
115
  o.on "--control-token TOKEN",
@@ -108,22 +117,23 @@ module Puma
108
117
  @control_options[:auth_token] = arg
109
118
  end
110
119
 
111
- o.on "-d", "--daemon", "Daemonize the server into the background" do
112
- c.daemonize
113
- c.quiet
114
- end
115
-
116
120
  o.on "--debug", "Log lowlevel debugging information" do
117
- c.debug
121
+ user_config.debug
118
122
  end
119
123
 
120
124
  o.on "--dir DIR", "Change to DIR before starting" do |d|
121
- c.directory d
125
+ user_config.directory d
122
126
  end
123
127
 
124
128
  o.on "-e", "--environment ENVIRONMENT",
125
129
  "The environment to run the Rack app on (default development)" do |arg|
126
- c.environment arg
130
+ user_config.environment arg
131
+ end
132
+
133
+ o.on "-f", "--fork-worker=[REQUESTS]", OptionParser::DecimalInteger,
134
+ "Fork new workers from existing worker. Cluster mode only",
135
+ "Auto-refork after REQUESTS (default 1000)" do |*args|
136
+ user_config.fork_worker(*args.compact)
127
137
  end
128
138
 
129
139
  o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
@@ -132,46 +142,54 @@ module Puma
132
142
 
133
143
  o.on "-p", "--port PORT", "Define the TCP port to bind to",
134
144
  "Use -b for more advanced options" do |arg|
135
- c.bind "tcp://#{Configuration::DefaultTCPHost}:#{arg}"
145
+ user_config.bind "tcp://#{Configuration::DefaultTCPHost}:#{arg}"
136
146
  end
137
147
 
138
148
  o.on "--pidfile PATH", "Use PATH as a pidfile" do |arg|
139
- c.pidfile arg
149
+ user_config.pidfile arg
140
150
  end
141
151
 
142
152
  o.on "--preload", "Preload the app. Cluster mode only" do
143
- c.preload_app!
153
+ user_config.preload_app!
144
154
  end
145
155
 
146
156
  o.on "--prune-bundler", "Prune out the bundler env if possible" do
147
- c.prune_bundler
157
+ user_config.prune_bundler
158
+ end
159
+
160
+ o.on "--extra-runtime-dependencies GEM1,GEM2", "Defines any extra needed gems when using --prune-bundler" do |arg|
161
+ user_config.extra_runtime_dependencies arg.split(',')
162
+ end
163
+
164
+ o.on "-q", "--quiet", "Do not log requests internally (default true)" do
165
+ user_config.quiet
148
166
  end
149
167
 
150
- o.on "-q", "--quiet", "Quiet down the output" do
151
- c.quiet
168
+ o.on "-v", "--log-requests", "Log requests as they occur" do
169
+ user_config.log_requests
152
170
  end
153
171
 
154
172
  o.on "-R", "--restart-cmd CMD",
155
173
  "The puma command to run during a hot restart",
156
174
  "Default: inferred" do |cmd|
157
- c.restart_command cmd
175
+ user_config.restart_command cmd
158
176
  end
159
177
 
160
178
  o.on "-S", "--state PATH", "Where to store the state details" do |arg|
161
- c.state_path arg
179
+ user_config.state_path arg
162
180
  end
163
181
 
164
182
  o.on '-t', '--threads INT', "min:max threads to use (default 0:16)" do |arg|
165
183
  min, max = arg.split(":")
166
184
  if max
167
- c.threads min, max
185
+ user_config.threads min, max
168
186
  else
169
- c.threads 0, max
187
+ user_config.threads min, min
170
188
  end
171
189
  end
172
190
 
173
- o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do
174
- c.tcp_mode!
191
+ o.on "--early-hints", "Enable early hints support" do
192
+ user_config.early_hints
175
193
  end
176
194
 
177
195
  o.on "-V", "--version", "Print the version information" do
@@ -181,11 +199,11 @@ module Puma
181
199
 
182
200
  o.on "-w", "--workers COUNT",
183
201
  "Activate cluster mode: How many worker processes to create" do |arg|
184
- c.workers arg
202
+ user_config.workers arg
185
203
  end
186
204
 
187
205
  o.on "--tag NAME", "Additional text to display in process listing" do |arg|
188
- c.tag arg
206
+ user_config.tag arg
189
207
  end
190
208
 
191
209
  o.on "--redirect-stdout FILE", "Redirect STDOUT to a specific file" do |arg|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class IO
2
4
  # We need to use this for a jruby work around on both 1.8 and 1.9.
3
5
  # So this either creates the constant (on 1.8), or harmlessly
@@ -7,6 +9,8 @@ class IO
7
9
  end
8
10
 
9
11
  require 'puma/detect'
12
+ require 'tempfile'
13
+ require 'forwardable'
10
14
 
11
15
  if Puma::IS_JRUBY
12
16
  # We have to work around some OpenSSL buffer/io-readiness bugs
@@ -19,8 +23,25 @@ module Puma
19
23
 
20
24
  class ConnectionError < RuntimeError; end
21
25
 
26
+ # An instance of this class represents a unique request from a client.
27
+ # For example, this could be a web request from a browser or from CURL.
28
+ #
29
+ # An instance of `Puma::Client` can be used as if it were an IO object
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.
34
+ #
35
+ # Instances of this class are responsible for knowing if
36
+ # the header and body are fully buffered via the `try_to_finish` method.
37
+ # They can be used to "time out" a response via the `timeout_at` reader.
22
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
+
23
43
  include Puma::Const
44
+ extend Forwardable
24
45
 
25
46
  def initialize(io, env=nil)
26
47
  @io = io
@@ -38,6 +59,7 @@ module Puma
38
59
  @ready = false
39
60
 
40
61
  @body = nil
62
+ @body_read_start = nil
41
63
  @buffer = nil
42
64
  @tempfile = nil
43
65
 
@@ -48,6 +70,10 @@ module Puma
48
70
 
49
71
  @peerip = nil
50
72
  @remote_addr_header = nil
73
+
74
+ @body_remain = 0
75
+
76
+ @in_last_chunk = false
51
77
  end
52
78
 
53
79
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
@@ -57,6 +83,8 @@ module Puma
57
83
 
58
84
  attr_accessor :remote_addr_header
59
85
 
86
+ def_delegators :@io, :closed?
87
+
60
88
  def inspect
61
89
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
62
90
  end
@@ -84,6 +112,9 @@ module Puma
84
112
  @tempfile = nil
85
113
  @parsed_bytes = 0
86
114
  @ready = false
115
+ @body_remain = 0
116
+ @peerip = nil
117
+ @in_last_chunk = false
87
118
 
88
119
  if @buffer
89
120
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
@@ -96,9 +127,16 @@ module Puma
96
127
  end
97
128
 
98
129
  return false
99
- elsif fast_check &&
100
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
101
- 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
+
102
140
  end
103
141
  end
104
142
 
@@ -106,66 +144,28 @@ module Puma
106
144
  begin
107
145
  @io.close
108
146
  rescue IOError
147
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
109
148
  end
110
149
  end
111
150
 
112
- # The object used for a request with no body. All requests with
113
- # no body share this one object since it has no state.
114
- EmptyBody = NullIO.new
115
-
116
- def setup_body
117
- @in_data_phase = true
118
- body = @parser.body
119
- cl = @env[CONTENT_LENGTH]
120
-
121
- unless cl
122
- @buffer = body.empty? ? nil : body
123
- @body = EmptyBody
124
- @requests_served += 1
125
- @ready = true
126
- return true
127
- end
128
-
129
- remain = cl.to_i - body.bytesize
130
-
131
- if remain <= 0
132
- @body = StringIO.new(body)
133
- @buffer = nil
134
- @requests_served += 1
135
- @ready = true
136
- return true
137
- end
138
-
139
- if remain > MAX_BODY
140
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
141
- @body.binmode
142
- @tempfile = @body
143
- else
144
- # The body[0,0] trick is to get an empty string in the same
145
- # encoding as body.
146
- @body = StringIO.new body[0,0]
147
- end
148
-
149
- @body.write body
150
-
151
- @body_remain = remain
152
-
153
- @read_header = false
154
-
155
- return false
156
- end
157
-
158
151
  def try_to_finish
159
152
  return read_body unless @read_header
160
153
 
161
154
  begin
162
155
  data = @io.read_nonblock(CHUNK_SIZE)
163
- rescue Errno::EAGAIN
156
+ rescue IO::WaitReadable
164
157
  return false
165
- rescue SystemCallError, IOError
158
+ rescue SystemCallError, IOError, EOFError
166
159
  raise ConnectionError, "Connection error detected during read"
167
160
  end
168
161
 
162
+ # No data means a closed socket
163
+ unless data
164
+ @buffer = nil
165
+ set_ready
166
+ raise EOFError
167
+ end
168
+
169
169
  if @buffer
170
170
  @buffer << data
171
171
  else
@@ -180,7 +180,7 @@ module Puma
180
180
  raise HttpParserError,
181
181
  "HEADER is longer than allowed, aborting client early."
182
182
  end
183
-
183
+
184
184
  false
185
185
  end
186
186
 
@@ -195,6 +195,13 @@ module Puma
195
195
  raise e
196
196
  end
197
197
 
198
+ # No data means a closed socket
199
+ unless data
200
+ @buffer = nil
201
+ set_ready
202
+ raise EOFError
203
+ end
204
+
198
205
  if @buffer
199
206
  @buffer << data
200
207
  else
@@ -231,17 +238,122 @@ module Puma
231
238
  return false unless IO.select([@to_io], nil, nil, 0)
232
239
  try_to_finish
233
240
  end
241
+
242
+ # For documentation, see https://github.com/puma/puma/issues/1754
243
+ send(:alias_method, :jruby_eagerly_finish, :eagerly_finish)
234
244
  end # IS_JRUBY
235
245
 
236
- def finish
246
+ def finish(timeout)
237
247
  return true if @ready
238
248
  until try_to_finish
239
- IO.select([@to_io], nil, nil)
249
+ can_read = begin
250
+ IO.select([@to_io], nil, nil, timeout)
251
+ rescue ThreadPool::ForceShutdown
252
+ nil
253
+ end
254
+ unless can_read
255
+ write_error(408) if in_data_phase
256
+ raise ConnectionError
257
+ end
240
258
  end
241
259
  true
242
260
  end
243
261
 
262
+ def write_error(status_code)
263
+ begin
264
+ @io << ERROR_RESPONSE[status_code]
265
+ rescue StandardError
266
+ end
267
+ end
268
+
269
+ def peerip
270
+ return @peerip if @peerip
271
+
272
+ if @remote_addr_header
273
+ hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
274
+ @peerip = hdr
275
+ return hdr
276
+ end
277
+
278
+ @peerip ||= @io.peeraddr.last
279
+ end
280
+
281
+ # Returns true if the persistent connection can be closed immediately
282
+ # without waiting for the configured idle/shutdown timeout.
283
+ def can_close?
284
+ # Allow connection to close if it's received at least one full request
285
+ # and hasn't received any data for a future request.
286
+ #
287
+ # From RFC 2616 section 8.1.4:
288
+ # Servers SHOULD always respond to at least one request per connection,
289
+ # if at all possible.
290
+ @requests_served > 0 && @parsed_bytes == 0
291
+ end
292
+
293
+ private
294
+
295
+ def setup_body
296
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
297
+
298
+ if @env[HTTP_EXPECT] == CONTINUE
299
+ # TODO allow a hook here to check the headers before
300
+ # going forward
301
+ @io << HTTP_11_100
302
+ @io.flush
303
+ end
304
+
305
+ @read_header = false
306
+
307
+ body = @parser.body
308
+
309
+ te = @env[TRANSFER_ENCODING2]
310
+
311
+ if te && CHUNKED.casecmp(te) == 0
312
+ return setup_chunked_body(body)
313
+ end
314
+
315
+ @chunked_body = false
316
+
317
+ cl = @env[CONTENT_LENGTH]
318
+
319
+ unless cl
320
+ @buffer = body.empty? ? nil : body
321
+ @body = EmptyBody
322
+ set_ready
323
+ return true
324
+ end
325
+
326
+ remain = cl.to_i - body.bytesize
327
+
328
+ if remain <= 0
329
+ @body = StringIO.new(body)
330
+ @buffer = nil
331
+ set_ready
332
+ return true
333
+ end
334
+
335
+ if remain > MAX_BODY
336
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
337
+ @body.binmode
338
+ @tempfile = @body
339
+ else
340
+ # The body[0,0] trick is to get an empty string in the same
341
+ # encoding as body.
342
+ @body = StringIO.new body[0,0]
343
+ end
344
+
345
+ @body.write body
346
+
347
+ @body_remain = remain
348
+
349
+ return false
350
+ end
351
+
244
352
  def read_body
353
+ if @chunked_body
354
+ return read_chunked_body
355
+ end
356
+
245
357
  # Read an odd sized chunk so we can read even sized ones
246
358
  # after this
247
359
  remain = @body_remain
@@ -254,7 +366,7 @@ module Puma
254
366
 
255
367
  begin
256
368
  chunk = @io.read_nonblock(want)
257
- rescue Errno::EAGAIN
369
+ rescue IO::WaitReadable
258
370
  return false
259
371
  rescue SystemCallError, IOError
260
372
  raise ConnectionError, "Connection error detected during read"
@@ -264,8 +376,7 @@ module Puma
264
376
  unless chunk
265
377
  @body.close
266
378
  @buffer = nil
267
- @requests_served += 1
268
- @ready = true
379
+ set_ready
269
380
  raise EOFError
270
381
  end
271
382
 
@@ -274,8 +385,7 @@ module Puma
274
385
  if remain <= 0
275
386
  @body.rewind
276
387
  @buffer = nil
277
- @requests_served += 1
278
- @ready = true
388
+ set_ready
279
389
  return true
280
390
  end
281
391
 
@@ -284,37 +394,124 @@ module Puma
284
394
  false
285
395
  end
286
396
 
287
- def write_400
288
- begin
289
- @io << ERROR_400_RESPONSE
290
- rescue StandardError
397
+ def read_chunked_body
398
+ while true
399
+ begin
400
+ chunk = @io.read_nonblock(4096)
401
+ rescue IO::WaitReadable
402
+ return false
403
+ rescue SystemCallError, IOError
404
+ raise ConnectionError, "Connection error detected during read"
405
+ end
406
+
407
+ # No chunk means a closed socket
408
+ unless chunk
409
+ @body.close
410
+ @buffer = nil
411
+ set_ready
412
+ raise EOFError
413
+ end
414
+
415
+ return true if decode_chunk(chunk)
291
416
  end
292
417
  end
293
418
 
294
- def write_408
295
- begin
296
- @io << ERROR_408_RESPONSE
297
- rescue StandardError
298
- end
419
+ def setup_chunked_body(body)
420
+ @chunked_body = true
421
+ @partial_part_left = 0
422
+ @prev_chunk = ""
423
+
424
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
425
+ @body.binmode
426
+ @tempfile = @body
427
+
428
+ return decode_chunk(body)
299
429
  end
300
430
 
301
- def write_500
302
- begin
303
- @io << ERROR_500_RESPONSE
304
- rescue StandardError
431
+ def decode_chunk(chunk)
432
+ if @partial_part_left > 0
433
+ if @partial_part_left <= chunk.size
434
+ if @partial_part_left > 2
435
+ @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
436
+ end
437
+ chunk = chunk[@partial_part_left..-1]
438
+ @partial_part_left = 0
439
+ else
440
+ @body << chunk if @partial_part_left > 2 # don't include the last \r\n
441
+ @partial_part_left -= chunk.size
442
+ return false
443
+ end
305
444
  end
306
- end
307
445
 
308
- def peerip
309
- return @peerip if @peerip
446
+ if @prev_chunk.empty?
447
+ io = StringIO.new(chunk)
448
+ else
449
+ io = StringIO.new(@prev_chunk+chunk)
450
+ @prev_chunk = ""
451
+ end
310
452
 
311
- if @remote_addr_header
312
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
313
- @peerip = hdr
314
- return hdr
453
+ while !io.eof?
454
+ line = io.gets
455
+ if line.end_with?("\r\n")
456
+ len = line.strip.to_i(16)
457
+ if len == 0
458
+ @in_last_chunk = true
459
+ @body.rewind
460
+ rest = io.read
461
+ last_crlf_size = "\r\n".bytesize
462
+ if rest.bytesize < last_crlf_size
463
+ @buffer = nil
464
+ @partial_part_left = last_crlf_size - rest.bytesize
465
+ return false
466
+ else
467
+ @buffer = rest[last_crlf_size..-1]
468
+ @buffer = nil if @buffer.empty?
469
+ set_ready
470
+ return true
471
+ end
472
+ end
473
+
474
+ len += 2
475
+
476
+ part = io.read(len)
477
+
478
+ unless part
479
+ @partial_part_left = len
480
+ next
481
+ end
482
+
483
+ got = part.size
484
+
485
+ case
486
+ when got == len
487
+ @body << part[0..-3] # to skip the ending \r\n
488
+ when got <= len - 2
489
+ @body << part
490
+ @partial_part_left = len - part.size
491
+ when got == len - 1 # edge where we get just \r but not \n
492
+ @body << part[0..-2]
493
+ @partial_part_left = len - part.size
494
+ end
495
+ else
496
+ @prev_chunk = line
497
+ return false
498
+ end
315
499
  end
316
500
 
317
- @peerip ||= @io.peeraddr.last
501
+ if @in_last_chunk
502
+ set_ready
503
+ true
504
+ else
505
+ false
506
+ end
507
+ end
508
+
509
+ def set_ready
510
+ if @body_read_start
511
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
512
+ end
513
+ @requests_served += 1
514
+ @ready = true
318
515
  end
319
516
  end
320
517
  end