puma 2.2.0 → 8.0.2

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.
Files changed (136) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +3334 -0
  3. data/LICENSE +23 -20
  4. data/README.md +387 -100
  5. data/bin/puma-wild +25 -0
  6. data/bin/pumactl +1 -1
  7. data/docs/5.0-Upgrade.md +98 -0
  8. data/docs/6.0-Upgrade.md +56 -0
  9. data/docs/7.0-Upgrade.md +52 -0
  10. data/docs/8.0-Upgrade.md +100 -0
  11. data/docs/architecture.md +74 -0
  12. data/docs/compile_options.md +55 -0
  13. data/docs/deployment.md +137 -0
  14. data/docs/fork_worker.md +41 -0
  15. data/docs/grpc.md +62 -0
  16. data/docs/images/favicon.svg +1 -0
  17. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  18. data/docs/images/puma-connection-flow.png +0 -0
  19. data/docs/images/puma-general-arch.png +0 -0
  20. data/docs/images/running-puma.svg +1 -0
  21. data/docs/images/standard-logo.svg +1 -0
  22. data/docs/java_options.md +54 -0
  23. data/docs/jungle/README.md +9 -0
  24. data/docs/jungle/rc.d/README.md +74 -0
  25. data/docs/jungle/rc.d/puma +61 -0
  26. data/docs/jungle/rc.d/puma.conf +10 -0
  27. data/docs/kubernetes.md +73 -0
  28. data/docs/nginx.md +5 -5
  29. data/docs/plugins.md +42 -0
  30. data/docs/rails_dev_mode.md +28 -0
  31. data/docs/restart.md +65 -0
  32. data/docs/signals.md +98 -0
  33. data/docs/stats.md +148 -0
  34. data/docs/systemd.md +253 -0
  35. data/docs/testing_benchmarks_local_files.md +150 -0
  36. data/docs/testing_test_rackup_ci_files.md +36 -0
  37. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  38. data/ext/puma_http11/extconf.rb +59 -2
  39. data/ext/puma_http11/http11_parser.c +319 -487
  40. data/ext/puma_http11/http11_parser.h +15 -13
  41. data/ext/puma_http11/http11_parser.java.rl +64 -94
  42. data/ext/puma_http11/http11_parser.rl +27 -24
  43. data/ext/puma_http11/http11_parser_common.rl +8 -8
  44. data/ext/puma_http11/mini_ssl.c +696 -38
  45. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  46. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  47. data/ext/puma_http11/org/jruby/puma/Http11.java +241 -145
  48. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +174 -221
  49. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +413 -193
  50. data/ext/puma_http11/puma_http11.c +183 -175
  51. data/lib/puma/app/status.rb +77 -29
  52. data/lib/puma/binder.rb +349 -119
  53. data/lib/puma/cli.rb +163 -801
  54. data/lib/puma/client.rb +627 -144
  55. data/lib/puma/client_env.rb +171 -0
  56. data/lib/puma/cluster/worker.rb +183 -0
  57. data/lib/puma/cluster/worker_handle.rb +127 -0
  58. data/lib/puma/cluster.rb +634 -0
  59. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  60. data/lib/puma/commonlogger.rb +115 -0
  61. data/lib/puma/configuration.rb +420 -198
  62. data/lib/puma/const.rb +266 -115
  63. data/lib/puma/control_cli.rb +219 -120
  64. data/lib/puma/detect.rb +56 -2
  65. data/lib/puma/dsl.rb +1562 -0
  66. data/lib/puma/error_logger.rb +115 -0
  67. data/lib/puma/events.rb +44 -60
  68. data/lib/puma/io_buffer.rb +48 -5
  69. data/lib/puma/jruby_restart.rb +2 -51
  70. data/lib/puma/json_serialization.rb +96 -0
  71. data/lib/puma/launcher/bundle_pruner.rb +102 -0
  72. data/lib/puma/launcher.rb +501 -0
  73. data/lib/puma/log_writer.rb +153 -0
  74. data/lib/puma/minissl/context_builder.rb +96 -0
  75. data/lib/puma/minissl.rb +355 -37
  76. data/lib/puma/null_io.rb +79 -12
  77. data/lib/puma/plugin/systemd.rb +90 -0
  78. data/lib/puma/plugin/tmp_restart.rb +36 -0
  79. data/lib/puma/plugin.rb +111 -0
  80. data/lib/puma/rack/builder.rb +297 -0
  81. data/lib/puma/rack/urlmap.rb +93 -0
  82. data/lib/puma/rack_default.rb +21 -4
  83. data/lib/puma/reactor.rb +102 -133
  84. data/lib/puma/response.rb +532 -0
  85. data/lib/puma/runner.rb +211 -0
  86. data/lib/puma/sd_notify.rb +146 -0
  87. data/lib/puma/server.rb +582 -445
  88. data/lib/puma/server_plugin_control.rb +32 -0
  89. data/lib/puma/single.rb +72 -0
  90. data/lib/puma/state_file.rb +69 -0
  91. data/lib/puma/thread_pool.rb +389 -57
  92. data/lib/puma/util.rb +125 -0
  93. data/lib/puma.rb +80 -6
  94. data/lib/rack/handler/puma.rb +125 -47
  95. data/tools/Dockerfile +26 -0
  96. data/tools/trickletest.rb +44 -0
  97. metadata +88 -148
  98. data/COPYING +0 -55
  99. data/Gemfile +0 -10
  100. data/History.txt +0 -302
  101. data/Manifest.txt +0 -60
  102. data/Rakefile +0 -165
  103. data/TODO +0 -5
  104. data/docs/config.md +0 -0
  105. data/ext/puma_http11/ext_help.h +0 -15
  106. data/ext/puma_http11/io_buffer.c +0 -154
  107. data/lib/puma/accept_nonblock.rb +0 -23
  108. data/lib/puma/capistrano.rb +0 -34
  109. data/lib/puma/compat.rb +0 -11
  110. data/lib/puma/daemon_ext.rb +0 -20
  111. data/lib/puma/delegation.rb +0 -11
  112. data/lib/puma/java_io_buffer.rb +0 -45
  113. data/lib/puma/rack_patch.rb +0 -25
  114. data/puma.gemspec +0 -45
  115. data/test/test_app_status.rb +0 -88
  116. data/test/test_cli.rb +0 -171
  117. data/test/test_config.rb +0 -16
  118. data/test/test_http10.rb +0 -27
  119. data/test/test_http11.rb +0 -126
  120. data/test/test_integration.rb +0 -154
  121. data/test/test_iobuffer.rb +0 -38
  122. data/test/test_minissl.rb +0 -25
  123. data/test/test_null_io.rb +0 -31
  124. data/test/test_persistent.rb +0 -238
  125. data/test/test_puma_server.rb +0 -224
  126. data/test/test_rack_handler.rb +0 -10
  127. data/test/test_rack_server.rb +0 -141
  128. data/test/test_thread_pool.rb +0 -146
  129. data/test/test_unix_socket.rb +0 -39
  130. data/test/test_ws.rb +0 -89
  131. data/tools/jungle/init.d/README.md +0 -54
  132. data/tools/jungle/init.d/puma +0 -332
  133. data/tools/jungle/init.d/run-puma +0 -3
  134. data/tools/jungle/upstart/README.md +0 -61
  135. data/tools/jungle/upstart/puma-manager.conf +0 -31
  136. data/tools/jungle/upstart/puma.conf +0 -52
