puma 5.2.2 → 6.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +483 -4
  3. data/README.md +101 -20
  4. data/bin/puma-wild +1 -1
  5. data/docs/architecture.md +50 -16
  6. data/docs/compile_options.md +38 -2
  7. data/docs/deployment.md +53 -67
  8. data/docs/fork_worker.md +1 -3
  9. data/docs/jungle/rc.d/README.md +1 -1
  10. data/docs/kubernetes.md +1 -1
  11. data/docs/nginx.md +1 -1
  12. data/docs/plugins.md +15 -15
  13. data/docs/rails_dev_mode.md +2 -3
  14. data/docs/restart.md +7 -7
  15. data/docs/signals.md +11 -10
  16. data/docs/stats.md +8 -8
  17. data/docs/systemd.md +65 -69
  18. data/docs/testing_benchmarks_local_files.md +150 -0
  19. data/docs/testing_test_rackup_ci_files.md +36 -0
  20. data/ext/puma_http11/extconf.rb +44 -13
  21. data/ext/puma_http11/http11_parser.c +24 -11
  22. data/ext/puma_http11/http11_parser.h +2 -2
  23. data/ext/puma_http11/http11_parser.java.rl +2 -2
  24. data/ext/puma_http11/http11_parser.rl +2 -2
  25. data/ext/puma_http11/http11_parser_common.rl +3 -3
  26. data/ext/puma_http11/mini_ssl.c +150 -23
  27. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  28. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +50 -48
  29. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +188 -102
  30. data/ext/puma_http11/puma_http11.c +18 -10
  31. data/lib/puma/app/status.rb +10 -7
  32. data/lib/puma/binder.rb +112 -62
  33. data/lib/puma/cli.rb +24 -20
  34. data/lib/puma/client.rb +162 -36
  35. data/lib/puma/cluster/worker.rb +31 -27
  36. data/lib/puma/cluster/worker_handle.rb +12 -1
  37. data/lib/puma/cluster.rb +102 -61
  38. data/lib/puma/commonlogger.rb +21 -14
  39. data/lib/puma/configuration.rb +78 -54
  40. data/lib/puma/const.rb +135 -97
  41. data/lib/puma/control_cli.rb +25 -20
  42. data/lib/puma/detect.rb +12 -2
  43. data/lib/puma/dsl.rb +308 -58
  44. data/lib/puma/error_logger.rb +20 -11
  45. data/lib/puma/events.rb +6 -126
  46. data/lib/puma/io_buffer.rb +39 -4
  47. data/lib/puma/jruby_restart.rb +2 -1
  48. data/lib/puma/{json.rb → json_serialization.rb} +1 -1
  49. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  50. data/lib/puma/launcher.rb +114 -173
  51. data/lib/puma/log_writer.rb +147 -0
  52. data/lib/puma/minissl/context_builder.rb +30 -16
  53. data/lib/puma/minissl.rb +132 -38
  54. data/lib/puma/null_io.rb +5 -0
  55. data/lib/puma/plugin/systemd.rb +90 -0
  56. data/lib/puma/plugin/tmp_restart.rb +1 -1
  57. data/lib/puma/plugin.rb +2 -2
  58. data/lib/puma/rack/builder.rb +7 -7
  59. data/lib/puma/rack_default.rb +19 -4
  60. data/lib/puma/reactor.rb +19 -10
  61. data/lib/puma/request.rb +373 -153
  62. data/lib/puma/runner.rb +74 -28
  63. data/lib/puma/sd_notify.rb +149 -0
  64. data/lib/puma/server.rb +127 -136
  65. data/lib/puma/single.rb +13 -11
  66. data/lib/puma/state_file.rb +39 -7
  67. data/lib/puma/thread_pool.rb +33 -26
  68. data/lib/puma/util.rb +20 -15
  69. data/lib/puma.rb +28 -11
  70. data/lib/rack/handler/puma.rb +113 -86
  71. data/tools/Dockerfile +1 -1
  72. metadata +15 -10
  73. data/lib/puma/queue_close.rb +0 -26
  74. data/lib/puma/systemd.rb +0 -46
data/lib/puma/request.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Puma
4
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
5
+
4
6
 
5
7
  # The methods here are included in Server, but are separated into this file.
6
8
  # All the methods here pertain to passing the request to the app, then
