puma 3.8.2 → 3.12.6

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 (67) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +153 -0
  3. data/README.md +140 -230
  4. data/docs/architecture.md +36 -0
  5. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  6. data/docs/images/puma-connection-flow.png +0 -0
  7. data/docs/images/puma-general-arch.png +0 -0
  8. data/docs/plugins.md +28 -0
  9. data/docs/restart.md +39 -0
  10. data/docs/signals.md +56 -3
  11. data/docs/systemd.md +112 -37
  12. data/ext/puma_http11/http11_parser.c +87 -85
  13. data/ext/puma_http11/http11_parser.rl +12 -10
  14. data/ext/puma_http11/mini_ssl.c +31 -5
  15. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +13 -16
  16. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +15 -2
  17. data/lib/puma/app/status.rb +8 -0
  18. data/lib/puma/binder.rb +22 -17
  19. data/lib/puma/cli.rb +22 -7
  20. data/lib/puma/client.rb +41 -2
  21. data/lib/puma/cluster.rb +28 -7
  22. data/lib/puma/commonlogger.rb +2 -0
  23. data/lib/puma/configuration.rb +21 -14
  24. data/lib/puma/const.rb +17 -2
  25. data/lib/puma/control_cli.rb +16 -14
  26. data/lib/puma/convenient.rb +2 -0
  27. data/lib/puma/daemon_ext.rb +2 -0
  28. data/lib/puma/delegation.rb +2 -0
  29. data/lib/puma/detect.rb +2 -0
  30. data/lib/puma/dsl.rb +46 -9
  31. data/lib/puma/events.rb +3 -2
  32. data/lib/puma/io_buffer.rb +2 -0
  33. data/lib/puma/java_io_buffer.rb +2 -0
  34. data/lib/puma/jruby_restart.rb +2 -1
  35. data/lib/puma/launcher.rb +42 -20
  36. data/lib/puma/minissl.rb +67 -28
  37. data/lib/puma/null_io.rb +2 -0
  38. data/lib/puma/plugin/tmp_restart.rb +0 -1
  39. data/lib/puma/plugin.rb +2 -0
  40. data/lib/puma/rack/builder.rb +2 -1
  41. data/lib/puma/reactor.rb +137 -0
  42. data/lib/puma/runner.rb +16 -3
  43. data/lib/puma/server.rb +145 -29
  44. data/lib/puma/single.rb +14 -3
  45. data/lib/puma/state_file.rb +2 -0
  46. data/lib/puma/tcp_logger.rb +2 -0
  47. data/lib/puma/thread_pool.rb +55 -6
  48. data/lib/puma/util.rb +1 -0
  49. data/lib/puma.rb +8 -0
  50. data/lib/rack/handler/puma.rb +13 -2
  51. data/tools/jungle/README.md +12 -2
  52. data/tools/jungle/init.d/README.md +2 -0
  53. data/tools/jungle/init.d/puma +2 -2
  54. data/tools/jungle/init.d/run-puma +1 -1
  55. data/tools/jungle/rc.d/README.md +74 -0
  56. data/tools/jungle/rc.d/puma +61 -0
  57. data/tools/jungle/rc.d/puma.conf +10 -0
  58. data/tools/trickletest.rb +1 -1
  59. metadata +21 -95
  60. data/.github/issue_template.md +0 -20
  61. data/Gemfile +0 -12
  62. data/Manifest.txt +0 -78
  63. data/Rakefile +0 -158
  64. data/Release.md +0 -9
  65. data/gemfiles/2.1-Gemfile +0 -12
  66. data/puma.gemspec +0 -52
  67. /data/{DEPLOYMENT.md → docs/deployment.md} +0 -0
data/lib/puma/reactor.rb CHANGED
@@ -1,16 +1,57 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/util'
2
4
  require 'puma/minissl'
3
5
 
4
6
  module Puma
