puma 4.3.12 → 6.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1729 -521
  3. data/LICENSE +23 -20
  4. data/README.md +169 -45
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +63 -26
  7. data/docs/compile_options.md +55 -0
  8. data/docs/deployment.md +60 -69
  9. data/docs/fork_worker.md +31 -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/jungle/README.md +9 -0
  14. data/{tools → docs}/jungle/rc.d/README.md +1 -1
  15. data/{tools → docs}/jungle/rc.d/puma +2 -2
  16. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  17. data/docs/kubernetes.md +66 -0
  18. data/docs/nginx.md +2 -2
  19. data/docs/plugins.md +15 -15
  20. data/docs/rails_dev_mode.md +28 -0
  21. data/docs/restart.md +46 -23
  22. data/docs/signals.md +13 -11
  23. data/docs/stats.md +142 -0
  24. data/docs/systemd.md +84 -128
  25. data/docs/testing_benchmarks_local_files.md +150 -0
  26. data/docs/testing_test_rackup_ci_files.md +36 -0
  27. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  28. data/ext/puma_http11/ext_help.h +1 -1
  29. data/ext/puma_http11/extconf.rb +49 -12
  30. data/ext/puma_http11/http11_parser.c +46 -48
  31. data/ext/puma_http11/http11_parser.h +2 -2
  32. data/ext/puma_http11/http11_parser.java.rl +3 -3
  33. data/ext/puma_http11/http11_parser.rl +3 -3
  34. data/ext/puma_http11/http11_parser_common.rl +2 -2
  35. data/ext/puma_http11/mini_ssl.c +278 -93
  36. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  37. data/ext/puma_http11/org/jruby/puma/Http11.java +6 -6
  38. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +4 -6
  39. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +241 -96
  40. data/ext/puma_http11/puma_http11.c +46 -57
  41. data/lib/puma/app/status.rb +53 -39
  42. data/lib/puma/binder.rb +237 -121
  43. data/lib/puma/cli.rb +34 -34
  44. data/lib/puma/client.rb +172 -98
  45. data/lib/puma/cluster/worker.rb +180 -0
  46. data/lib/puma/cluster/worker_handle.rb +97 -0
  47. data/lib/puma/cluster.rb +226 -231
  48. data/lib/puma/commonlogger.rb +21 -14
  49. data/lib/puma/configuration.rb +114 -87
  50. data/lib/puma/const.rb +139 -95
  51. data/lib/puma/control_cli.rb +99 -79
  52. data/lib/puma/detect.rb +33 -2
  53. data/lib/puma/dsl.rb +516 -110
  54. data/lib/puma/error_logger.rb +113 -0
  55. data/lib/puma/events.rb +16 -115
  56. data/lib/puma/io_buffer.rb +44 -2
  57. data/lib/puma/jruby_restart.rb +2 -59
  58. data/lib/puma/json_serialization.rb +96 -0
  59. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  60. data/lib/puma/launcher.rb +164 -155
  61. data/lib/puma/log_writer.rb +147 -0
  62. data/lib/puma/minissl/context_builder.rb +36 -19
  63. data/lib/puma/minissl.rb +230 -55
  64. data/lib/puma/null_io.rb +18 -1
  65. data/lib/puma/plugin/systemd.rb +90 -0
  66. data/lib/puma/plugin/tmp_restart.rb +1 -1
  67. data/lib/puma/plugin.rb +3 -12
  68. data/lib/puma/rack/builder.rb +7 -11
  69. data/lib/puma/rack/urlmap.rb +0 -0
  70. data/lib/puma/rack_default.rb +19 -4
  71. data/lib/puma/reactor.rb +93 -368
  72. data/lib/puma/request.rb +671 -0
  73. data/lib/puma/runner.rb +92 -75
  74. data/lib/puma/sd_notify.rb +149 -0
  75. data/lib/puma/server.rb +321 -794
  76. data/lib/puma/single.rb +20 -74
  77. data/lib/puma/state_file.rb +45 -8
  78. data/lib/puma/thread_pool.rb +140 -68
  79. data/lib/puma/util.rb +21 -4
  80. data/lib/puma.rb +54 -7
  81. data/lib/rack/handler/puma.rb +113 -87
  82. data/tools/{docker/Dockerfile → Dockerfile} +1 -1
  83. data/tools/trickletest.rb +0 -0
  84. metadata +33 -24
  85. data/docs/tcp_mode.md +0 -96
  86. data/ext/puma_http11/io_buffer.c +0 -155
  87. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  88. data/lib/puma/accept_nonblock.rb +0 -29
  89. data/lib/puma/tcp_logger.rb +0 -41
  90. data/tools/jungle/README.md +0 -19
  91. data/tools/jungle/init.d/README.md +0 -61
  92. data/tools/jungle/init.d/puma +0 -421
  93. data/tools/jungle/init.d/run-puma +0 -18
  94. data/tools/jungle/upstart/README.md +0 -61
  95. data/tools/jungle/upstart/puma-manager.conf +0 -31
  96. data/tools/jungle/upstart/puma.conf +0 -69