@@ -10,7 +12,24 @@ module Puma
10
12
  # #handle_request, which is called in Server#process_client.
11
13
  # @version 5.0.3
12
14
  #
13
- module Request
15
+ module Request # :nodoc:
16
+
17
+ # Single element array body: smaller bodies are written to io_buffer first,
18
+ # then a single write from io_buffer. Larger sizes are written separately.
19
+ # Also fixes max size of chunked file body read.
20
+ BODY_LEN_MAX = 1_024 * 256
21
+
22
+ # File body: smaller bodies are combined with io_buffer, then written to
23
+ # socket. Larger bodies are written separately using `copy_stream`
24
+ IO_BODY_MAX = 1_024 * 64
25
+
26
+ # Array body: elements are collected in io_buffer. When io_buffer's size
27
+ # exceeds value, they are written to the socket.
28
+ IO_BUFFER_LEN_MAX = 1_024 * 512
29
+
30
+ SOCKET_WRITE_ERR_MSG = "Socket timeout writing data"
31
+
32
+ CUSTOM_STAT = 'CUSTOM'
14
33
 
15
34
  include Puma::Const
16
35
 
@@ -25,39 +44,44 @@ module Puma
25
44
  #
26
45
  # Finally, it'll return +true+ on keep-alive connections.
27
46
  # @param client [Puma::Client]
28
- # @param lines [Puma::IOBuffer]
47
+ # @param requests [Integer]
29
48
  # @return [Boolean,:async]
30
49
  #
31
- def handle_request(client, lines)
50
+ def handle_request(client, requests)
32
51
  env = client.env
33
- io = client.io # io may be a MiniSSL::Socket
52
+ io_buffer = client.io_buffer
53
+ socket = client.io # io may be a MiniSSL::Socket
54
+ app_body = nil
55
+
34
56
 
35
- return false if closed_socket?(io)
57
+ return false if closed_socket?(socket)
58
+
59
+ if client.http_content_length_limit_exceeded
60
+ return prepare_response(413, {}, ["Payload Too Large"], requests, client)
61
+ end
36
62
 
37
63
  normalize_env env, client
38
64
 
39
- env[PUMA_SOCKET] = io
65
+ env[PUMA_SOCKET] = socket
40
66
 
41
- if env[HTTPS_KEY] && io.peercert
42
- env[PUMA_PEERCERT] = io.peercert
67
+ if env[HTTPS_KEY] && socket.peercert
68
+ env[PUMA_PEERCERT] = socket.peercert
43
69
  end
44
70
 
45
71
  env[HIJACK_P] = true
46
72
  env[HIJACK] = client
47
73
 
48
- body = client.body
49
-
50
- head = env[REQUEST_METHOD] == HEAD
51
-
52
- env[RACK_INPUT] = body
53
- env[RACK_URL_SCHEME] = default_server_port(env) == PORT_443 ? HTTPS : HTTP
74
+ env[RACK_INPUT] = client.body
75
+ env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP
54
76
 
55
77
  if @early_hints
56
78
  env[EARLY_HINTS] = lambda { |headers|
57
79
  begin
58
- fast_write io, str_early_hints(headers)
80
+ unless (str = str_early_hints headers).empty?
81
+ fast_write_str socket, "HTTP/1.1 103 Early Hints\r\n#{str}\r\n"
82
+ end
59
83
  rescue ConnectionError => e
60
- @events.debug_error e
84
+ @log_writer.debug_error e
61
85
  # noop, if we lost the socket we just won't send the early hints
62
86
  end
63
87
  }
@@ -68,113 +92,174 @@ module Puma
68
92
  # A rack extension. If the app writes #call'ables to this
69
93
  # array, we will invoke them when the request is done.
70
94
  #
71
- after_reply = env[RACK_AFTER_REPLY] = []
95
+ env[RACK_AFTER_REPLY] ||= []
72
96
 
73
97
  begin
74
- begin
75
- status, headers, res_body = @thread_pool.with_force_shutdown do
98
+ if @supported_http_methods == :any || @supported_http_methods.key?(env[REQUEST_METHOD])
99
+ status, headers, app_body = @thread_pool.with_force_shutdown do
76
100
  @app.call(env)
77
101
  end