7
+ # Internal Docs, Not a public interface.
8
+ #
9
+ # The Reactor object is responsible for ensuring that a request has been
10
+ # completely received before it starts to be processed. This may be known as read buffering.
11
+ # If read buffering is not done, and no other read buffering is performed (such as by an application server
12
+ # such as nginx) then the application would be subject to a slow client attack.
13
+ #
14
+ # Each Puma "worker" process has its own Reactor. For example if you start puma with `$ puma -w 5` then
15
+ # it will have 5 workers and each worker will have it's own reactor.
16
+ #
17
+ # For a graphical representation of how the reactor works see [architecture.md](https://github.com/puma/puma/blob/master/docs/architecture.md#connection-pipeline).
18
+ #
19
+ # ## Reactor Flow
20
+ #
21
+ # A request comes into a `Puma::Server` instance, it is then passed to a `Puma::Reactor` instance.
22
+ # The reactor stores the request in an array and calls `IO.select` on the array in a loop.
23
+ #
24
+ # When the request is written to by the client then the `IO.select` will "wake up" and
25
+ # return the references to any objects that caused it to "wake". The reactor
26
+ # then loops through each of these request objects, and sees if they're complete. If they
27
+ # have a full header and body then the reactor passes the request to a thread pool.
28
+ # Once in a thread pool, a "worker thread" can run the the application's Ruby code against the request.
29
+ #
30
+ # If the request is not complete, then it stays in the array, and the next time any
31
+ # data is written to that socket reference, then the loop is woken up and it is checked for completeness again.
32
+ #
33
+ # A detailed example is given in the docs for `run_internal` which is where the bulk
34
+ # of this logic lives.
5
35
  class Reactor
6
36
  DefaultSleepFor = 5
7
37
 
38
+ # Creates an instance of Puma::Reactor
39
+ #
40
+ # The `server` argument is an instance of `Puma::Server`
41
+ # this is used to write a response for "low level errors"
42
+ # when there is an exception inside of the reactor.
43
+ #
44
+ # The `app_pool` is an instance of `Puma::ThreadPool`.
45
+ # Once a request is fully formed (header and body are received)
46
+ # it will be passed to the `app_pool`.
8
47
  def initialize(server, app_pool)
9
48
  @server = server
10
49
  @events = server.events
11
50
  @app_pool = app_pool
12
51
 
13
52
  @mutex = Mutex.new
53
+
54
+ # Read / Write pipes to wake up internal while loop
14
55
  @ready, @trigger = Puma::Util.pipe
15
56
  @input = []
16
57
  @sleep_for = DefaultSleepFor
@@ -21,6 +62,64 @@ module Puma
21
62
 
22
63
  private
23
64
 
