puma 4.3.3 → 5.3.2

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