102
+ else
103
+ @log_writer.log "Unsupported HTTP method used: #{env[REQUEST_METHOD]}"
104
+ status, headers, app_body = [501, {}, ["#{env[REQUEST_METHOD]} method is not supported"]]
105
+ end
78
106
 
79
- return :async if client.hijacked
107
+ # app_body needs to always be closed, hold value in case lowlevel_error
108
+ # is called
109
+ res_body = app_body
80
110
 
81
- status = status.to_i
111
+ # full hijack, app called env['rack.hijack']
112
+ return :async if client.hijacked
82
113
 
83
- if status == -1
84
- unless headers.empty? and res_body == []
85
- raise "async response must have empty headers and body"
86
- end
114
+ status = status.to_i
87
115
 
88
- return :async
116
+ if status == -1
117
+ unless headers.empty? and res_body == []
118
+ raise "async response must have empty headers and body"
89
119
  end
90
- rescue ThreadPool::ForceShutdown => e
91
- @events.unknown_error e, client, "Rack app"
92
- @events.log "Detected force shutdown of a thread"
93
120
 
94
- status, headers, res_body = lowlevel_error(e, env, 503)
95
- rescue Exception => e
96
- @events.unknown_error e, client, "Rack app"
97
-
98
- status, headers, res_body = lowlevel_error(e, env, 500)
121
+ return :async
99
122
  end
123
+ rescue ThreadPool::ForceShutdown => e
124
+ @log_writer.unknown_error e, client, "Rack app"
125
+ @log_writer.log "Detected force shutdown of a thread"
100
126
 
101
- res_info = {}
102
- res_info[:content_length] = nil
103
- res_info[:no_body] = head
127
+ status, headers, res_body = lowlevel_error(e, env, 503)
128
+ rescue Exception => e
129
+ @log_writer.unknown_error e, client, "Rack app"
130
+
131
+ status, headers, res_body = lowlevel_error(e, env, 500)
132
+ end
133
+ prepare_response(status, headers, res_body, requests, client)
134
+ ensure
135
+ io_buffer.reset
136
+ uncork_socket client.io
137
+ app_body.close if app_body.respond_to? :close
138
+ client.tempfile&.unlink
139
+ after_reply = env[RACK_AFTER_REPLY] || []
140
+ begin
141
+ after_reply.each { |o| o.call }
142
+ rescue StandardError => e
143
+ @log_writer.debug_error e
144
+ end unless after_reply.empty?
145
+ end
104
146
 
105
- res_info[:content_length] = if res_body.kind_of? Array and res_body.size == 1
106
- res_body[0].bytesize
147
+ # Assembles the headers and prepares the body for actually sending the
148
+ # response via `#fast_write_response`.
149
+ #
150
+ # @param status [Integer] the status returned by the Rack application
151
+ # @param headers [Hash] the headers returned by the Rack application
152
+ # @param res_body [Array] the body returned by the Rack application or
153
+ # a call to `Server#lowlevel_error`
154
+ # @param requests [Integer] number of inline requests handled
155
+ # @param client [Puma::Client]
156
+ # @return [Boolean,:async] keep-alive status or `:async`
157
+ def prepare_response(status, headers, res_body, requests, client)
158
+ env = client.env
159
+ socket = client.io
160
+ io_buffer = client.io_buffer
161
+
162
+ return false if closed_socket?(socket)
163
+
164
+ # Close the connection after a reasonable number of inline requests
165
+ # if the server is at capacity and the listener has a new connection ready.
166
+ # This allows Puma to service connections fairly when the number
167
+ # of concurrent connections exceeds the size of the threadpool.
168
+ force_keep_alive = requests < @max_fast_inline ||
169
+ @thread_pool.busy_threads < @max_threads ||
170
+ !client.listener.to_io.wait_readable(0)
171
+
172
+ resp_info = str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
173
+
174
+ close_body = false
175
+ response_hijack = nil
176
+ content_length = resp_info[:content_length]
177
+ keep_alive = resp_info[:keep_alive]
178
+
179
+ if res_body.respond_to?(:each) && !resp_info[:response_hijack]
180
+ # below converts app_body into body, dependent on app_body's characteristics, and
181
+ # content_length will be set if it can be determined
182
+ if !content_length && !resp_info[:transfer_encoding] && status != 204
183
+ if res_body.respond_to?(:to_ary) && (array_body = res_body.to_ary) &&
184
+ array_body.is_a?(Array)
185
+ body = array_body.compact
186
+ content_length = body.sum(&:bytesize)
187
+ elsif res_body.is_a?(File) && res_body.respond_to?(:size)
188
+ body = res_body
189
+ content_length = body.size
190
+ elsif res_body.respond_to?(:to_path) && File.readable?(fn = res_body.to_path)
191
+ body = File.open fn, 'rb'
192
+ content_length = body.size
193
+ close_body = true
194
+ else
195
+ body = res_body
196
+ end
197
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:to_path) &&
198
+ File.readable?(fn = res_body.to_path)
199
+ body = File.open fn, 'rb'
200
+ content_length = body.size
201
+ close_body = true
202
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:filename) &&
203
+ res_body.respond_to?(:bytesize) && File.readable?(fn = res_body.filename)
204
+ # Sprockets::Asset
205
+ content_length = res_body.bytesize unless content_length
206
+ if (body_str = res_body.to_hash[:source])
207
+ body = [body_str]
208
+ else # avoid each and use a File object
209
+ body = File.open fn, 'rb'
210
+ close_body = true
211
+ end
107
212
  else