65
+
66
+ # Until a request is added via the `add` method this method will internally
67
+ # loop, waiting on the `sockets` array objects. The only object in this
68
+ # array at first is the `@ready` IO object, which is the read end of a pipe
69
+ # connected to `@trigger` object. When `@trigger` is written to, then the loop
70
+ # will break on `IO.select` and return an array.
71
+ #
72
+ # ## When a request is added:
73
+ #
74
+ # When the `add` method is called, an instance of `Puma::Client` is added to the `@input` array.
75
+ # Next the `@ready` pipe is "woken" by writing a string of `"*"` to `@trigger`.
76
+ #
77
+ # When that happens, the internal loop stops blocking at `IO.select` and returns a reference
78
+ # to whatever "woke" it up. On the very first loop, the only thing in `sockets` is `@ready`.
79
+ # When `@trigger` is written-to, the loop "wakes" and the `ready`
80
+ # variable returns an array of arrays that looks like `[[#<IO:fd 10>], [], []]` where the
81
+ # first IO object is the `@ready` object. This first array `[#<IO:fd 10>]`
82
+ # is saved as a `reads` variable.
83
+ #
84
+ # The `reads` variable is iterated through. In the case that the object
85
+ # is the same as the `@ready` input pipe, then we know that there was a `trigger` event.
86
+ #
87
+ # If there was a trigger event, then one byte of `@ready` is read into memory. In the case of the first request,
88
+ # the reactor sees that it's a `"*"` value and the reactor adds the contents of `@input` into the `sockets` array.
89
+ # The while then loop continues to iterate again, but now the `sockets` array contains a `Puma::Client` instance in addition
90
+ # to the `@ready` IO object. For example: `[#<IO:fd 10>, #<Puma::Client:0x3fdc1103bee8 @ready=false>]`.
91
+ #
92
+ # Since the `Puma::Client` in this example has data that has not been read yet,
93
+ # the `IO.select` is immediately able to "wake" and read from the `Puma::Client`. At this point the
94
+ # `ready` output looks like this: `[[#<Puma::Client:0x3fdc1103bee8 @ready=false>], [], []]`.
95
+ #
96
+ # Each element in the first entry is iterated over. The `Puma::Client` object is not
97
+ # the `@ready` pipe, so the reactor checks to see if it has the fully header and body with
98
+ # the `Puma::Client#try_to_finish` method. If the full request has been sent,
99
+ # then the request is passed off to the `@app_pool` thread pool so that a "worker thread"
100
+ # can pick up the request and begin to execute application logic. This is done
101
+ # via `@app_pool << c`. The `Puma::Client` is then removed from the `sockets` array.
102
+ #
103
+ # If the request body is not present then nothing will happen, and the loop will iterate
104
+ # again. When the client sends more data to the socket the `Puma::Client` object will
105
+ # wake up the `IO.select` and it can again be checked to see if it's ready to be
106
+ # passed to the thread pool.
107
+ #
108
+ # ## Time Out Case
109
+ #
110
+ # In addition to being woken via a write to one of the sockets the `IO.select` will
111
+ # periodically "time out" of the sleep. One of the functions of this is to check for
112
+ # any requests that have "timed out". At the end of the loop it's checked to see if
113
+ # the first element in the `@timeout` array has exceed it's allowed time. If so,
114
+ # the client object is removed from the timeout aray, a 408 response is written.
115
+ # Then it's connection is closed, and the object is removed from the `sockets` array
116
+ # that watches for new data.
117
+ #
118
+ # This behavior loops until all the objects that have timed out have been removed.
119
+ #
120
+ # Once all the timeouts have been processed, the next duration of the `IO.select` sleep
121
+ # will be set to be equal to the amount of time it will take for the next timeout to occur.
122
+ # This calculation happens in `calculate_sleep`.
24
123
  def run_internal
25
124
  sockets = @sockets
26
125
 
@@ -28,6 +127,7 @@ module Puma
28
127
  begin
29
128
  ready = IO.select sockets, nil, nil, @sleep_for
30
129
  rescue IOError => e
130
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
31
131
  if sockets.any? { |socket| socket.closed? }
32
132
  STDERR.puts "Error in select: #{e.message} (#{e.class})"
33
133
  STDERR.puts e.backtrace
@@ -162,6 +262,16 @@ module Puma
162
262
  end
163
263
  end
164
264
 
265
+ # The `calculate_sleep` sets the value that the `IO.select` will
266
+ # sleep for in the main reactor loop when no sockets are being written to.
267
+ #
268
+ # The values kept in `@timeouts` are sorted so that the first timeout
269
+ # comes first in the array. When there are no timeouts the default timeout is used.
270
+ #
271
+ # Otherwise a sleep value is set that is the same as the amount of time it
272
+ # would take for the first element to time out.
273
+ #
274
+ # If that value is in the past, then a sleep value of zero is used.
165
275
  def calculate_sleep
166
276
  if @timeouts.empty?
167
277
  @sleep_for = DefaultSleepFor
@@ -176,6 +286,31 @@ module Puma
176
286
  end
177
287
  end
178
288
 
289
+ # This method adds a connection to the reactor
290
+ #
291
+ # Typically called by `Puma::Server` the value passed in
292
+ # is usually a `Puma::Client` object that responds like an IO
293
+ # object.
294
+ #
295
+ # The main body of the reactor loop is in `run_internal` and it
296
+ # will sleep on `IO.select`. When a new connection is added to the
297
+ # reactor it cannot be added directly to the `sockets` aray, because
298
+ # the `IO.select` will not be watching for it yet.
299
+ #
300
+ # Instead what needs to happen is that `IO.select` needs to be woken up,
301
+ # the contents of `@input` added to the `sockets` array, and then
302
+ # another call to `IO.select` needs to happen. Since the `Puma::Client`
303
+ # object can be read immediately, it does not block, but instead returns
304
+ # right away.
305
+ #
306
+ # This behavior is accomplished by writing to `@trigger` which wakes up
307
+ # the `IO.select` and then there is logic to detect the value of `*`,
308
+ # pull the contents from `@input` and add them to the sockets array.
309
+ #
310
+ # If the object passed in has a timeout value in `timeout_at` then
311
+ # it is added to a `@timeouts` array. This array is then re-arranged
312
+ # so that the first element to timeout will be at the front of the
313
+ # array. Then a value to sleep for is derived in the call to `calculate_sleep`
179
314
  def add(c)
