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