puma 5.0.2 → 5.0.3
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.
- checksums.yaml +4 -4
- data/History.md +608 -566
- data/README.md +4 -4
- data/bin/puma-wild +3 -9
- data/docs/deployment.md +5 -6
- data/docs/jungle/README.md +0 -4
- data/docs/jungle/rc.d/puma +2 -2
- data/docs/nginx.md +1 -1
- data/docs/restart.md +46 -23
- data/docs/systemd.md +1 -1
- data/ext/puma_http11/ext_help.h +1 -1
- data/ext/puma_http11/mini_ssl.c +39 -37
- data/ext/puma_http11/puma_http11.c +17 -10
- data/lib/puma/app/status.rb +44 -43
- data/lib/puma/binder.rb +9 -1
- data/lib/puma/client.rb +24 -72
- data/lib/puma/cluster.rb +25 -196
- data/lib/puma/cluster/worker.rb +170 -0
- data/lib/puma/cluster/worker_handle.rb +83 -0
- data/lib/puma/configuration.rb +8 -7
- data/lib/puma/const.rb +1 -1
- data/lib/puma/launcher.rb +5 -9
- data/lib/puma/queue_close.rb +26 -0
- data/lib/puma/reactor.rb +77 -362
- data/lib/puma/request.rb +438 -0
- data/lib/puma/runner.rb +4 -17
- data/lib/puma/server.rb +166 -501
- data/lib/puma/single.rb +2 -2
- data/lib/puma/util.rb +11 -0
- metadata +6 -6
- data/docs/jungle/upstart/README.md +0 -61
- data/docs/jungle/upstart/puma-manager.conf +0 -31
- data/docs/jungle/upstart/puma.conf +0 -69
- data/lib/puma/accept_nonblock.rb +0 -29
data/lib/puma/request.rb
ADDED
@@ -0,0 +1,438 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Puma
|
4
|
+
|
5
|
+
# The methods here are included in Server, but are separated into this file.
|
6
|
+
# All the methods here pertain to passing the request to the app, then
|
7
|
+
# writing the response back to the client.
|
8
|
+
#
|
9
|
+
# None of the methods here are called externally, with the exception of
|
10
|
+
# #handle_request, which is called in Server#process_client.
|
11
|
+
# @version 5.0.3
|
12
|
+
#
|
13
|
+
module Request
|
14
|
+
|
15
|
+
include Puma::Const
|
16
|
+
|
17
|
+
# Takes the request contained in +client+, invokes the Rack application to construct
|
18
|
+
# the response and writes it back to +client.io+.
|
19
|
+
#
|
20
|
+
# It'll return +false+ when the connection is closed, this doesn't mean
|
21
|
+
# that the response wasn't successful.
|
22
|
+
#
|
23
|
+
# It'll return +:async+ if the connection remains open but will be handled
|
24
|
+
# elsewhere, i.e. the connection has been hijacked by the Rack application.
|
25
|
+
#
|
26
|
+
# Finally, it'll return +true+ on keep-alive connections.
|
27
|
+
# @param client [Puma::Client]
|
28
|
+
# @param lines [Puma::IOBuffer]
|
29
|
+
# @return [Boolean,:async]
|
30
|
+
#
|
31
|
+
def handle_request(client, lines)
|
32
|
+
env = client.env
|
33
|
+
io = client.io
|
34
|
+
|
35
|
+
return false if closed_socket?(io)
|
36
|
+
|
37
|
+
normalize_env env, client
|
38
|
+
|
39
|
+
env[PUMA_SOCKET] = io
|
40
|
+
|
41
|
+
if env[HTTPS_KEY] && io.peercert
|
42
|
+
env[PUMA_PEERCERT] = io.peercert
|
43
|
+
end
|
44
|
+
|
45
|
+
env[HIJACK_P] = true
|
46
|
+
env[HIJACK] = client
|
47
|
+
|
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
|
54
|
+
|
55
|
+
if @early_hints
|
56
|
+
env[EARLY_HINTS] = lambda { |headers|
|
57
|
+
begin
|
58
|
+
fast_write io, str_early_hints(headers)
|
59
|
+
rescue ConnectionError => e
|
60
|
+
@events.debug_error e
|
61
|
+
# noop, if we lost the socket we just won't send the early hints
|
62
|
+
end
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
req_env_post_parse env
|
67
|
+
|
68
|
+
# A rack extension. If the app writes #call'ables to this
|
69
|
+
# array, we will invoke them when the request is done.
|
70
|
+
#
|
71
|
+
after_reply = env[RACK_AFTER_REPLY] = []
|
72
|
+
|
73
|
+
begin
|
74
|
+
begin
|
75
|
+
status, headers, res_body = @thread_pool.with_force_shutdown do
|
76
|
+
@app.call(env)
|
77
|
+
end
|
78
|
+
|
79
|
+
return :async if client.hijacked
|
80
|
+
|
81
|
+
status = status.to_i
|
82
|
+
|
83
|
+
if status == -1
|
84
|
+
unless headers.empty? and res_body == []
|
85
|
+
raise "async response must have empty headers and body"
|
86
|
+
end
|
87
|
+
|
88
|
+
return :async
|
89
|
+
end
|
90
|
+
rescue ThreadPool::ForceShutdown => e
|
91
|
+
@events.unknown_error e, client, "Rack app"
|
92
|
+
@events.log "Detected force shutdown of a thread"
|
93
|
+
|
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)
|
99
|
+
end
|
100
|
+
|
101
|
+
res_info = {}
|
102
|
+
res_info[:content_length] = nil
|
103
|
+
res_info[:no_body] = head
|
104
|
+
|
105
|
+
res_info[:content_length] = if res_body.kind_of? Array and res_body.size == 1
|
106
|
+
res_body[0].bytesize
|
107
|
+
else
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
|
111
|
+
cork_socket io
|
112
|
+
|
113
|
+
str_headers(env, status, headers, res_info, lines)
|
114
|
+
|
115
|
+
line_ending = LINE_END
|
116
|
+
|
117
|
+
content_length = res_info[:content_length]
|
118
|
+
response_hijack = res_info[:response_hijack]
|
119
|
+
|
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
|
123
|
+
end
|
124
|
+
|
125
|
+
lines << LINE_END
|
126
|
+
fast_write io, lines.to_s
|
127
|
+
return res_info[:keep_alive]
|
128
|
+
end
|
129
|
+
|
130
|
+
if content_length
|
131
|
+
lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
|
132
|
+
chunked = false
|
133
|
+
elsif !response_hijack and res_info[:allow_chunked]
|
134
|
+
lines << TRANSFER_ENCODING_CHUNKED
|
135
|
+
chunked = true
|
136
|
+
end
|
137
|
+
|
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
|
173
|
+
|
174
|
+
after_reply.each { |o| o.call }
|
175
|
+
end
|
176
|
+
|
177
|
+
return res_info[:keep_alive]
|
178
|
+
end
|
179
|
+
|
180
|
+
# @param env [Hash] see Puma::Client#env, from request
|
181
|
+
# @return [Puma::Const::PORT_443,Puma::Const::PORT_80]
|
182
|
+
#
|
183
|
+
def default_server_port(env)
|
184
|
+
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"
|
185
|
+
PORT_443
|
186
|
+
else
|
187
|
+
PORT_80
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Writes to an io (normally Client#io) using #syswrite
|
192
|
+
# @param io [#syswrite] the io to write to
|
193
|
+
# @param str [String] the string written to the io
|
194
|
+
# @raise [ConnectionError]
|
195
|
+
#
|
196
|
+
def fast_write(io, str)
|
197
|
+
n = 0
|
198
|
+
while true
|
199
|
+
begin
|
200
|
+
n = io.syswrite str
|
201
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
|
202
|
+
if !IO.select(nil, [io], nil, WRITE_TIMEOUT)
|
203
|
+
raise ConnectionError, "Socket timeout writing data"
|
204
|
+
end
|
205
|
+
|
206
|
+
retry
|
207
|
+
rescue Errno::EPIPE, SystemCallError, IOError
|
208
|
+
raise ConnectionError, "Socket timeout writing data"
|
209
|
+
end
|
210
|
+
|
211
|
+
return if n == str.bytesize
|
212
|
+
str = str.byteslice(n..-1)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
private :fast_write
|
216
|
+
|
217
|
+
# @param status [Integer] status from the app
|
218
|
+
# @return [String] the text description from Puma::HTTP_STATUS_CODES
|
219
|
+
#
|
220
|
+
def fetch_status_code(status)
|
221
|
+
HTTP_STATUS_CODES.fetch(status) { 'CUSTOM' }
|
222
|
+
end
|
223
|
+
private :fetch_status_code
|
224
|
+
|
225
|
+
# Given a Hash +env+ for the request read from +client+, add
|
226
|
+
# and fixup keys to comply with Rack's env guidelines.
|
227
|
+
# @param env [Hash] see Puma::Client#env, from request
|
228
|
+
# @param client [Puma::Client] only needed for Client#peerip
|
229
|
+
# @todo make private in 6.0.0
|
230
|
+
#
|
231
|
+
def normalize_env(env, client)
|
232
|
+
if host = env[HTTP_HOST]
|
233
|
+
if colon = host.index(":")
|
234
|
+
env[SERVER_NAME] = host[0, colon]
|
235
|
+
env[SERVER_PORT] = host[colon+1, host.bytesize]
|
236
|
+
else
|
237
|
+
env[SERVER_NAME] = host
|
238
|
+
env[SERVER_PORT] = default_server_port(env)
|
239
|
+
end
|
240
|
+
else
|
241
|
+
env[SERVER_NAME] = LOCALHOST
|
242
|
+
env[SERVER_PORT] = default_server_port(env)
|
243
|
+
end
|
244
|
+
|
245
|
+
unless env[REQUEST_PATH]
|
246
|
+
# it might be a dumbass full host request header
|
247
|
+
uri = URI.parse(env[REQUEST_URI])
|
248
|
+
env[REQUEST_PATH] = uri.path
|
249
|
+
|
250
|
+
raise "No REQUEST PATH" unless env[REQUEST_PATH]
|
251
|
+
|
252
|
+
# A nil env value will cause a LintError (and fatal errors elsewhere),
|
253
|
+
# so only set the env value if there actually is a value.
|
254
|
+
env[QUERY_STRING] = uri.query if uri.query
|
255
|
+
end
|
256
|
+
|
257
|
+
env[PATH_INFO] = env[REQUEST_PATH]
|
258
|
+
|
259
|
+
# From https://www.ietf.org/rfc/rfc3875 :
|
260
|
+
# "Script authors should be aware that the REMOTE_ADDR and
|
261
|
+
# REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
|
262
|
+
# may not identify the ultimate source of the request.
|
263
|
+
# They identify the client for the immediate request to the
|
264
|
+
# server; that client may be a proxy, gateway, or other
|
265
|
+
# intermediary acting on behalf of the actual source client."
|
266
|
+
#
|
267
|
+
|
268
|
+
unless env.key?(REMOTE_ADDR)
|
269
|
+
begin
|
270
|
+
addr = client.peerip
|
271
|
+
rescue Errno::ENOTCONN
|
272
|
+
# Client disconnects can result in an inability to get the
|
273
|
+
# peeraddr from the socket; default to localhost.
|
274
|
+
addr = LOCALHOST_IP
|
275
|
+
end
|
276
|
+
|
277
|
+
# Set unix socket addrs to localhost
|
278
|
+
addr = LOCALHOST_IP if addr.empty?
|
279
|
+
|
280
|
+
env[REMOTE_ADDR] = addr
|
281
|
+
end
|
282
|
+
end
|
283
|
+
# private :normalize_env
|
284
|
+
|
285
|
+
# @param header_value [#to_s]
|
286
|
+
# @return [Boolean]
|
287
|
+
#
|
288
|
+
def possible_header_injection?(header_value)
|
289
|
+
!!(HTTP_INJECTION_REGEX =~ header_value.to_s)
|
290
|
+
end
|
291
|
+
private :possible_header_injection?
|
292
|
+
|
293
|
+
# Fixup any headers with `,` in the name to have `_` now. We emit
|
294
|
+
# headers with `,` in them during the parse phase to avoid ambiguity
|
295
|
+
# with the `-` to `_` conversion for critical headers. But here for
|
296
|
+
# compatibility, we'll convert them back. This code is written to
|
297
|
+
# avoid allocation in the common case (ie there are no headers
|
298
|
+
# with `,` in their names), that's why it has the extra conditionals.
|
299
|
+
# @param env [Hash] see Puma::Client#env, from request, modifies in place
|
300
|
+
# @version 5.0.3
|
301
|
+
#
|
302
|
+
def req_env_post_parse(env)
|
303
|
+
to_delete = nil
|
304
|
+
to_add = nil
|
305
|
+
|
306
|
+
env.each do |k,v|
|
307
|
+
if k.start_with?("HTTP_") and k.include?(",") and k != "HTTP_TRANSFER,ENCODING"
|
308
|
+
if to_delete
|
309
|
+
to_delete << k
|
310
|
+
else
|
311
|
+
to_delete = [k]
|
312
|
+
end
|
313
|
+
|
314
|
+
unless to_add
|
315
|
+
to_add = {}
|
316
|
+
end
|
317
|
+
|
318
|
+
to_add[k.tr(",", "_")] = v
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
if to_delete
|
323
|
+
to_delete.each { |k| env.delete(k) }
|
324
|
+
env.merge! to_add
|
325
|
+
end
|
326
|
+
end
|
327
|
+
private :req_env_post_parse
|
328
|
+
|
329
|
+
# Used in the lambda for env[ `Puma::Const::EARLY_HINTS` ]
|
330
|
+
# @param headers [Hash] the headers returned by the Rack application
|
331
|
+
# @return [String]
|
332
|
+
# @version 5.0.3
|
333
|
+
#
|
334
|
+
def str_early_hints(headers)
|
335
|
+
eh_str = "HTTP/1.1 103 Early Hints\r\n".dup
|
336
|
+
headers.each_pair do |k, vs|
|
337
|
+
if vs.respond_to?(:to_s) && !vs.to_s.empty?
|
338
|
+
vs.to_s.split(NEWLINE).each do |v|
|
339
|
+
next if possible_header_injection?(v)
|
340
|
+
eh_str << "#{k}: #{v}\r\n"
|
341
|
+
end
|
342
|
+
else
|
343
|
+
eh_str << "#{k}: #{vs}\r\n"
|
344
|
+
end
|
345
|
+
end
|
346
|
+
"#{eh_str}\r\n".freeze
|
347
|
+
end
|
348
|
+
private :str_early_hints
|
349
|
+
|
350
|
+
# Processes and write headers to the IOBuffer.
|
351
|
+
# @param env [Hash] see Puma::Client#env, from request
|
352
|
+
# @param status [Integer] the status returned by the Rack application
|
353
|
+
# @param headers [Hash] the headers returned by the Rack application
|
354
|
+
# @param res_info [Hash] used to pass info between this method and #handle_request
|
355
|
+
# @param lines [Puma::IOBuffer] modified inn place
|
356
|
+
# @version 5.0.3
|
357
|
+
#
|
358
|
+
def str_headers(env, status, headers, res_info, lines)
|
359
|
+
line_ending = LINE_END
|
360
|
+
colon = COLON
|
361
|
+
|
362
|
+
http_11 = env[HTTP_VERSION] == HTTP_11
|
363
|
+
if http_11
|
364
|
+
res_info[:allow_chunked] = true
|
365
|
+
res_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase != CLOSE
|
366
|
+
|
367
|
+
# An optimization. The most common response is 200, so we can
|
368
|
+
# reply with the proper 200 status without having to compute
|
369
|
+
# the response header.
|
370
|
+
#
|
371
|
+
if status == 200
|
372
|
+
lines << HTTP_11_200
|
373
|
+
else
|
374
|
+
lines.append "HTTP/1.1 ", status.to_s, " ",
|
375
|
+
fetch_status_code(status), line_ending
|
376
|
+
|
377
|
+
res_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
|
378
|
+
end
|
379
|
+
else
|
380
|
+
res_info[:allow_chunked] = false
|
381
|
+
res_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase == KEEP_ALIVE
|
382
|
+
|
383
|
+
# Same optimization as above for HTTP/1.1
|
384
|
+
#
|
385
|
+
if status == 200
|
386
|
+
lines << HTTP_10_200
|
387
|
+
else
|
388
|
+
lines.append "HTTP/1.0 ", status.to_s, " ",
|
389
|
+
fetch_status_code(status), line_ending
|
390
|
+
|
391
|
+
res_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# regardless of what the client wants, we always close the connection
|
396
|
+
# if running without request queueing
|
397
|
+
res_info[:keep_alive] &&= @queue_requests
|
398
|
+
|
399
|
+
res_info[:response_hijack] = nil
|
400
|
+
|
401
|
+
headers.each do |k, vs|
|
402
|
+
case k.downcase
|
403
|
+
when CONTENT_LENGTH2
|
404
|
+
next if possible_header_injection?(vs)
|
405
|
+
res_info[:content_length] = vs
|
406
|
+
next
|
407
|
+
when TRANSFER_ENCODING
|
408
|
+
res_info[:allow_chunked] = false
|
409
|
+
res_info[:content_length] = nil
|
410
|
+
when HIJACK
|
411
|
+
res_info[:response_hijack] = vs
|
412
|
+
next
|
413
|
+
end
|
414
|
+
|
415
|
+
if vs.respond_to?(:to_s) && !vs.to_s.empty?
|
416
|
+
vs.to_s.split(NEWLINE).each do |v|
|
417
|
+
next if possible_header_injection?(v)
|
418
|
+
lines.append k, colon, v, line_ending
|
419
|
+
end
|
420
|
+
else
|
421
|
+
lines.append k, colon, line_ending
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
# HTTP/1.1 & 1.0 assume different defaults:
|
426
|
+
# - HTTP 1.0 assumes the connection will be closed if not specified
|
427
|
+
# - HTTP 1.1 assumes the connection will be kept alive if not specified.
|
428
|
+
# Only set the header if we're doing something which is not the default
|
429
|
+
# for this protocol version
|
430
|
+
if http_11
|
431
|
+
lines << CONNECTION_CLOSE if !res_info[:keep_alive]
|
432
|
+
else
|
433
|
+
lines << CONNECTION_KEEP_ALIVE if res_info[:keep_alive]
|
434
|
+
end
|
435
|
+
end
|
436
|
+
private :str_headers
|
437
|
+
end
|
438
|
+
end
|
data/lib/puma/runner.rb
CHANGED
@@ -54,9 +54,8 @@ module Puma
|
|
54
54
|
|
55
55
|
app = Puma::App::Status.new @launcher, token
|
56
56
|
|
57
|
-
control = Puma::Server.new app, @launcher.events
|
58
|
-
|
59
|
-
control.max_threads = 1
|
57
|
+
control = Puma::Server.new app, @launcher.events,
|
58
|
+
{ min_threads: 0, max_threads: 1 }
|
60
59
|
|
61
60
|
control.binder.parse [str], self, 'Starting control server'
|
62
61
|
|
@@ -69,6 +68,7 @@ module Puma
|
|
69
68
|
@control.binder.close_listeners if @control
|
70
69
|
end
|
71
70
|
|
71
|
+
# @!attribute [r] ruby_engine
|
72
72
|
def ruby_engine
|
73
73
|
if !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby"
|
74
74
|
"ruby #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"
|
@@ -137,27 +137,14 @@ module Puma
|
|
137
137
|
@launcher.binder.parse @options[:binds], self
|
138
138
|
end
|
139
139
|
|
140
|
+
# @!attribute [r] app
|
140
141
|
def app
|
141
142
|
@app ||= @launcher.config.app
|
142
143
|
end
|
143
144
|
|
144
145
|
def start_server
|
145
|
-
min_t = @options[:min_threads]
|
146
|
-
max_t = @options[:max_threads]
|
147
|
-
|
148
146
|
server = Puma::Server.new app, @launcher.events, @options
|
149
|
-
server.min_threads = min_t
|
150
|
-
server.max_threads = max_t
|
151
147
|
server.inherit_binder @launcher.binder
|
152
|
-
|
153
|
-
if @options[:early_hints]
|
154
|
-
server.early_hints = true
|
155
|
-
end
|
156
|
-
|
157
|
-
unless development? || test?
|
158
|
-
server.leak_stack_on_error = false
|
159
|
-
end
|
160
|
-
|
161
148
|
server
|
162
149
|
end
|
163
150
|
end
|