108
- nil
213
+ body = res_body
109
214
  end
215
+ else
216
+ # partial hijack, from Rack spec:
217
+ # Servers must ignore the body part of the response tuple when the
218
+ # rack.hijack response header is present.
219
+ response_hijack = resp_info[:response_hijack] || res_body
220
+ end
110
221
 
111
- cork_socket io
112
-
113
- str_headers(env, status, headers, res_info, lines)
114
-
115
- line_ending = LINE_END
222
+ line_ending = LINE_END
116
223
 
117
- content_length = res_info[:content_length]
118
- response_hijack = res_info[:response_hijack]
224
+ cork_socket socket
119
225
 
120
- if res_info[:no_body]
121
- if content_length and status != 204
122
- lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
226
+ if resp_info[:no_body]
227
+ # 101 (Switching Protocols) doesn't return here or have content_length,
228
+ # it should be using `response_hijack`
229
+ unless status == 101
230
+ if content_length && status != 204
231
+ io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
123
232
  end
124
233
 
125
- lines << LINE_END
126
- fast_write io, lines.to_s
127
- return res_info[:keep_alive]
234
+ io_buffer << LINE_END
235
+ fast_write_str socket, io_buffer.read_and_reset
236
+ socket.flush
237
+ return keep_alive
128
238
  end
129
-
239
+ else
130
240
  if content_length
131
- lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
241
+ io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
132
242
  chunked = false
133
- elsif !response_hijack and res_info[:allow_chunked]
134
- lines << TRANSFER_ENCODING_CHUNKED
243
+ elsif !response_hijack && resp_info[:allow_chunked]
244
+ io_buffer << TRANSFER_ENCODING_CHUNKED
135
245
  chunked = true
136
246
  end
247
+ end
137
248
 
138
- lines << line_ending
139
-
140
- fast_write io, lines.to_s
141
-
142
- if response_hijack
143
- response_hijack.call io
144
- return :async
145
- end
146
-
147
- begin
148
- res_body.each do |part|
149
- next if part.bytesize.zero?
150
- if chunked
151
- str = part.bytesize.to_s(16) << line_ending << part << line_ending
152
- fast_write io, str
153
- else
154
- fast_write io, part
155
- end
156
- io.flush
157
- end
158
-
159
- if chunked
160
- fast_write io, CLOSE_CHUNKED
161
- io.flush
162
- end
163
- rescue SystemCallError, IOError
164
- raise ConnectionError, "Connection error detected during write"
165
- end
166
-
167
- ensure
168
- uncork_socket io
169
-
170
- body.close
171
- client.tempfile.unlink if client.tempfile
172
- res_body.close if res_body.respond_to? :close
249
+ io_buffer << line_ending
173
250
 
174
- after_reply.each { |o| o.call }
251
+ # partial hijack, we write headers, then hand the socket to the app via
252
+ # response_hijack.call
253
+ if response_hijack
254
+ fast_write_str socket, io_buffer.read_and_reset
255
+ uncork_socket socket
256
+ response_hijack.call socket
257
+ return :async
175
258
  end
176
259
 
177
- return res_info[:keep_alive]
260
+ fast_write_response socket, body, io_buffer, chunked, content_length.to_i
261
+ body.close if close_body
262
+ keep_alive
178
263
  end