data/lib/puma/client.rb CHANGED
@@ -1,12 +1,9 @@
1
- class IO
2
- # We need to use this for a jruby work around on both 1.8 and 1.9.
3
- # So this either creates the constant (on 1.8), or harmlessly
4
- # reopens it (on 1.9).
5
- module WaitReadable
6
- end
7
- end
1
+ # frozen_string_literal: true
8
2
 
9
- require 'puma/detect'
3
+ require_relative 'detect'
4
+ require_relative 'io_buffer'
5
+ require_relative 'client_env'
6
+ require 'tempfile'
10
7
 
11
8
  if Puma::IS_JRUBY
12
9
  # We have to work around some OpenSSL buffer/io-readiness bugs
@@ -19,255 +16,741 @@ module Puma
19
16
 
20
17
  class ConnectionError < RuntimeError; end
21
18
 
22
- class Client
23
- include Puma::Const
19
+ class HttpParserError501 < IOError; end
20
+
21
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
22
+
23
+
24
+ # An instance of this class wraps a connection/socket.
25
+ # For example, this could be an http request from a browser or from CURL.
26
+ #
27
+ # An instance of `Puma::Client` can be used as if it were an IO object
28
+ # by the reactor. The reactor is expected to call `#to_io`
29
+ # on any non-IO objects it polls. For example, nio4r internally calls
30
+ # `IO::try_convert` (which may call `#to_io`) when a new socket is
31
+ # registered.
32
+ #
33
+ # Instances of this class are responsible for knowing if the request line,
34
+ # headers and body are fully buffered and verified via the `try_to_finish` method.
35
+ # All verification of each request is done in the `Client` object.
36
+ # They can be used to "time out" a response via the `timeout_at` reader.
37
+ #
38
+ # Most of the code for env processing and verification is contained
39
+ # in `Puma::ClientEnv`, which is included.
40
+ #
41
+ class Client # :nodoc:
42
+
43
+ include ClientEnv
44
+
45
+ # this tests all values but the last, which must be chunked
46
+ ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
47
+
48
+ # chunked body validation
49
+ CHUNK_SIZE_INVALID = /[^\h]/.freeze
50
+ CHUNK_VALID_ENDING = Const::LINE_END
51
+ CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
52
+
53
+ # The maximum number of bytes we'll buffer looking for a valid
54
+ # chunk header.
55
+ MAX_CHUNK_HEADER_SIZE = 4096
56
+
57
+ # The maximum amount of excess data the client sends
58
+ # using chunk size extensions before we abort the connection.
59
+ MAX_CHUNK_EXCESS = 16 * 1024
60
+
61
+ # Content-Length header value validation
62
+ CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
63
+
64
+ TE_ERR_MSG = 'Invalid Transfer-Encoding'
65
+
66
+ # See:
67
+ # https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.1.1
68
+ # https://httpwg.org/specs/rfc9112.html#rfc.section.6.1
69
+ STRIP_OWS = /\A[ \t]+|[ \t]+\z/
24
70
 