180
315
  @mutex.synchronize do
181
316
  @input << c
@@ -195,6 +330,7 @@ module Puma
195
330
  begin
196
331
  @trigger << "c"
197
332
  rescue IOError
333
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
198
334
  end
199
335
  end
200
336
 
@@ -202,6 +338,7 @@ module Puma
202
338
  begin
203
339
  @trigger << "!"
204
340
  rescue IOError
341
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
205
342
  end
206
343
 
207
344
  @thread.join
data/lib/puma/runner.rb CHANGED
@@ -1,7 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/server'
2
4
  require 'puma/const'
3
5
 
4
6
  module Puma
7
+ # Generic class that is used by `Puma::Cluster` and `Puma::Single` to
8
+ # serve requests. This class spawns a new instance of `Puma::Server` via
9
+ # a call to `start_server`.
5
10
  class Runner
6
11
  def initialize(cli, events)
7
12
  @launcher = cli
@@ -19,6 +24,10 @@ module Puma
19
24
  @options[:environment] == "development"
20
25
  end
21
26
 
27
+ def test?
28
+ @options[:environment] == "test"
29
+ end
30
+
22
31
  def log(str)
23
32
  @events.log str
24
33
  end
@@ -107,7 +116,7 @@ module Puma
107
116
  append = @options[:redirect_append]
108
117
 
109
118
  if stdout
110
- unless Dir.exists?(File.dirname(stdout))
119
+ unless Dir.exist?(File.dirname(stdout))
111
120
  raise "Cannot redirect STDOUT to #{stdout}"
112
121
  end
113
122
 
@@ -117,7 +126,7 @@ module Puma
117
126
  end
118
127
 
119
128
  if stderr
120
- unless Dir.exists?(File.dirname(stderr))
129
+ unless Dir.exist?(File.dirname(stderr))
121
130
  raise "Cannot redirect STDERR to #{stderr}"
122
131
  end
123
132
 
@@ -161,7 +170,11 @@ module Puma
161
170
  server.tcp_mode!
162
171
  end
163
172
 
164
- unless development?
173
+ if @options[:early_hints]
174
+ server.early_hints = true
175
+ end
176
+
177
+ unless development? || test?
165
178
  server.leak_stack_on_error = false
166
179
  end
167
180
 
data/lib/puma/server.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'stringio'
2
4
 
3
5
  require 'puma/thread_pool'
@@ -23,6 +25,15 @@ require 'socket'
23
25
  module Puma
24
26
 
25
27
  # The HTTP Server itself. Serves out a single Rack app.
28
+ #
29
+ # This class is used by the `Puma::Single` and `Puma::Cluster` classes
30
+ # to generate one or more `Puma::Server` instances capable of handling requests.
31
+ # Each Puma process will contain one `Puma::Server` instacne.
32
+ #
33
+ # The `Puma::Server` instance pulls requests from the socket, adds them to a
34
+ # `Puma::Reactor` where they get eventually passed to a `Puma::ThreadPool`.
35
+ #
36
+ # Each `Puma::Server` will have one reactor and one thread pool.
26
37
  class Server
27
38
 
28
39
  include Puma::Const
@@ -62,14 +73,14 @@ module Puma
62
73
 
63
74
  @thread = nil
64
75
  @thread_pool = nil
76
+ @early_hints = nil
65
77
 
66
78
  @persistent_timeout = options.fetch(:persistent_timeout, PERSISTENT_TIMEOUT)
79
+ @first_data_timeout = options.fetch(:first_data_timeout, FIRST_DATA_TIMEOUT)
67
80
 
68
81
  @binder = Binder.new(events)