179
264
 
180
265
  # @param env [Hash] see Puma::Client#env, from request
@@ -188,49 +273,140 @@ module Puma
188
273
  end
189
274
  end
190
275
 
191
- # Writes to an io (normally Client#io) using #syswrite
192
- # @param io [#syswrite] the io to write to
276
+ # Used to write 'early hints', 'no body' responses, 'hijacked' responses,
277
+ # and body segments (called by `fast_write_response`).
278
+ # Writes a string to a socket (normally `Client#io`) using `write_nonblock`.
279
+ # Large strings may not be written in one pass, especially if `io` is a
280
+ # `MiniSSL::Socket`.
281
+ # @param socket [#write_nonblock] the request/response socket
193
282
  # @param str [String] the string written to the io
194
283
  # @raise [ConnectionError]
195
284
  #
196
- def fast_write(io, str)
285
+ def fast_write_str(socket, str)
197
286
  n = 0
198
- while true
287
+ byte_size = str.bytesize
288
+ while n < byte_size
199
289
  begin
200
- n = io.syswrite str
290
+ n += socket.write_nonblock(n.zero? ? str : str.byteslice(n..-1))
201
291
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
202
- if !IO.select(nil, [io], nil, WRITE_TIMEOUT)
203
- raise ConnectionError, "Socket timeout writing data"
292
+ unless socket.wait_writable WRITE_TIMEOUT
293
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
204
294
  end
205
-
206
295
  retry
207
296
  rescue Errno::EPIPE, SystemCallError, IOError
208
- raise ConnectionError, "Socket timeout writing data"
297
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
209
298
  end
210
-
211
- return if n == str.bytesize
212
- str = str.byteslice(n..-1)
213
299
  end
214
300
  end
215
- private :fast_write
216
301
 
217
- # @param status [Integer] status from the app
218
- # @return [String] the text description from Puma::HTTP_STATUS_CODES
302
+ # Used to write headers and body.
303
+ # Writes to a socket (normally `Client#io`) using `#fast_write_str`.
304
+ # Accumulates `body` items into `io_buffer`, then writes to socket.
305
+ # @param socket [#write] the response socket
306
+ # @param body [Enumerable, File] the body object
307
+ # @param io_buffer [Puma::IOBuffer] contains headers
308
+ # @param chunked [Boolean]
309
+ # @paramn content_length [Integer
310
+ # @raise [ConnectionError]
219
311
  #