data/lib/puma/cli.rb CHANGED
@@ -3,36 +3,31 @@
3
3
  require 'optparse'
4
4
  require 'uri'
5
5
 
6
- require 'puma'
7
- require 'puma/configuration'
8
- require 'puma/launcher'
9
- require 'puma/const'
10
- require 'puma/events'
6
+ require_relative '../puma'
7
+ require_relative 'configuration'
8
+ require_relative 'launcher'
9
+ require_relative 'const'
10
+ require_relative 'log_writer'
11
11
 
12
12
  module Puma
13
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.
14
+ # The CLI exports a Puma::Configuration instance here to allow
15
+ # apps to pick it up. An app must load this object conditionally
16
+ # because it is not set if the app is launched via any mechanism
17
+ # other than the CLI class.
18
18
  attr_accessor :cli_config
19
19
  end
20
20
 
21
21
  # Handles invoke a Puma::Server in a command line style.
22
22
  #
23
23
  class CLI
24
- KEYS_NOT_TO_PERSIST_IN_STATE = Launcher::KEYS_NOT_TO_PERSIST_IN_STATE
25
-
26
24
  # Create a new CLI object using +argv+ as the command line
27
25
  # arguments.
28
26
  #
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)
27
+ def initialize(argv, log_writer = LogWriter.stdio, events = Events.new)
33
28
  @debug = false
34
29
  @argv = argv.dup
35
-
30
+ @log_writer = log_writer
36
31
  @events = events
37
32
 
38
33
  @conf = nil
@@ -68,7 +63,7 @@ module Puma
68
63
  end
69
64
  end
70
65
 
71
- @launcher = Puma::Launcher.new(@conf, :events => @events, :argv => argv)
66
+ @launcher = Puma::Launcher.new(@conf, :log_writer => @log_writer, :events => @events, :argv => argv)
72
67
  end
73
68
 
74
69
  attr_reader :launcher
@@ -80,9 +75,9 @@ module Puma
80
75
  @launcher.run
81
76
  end
82
77
 
83
- private
78
+ private
84
79
  def unsupported(str)
85
- @events.error(str)
80
+ @log_writer.error(str)
86
81
  raise UnsupportedOption
87
82
  end
88
83
 
@@ -98,22 +93,26 @@ module Puma
98
93
  #
99
94
 
100
95
  def setup_options
101
- @conf = Configuration.new do |user_config, file_config|
96
+ @conf = Configuration.new({}, {events: @events}) do |user_config, file_config|
102
97
  @parser = OptionParser.new do |o|
103
98
  o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg|
104
99
  user_config.bind arg
105
100
  end
106
101
 
102
+ o.on "--bind-to-activated-sockets [only]", "Bind to all activated sockets" do |arg|
103
+ user_config.bind_to_activated_sockets(arg || true)
104
+ end
105
+
107
106
  o.on "-C", "--config PATH", "Load PATH as a config file" do |arg|
108
107
  file_config.load arg
109
108
  end
110
109
 
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)
110
+ # Identical to supplying --config "-", but more semantic
111
+ o.on "--no-config", "Prevent Puma from searching for a config file" do |arg|
112
+ file_config.load "-"
113
113
  end
114
114
 
115
- # alias --control-url for backwards-compatibility
116
- o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
115
+ o.on "--control-url URL", "The bind url to use for the control server. Use 'auto' to use temp unix server" do |arg|
117
116
  configure_control_url(arg)
118
117
  end
119
118
 
@@ -122,11 +121,6 @@ module Puma
122
121
  @control_options[:auth_token] = arg
123
122
  end
124
123
 
125
- o.on "-d", "--daemon", "Daemonize the server into the background" do
126
- user_config.daemonize
127
- user_config.quiet
128
- end
129
-
130
124
  o.on "--debug", "Log lowlevel debugging information" do
131
125
  user_config.debug
132
126
  end
@@ -140,13 +134,19 @@ module Puma
140
134
  user_config.environment arg
