puma 4.3.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +1532 -0
  3. data/LICENSE +26 -0
  4. data/README.md +291 -0
  5. data/bin/puma +10 -0
  6. data/bin/puma-wild +31 -0
  7. data/bin/pumactl +12 -0
  8. data/docs/architecture.md +37 -0
  9. data/docs/deployment.md +111 -0
  10. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  11. data/docs/images/puma-connection-flow.png +0 -0
  12. data/docs/images/puma-general-arch.png +0 -0
  13. data/docs/nginx.md +80 -0
  14. data/docs/plugins.md +38 -0
  15. data/docs/restart.md +41 -0
  16. data/docs/signals.md +96 -0
  17. data/docs/systemd.md +290 -0
  18. data/docs/tcp_mode.md +96 -0
  19. data/ext/puma_http11/PumaHttp11Service.java +19 -0
  20. data/ext/puma_http11/ext_help.h +15 -0
  21. data/ext/puma_http11/extconf.rb +28 -0
  22. data/ext/puma_http11/http11_parser.c +1044 -0
  23. data/ext/puma_http11/http11_parser.h +65 -0
  24. data/ext/puma_http11/http11_parser.java.rl +145 -0
  25. data/ext/puma_http11/http11_parser.rl +147 -0
  26. data/ext/puma_http11/http11_parser_common.rl +54 -0
  27. data/ext/puma_http11/io_buffer.c +155 -0
  28. data/ext/puma_http11/mini_ssl.c +553 -0
  29. data/ext/puma_http11/org/jruby/puma/Http11.java +226 -0
  30. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +455 -0
  31. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  32. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +363 -0
  33. data/ext/puma_http11/puma_http11.c +502 -0
  34. data/lib/puma.rb +31 -0
  35. data/lib/puma/accept_nonblock.rb +29 -0
  36. data/lib/puma/app/status.rb +80 -0
  37. data/lib/puma/binder.rb +385 -0
  38. data/lib/puma/cli.rb +239 -0
  39. data/lib/puma/client.rb +494 -0
  40. data/lib/puma/cluster.rb +554 -0
  41. data/lib/puma/commonlogger.rb +108 -0
  42. data/lib/puma/configuration.rb +362 -0
  43. data/lib/puma/const.rb +235 -0
  44. data/lib/puma/control_cli.rb +289 -0
  45. data/lib/puma/detect.rb +15 -0
  46. data/lib/puma/dsl.rb +740 -0
  47. data/lib/puma/events.rb +156 -0
  48. data/lib/puma/io_buffer.rb +4 -0
  49. data/lib/puma/jruby_restart.rb +84 -0
  50. data/lib/puma/launcher.rb +475 -0
  51. data/lib/puma/minissl.rb +278 -0
  52. data/lib/puma/minissl/context_builder.rb +76 -0
  53. data/lib/puma/null_io.rb +44 -0
  54. data/lib/puma/plugin.rb +120 -0
  55. data/lib/puma/plugin/tmp_restart.rb +36 -0
  56. data/lib/puma/rack/builder.rb +301 -0
  57. data/lib/puma/rack/urlmap.rb +93 -0
  58. data/lib/puma/rack_default.rb +9 -0
  59. data/lib/puma/reactor.rb +400 -0
  60. data/lib/puma/runner.rb +192 -0
  61. data/lib/puma/server.rb +1030 -0
  62. data/lib/puma/single.rb +123 -0
  63. data/lib/puma/state_file.rb +31 -0
  64. data/lib/puma/tcp_logger.rb +41 -0
  65. data/lib/puma/thread_pool.rb +328 -0
  66. data/lib/puma/util.rb +124 -0
  67. data/lib/rack/handler/puma.rb +115 -0
  68. data/tools/docker/Dockerfile +16 -0
  69. data/tools/jungle/README.md +19 -0
  70. data/tools/jungle/init.d/README.md +61 -0
  71. data/tools/jungle/init.d/puma +421 -0
  72. data/tools/jungle/init.d/run-puma +18 -0
  73. data/tools/jungle/rc.d/README.md +74 -0
  74. data/tools/jungle/rc.d/puma +61 -0
  75. data/tools/jungle/rc.d/puma.conf +10 -0
  76. data/tools/jungle/upstart/README.md +61 -0
  77. data/tools/jungle/upstart/puma-manager.conf +31 -0
  78. data/tools/jungle/upstart/puma.conf +69 -0
  79. data/tools/trickletest.rb +44 -0
  80. metadata +144 -0
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'uri'
5
+
6
+ require 'puma'
7
+ require 'puma/configuration'
8
+ require 'puma/launcher'
9
+ require 'puma/const'
10
+ require 'puma/events'
11
+
12
+ module Puma
13
+ class << self
14
+ # The CLI exports its Puma::Configuration object here to allow
15
+ # apps to pick it up. An app needs to use it conditionally though
16
+ # since it is not set if the app is launched via another
17
+ # mechanism than the CLI class.
18
+ attr_accessor :cli_config
19
+ end
20
+
21
+ # Handles invoke a Puma::Server in a command line style.
22
+ #
23
+ class CLI
24
+ KEYS_NOT_TO_PERSIST_IN_STATE = Launcher::KEYS_NOT_TO_PERSIST_IN_STATE
25
+
26
+ # Create a new CLI object using +argv+ as the command line
27
+ # arguments.
28
+ #
29
+ # +stdout+ and +stderr+ can be set to IO-like objects which
30
+ # this object will report status on.
31
+ #
32
+ def initialize(argv, events=Events.stdio)
33
+ @debug = false
34
+ @argv = argv.dup
35
+
36
+ @events = events
37
+
38
+ @conf = nil
39
+
40
+ @stdout = nil
41
+ @stderr = nil
42
+ @append = false
43
+
44
+ @control_url = nil
45
+ @control_options = {}
46
+
47
+ setup_options
48
+
49
+ begin
50
+ @parser.parse! @argv
51
+
52
+ if file = @argv.shift
53
+ @conf.configure do |user_config, file_config|
54
+ file_config.rackup file
55
+ end
56
+ end
57
+ rescue UnsupportedOption
58
+ exit 1
59
+ end
60
+
61
+ @conf.configure do |user_config, file_config|
62
+ if @stdout || @stderr
63
+ user_config.stdout_redirect @stdout, @stderr, @append
64
+ end
65
+
66
+ if @control_url
67
+ user_config.activate_control_app @control_url, @control_options
68
+ end
69
+ end
70
+
71
+ @launcher = Puma::Launcher.new(@conf, :events => @events, :argv => argv)
72
+ end
73
+
74
+ attr_reader :launcher
75
+
76
+ # Parse the options, load the rackup, start the server and wait
77
+ # for it to finish.
78
+ #
79
+ def run
80
+ @launcher.run
81
+ end
82
+
83
+ private
84
+ def unsupported(str)
85
+ @events.error(str)
86
+ raise UnsupportedOption
87
+ end
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
+
97
+ # Build the OptionParser object to handle the available options.
98
+ #
99
+
100
+ def setup_options
101
+ @conf = Configuration.new do |user_config, file_config|
102
+ @parser = OptionParser.new do |o|
103
+ o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg|
104
+ user_config.bind arg
105
+ end
106
+
107
+ o.on "-C", "--config PATH", "Load PATH as a config file" do |arg|
108
+ file_config.load arg
109
+ end
110
+
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)
113
+ end
114
+
115
+ # alias --control-url for backwards-compatibility
116
+ o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
117
+ configure_control_url(arg)
118
+ end
119
+
120
+ o.on "--control-token TOKEN",
121
+ "The token to use as authentication for the control server" do |arg|
122
+ @control_options[:auth_token] = arg
123
+ end
124
+
125
+ o.on "-d", "--daemon", "Daemonize the server into the background" do
126
+ user_config.daemonize
127
+ user_config.quiet
128
+ end
129
+
130
+ o.on "--debug", "Log lowlevel debugging information" do
131
+ user_config.debug
132
+ end
133
+
134
+ o.on "--dir DIR", "Change to DIR before starting" do |d|
135
+ user_config.directory d
136
+ end
137
+
138
+ o.on "-e", "--environment ENVIRONMENT",
139
+ "The environment to run the Rack app on (default development)" do |arg|
140
+ user_config.environment arg
141
+ end
142
+
143
+ o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
144
+ $LOAD_PATH.unshift(*arg.split(':'))
145
+ end
146
+
147
+ o.on "-p", "--port PORT", "Define the TCP port to bind to",
148
+ "Use -b for more advanced options" do |arg|
149
+ user_config.bind "tcp://#{Configuration::DefaultTCPHost}:#{arg}"
150
+ end
151
+
152
+ o.on "--pidfile PATH", "Use PATH as a pidfile" do |arg|
153
+ user_config.pidfile arg
154
+ end
155
+
156
+ o.on "--preload", "Preload the app. Cluster mode only" do
157
+ user_config.preload_app!
158
+ end
159
+
160
+ o.on "--prune-bundler", "Prune out the bundler env if possible" do
161
+ user_config.prune_bundler
162
+ end
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
+
168
+ o.on "-q", "--quiet", "Do not log requests internally (default true)" do
169
+ user_config.quiet
170
+ end
171
+
172
+ o.on "-v", "--log-requests", "Log requests as they occur" do
173
+ user_config.log_requests
174
+ end
175
+
176
+ o.on "-R", "--restart-cmd CMD",
177
+ "The puma command to run during a hot restart",
178
+ "Default: inferred" do |cmd|
179
+ user_config.restart_command cmd
180
+ end
181
+
182
+ o.on "-S", "--state PATH", "Where to store the state details" do |arg|
183
+ user_config.state_path arg
184
+ end
185
+
186
+ o.on '-t', '--threads INT', "min:max threads to use (default 0:16)" do |arg|
187
+ min, max = arg.split(":")
188
+ if max
189
+ user_config.threads min, max
190
+ else
191
+ user_config.threads min, min
192
+ end
193
+ end
194
+
195
+ o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do
196
+ user_config.tcp_mode!
197
+ end
198
+
199
+ o.on "--early-hints", "Enable early hints support" do
200
+ user_config.early_hints
201
+ end
202
+
203
+ o.on "-V", "--version", "Print the version information" do
204
+ puts "puma version #{Puma::Const::VERSION}"
205
+ exit 0
206
+ end
207
+
208
+ o.on "-w", "--workers COUNT",
209
+ "Activate cluster mode: How many worker processes to create" do |arg|
210
+ user_config.workers arg
211
+ end
212
+
213
+ o.on "--tag NAME", "Additional text to display in process listing" do |arg|
214
+ user_config.tag arg
215
+ end
216
+
217
+ o.on "--redirect-stdout FILE", "Redirect STDOUT to a specific file" do |arg|
218
+ @stdout = arg.to_s
219
+ end
220
+
221
+ o.on "--redirect-stderr FILE", "Redirect STDERR to a specific file" do |arg|
222
+ @stderr = arg.to_s
223
+ end
224
+
225
+ o.on "--[no-]redirect-append", "Append to redirected files" do |val|
226
+ @append = val
227
+ end
228
+
229
+ o.banner = "puma <options> <rackup file>"
230
+
231
+ o.on_tail "-h", "--help", "Show help" do
232
+ $stdout.puts o
233
+ exit 0
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,494 @@
1
+ # frozen_string_literal: true
2
+
3
+ class IO
4
+ # We need to use this for a jruby work around on both 1.8 and 1.9.
5
+ # So this either creates the constant (on 1.8), or harmlessly
6
+ # reopens it (on 1.9).
7
+ module WaitReadable
8
+ end
9
+ end
10
+
11
+ require 'puma/detect'
12
+ require 'tempfile'
13
+ require 'forwardable'
14
+
15
+ if Puma::IS_JRUBY
16
+ # We have to work around some OpenSSL buffer/io-readiness bugs
17
+ # so we pull it in regardless of if the user is binding
18
+ # to an SSL socket
19
+ require 'openssl'
20
+ end
21
+
22
+ module Puma
23
+
24
+ class ConnectionError < RuntimeError; end
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.
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
+
43
+ include Puma::Const
44
+ extend Forwardable
45
+
46
+ def initialize(io, env=nil)
47
+ @io = io
48
+ @to_io = io.to_io
49
+ @proto_env = env
50
+ if !env
51
+ @env = nil
52
+ else
53
+ @env = env.dup
54
+ end
55
+
56
+ @parser = HttpParser.new
57
+ @parsed_bytes = 0
58
+ @read_header = true
59
+ @ready = false
60
+
61
+ @body = nil
62
+ @body_read_start = nil
63
+ @buffer = nil
64
+ @tempfile = nil
65
+
66
+ @timeout_at = nil
67
+
68
+ @requests_served = 0
69
+ @hijacked = false
70
+
71
+ @peerip = nil
72
+ @remote_addr_header = nil
73
+
74
+ @body_remain = 0
75
+
76
+ @in_last_chunk = false
77
+ end
78
+
79
+ attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
80
+ :tempfile
81
+
82
+ attr_writer :peerip
83
+
84
+ attr_accessor :remote_addr_header
85
+
86
+ def_delegators :@io, :closed?
87
+
88
+ def inspect
89
+ "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
90
+ end
91
+
92
+ # For the hijack protocol (allows us to just put the Client object
93
+ # into the env)
94
+ def call
95
+ @hijacked = true
96
+ env[HIJACK_IO] ||= @io
97
+ end
98
+
99
+ def in_data_phase
100
+ !@read_header
101
+ end
102
+
103
+ def set_timeout(val)
104
+ @timeout_at = Time.now + val
105
+ end
106
+
107
+ def reset(fast_check=true)
108
+ @parser.reset
109
+ @read_header = true
110
+ @env = @proto_env.dup
111
+ @body = nil
112
+ @tempfile = nil
113
+ @parsed_bytes = 0
114
+ @ready = false
115
+ @body_remain = 0
116
+ @peerip = nil
117
+ @in_last_chunk = false
118
+
119
+ if @buffer
120
+ @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
121
+
122
+ if @parser.finished?
123
+ return setup_body
124
+ elsif @parsed_bytes >= MAX_HEADER
125
+ raise HttpParserError,
126
+ "HEADER is longer than allowed, aborting client early."
127
+ end
128
+
129
+ return false
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
+
140
+ end
141
+ end
142
+
143
+ def close
144
+ begin
145
+ @io.close
146
+ rescue IOError
147
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
148
+ end
149
+ end
150
+
151
+ def try_to_finish
152
+ return read_body unless @read_header
153
+
154
+ begin
155
+ data = @io.read_nonblock(CHUNK_SIZE)
156
+ rescue Errno::EAGAIN
157
+ return false
158
+ rescue SystemCallError, IOError, EOFError
159
+ raise ConnectionError, "Connection error detected during read"
160
+ end
161
+
162
+ # No data means a closed socket
163
+ unless data
164
+ @buffer = nil
165
+ set_ready
166
+ raise EOFError
167
+ end
168
+
169
+ if @buffer
170
+ @buffer << data
171
+ else
172
+ @buffer = data
173
+ end
174
+
175
+ @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
176
+
177
+ if @parser.finished?
178
+ return setup_body
179
+ elsif @parsed_bytes >= MAX_HEADER
180
+ raise HttpParserError,
181
+ "HEADER is longer than allowed, aborting client early."
182
+ end
183
+
184
+ false
185
+ end
186
+
187
+ if IS_JRUBY
188
+ def jruby_start_try_to_finish
189
+ return read_body unless @read_header
190
+
191
+ begin
192
+ data = @io.sysread_nonblock(CHUNK_SIZE)
193
+ rescue OpenSSL::SSL::SSLError => e
194
+ return false if e.kind_of? IO::WaitReadable
195
+ raise e
196
+ end
197
+
198
+ # No data means a closed socket
199
+ unless data
200
+ @buffer = nil
201
+ set_ready
202
+ raise EOFError
203
+ end
204
+
205
+ if @buffer
206
+ @buffer << data
207
+ else
208
+ @buffer = data
209
+ end
210
+
211
+ @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
212
+
213
+ if @parser.finished?
214
+ return setup_body
215
+ elsif @parsed_bytes >= MAX_HEADER
216
+ raise HttpParserError,
217
+ "HEADER is longer than allowed, aborting client early."
218
+ end
219
+
220
+ false
221
+ end
222
+
223
+ def eagerly_finish
224
+ return true if @ready
225
+
226
+ if @io.kind_of? OpenSSL::SSL::SSLSocket
227
+ return true if jruby_start_try_to_finish
228
+ end
229
+
230
+ return false unless IO.select([@to_io], nil, nil, 0)
231
+ try_to_finish
232
+ end
233
+
234
+ else
235
+
236
+ def eagerly_finish
237
+ return true if @ready
238
+ return false unless IO.select([@to_io], nil, nil, 0)
239
+ try_to_finish
240
+ end
241
+ end # IS_JRUBY
242
+
243
+ def finish
244
+ return true if @ready
245
+ until try_to_finish
246
+ IO.select([@to_io], nil, nil)
247
+ end
248
+ true
249
+ end
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
+
329
+ def read_body
330
+ if @chunked_body
331
+ return read_chunked_body
332
+ end
333
+
334
+ # Read an odd sized chunk so we can read even sized ones
335
+ # after this
336
+ remain = @body_remain
337
+
338
+ if remain > CHUNK_SIZE
339
+ want = CHUNK_SIZE
340
+ else
341
+ want = remain
342
+ end
343
+
344
+ begin
345
+ chunk = @io.read_nonblock(want)
346
+ rescue Errno::EAGAIN
347
+ return false
348
+ rescue SystemCallError, IOError
349
+ raise ConnectionError, "Connection error detected during read"
350
+ end
351
+
352
+ # No chunk means a closed socket
353
+ unless chunk
354
+ @body.close
355
+ @buffer = nil
356
+ set_ready
357
+ raise EOFError
358
+ end
359
+
360
+ remain -= @body.write(chunk)
361
+
362
+ if remain <= 0
363
+ @body.rewind
364
+ @buffer = nil
365
+ set_ready
366
+ return true
367
+ end
368
+
369
+ @body_remain = remain
370
+
371
+ false
372
+ end
373
+
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)
393
+ end
394
+ end
395
+
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)
406
+ end
407
+
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
421
+ end
422
+
423
+ if @prev_chunk.empty?
424
+ io = StringIO.new(chunk)
425
+ else
426
+ io = StringIO.new(@prev_chunk+chunk)
427
+ @prev_chunk = ""
428
+ end
429
+
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
476
+ end
477
+
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
492
+ end
493
+ end
494
+ end