220
- def fetch_status_code(status)
221
- HTTP_STATUS_CODES.fetch(status) { 'CUSTOM' }
312
+ def fast_write_response(socket, body, io_buffer, chunked, content_length)
313
+ if body.is_a?(::File) && body.respond_to?(:read)
314
+ if chunked # would this ever happen?
315
+ while chunk = body.read(BODY_LEN_MAX)
316
+ io_buffer.append chunk.bytesize.to_s(16), LINE_END, chunk, LINE_END
317
+ end
318
+ fast_write_str socket, CLOSE_CHUNKED
319
+ else
320
+ if content_length <= IO_BODY_MAX
321
+ io_buffer.write body.read(content_length)
322
+ fast_write_str socket, io_buffer.read_and_reset
323
+ else
324
+ fast_write_str socket, io_buffer.read_and_reset
325
+ IO.copy_stream body, socket
326
+ end
327
+ end
328
+ elsif body.is_a?(::Array) && body.length == 1
329
+ body_first = nil
330
+ # using body_first = body.first causes issues?
331
+ body.each { |str| body_first ||= str }
332
+
333
+ if body_first.is_a?(::String) && body_first.bytesize < BODY_LEN_MAX
334
+ # smaller body, write to io_buffer first
335
+ io_buffer.write body_first
336
+ fast_write_str socket, io_buffer.read_and_reset
337
+ else
338
+ # large body, write both header & body to socket
339
+ fast_write_str socket, io_buffer.read_and_reset
340
+ fast_write_str socket, body_first
341
+ end
342
+ elsif body.is_a?(::Array)
343
+ # for array bodies, flush io_buffer to socket when size is greater than
344
+ # IO_BUFFER_LEN_MAX
345
+ if chunked
346
+ body.each do |part|
347
+ next if (byte_size = part.bytesize).zero?
348
+ io_buffer.append byte_size.to_s(16), LINE_END, part, LINE_END
349
+ if io_buffer.length > IO_BUFFER_LEN_MAX
350
+ fast_write_str socket, io_buffer.read_and_reset
351
+ end
352
+ end
353
+ io_buffer.write CLOSE_CHUNKED
354
+ else
355
+ body.each do |part|
356
+ next if part.bytesize.zero?
357
+ io_buffer.write part
358
+ if io_buffer.length > IO_BUFFER_LEN_MAX
359
+ fast_write_str socket, io_buffer.read_and_reset
360
+ end
361
+ end
362
+ end
363
+ # may write last body part for non-chunked, also headers if array is empty
364
+ fast_write_str(socket, io_buffer.read_and_reset) unless io_buffer.length.zero?
365
+ else
366
+ # for enum bodies
367
+ if chunked
368
+ empty_body = true
369
+ body.each do |part|
370
+ next if part.nil? || (byte_size = part.bytesize).zero?
371
+ empty_body = false
372
+ io_buffer.append byte_size.to_s(16), LINE_END, part, LINE_END
373
+ fast_write_str socket, io_buffer.read_and_reset
374
+ end
375
+ if empty_body
376
+ io_buffer << CLOSE_CHUNKED
377
+ fast_write_str socket, io_buffer.read_and_reset
378
+ else
379
+ fast_write_str socket, CLOSE_CHUNKED
380
+ end
381
+ else
382
+ fast_write_str socket, io_buffer.read_and_reset
383
+ body.each do |part|
384
+ next if part.bytesize.zero?
385
+ fast_write_str socket, part
386
+ end
387
+ end
388
+ end
389
+ socket.flush
390
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
391
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
392
+ rescue Errno::EPIPE, SystemCallError, IOError
393
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
222
394
  end
223
- private :fetch_status_code
395
+
396
+ private :fast_write_str, :fast_write_response
224
397
 
225
398
  # Given a Hash +env+ for the request read from +client+, add
226
399
  # and fixup keys to comply with Rack's env guidelines.
227
400
  # @param env [Hash] see Puma::Client#env, from request
228
401
  # @param client [Puma::Client] only needed for Client#peerip
229
- # @todo make private in 6.0.0
230
402
  #
231
403
  def normalize_env(env, client)
232
404
  if host = env[HTTP_HOST]
233
- if colon = host.index(":")
405
+ # host can be a hostname, ipv4 or bracketed ipv6. Followed by an optional port.
406
+ if colon = host.rindex("]:") # IPV6 with port
407
+ env[SERVER_NAME] = host[0, colon+1]
408
+ env[SERVER_PORT] = host[colon+2, host.bytesize]
409
+ elsif !host.start_with?("[") && colon = host.index(":") # not hostname or IPV4 with port
234
410
  env[SERVER_NAME] = host[0, colon]
235
411
  env[SERVER_PORT] = host[colon+1, host.bytesize]
236
412
  else
@@ -244,17 +420,19 @@ module Puma
244
420
 
245
421
  unless env[REQUEST_PATH]
246
422
  # it might be a dumbass full host request header
247
- uri = URI.parse(env[REQUEST_URI])
423
+ uri = begin
424
+ URI.parse(env[REQUEST_URI])
425
+ rescue URI::InvalidURIError
426
+ raise Puma::HttpParserError
427
+ end
248
428
  env[REQUEST_PATH] = uri.path
249
429
 
250
- raise "No REQUEST PATH" unless env[REQUEST_PATH]
251
-
252
430
  # A nil env value will cause a LintError (and fatal errors elsewhere),
253
431
  # so only set the env value if there actually is a value.
254
432
  env[QUERY_STRING] = uri.query if uri.query
255
433
  end
256
434
 
257
- env[PATH_INFO] = env[REQUEST_PATH]
435
+ env[PATH_INFO] = env[REQUEST_PATH].to_s # #to_s in case it's nil
258
436
 
259
437
  # From https://www.ietf.org/rfc/rfc3875 :
260
438
  # "Script authors should be aware that the REMOTE_ADDR and
@@ -270,17 +448,31 @@ module Puma
270
448
  addr = client.peerip
271
449
  rescue Errno::ENOTCONN
272
450
  # Client disconnects can result in an inability to get the