69
82
  @own_binder = true
70
83
 
71
- @first_data_timeout = FIRST_DATA_TIMEOUT
72
-
73
84
  @leak_stack_on_error = true
74
85
 
75
86
  @options = options
@@ -82,7 +93,7 @@ module Puma
82
93
  @precheck_closing = true
83
94
  end
84
95
 
85
- attr_accessor :binder, :leak_stack_on_error
96
+ attr_accessor :binder, :leak_stack_on_error, :early_hints
86
97
 
87
98
  forward :add_tcp_listener, :@binder
88
99
  forward :add_ssl_listener, :@binder
@@ -111,6 +122,7 @@ module Puma
111
122
  begin
112
123
  socket.setsockopt(6, 3, 1) if socket.kind_of? TCPSocket
113
124
  rescue IOError, SystemCallError
125
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
114
126
  end
115
127
  end
116
128
 
@@ -118,6 +130,7 @@ module Puma
118
130
  begin
119
131
  socket.setsockopt(6, 3, 0) if socket.kind_of? TCPSocket
120
132
  rescue IOError, SystemCallError
133
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
121
134
  end
122
135
  end
123
136
 
@@ -128,6 +141,7 @@ module Puma
128
141
  begin
129
142
  tcp_info = socket.getsockopt(Socket::SOL_TCP, Socket::TCP_INFO)
130
143
  rescue IOError, SystemCallError
144
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
131
145
  @precheck_closing = false
132
146
  false
133
147
  else
@@ -156,6 +170,18 @@ module Puma
156
170
  @thread_pool and @thread_pool.spawned
157
171
  end
158
172
 
173
+
174
+ # This number represents the number of requests that
175
+ # the server is capable of taking right now.
176
+ #
177
+ # For example if the number is 5 then it means
178
+ # there are 5 threads sitting idle ready to take
179
+ # a request. If one request comes in, then the
180
+ # value would be 4 until it finishes processing.
181
+ def pool_capacity
182
+ @thread_pool and @thread_pool.pool_capacity
183
+ end
184
+
159
185
  # Lopez Mode == raw tcp apps
160
186
 
161
187
  def run_lopez_mode(background=true)
@@ -217,7 +243,11 @@ module Puma
217
243
  # nothing
218
244
  rescue Errno::ECONNABORTED
219
245
  # client closed the socket even before accept
220
- io.close rescue nil
246
+ begin
247
+ io.close
248
+ rescue
249
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
250
+ end
221
251
  end
222
252
  end
223
253
  end
@@ -234,7 +264,12 @@ module Puma
234
264
  STDERR.puts "Exception handling servers: #{e.message} (#{e.class})"
235
265
  STDERR.puts e.backtrace
236
266
  ensure
237
- @check.close
267
+ begin
268
+ @check.close
269
+ rescue
270
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
271
+ end
272
+
238
273
  @notify.close
239
274
 
240
275
  if @status != :restart and @own_binder
@@ -292,7 +327,7 @@ module Puma
292
327
  client.close
293
328
 
294
329
  @events.parse_error self, client.env, e
295
- rescue ConnectionError
330
+ rescue ConnectionError, EOFError
296
331
  client.close
297
332
  else
298
333
  if process_now
@@ -363,13 +398,17 @@ module Puma
363
398
  end
364
399
 
365
400
  pool << client
366
- pool.wait_until_not_full unless queue_requests
401
+ pool.wait_until_not_full
367
402
  end
368
403
  rescue SystemCallError
369
404
  # nothing
370
405
  rescue Errno::ECONNABORTED
371
406
  # client closed the socket even before accept
372
- io.close rescue nil
407
+ begin
408
+ io.close
409
+ rescue
410
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
411
+ end
373
412
  end
374
413
  end
375
414
  end
@@ -431,6 +470,8 @@ module Puma
431
470
  clean_thread_locals = @options[:clean_thread_locals]
432
471
  close_socket = true
433
472
 
473
+ requests = 0
474
+
434
475
  while true
435
476
  case handle_request(client, buffer)
436
477
  when false
@@ -444,7 +485,19 @@ module Puma
444
485
 
