gitlab-puma 4.3.1.gitlab.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +1537 -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 +242 -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 +1053 -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 +348 -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 +147 -0
data/lib/puma/cli.rb ADDED
@@ -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