273
- # peeraddr from the socket; default to localhost.
274
- addr = LOCALHOST_IP
451
+ # peeraddr from the socket; default to unspec.
452
+ if client.peer_family == Socket::AF_INET6
453
+ addr = UNSPECIFIED_IPV6
454
+ else
455
+ addr = UNSPECIFIED_IPV4
456
+ end
275
457
  end
276
458
 
277
459
  # Set unix socket addrs to localhost
278
- addr = LOCALHOST_IP if addr.empty?
460
+ if addr.empty?
461
+ if client.peer_family == Socket::AF_INET6
462
+ addr = LOCALHOST_IPV6
463
+ else
464
+ addr = LOCALHOST_IPV4
465
+ end
466
+ end
279
467
 
280
468
  env[REMOTE_ADDR] = addr
281
469
  end
470
+
471
+ # The legacy HTTP_VERSION header can be sent as a client header.
472
+ # Rack v4 may remove using HTTP_VERSION. If so, remove this line.
473
+ env[HTTP_VERSION] = env[SERVER_PROTOCOL]
282
474
  end
283
- # private :normalize_env
475
+ private :normalize_env
284
476
 
285
477
  # @param header_key [#to_s]
286
478
  # @return [Boolean]
@@ -311,7 +503,7 @@ module Puma
311
503
  to_add = nil
312
504
 
313
505
  env.each do |k,v|
314
- if k.start_with?("HTTP_") and k.include?(",") and k != "HTTP_TRANSFER,ENCODING"
506
+ if k.start_with?("HTTP_") && k.include?(",") && k != "HTTP_TRANSFER,ENCODING"
315
507
  if to_delete
316
508
  to_delete << k
317
509
  else
@@ -339,7 +531,7 @@ module Puma
339
531
  # @version 5.0.3
340
532
  #
341
533
  def str_early_hints(headers)
342
- eh_str = "HTTP/1.1 103 Early Hints\r\n".dup
534
+ eh_str = +""
343
535
  headers.each_pair do |k, vs|
344
536
  next if illegal_header_key?(k)
345
537
 
@@ -348,64 +540,82 @@ module Puma
348
540
  next if illegal_header_value?(v)
349
541
  eh_str << "#{k}: #{v}\r\n"
350
542
  end
351
- else
543
+ elsif !(vs.to_s.empty? || !illegal_header_value?(vs))
352
544
  eh_str << "#{k}: #{vs}\r\n"
353
545
  end
354
546
  end
355
- "#{eh_str}\r\n".freeze
547
+ eh_str.freeze
356
548
  end
357
549
  private :str_early_hints
358
550
 
551
+ # @param status [Integer] status from the app
552
+ # @return [String] the text description from Puma::HTTP_STATUS_CODES
553
+ #
554
+ def fetch_status_code(status)
555
+ HTTP_STATUS_CODES.fetch(status) { CUSTOM_STAT }
556
+ end
557
+ private :fetch_status_code
558
+
359
559
  # Processes and write headers to the IOBuffer.
360
560
  # @param env [Hash] see Puma::Client#env, from request
361
561
  # @param status [Integer] the status returned by the Rack application
362
562
  # @param headers [Hash] the headers returned by the Rack application
363
- # @param res_info [Hash] used to pass info between this method and #handle_request
364
- # @param lines [Puma::IOBuffer] modified inn place
563
+ # @param content_length [Integer,nil] content length if it can be determined from the
564
+ # response body
565
+ # @param io_buffer [Puma::IOBuffer] modified inn place
566
+ # @param force_keep_alive [Boolean] 'anded' with keep_alive, based on system
567
+ # status and `@max_fast_inline`
568
+ # @return [Hash] resp_info
365
569
  # @version 5.0.3
366
570
  #
367
- def str_headers(env, status, headers, res_info, lines)
571
+ def str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
572
+
368
573
  line_ending = LINE_END
369
574
  colon = COLON
370
575
 
371
- http_11 = env[HTTP_VERSION] == HTTP_11
576
+ resp_info = {}
577
+ resp_info[:no_body] = env[REQUEST_METHOD] == HEAD
578
+
579
+ http_11 = env[SERVER_PROTOCOL] == HTTP_11
372
580
  if http_11
373
- res_info[:allow_chunked] = true
374
- res_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase != CLOSE
581
+ resp_info[:allow_chunked] = true
582
+ resp_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase != CLOSE
375
583
 