445
486
  ThreadPool.clean_thread_locals if clean_thread_locals
446
487
 
447
- unless client.reset(@status == :run)
488
+ requests += 1
489
+
490
+ check_for_more_data = @status == :run
491
+
492
+ if requests >= MAX_FAST_INLINE
493
+ # This will mean that reset will only try to use the data it already
494
+ # has buffered and won't try to read more data. What this means is that
495
+ # every client, independent of their request speed, gets treated like a slow
496
+ # one once every MAX_FAST_INLINE requests.
497
+ check_for_more_data = false
498
+ end
499
+
500
+ unless client.reset(check_for_more_data)
448
501
  close_socket = false
449
502
  client.set_timeout @persistent_timeout
450
503
  @reactor.add client
@@ -491,6 +544,7 @@ module Puma
491
544
  begin
492
545
  client.close if close_socket
493
546
  rescue IOError, SystemCallError
547
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
494
548
  # Already closed
495
549
  rescue StandardError => e
496
550
  @events.unknown_error self, e, "Client"
@@ -522,7 +576,9 @@ module Puma
522
576
 
523
577
  raise "No REQUEST PATH" unless env[REQUEST_PATH]
524
578
 
525
- env[QUERY_STRING] = uri.query
579
+ # A nil env value will cause a LintError (and fatal errors elsewhere),
580
+ # so only set the env value if there actually is a value.
581
+ env[QUERY_STRING] = uri.query if uri.query
526
582
  end
527
583
 
528
584
  env[PATH_INFO] = env[REQUEST_PATH]
@@ -590,6 +646,56 @@ module Puma
590
646
  env[RACK_INPUT] = body
591
647
  env[RACK_URL_SCHEME] = env[HTTPS_KEY] ? HTTPS : HTTP
592
648
 
649
+ if @early_hints
650
+ env[EARLY_HINTS] = lambda { |headers|
651
+ fast_write client, "HTTP/1.1 103 Early Hints\r\n".freeze
652
+
653
+ headers.each_pair do |k, vs|
654
+ if vs.respond_to?(:to_s) && !vs.to_s.empty?
655
+ vs.to_s.split(NEWLINE).each do |v|
656
+ next if possible_header_injection?(v)
657
+ fast_write client, "#{k}: #{v}\r\n"
658
+ end
659
+ else
660
+ fast_write client, "#{k}: #{vs}\r\n"
661
+ end
662
+ end
663
+
664
+ fast_write client, "\r\n".freeze
665
+ }
666
+ end
667
+
668
+ # Fixup any headers with , in the name to have _ now. We emit
669
+ # headers with , in them during the parse phase to avoid ambiguity
670
+ # with the - to _ conversion for critical headers. But here for
671
+ # compatibility, we'll convert them back. This code is written to
672
+ # avoid allocation in the common case (ie there are no headers
673
+ # with , in their names), that's why it has the extra conditionals.
674
+
675
+ to_delete = nil
676
+ to_add = nil
677
+
678
+ env.each do |k,v|
679
+ if k.start_with?("HTTP_") and k.include?(",") and k != "HTTP_TRANSFER,ENCODING"
680
+ if to_delete
681
+ to_delete << k
682
+ else
683
+ to_delete = [k]
684
+ end
685
+
686
+ unless to_add
687
+ to_add = {}
688
+ end
689
+
690
+ to_add[k.gsub(",", "_")] = v
691
+ end
692
+ end
693
+
694
+ if to_delete
695
+ to_delete.each { |k| env.delete(k) }
696
+ env.merge! to_add
697
+ end
698
+
593
699
  # A rack extension. If the app writes #call'ables to this
594
700
  # array, we will invoke them when the request is done.
595
701
  #
@@ -677,6 +783,7 @@ module Puma
677
783
  headers.each do |k, vs|
678
784
  case k.downcase
679
785
  when CONTENT_LENGTH2
786
+ next if possible_header_injection?(vs)
680
787
  content_length = vs
681
788
  next
682
789
  when TRANSFER_ENCODING
@@ -687,8 +794,9 @@ module Puma
687
794
  next
688
795
  end
689
796
 
