jun-puma 1.0.0-java

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