376
584
  # An optimization. The most common response is 200, so we can
377
585
  # reply with the proper 200 status without having to compute
378
586
  # the response header.
379
587
  #
380
588
  if status == 200
381
- lines << HTTP_11_200
589
+ io_buffer << HTTP_11_200
382
590
  else
383
- lines.append "HTTP/1.1 ", status.to_s, " ",
384
- fetch_status_code(status), line_ending
591
+ io_buffer.append "#{HTTP_11} #{status} ", fetch_status_code(status), line_ending
385
592
 
386
- res_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
593
+ resp_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
387
594
  end
388
595
  else
389
- res_info[:allow_chunked] = false
390
- res_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase == KEEP_ALIVE
596
+ resp_info[:allow_chunked] = false
597
+ resp_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase == KEEP_ALIVE
391
598
 
392
599
  # Same optimization as above for HTTP/1.1
393
600
  #
394
601
  if status == 200
395
- lines << HTTP_10_200
602
+ io_buffer << HTTP_10_200
396
603
  else
397
- lines.append "HTTP/1.0 ", status.to_s, " ",
604
+ io_buffer.append "HTTP/1.0 #{status} ",
398
605
  fetch_status_code(status), line_ending
399
606
 
400
- res_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
607
+ resp_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
401
608
  end
402
609
  end
403
610
 
404
611
  # regardless of what the client wants, we always close the connection
405
612
  # if running without request queueing
406
- res_info[:keep_alive] &&= @queue_requests
613
+ resp_info[:keep_alive] &&= @queue_requests
407
614
 
408
- res_info[:response_hijack] = nil
615
+ # see prepare_response
616
+ resp_info[:keep_alive] &&= force_keep_alive
617
+
618
+ resp_info[:response_hijack] = nil
409
619
 
410
620
  headers.each do |k, vs|
411
621
  next if illegal_header_key?(k)
@@ -413,25 +623,34 @@ module Puma
413
623
  case k.downcase
414
624
  when CONTENT_LENGTH2
415
625
  next if illegal_header_value?(vs)
416
- res_info[:content_length] = vs
626
+ # nil.to_i is 0, nil&.to_i is nil
627
+ resp_info[:content_length] = vs&.to_i
417
628
  next
418
629
  when TRANSFER_ENCODING
419
- res_info[:allow_chunked] = false
420
- res_info[:content_length] = nil
630
+ resp_info[:allow_chunked] = false
631
+ resp_info[:content_length] = nil
632
+ resp_info[:transfer_encoding] = vs
421
633
  when HIJACK
422
- res_info[:response_hijack] = vs
634
+ resp_info[:response_hijack] = vs
423
635
  next
424
636
  when BANNED_HEADER_KEY
425
637
  next
426
638
  end
427
639
 
428
- if vs.respond_to?(:to_s) && !vs.to_s.empty?
429
- vs.to_s.split(NEWLINE).each do |v|
640
+ ary = if vs.is_a?(::Array) && !vs.empty?
641
+ vs
642
+ elsif vs.respond_to?(:to_s) && !vs.to_s.empty?
643
+ vs.to_s.split NEWLINE
644
+ else
645
+ nil
646
+ end
647
+ if ary
648
+ ary.each do |v|
430
649
  next if illegal_header_value?(v)
431
- lines.append k, colon, v, line_ending
650
+ io_buffer.append k, colon, v, line_ending
432
651
  end
433
652
  else
434
- lines.append k, colon, line_ending
653
+ io_buffer.append k, colon, line_ending
435
654
  end
436
655
  end
437
656
 
@@ -441,10 +660,11 @@ module Puma
441
660
  # Only set the header if we're doing something which is not the default
442
661
  # for this protocol version
443
662
  if http_11
444
- lines << CONNECTION_CLOSE if !res_info[:keep_alive]
663
+ io_buffer << CONNECTION_CLOSE if !resp_info[:keep_alive]
445
664
  else
446
- lines << CONNECTION_KEEP_ALIVE if res_info[:keep_alive]
665
+ io_buffer << CONNECTION_KEEP_ALIVE if resp_info[:keep_alive]
447
666
  end
667
+ resp_info
448
668
  end
449
669
  private :str_headers
450
670
  end