690
- if vs.respond_to?(:to_s)
797
+ if vs.respond_to?(:to_s) && !vs.to_s.empty?
691
798
  vs.to_s.split(NEWLINE).each do |v|
799
+ next if possible_header_injection?(v)
692
800
  lines.append k, colon, v, line_ending
693
801
  end
694
802
  else
@@ -731,8 +839,8 @@ module Puma
731
839
 
732
840
  begin
733
841
  res_body.each do |part|
842
+ next if part.bytesize.zero?
734
843
  if chunked
735
- next if part.bytesize.zero?
736
844
  fast_write client, part.bytesize.to_s(16)
737
845
  fast_write client, line_ending
738
846
  fast_write client, part
@@ -891,35 +999,38 @@ module Puma
891
999
  end
892
1000
  end
893
1001
 
894
- # Stops the acceptor thread and then causes the worker threads to finish
895
- # off the request queue before finally exiting.
896
- #
897
- def stop(sync=false)
1002
+ def notify_safely(message)
898
1003
  begin
899
- @notify << STOP_COMMAND
1004
+ @notify << message
900
1005
  rescue IOError
901
- # The server, in another thread, is shutting down
1006
+ # The server, in another thread, is shutting down
1007
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
1008
+ rescue RuntimeError => e
1009
+ # Temporary workaround for https://bugs.ruby-lang.org/issues/13239
1010
+ if e.message.include?('IOError')
1011
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
1012
+ else
1013
+ raise e
1014
+ end
902
1015
  end
1016
+ end
1017
+ private :notify_safely
1018
+
1019
+ # Stops the acceptor thread and then causes the worker threads to finish
1020
+ # off the request queue before finally exiting.
903
1021
 
1022
+ def stop(sync=false)
1023
+ notify_safely(STOP_COMMAND)
904
1024
  @thread.join if @thread && sync
905
1025
  end
906
1026
 
907
1027
  def halt(sync=false)
908
- begin
909
- @notify << HALT_COMMAND
910
- rescue IOError
911
- # The server, in another thread, is shutting down
912
- end
913
-
1028
+ notify_safely(HALT_COMMAND)
914
1029
  @thread.join if @thread && sync
915
1030
  end
916
1031
 
917
1032
  def begin_restart
918
- begin
919
- @notify << RESTART_COMMAND
920
- rescue IOError
921
- # The server, in another thread, is shutting down
922
- end
1033
+ notify_safely(RESTART_COMMAND)
923
1034
  end
924
1035
 
925
1036
  def fast_write(io, str)
@@ -952,5 +1063,10 @@ module Puma
952
1063
  def shutting_down?
953
1064
  @status == :stop || @status == :restart
954
1065
  end
1066
+
1067
+ def possible_header_injection?(header_value)
1068
+ HTTP_INJECTION_REGEX =~ header_value.to_s
1069
+ end
1070
+ private :possible_header_injection?
955
1071
  end
956
1072
  end
data/lib/puma/single.rb CHANGED
@@ -1,13 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/runner'
2
4
  require 'puma/detect'
3
5
  require 'puma/plugin'
4
6
 
5
7
  module Puma
8
+ # This class is instantiated by the `Puma::Launcher` and used
9
+ # to boot and serve a Ruby application when no puma "workers" are needed
10
+ # i.e. only using "threaded" mode. For example `$ puma -t 1:5`
11
+ #
12
+ # At the core of this class is running an instance of `Puma::Server` which
13
+ # gets created via the `start_server` method from the `Puma::Runner` class
14
+ # that this inherits from.
6
15
  class Single < Runner
7
16
  def stats
8
- b = @server.backlog
9
- r = @server.running
10
- %Q!{ "backlog": #{b}, "running": #{r} }!
17
+ b = @server.backlog || 0
18
+ r = @server.running || 0
19
+ t = @server.pool_capacity || 0
20
+ m = @server.max_threads || 0
21
+ %Q!{ "backlog": #{b}, "running": #{r}, "pool_capacity": #{t}, "max_threads": #{m} }!
11
22
  end
12
23
 
13
24
  def restart
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
 
3
5
  module Puma
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Puma
2
4
  class TCPLogger
3
5
  def initialize(logger, app, quiet=false)