141
135
  end
142
136
 
137
+ o.on "-f", "--fork-worker=[REQUESTS]", OptionParser::DecimalInteger,
138
+ "Fork new workers from existing worker. Cluster mode only",
139
+ "Auto-refork after REQUESTS (default 1000)" do |*args|
140
+ user_config.fork_worker(*args.compact)
141
+ end
142
+
143
143
  o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
144
144
  $LOAD_PATH.unshift(*arg.split(':'))
145
145
  end
146
146
 
147
147
  o.on "-p", "--port PORT", "Define the TCP port to bind to",
148
148
  "Use -b for more advanced options" do |arg|
149
- user_config.bind "tcp://#{Configuration::DefaultTCPHost}:#{arg}"
149
+ user_config.bind "tcp://#{Configuration::DEFAULTS[:tcp_host]}:#{arg}"
150
150
  end
151
151
 
152
152
  o.on "--pidfile PATH", "Use PATH as a pidfile" do |arg|
@@ -179,6 +179,10 @@ module Puma
179
179
  user_config.restart_command cmd
180
180
  end
181
181
 
182
+ o.on "-s", "--silent", "Do not log prompt messages other than errors" do
183
+ @log_writer = LogWriter.new(NullIO.new, $stderr)
184
+ end
185
+
182
186
  o.on "-S", "--state PATH", "Where to store the state details" do |arg|
183
187
  user_config.state_path arg
184
188
  end
@@ -192,10 +196,6 @@ module Puma
192
196
  end
193
197
  end
194
198
 
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
199
  o.on "--early-hints", "Enable early hints support" do
200
200
  user_config.early_hints
201
201
  end
data/lib/puma/client.rb CHANGED
@@ -8,7 +8,8 @@ class IO
8
8
  end
9
9
  end
10
10
 
11
- require 'puma/detect'
11
+ require_relative 'detect'
12
+ require_relative 'io_buffer'
12
13
  require 'tempfile'
13
14
  require 'forwardable'
14
15
 
@@ -25,6 +26,9 @@ module Puma
25
26
 
26
27
  class HttpParserError501 < IOError; end
27
28
 
29
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
30
+
31
+
28
32
  # An instance of this class represents a unique request from a client.
29
33
  # For example, this could be a web request from a browser or from CURL.
30
34
  #
@@ -38,14 +42,15 @@ module Puma
38
42
  # the header and body are fully buffered via the `try_to_finish` method.
39
43
  # They can be used to "time out" a response via the `timeout_at` reader.
40
44
  #
41
- class Client
45
+ class Client # :nodoc:
42
46
 
43
47
  # this tests all values but the last, which must be chunked
44
48
  ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
45
49
 
46
50
  # chunked body validation
47
51
  CHUNK_SIZE_INVALID = /[^\h]/.freeze
48
- CHUNK_VALID_ENDING = "\r\n".freeze
52
+ CHUNK_VALID_ENDING = Const::LINE_END
53
+ CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
49
54
 
50
55
  # Content-Length header value validation
51
56
  CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
@@ -62,16 +67,14 @@ module Puma
62
67
  def initialize(io, env=nil)
63
68
  @io = io
64
69
  @to_io = io.to_io
70
+ @io_buffer = IOBuffer.new
65
71
  @proto_env = env
66
- if !env
67
- @env = nil
68
- else
69
- @env = env.dup
70
- end
72
+ @env = env&.dup
71
73
 
72
74
  @parser = HttpParser.new
73
75
  @parsed_bytes = 0
74
76
  @read_header = true
77
+ @read_proxy = false
75
78
  @ready = false
76
79
 
77
80
  @body = nil
@@ -84,23 +87,39 @@ module Puma
84
87
  @requests_served = 0
85
88
  @hijacked = false
86
89
 
90
+ @http_content_length_limit = nil
91
+ @http_content_length_limit_exceeded = false
92
+
87
93
  @peerip = nil
94
+ @peer_family = nil
95
+ @listener = nil
88
96
  @remote_addr_header = nil
97
+ @expect_proxy_proto = false
89
98
 
90
99
  @body_remain = 0
91
100
 
92
101
  @in_last_chunk = false
102
+
103
+ # need unfrozen ASCII-8BIT, +'' is UTF-8
104
+ @read_buffer = String.new # rubocop: disable Performance/UnfreezeString
93
105
  end
94
106
 
95
107
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
96
- :tempfile
108
+ :tempfile, :io_buffer, :http_content_length_limit_exceeded
97
109
 