25
- def initialize(io, env)
71
+ # The object used for a request with no body. All requests with
72
+ # no body share this one object since it has no state.
73
+ EmptyBody = NullIO.new
74
+
75
+ attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
76
+ :tempfile, :io_buffer, :http_content_length_limit_exceeded,
77
+ :requests_served, :error_status_code
78
+
79
+ attr_writer :peerip, :http_content_length_limit, :supported_http_methods
80
+
81
+ attr_accessor :remote_addr_header, :listener, :env_set_http_version
82
+
83
+ def initialize(io, env=nil)
26
84
  @io = io
27
85
  @to_io = io.to_io
86
+ @io_buffer = IOBuffer.new
28
87
  @proto_env = env
29
- @env = env.dup
88
+ @env = env&.dup
30
89
 
31
90
  @parser = HttpParser.new
32
91
  @parsed_bytes = 0
33
92
  @read_header = true
93
+ @read_proxy = false
34
94
  @ready = false
35
95
 
36
96
  @body = nil
97
+ @body_read_start = nil
37
98
  @buffer = nil
99
+ @tempfile = nil
38
100
 
39
101
  @timeout_at = nil
40
102
 
41
103
  @requests_served = 0
42
104
  @hijacked = false
105
+
106
+ @http_content_length_limit = nil
107
+ @http_content_length_limit_exceeded = nil
108
+ @error_status_code = nil
109
+
110
+ @peerip = nil
111
+ @peer_family = nil
112
+ @listener = nil
113
+ @remote_addr_header = nil
114
+ @expect_proxy_proto = false
115
+
116
+ @body_remain = 0
117
+
118
+ @in_last_chunk = false
119
+
120
+ # need unfrozen ASCII-8BIT, +'' is UTF-8
121
+ @read_buffer = String.new # rubocop: disable Performance/UnfreezeString
43
122
  end
44
123
 
45
- attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked
124
+ # Remove in Puma 7?
125
+ def closed?
126
+ @to_io.closed?
127
+ end
128
+
129
+ # Test to see if io meets a bare minimum of functioning, @to_io needs to be
130
+ # used for MiniSSL::Socket
131
+ def io_ok?
132
+ @to_io.is_a?(::BasicSocket) && !closed?
133
+ end
46
134
 
135
+ # @!attribute [r] inspect
47
136
  def inspect
48
137
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
49
138
  end
50
139
 
51
- # For the hijack protocol (allows us to just put the Client object
52
- # into the env)
53
- def call
140
+ # For the full hijack protocol, `env['rack.hijack']` is set to
141
+ # `client.method :full_hijack`
142
+ def full_hijack
54
143
  @hijacked = true
55
144
  env[HIJACK_IO] ||= @io
56
145
  end
57
146
 
147
+ # @!attribute [r] in_data_phase
148
+ def in_data_phase
149
+ !(@read_header || @read_proxy)
150
+ end
151
+
58
152
  def set_timeout(val)