98
- attr_writer :peerip
110
+ attr_writer :peerip, :http_content_length_limit
99
111
 
100
- attr_accessor :remote_addr_header
112
+ attr_accessor :remote_addr_header, :listener
101
113
 
102
114
  def_delegators :@io, :closed?
103
115
 
116
+ # Test to see if io meets a bare minimum of functioning, @to_io needs to be
117
+ # used for MiniSSL::Socket
118
+ def io_ok?
119
+ @to_io.is_a?(::BasicSocket) && !closed?
120
+ end
121
+
122
+ # @!attribute [r] inspect
104
123
  def inspect
105
124
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
106
125
  end
@@ -112,27 +131,38 @@ module Puma
112
131
  env[HIJACK_IO] ||= @io
113
132
  end
114
133
 
134
+ # @!attribute [r] in_data_phase
115
135
  def in_data_phase
116
- !@read_header
136
+ !(@read_header || @read_proxy)
117
137
  end
118
138
 
119
139
  def set_timeout(val)
120
- @timeout_at = Time.now + val
140
+ @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
141
+ end
142
+
143
+ # Number of seconds until the timeout elapses.
144
+ def timeout
145
+ [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
121
146
  end
122
147
 
123
148
  def reset(fast_check=true)
124
149
  @parser.reset
150
+ @io_buffer.reset
125
151
  @read_header = true
152
+ @read_proxy = !!@expect_proxy_proto
126
153
  @env = @proto_env.dup
127
154
  @body = nil
128
155
  @tempfile = nil
129
156
  @parsed_bytes = 0
130
157
  @ready = false
131
158
  @body_remain = 0
132
- @peerip = nil
159
+ @peerip = nil if @remote_addr_header
133
160
  @in_last_chunk = false
161
+ @http_content_length_limit_exceeded = false
134
162
 
135
163
  if @buffer
164
+ return false unless try_to_parse_proxy_protocol
165
+
136
166
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
137
167
 
138
168
  if @parser.finished?
@@ -145,8 +175,7 @@ module Puma
145
175
  return false
146
176
  else
147
177
  begin
148
- if fast_check &&
149
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
178
+ if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
150
179
  return try_to_finish
151
180
  end
152
181
  rescue IOError
@@ -159,19 +188,56 @@ module Puma
159
188
  def close
160
189
  begin
161
190
  @io.close
162
- rescue IOError
163
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
191
+ rescue IOError, Errno::EBADF
192
+ Puma::Util.purge_interrupt_queue
164
193
  end
165
194
  end
166
195
 
196
+ # If necessary, read the PROXY protocol from the buffer. Returns
197
+ # false if more data is needed.
198
+ def try_to_parse_proxy_protocol
199
+ if @read_proxy
200
+ if @expect_proxy_proto == :v1
201
+ if @buffer.include? "\r\n"
202
+ if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
203
+ if md[1]
204
+ @peerip = md[1].split(" ")[0]
205
+ end
206
+ @buffer = md.post_match
207
+ end
208
+ # if the buffer has a \r\n but doesn't have a PROXY protocol
209
+ # request, this is just HTTP from a non-PROXY client; move on
210
+ @read_proxy = false
211
+ return @buffer.size > 0
212
+ else
213
+ return false
214
+ end
215
+ end
216
+ end
217
+ true
218
+ end
219
+
167
220
  def try_to_finish
168
- return read_body unless @read_header
221
+ if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
222
+ @http_content_length_limit_exceeded = true
223
+ end
224
+
225
+ if @http_content_length_limit_exceeded
226
+ @buffer = nil
227
+ @body = EmptyBody
228
+ set_ready
229
+ return true
230
+ end
231
+
232
+ return read_body if in_data_phase
169
233
 
170
234
  begin
171
235
  data = @io.read_nonblock(CHUNK_SIZE)
172
236
  rescue IO::WaitReadable
173
237
  return false
174
- rescue SystemCallError, IOError, EOFError
238
+ rescue EOFError
239
+ # Swallow error, don't log
240
+ rescue SystemCallError, IOError
175
241
  raise ConnectionError, "Connection error detected during read"
176
242
  end
177
243
 
@@ -188,8 +254,14 @@ module Puma
188
254
  @buffer = data
189
255
  end
190
256
 
257
+ return false unless try_to_parse_proxy_protocol
258
+
191
259
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
192
260
 
261
+ if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
262
+ @http_content_length_limit_exceeded = true
263
+ end
264
+
193
265
  if @parser.finished?
194
266
  return setup_body
195
267
  elsif @parsed_bytes >= MAX_HEADER
@@ -200,68 +272,20 @@ module Puma
200
272
  false
201
273
  end
202
274
 
203
- if IS_JRUBY
204
- def jruby_start_try_to_finish
205
- return read_body unless @read_header
206
-
207
- begin
208
- data = @io.sysread_nonblock(CHUNK_SIZE)
209
- rescue OpenSSL::SSL::SSLError => e
210
- return false if e.kind_of? IO::WaitReadable
211
- raise e
212
- end
213
-
214
- # No data means a closed socket
215
- unless data
216
- @buffer = nil
217
- set_ready
218
- raise EOFError
219
- end
220
-
221
- if @buffer
222
- @buffer << data
223
- else
224
- @buffer = data
225
- end
226
-
227
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
228
-
229
- if @parser.finished?
230
- return setup_body
231
- elsif @parsed_bytes >= MAX_HEADER
232
- raise HttpParserError,
233
- "HEADER is longer than allowed, aborting client early."
234
- end
235
-
236
- false
237
- end
238
-
239
- def eagerly_finish
240
- return true if @ready
241
-
242
- if @io.kind_of? OpenSSL::SSL::SSLSocket
243
- return true if jruby_start_try_to_finish
244
- end
245
-
246
- return false unless IO.select([@to_io], nil, nil, 0)
247
- try_to_finish
248
- end
249
-
250
- else
275
+ def eagerly_finish
276
+ return true if @ready
277
+ return false unless @to_io.wait_readable(0)
278
+ try_to_finish
279
+ end
251
280
 
252
- def eagerly_finish
253
- return true if @ready
254
- return false unless IO.select([@to_io], nil, nil, 0)
255
- try_to_finish
256
- end
257
- end # IS_JRUBY
281
+ def finish(timeout)
282
+ return if @ready
283
+ @to_io.wait_readable(timeout) || timeout! until try_to_finish
284
+ end
258
285
 
259
- def finish
260
- return true if @ready
261
- until try_to_finish
262
- IO.select([@to_io], nil, nil)
263
- end
264
- true
286
+ def timeout!
287
+ write_error(408) if in_data_phase
288
+ raise ConnectionError
265
289
  end
266
290
 
267
291
  def write_error(status_code)
@@ -275,7 +299,7 @@ module Puma
275
299
  return @peerip if @peerip
276
300
 
277
301
  if @remote_addr_header
278
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
302
+ hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
279
303
  @peerip = hdr
280
304
  return hdr
281
305
  end
@@ -283,10 +307,40 @@ module Puma
283
307
  @peerip ||= @io.peeraddr.last
284
308
  end
285
309
 
310
+ def peer_family
311
+ return @peer_family if @peer_family
312
+
313
+ @peer_family ||= begin
314
+ @io.local_address.afamily
315
+ rescue
316
+ Socket::AF_INET
317
+ end
318
+ end
319
+
320
+ # Returns true if the persistent connection can be closed immediately
321
+ # without waiting for the configured idle/shutdown timeout.
322
+ # @version 5.0.0
323
+ #
324
+ def can_close?
325
+ # Allow connection to close if we're not in the middle of parsing a request.
326
+ @parsed_bytes == 0
327
+ end
328
+
329
+ def expect_proxy_proto=(val)
330
+ if val
331
+ if @read_header
332
+ @read_proxy = true
333
+ end
334
+ else
335
+ @read_proxy = false
336
+ end
337
+ @expect_proxy_proto = val
338
+ end
339
+
286
340
  private
287
341
 
288
342
  def setup_body
289
- @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
343
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
290
344
 
291
345
  if @env[HTTP_EXPECT] == CONTINUE
292
346
  # TODO allow a hook here to check the headers before
@@ -329,8 +383,8 @@ module Puma
329
383
  cl = @env[CONTENT_LENGTH]
330
384
 
331
385
  if cl
332
- # cannot contain characters that are not \d
333
- if cl =~ CONTENT_LENGTH_VALUE_INVALID
386
+ # cannot contain characters that are not \d, or be empty
387
+ if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
334
388
  raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
335
389
  end
336
390
  else
@@ -351,6 +405,7 @@ module Puma
351
405
 
352
406
  if remain > MAX_BODY
353
407
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
408
+ @body.unlink
354
409
  @body.binmode
355
410
  @tempfile = @body
356
411
  else
@@ -363,7 +418,7 @@ module Puma
363
418
 
364
419
  @body_remain = remain
365
420
 
366
- return false
421
+ false
367
422
  end
368
423
 
369
424
  def read_body
@@ -382,7 +437,7 @@ module Puma
382
437
  end
383
438
 
384
439
  begin
385
- chunk = @io.read_nonblock(want)
440
+ chunk = @io.read_nonblock(want, @read_buffer)
386
441
  rescue IO::WaitReadable
387
442
  return false
388
443
  rescue SystemCallError, IOError
@@ -414,7 +469,7 @@ module Puma
414
469
  def read_chunked_body
415
470
  while true
416
471
  begin
417
- chunk = @io.read_nonblock(4096)
472
+ chunk = @io.read_nonblock(4096, @read_buffer)
418
473
  rescue IO::WaitReadable
419
474
  return false
420
475
  rescue SystemCallError, IOError
@@ -430,7 +485,7 @@ module Puma
430
485
  end
431
486
 
432
487
  if decode_chunk(chunk)
433
- @env[CONTENT_LENGTH] = @chunked_content_length
488
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
434
489
  return true
435
490
  end
436
491
  end
@@ -442,17 +497,18 @@ module Puma
442
497
  @prev_chunk = ""
443
498
 
444
499
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
500
+ @body.unlink
445
501
  @body.binmode
446
502
  @tempfile = @body
447
-
448
503
  @chunked_content_length = 0
449
504
 
450
505
  if decode_chunk(body)
451
- @env[CONTENT_LENGTH] = @chunked_content_length
506
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
452
507
  return true
453
508
  end
454
509
  end
455
510
 
511
+ # @version 5.0.0
456
512
  def write_chunk(str)
457
513
  @chunked_content_length += @body.write(str)
458
514
  end
@@ -466,7 +522,15 @@ module Puma
466
522
  chunk = chunk[@partial_part_left..-1]
467
523
  @partial_part_left = 0
468
524
  else
469
- write_chunk(chunk) if @partial_part_left > 2 # don't include the last \r\n
525
+ if @partial_part_left > 2
526
+ if @partial_part_left == chunk.size + 1
527
+ # Don't include the last \r
528
+ write_chunk(chunk[0..(@partial_part_left-3)])
529
+ else
530
+ # don't include the last \r\n
531
+ write_chunk(chunk)
532
+ end
533
+ end
470
534
  @partial_part_left -= chunk.size
471
535
  return false
472
536
  end
@@ -481,11 +545,11 @@ module Puma
481
545
 
482
546
  while !io.eof?
483
547
  line = io.gets
484
- if line.end_with?("\r\n")
548
+ if line.end_with?(CHUNK_VALID_ENDING)
485
549
  # Puma doesn't process chunk extensions, but should parse if they're
486
550
  # present, which is the reason for the semicolon regex
487
551
  chunk_hex = line.strip[/\A[^;]+/]
488
- if chunk_hex =~ CHUNK_SIZE_INVALID
552
+ if CHUNK_SIZE_INVALID.match? chunk_hex
489
553
  raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
490
554
  end
491
555
  len = chunk_hex.to_i(16)
@@ -493,13 +557,19 @@ module Puma
493
557
  @in_last_chunk = true
494
558
  @body.rewind
495
559
  rest = io.read
496
- last_crlf_size = "\r\n".bytesize
497
- if rest.bytesize < last_crlf_size
560
+ if rest.bytesize < CHUNK_VALID_ENDING_SIZE
498
561
  @buffer = nil
499
- @partial_part_left = last_crlf_size - rest.bytesize
562
+ @partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
500
563
  return false
501
564
  else
502
- @buffer = rest[last_crlf_size..-1]
565
+ # if the next character is a CRLF, set buffer to everything after that CRLF
566
+ start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
567
+ CHUNK_VALID_ENDING_SIZE
568
+ else # we have started a trailer section, which we do not support. skip it!
569
+ rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
570
+ end
571
+
572
+ @buffer = rest[start_of_rest..-1]
503
573
  @buffer = nil if @buffer.empty?
504
574
  set_ready
505
575
  return true
@@ -548,10 +618,14 @@ module Puma
548
618
 
549
619
  def set_ready
550
620
  if @body_read_start
551
- @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
621
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
552
622
  end
553
623
  @requests_served += 1
554
624
  @ready = true
555
625
  end
626
+
627
+ def above_http_content_limit(value)
628
+ @http_content_length_limit&.< value
629
+ end
556
630
  end
557
631
  end