59
- @timeout_at = Time.now + val
153
+ @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
154
+ end
155
+
156
+ # Number of seconds until the timeout elapses.
157
+ # @!attribute [r] timeout
158
+ def timeout
159
+ [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
60
160
  end
61
161
 
62
- def reset(fast_check=true)
162
+ def reset
63
163
  @parser.reset
164
+ @io_buffer.reset
64
165
  @read_header = true
166
+ @read_proxy = !!@expect_proxy_proto && @requests_served.zero?
65
167
  @env = @proto_env.dup
66
- @body = nil
67
168
  @parsed_bytes = 0
68
169
  @ready = false
170
+ @body_remain = 0
171
+ @peerip = nil if @remote_addr_header
172
+ @in_last_chunk = false
173
+ @http_content_length_limit_exceeded = nil
174
+ @error_status_code = nil
175
+ end
69
176
 
177
+ # only used with back-to-back requests contained in the buffer
178
+ def process_back_to_back_requests
70
179
  if @buffer
71
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
180
+ return false unless try_to_parse_proxy_protocol
72
181
 
73
- if @parser.finished?
74
- return setup_body
75
- elsif @parsed_bytes >= MAX_HEADER
76
- raise HttpParserError,
77
- "HEADER is longer than allowed, aborting client early."
78
- end
182
+ @parsed_bytes = parser_execute
79
183
 
80
- return false
81
- elsif fast_check &&
82
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
83
- return try_to_finish
184
+ @parser.finished? ? process_env_body : false
84
185
  end
85
186
  end
86
187
 
188
+ # if a client sends back-to-back requests, the buffer may contain one or more
189
+ # of them.
190
+ def has_back_to_back_requests?
191
+ !(@buffer.nil? || @buffer.empty?)
192
+ end
193
+
87
194
  def close
195
+ tempfile_close
88
196
  begin
89
197
  @io.close
90
- rescue IOError
198
+ rescue IOError, Errno::EBADF
91
199
  end
92
200
  end
93
201
 
94
- # The object used for a request with no body. All requests with
95
- # no body share this one object since it has no state.
96
- EmptyBody = NullIO.new
97
-
98
- def setup_body
99
- body = @parser.body
100
- cl = @env[CONTENT_LENGTH]
101
-
102
- unless cl
103
- @buffer = body.empty? ? nil : body
104
- @body = EmptyBody
105
- @requests_served += 1
106
- @ready = true
107
- return true
108
- end
109
-
110
- remain = cl.to_i - body.bytesize
111
-
112
- if remain <= 0
113
- @body = StringIO.new(body)
114
- @buffer = nil
115
- @requests_served += 1
116
- @ready = true
117
- return true
118
- end
202
+ def tempfile_close
203
+ tf_path = @tempfile&.path
204
+ @tempfile&.close
205
+ File.unlink(tf_path) if tf_path
206
+ @tempfile = nil
207
+ @body = nil
208
+ rescue Errno::ENOENT, IOError
209
+ end
119
210
 
120
- if remain > MAX_BODY
121
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
122
- @body.binmode
123
- else
124
- # The body[0,0] trick is to get an empty string in the same
125
- # encoding as body.
126
- @body = StringIO.new body[0,0]
211
+ # If necessary, read the PROXY protocol from the buffer. Returns
212
+ # false if more data is needed.
213
+ def try_to_parse_proxy_protocol
214
+ if @read_proxy
215
+ if @expect_proxy_proto == :v1
216
+ crlf_index = @buffer.index "\r\n"
217
+
218
+ unless crlf_index
219
+ if "PROXY ".start_with? @buffer
220
+ return false
221
+ elsif @buffer.start_with? "PROXY "
222
+ if @buffer.bytesize >= PROXY_PROTOCOL_V1_MAX_LENGTH
223
+ raise ConnectionError, "PROXY protocol v1 line is too long"
224
+ end
225
+ return false
226
+ end
227
+
228
+ @read_proxy = false
229
+ return true
230
+ end
231
+
232
+ if @buffer.start_with?("PROXY ") && crlf_index + 2 > PROXY_PROTOCOL_V1_MAX_LENGTH
233
+ raise ConnectionError, "PROXY protocol v1 line is too long"
234
+ end
235
+
236
+ if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
237
+ if md[1]
238
+ @peerip = md[1].split(" ")[0]
239
+ end
240
+ @buffer = md.post_match
241
+ end
242
+ # if the buffer has a \r\n but doesn't have a PROXY protocol
243
+ # request, this is just HTTP from a non-PROXY client; move on
244
+ @read_proxy = false
245
+ return @buffer.size > 0
246
+ end
127
247
  end
128
-
129
- @body.write body
130
-
131
- @body_remain = remain
132
-
133
- @read_header = false
134
-
135
- return false
248
+ true
136
249
  end
137
250
 
138
251
  def try_to_finish
139
- return read_body unless @read_header
252
+ return read_body if in_data_phase
140
253
 
254
+ data = nil
141
255
  begin
142
256
  data = @io.read_nonblock(CHUNK_SIZE)
143
- rescue Errno::EAGAIN
257
+ rescue IO::WaitReadable
144
258
  return false
259
+ rescue EOFError
260
+ # Swallow error, don't log
145
261
  rescue SystemCallError, IOError
146
262
  raise ConnectionError, "Connection error detected during read"
147
263
  end
148
264
 
265
+ # No data means a closed socket
266
+ unless data
267
+ @buffer = nil
268
+ set_ready
269
+ raise EOFError
270
+ end
271
+
149
272
  if @buffer
150
273
  @buffer << data
151
274
  else
152
275
  @buffer = data
153
276
  end
154
277
 
155
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
278
+ return false unless try_to_parse_proxy_protocol
279
+
280
+ @parsed_bytes = parser_execute
281
+
282
+ @parser.finished? ? process_env_body : false
283
+ end
156
284
 
157
- if @parser.finished?
158
- return setup_body
159
- elsif @parsed_bytes >= MAX_HEADER
160
- raise HttpParserError,
161
- "HEADER is longer than allowed, aborting client early."
285
+ def eagerly_finish
286
+ return true if @ready
287
+ while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
288
+ return true if try_to_finish
162
289
  end
163
-
164
290
  false
165
291
  end
166
292
 
167
- if IS_JRUBY
168
- def jruby_start_try_to_finish
169
- return read_body unless @read_header
293
+ def finish(timeout)
294
+ return if @ready
295
+ @to_io.wait_readable(timeout) || timeout! until try_to_finish
296
+ end
170
297
 
171
- begin
172
- data = @io.sysread_nonblock(CHUNK_SIZE)
173
- rescue OpenSSL::SSL::SSLError => e
174
- return false if e.kind_of? IO::WaitReadable
175
- raise e
176
- end
298
+ # Wraps `@parser.execute` and adds meaningful error messages
299
+ # @return [Integer] bytes of buffer read by parser
300
+ #
301
+ def parser_execute
302
+ ret = @parser.execute(@env, @buffer, @parsed_bytes)
177
303
 
178
- if @buffer
179
- @buffer << data
304
+ if @env[REQUEST_METHOD] && @supported_http_methods != :any && !@supported_http_methods.key?(@env[REQUEST_METHOD])
305
+ raise HttpParserError501, "#{@env[REQUEST_METHOD]} method is not supported"
306
+ end
307
+ ret
308
+ rescue => e
309
+ @env[HTTP_CONNECTION] = 'close'
310
+ raise e unless HttpParserError === e && e.message.include?('non-SSL')
311
+
312
+ req, _ = @buffer.split "\r\n\r\n"
313
+ request_line, headers = req.split "\r\n", 2
314
+
315
+ # below checks for request issues and changes error message accordingly
316
+ if !@env.key? REQUEST_METHOD
317
+ if request_line.count(' ') != 2
318
+ # maybe this is an SSL connection ?
319
+ raise e
180
320
  else
181
- @buffer = data
321
+ method = request_line[/\A[^ ]+/]
322
+ raise e, "Invalid HTTP format, parsing fails. Bad method #{method}"
182
323
  end
324
+ elsif !@env.key? REQUEST_PATH
325
+ path = request_line[/\A[^ ]+ +([^ ?\r\n]+)/, 1]
326
+ raise e, "Invalid HTTP format, parsing fails. Bad path #{path}"
327
+ elsif request_line.match?(/\A[^ ]+ +[^ ?\r\n]+\?/) && !@env.key?(QUERY_STRING)
328
+ query = request_line[/\A[^ ]+ +[^? ]+\?([^ ]+)/, 1]
329
+ raise e, "Invalid HTTP format, parsing fails. Bad query #{query}"
330
+ elsif !@env.key? SERVER_PROTOCOL
331
+ # protocol is bad
332
+ text = request_line[/[^ ]*\z/]
333
+ raise HttpParserError, "Invalid HTTP format, parsing fails. Bad protocol #{text}"
334
+ elsif !headers.empty?
335
+ # headers are bad
336
+ hdrs = headers.split("\r\n").map { |h| h.gsub "\n", '\n'}.join "\n"
337
+ raise HttpParserError, "Invalid HTTP format, parsing fails. Bad headers\n#{hdrs}"
338
+ end
339
+ end
340
+
341
+ # processes the `env` and the request body
342
+ def process_env_body
343
+ temp = setup_body
344
+ normalize_env
345
+ req_env_post_parse
346
+ raise HttpParserError if @error_status_code
347
+ temp
348
+ end
349
+
350
+ def timeout!
351
+ write_error(408) if in_data_phase
352
+ raise ConnectionError
353
+ end
354
+
355
+ def write_error(status_code)
356
+ begin
357
+ @io << ERROR_RESPONSE[status_code]
358
+ rescue StandardError
359
+ end
360
+ end
361
+
362
+ def peerip
363
+ return @peerip if @peerip
364
+
365
+ if @remote_addr_header
366
+ hdr = (@env[@remote_addr_header] || socket_peerip).split(/[\s,]/).first
367
+ @peerip = hdr
368
+ return hdr
369
+ end
370
+
371
+ @peerip ||= socket_peerip
372
+ end
373
+
374
+ def peer_family
375
+ return @peer_family if @peer_family
376
+
377
+ @peer_family ||= begin
378
+ @io.local_address.afamily
379
+ rescue
380
+ Socket::AF_INET
381
+ end
382
+ end
183
383
 
184
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
384
+ # Returns true if the persistent connection can be closed immediately
385
+ # without waiting for the configured idle/shutdown timeout.
386
+ # @version 5.0.0
387
+ #
388
+ def can_close?
389
+ # Allow connection to close if we're not in the middle of parsing a request.
390
+ @parsed_bytes == 0
391
+ end
185
392
 
186
- if @parser.finished?
187
- return setup_body
188
- elsif @parsed_bytes >= MAX_HEADER
189
- raise HttpParserError,
190
- "HEADER is longer than allowed, aborting client early."
393
+ def expect_proxy_proto=(val)
394
+ if val
395
+ if @read_header
396
+ @read_proxy = true
191
397
  end
398
+ else
399
+ @read_proxy = false
400
+ end
401
+ @expect_proxy_proto = val
402
+ end
192
403
 
193
- false
404
+ private
405
+
406
+ IPV4_MAPPED_IPV6_PREFIX = "::ffff:"
407
+ private_constant :IPV4_MAPPED_IPV6_PREFIX
408
+
409
+ def socket_peerip
410
+ unmap_ipv6(@io.peeraddr.last)
411
+ end
412
+
413
+ # Converts IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1) back to
414
+ # their IPv4 form. These addresses appear when IPv4 clients connect to
415
+ # a dual-stack IPv6 socket.
416
+ def unmap_ipv6(addr)
417
+ addr.delete_prefix(IPV4_MAPPED_IPV6_PREFIX)
418
+ end
419
+
420
+ # Checks the request `Transfer-Encoding` and/or `Content-Length` to see if
421
+ # they are valid. Raises errors if not, otherwise reads the body.
422
+ # @return [Boolean] true if the body can be completely read, false otherwise
423
+ #
424
+ def setup_body
425
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
426
+
427
+ if @env[HTTP_EXPECT] == CONTINUE
428
+ # TODO allow a hook here to check the headers before going forward
429
+ @io << HTTP_11_100
430
+ @io.flush
194
431
  end
195
432
 
196
- def eagerly_finish
197
- return true if @ready
433
+ @read_header = false
198
434
 
199
- if @io.kind_of? OpenSSL::SSL::SSLSocket
200
- return true if jruby_start_try_to_finish
435
+ parser_body = @parser.body
436
+
437
+ te = @env[TRANSFER_ENCODING2]
438
+ if te
439
+ te_lwr = te.downcase
440
+ if te.include? ','
441
+ te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
442
+ te_count = te_ary.count CHUNKED
443
+ te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
444
+ if te_count > 1
445
+ raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
446
+ elsif te_ary.last != CHUNKED
447
+ raise HttpParserError , "#{TE_ERR_MSG}, last value must be chunked: '#{te}'"
448
+ elsif !te_valid
449
+ raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
450
+ end
451
+ @env.delete TRANSFER_ENCODING2
452
+ return setup_chunked_body parser_body
453
+ elsif te_lwr == CHUNKED
454
+ @env.delete TRANSFER_ENCODING2
455
+ return setup_chunked_body parser_body
456
+ elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
457
+ raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
458
+ else
459
+ raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
201
460
  end
461
+ end
462
+
463
+ @chunked_body = false
464
+
465
+ cl = @env[CONTENT_LENGTH]
202
466
 
203
- return false unless IO.select([@to_io], nil, nil, 0)
204
- try_to_finish
467
+ if cl
468
+ # cannot contain characters that are not \d, or be empty
469
+ if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
470
+ @error_status_code = 400
471
+ @env[HTTP_CONNECTION] = 'close'
472
+ raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
473
+ end
474
+ else
475
+ @buffer = parser_body.empty? ? nil : parser_body
476
+ @body = EmptyBody
477
+ set_ready
478
+ return true
205
479
  end
206
480
 
207
- else
481
+ content_length = cl.to_i
482
+
483
+ raise_above_http_content_limit if @http_content_length_limit&.< content_length
208
484
 
209
- def eagerly_finish
210
- return true if @ready
211
- return false unless IO.select([@to_io], nil, nil, 0)
212
- try_to_finish
485
+ remain = content_length - parser_body.bytesize
486
+
487
+ if remain <= 0
488
+ # Part of the parser_body is a pipelined request OR garbage. We'll deal with that later.
489
+ if content_length == 0
490
+ @body = EmptyBody
491
+ if parser_body.empty?
492
+ @buffer = nil
493
+ else
494
+ @buffer = parser_body
495
+ end
496
+ elsif remain == 0
497
+ @body = StringIO.new parser_body
498
+ @buffer = nil
499
+ else
500
+ @body = StringIO.new(parser_body[0,content_length])
501
+ @buffer = parser_body[content_length..-1]
502
+ end
503
+ set_ready
504
+ return true
505
+ end
506
+
507
+ if remain > MAX_BODY
508
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
509
+ File.unlink @body.path unless IS_WINDOWS
510
+ @body.binmode
511
+ @tempfile = @body
512
+ else
513
+ # The parser_body[0,0] trick is to get an empty string in the same
514
+ # encoding as parser_body.
515
+ @body = StringIO.new parser_body[0,0]
213
516
  end
214
- end # IS_JRUBY
517
+
518
+ @body.write parser_body
519
+
520
+ @body_remain = remain
521
+
522
+ false
523
+ end
215
524
 
216
525
  def read_body
526
+ if @chunked_body
527
+ return read_chunked_body
528
+ end
529
+
217
530
  # Read an odd sized chunk so we can read even sized ones
218
531
  # after this
219
532
  remain = @body_remain
220
533
 
221
- if remain > CHUNK_SIZE
222
- want = CHUNK_SIZE
223
- else
224
- want = remain
225
- end
534
+ # don't bother with reading zero bytes
535
+ unless remain.zero?
536
+ begin
537
+ chunk = @io.read_nonblock(remain.clamp(0, CHUNK_SIZE), @read_buffer)
538
+ rescue IO::WaitReadable
539
+ return false
540
+ rescue SystemCallError, IOError
541
+ raise ConnectionError, "Connection error detected during read"
542
+ end
226
543
 
227
- begin
228
- chunk = @io.read_nonblock(want)
229
- rescue Errno::EAGAIN
230
- return false
231
- rescue SystemCallError, IOError
232
- raise ConnectionError, "Connection error detected during read"
233
- end
544
+ # No chunk means a closed socket
545
+ unless chunk
546
+ @body.close
547
+ @buffer = nil
548
+ set_ready
549
+ raise EOFError
550
+ end
234
551
 
235
- # No chunk means a closed socket
236
- unless chunk
237
- @body.close
238
- @buffer = nil
239
- @requests_served += 1
240
- @ready = true
241
- raise EOFError
552
+ remain -= @body.write(chunk)
242
553
  end
243
554
 
244
- remain -= @body.write(chunk)
245
-
246
555
  if remain <= 0
247
556
  @body.rewind
248
557
  @buffer = nil
249
- @requests_served += 1
250
- @ready = true
558
+ set_ready
559
+ true
560
+ else
561
+ @body_remain = remain
562
+ false
563
+ end
564
+ end
565
+
566
+ def read_chunked_body
567
+ while true
568
+ begin
569
+ chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
570
+ rescue IO::WaitReadable
571
+ return false
572
+ rescue SystemCallError, IOError
573
+ raise ConnectionError, "Connection error detected during read"
574
+ end
575
+
576
+ # No chunk means a closed socket
577
+ unless chunk
578
+ @body.close
579
+ @buffer = nil
580
+ set_ready
581
+ raise EOFError
582
+ end
583
+
584
+ if decode_chunk(chunk)
585
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
586
+ return true
587
+ end
588
+ end
589
+ end
590
+
591
+ def setup_chunked_body(body)
592
+ @chunked_body = true
593
+ @partial_part_left = 0
594
+ @prev_chunk = ""
595
+ @excess_cr = 0
596
+
597
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
598
+ File.unlink @body.path unless IS_WINDOWS
599
+ @body.binmode
600
+ @tempfile = @body
601
+ @chunked_content_length = 0
602
+
603
+ if decode_chunk(body)
604
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
251
605
  return true
252
606
  end
607
+ end
253
608
 
254
- @body_remain = remain
609
+ # @version 5.0.0
610
+ def write_chunk(str)
611
+ if @http_content_length_limit&.< @chunked_content_length + str.bytesize
612
+ raise_above_http_content_limit
613
+ end
255
614
 
256
- false
615
+ @chunked_content_length += @body.write(str)
257
616
  end
258
617
 
259
- def write_400
260
- begin
261
- @io << ERROR_400_RESPONSE
262
- rescue StandardError
618
+ def decode_chunk(chunk)
619
+ if @partial_part_left > 0
620
+ if @partial_part_left <= chunk.size
621
+ if @partial_part_left > 2
622
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
623
+ end
624
+ chunk = chunk[@partial_part_left..-1]
625
+ @partial_part_left = 0
626
+ else
627
+ if @partial_part_left > 2
628
+ if @partial_part_left == chunk.size + 1
629
+ # Don't include the last \r
630
+ write_chunk(chunk[0..(@partial_part_left-3)])
631
+ else
632
+ # don't include the last \r\n
633
+ write_chunk(chunk)
634
+ end
635
+ end
636
+ @partial_part_left -= chunk.size
637
+ return false
638
+ end
639
+ end
640
+
641
+ if @prev_chunk.empty?
642
+ io = StringIO.new(chunk)
643
+ else
644
+ io = StringIO.new(@prev_chunk+chunk)
645
+ @prev_chunk = ""
646
+ end
647
+
648
+ while !io.eof?
649
+ line = io.gets
650
+ if line.end_with?(CHUNK_VALID_ENDING)
651
+ # Puma doesn't process chunk extensions, but should parse if they're
652
+ # present, which is the reason for the semicolon regex
653
+ chunk_hex = line.strip[/\A[^;]+/]
654
+ if CHUNK_SIZE_INVALID.match? chunk_hex
655
+ raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
656
+ end
657
+ len = chunk_hex.to_i(16)
658
+ if len == 0
659
+ @in_last_chunk = true
660
+ @body.rewind
661
+ rest = io.read
662
+ if rest.bytesize < CHUNK_VALID_ENDING_SIZE
663
+ @buffer = nil
664
+ @partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
665
+ return false
666
+ else
667
+ # if the next character is a CRLF, set buffer to everything after that CRLF
668
+ start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
669
+ CHUNK_VALID_ENDING_SIZE
670
+ else # we have started a trailer section, which we do not support. skip it!
671
+ rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
672
+ end
673
+
674
+ @buffer = rest[start_of_rest..-1]
675
+ @buffer = nil if @buffer.empty?
676
+ set_ready
677
+ return true
678
+ end
679
+ end
680
+
681
+ # Track the excess as a function of the size of the
682
+ # header vs the size of the actual data. Excess can
683
+ # go negative (and is expected to) when the body is
684
+ # significant.
685
+ # The additional of chunk_hex.size and 2 compensates
686
+ # for a client sending 1 byte in a chunked body over
687
+ # a long period of time, making sure that that client
688
+ # isn't accidentally eventually punished.
689
+ @excess_cr += (line.size - len - chunk_hex.size - 2)
690
+
691
+ if @excess_cr >= MAX_CHUNK_EXCESS
692
+ raise HttpParserError, "Maximum chunk excess detected"
693
+ end
694
+
695
+ len += 2
696
+
697
+ part = io.read(len)
698
+
699
+ unless part
700
+ @partial_part_left = len
701
+ next
702
+ end
703
+
704
+ got = part.size
705
+
706
+ case
707
+ when got == len
708
+ # proper chunked segment must end with "\r\n"
709
+ if part.end_with? CHUNK_VALID_ENDING
710
+ write_chunk(part[0..-3]) # to skip the ending \r\n
711
+ else
712
+ raise HttpParserError, "Chunk size mismatch"
713
+ end
714
+ when got <= len - 2
715
+ write_chunk(part)
716
+ @partial_part_left = len - part.size
717
+ when got == len - 1 # edge where we get just \r but not \n
718
+ write_chunk(part[0..-2])
719
+ @partial_part_left = len - part.size
720
+ end
721
+ else
722
+ if @prev_chunk.size + line.size >= MAX_CHUNK_HEADER_SIZE
723
+ raise HttpParserError, "maximum size of chunk header exceeded"
724
+ end
725
+
726
+ @prev_chunk = line
727
+ return false
728
+ end
729
+ end
730
+
731
+ if @in_last_chunk
732
+ set_ready
733
+ true
734
+ else
735
+ false
263
736
  end
264
737
  end
265
738
 
266
- def write_500
267
- begin
268
- @io << ERROR_500_RESPONSE
269
- rescue StandardError
739
+ def set_ready
740
+ if @body_read_start
741
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
270
742
  end
743
+ @requests_served += 1
744
+ @ready = true
745
+ end
746
+
747
+ def raise_above_http_content_limit
748
+ @http_content_length_limit_exceeded = true
749
+ @buffer = nil
750
+ @body = EmptyBody
751
+ @error_status_code = 413
752
+ @env[HTTP_CONNECTION] = 'close'
753
+ raise HttpParserError, "Payload Too Large"
271
754
  end
272
755
  end
273
756
  end