puma 5.0.4 → 5.1.0

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.

@@ -40,7 +40,9 @@ static VALUE global_http_version;
40
40
  static VALUE global_request_path;
41
41
 
42
42
  /** Defines common length and error messages for input length validation. */
43
- #define DEF_MAX_LENGTH(N,length) const size_t MAX_##N##_LENGTH = length; const char *MAX_##N##_LENGTH_ERR = "HTTP element " # N " is longer than the " # length " allowed length (was %d)"
43
+ #define QUOTE(s) #s
44
+ #define EXPLAND_MAX_LENGHT_VALUE(s) QUOTE(s)
45
+ #define DEF_MAX_LENGTH(N,length) const size_t MAX_##N##_LENGTH = length; const char *MAX_##N##_LENGTH_ERR = "HTTP element " # N " is longer than the " EXPLAND_MAX_LENGHT_VALUE(length) " allowed length (was %d)"
44
46
 
45
47
  /** Validates the max length of given input and throws an HttpParserError exception if over. */
46
48
  #define VALIDATE_MAX_LENGTH(len, N) if(len > MAX_##N##_LENGTH) { rb_raise(eHttpParserError, MAX_##N##_LENGTH_ERR, len); }
@@ -50,12 +52,16 @@ static VALUE global_request_path;
50
52
 
51
53
 
52
54
  /* Defines the maximum allowed lengths for various input elements.*/
55
+ #ifndef PUMA_QUERY_STRING_MAX_LENGTH
56
+ #define PUMA_QUERY_STRING_MAX_LENGTH (1024 * 10)
57
+ #endif
58
+
53
59
  DEF_MAX_LENGTH(FIELD_NAME, 256);
54
60
  DEF_MAX_LENGTH(FIELD_VALUE, 80 * 1024);
55
61
  DEF_MAX_LENGTH(REQUEST_URI, 1024 * 12);
56
62
  DEF_MAX_LENGTH(FRAGMENT, 1024); /* Don't know if this length is specified somewhere or not */
57
63
  DEF_MAX_LENGTH(REQUEST_PATH, 8192);
58
- DEF_MAX_LENGTH(QUERY_STRING, (1024 * 10));
64
+ DEF_MAX_LENGTH(QUERY_STRING, PUMA_QUERY_STRING_MAX_LENGTH);
59
65
  DEF_MAX_LENGTH(HEADER, (1024 * (80 + 32)));
60
66
 
61
67
  struct common_field {
@@ -12,6 +12,7 @@ require 'thread'
12
12
 
13
13
  require 'puma/puma_http11'
14
14
  require 'puma/detect'
15
+ require 'puma/json'
15
16
 
16
17
  module Puma
17
18
  autoload :Const, 'puma/const'
@@ -25,8 +26,7 @@ module Puma
25
26
 
26
27
  # @!attribute [rw] stats_object
27
28
  def self.stats
28
- require 'json'
29
- @get_stats.stats.to_json
29
+ Puma::JSON.generate @get_stats.stats
30
30
  end
31
31
 
32
32
  # @!attribute [r] stats_hash
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'puma/json'
2
3
 
3
4
  module Puma
4
5
  module App
@@ -22,10 +23,6 @@ module Puma
22
23
  return rack_response(403, 'Invalid auth token', 'text/plain')
23
24
  end
24
25
 
25
- if env['PATH_INFO'] =~ /\/(gc-stats|stats|thread-backtraces)$/
26
- require 'json'
27
- end
28
-
29
26
  # resp_type is processed by following case statement, return
30
27
  # is a number (status) or a string used as the body of a 200 response
31
28
  resp_type =
@@ -49,17 +46,17 @@ module Puma
49
46
  GC.start ; 200
50
47
 
51
48
  when 'gc-stats'
52
- GC.stat.to_json
49
+ Puma::JSON.generate GC.stat
53
50
 
54
51
  when 'stats'
55
- @launcher.stats.to_json
52
+ Puma::JSON.generate @launcher.stats
56
53
 
57
54
  when 'thread-backtraces'
58
55
  backtraces = []
59
56
  @launcher.thread_status do |name, backtrace|
60
57
  backtraces << { name: name, backtrace: backtrace }
61
58
  end
62
- backtraces.to_json
59
+ Puma::JSON.generate backtraces
63
60
 
64
61
  else
65
62
  return rack_response(404, "Unsupported action", 'text/plain')
@@ -111,6 +111,43 @@ module Puma
111
111
  ["LISTEN_FDS", "LISTEN_PID"] # Signal to remove these keys from ENV
112
112
  end
113
113
 
114
+ # Synthesize binds from systemd socket activation
115
+ #
116
+ # When systemd socket activation is enabled, it can be tedious to keep the
117
+ # binds in sync. This method can synthesize any binds based on the received
118
+ # activated sockets. Any existing matching binds will be respected.
119
+ #
120
+ # When only_matching is true in, all binds that do not match an activated
121
+ # socket is removed in place.
122
+ #
123
+ # It's a noop if no activated sockets were received.
124
+ def synthesize_binds_from_activated_fs(binds, only_matching)
125
+ return binds unless activated_sockets.any?
126
+
127
+ activated_binds = []
128
+
129
+ activated_sockets.keys.each do |proto, addr, port|
130
+ if port
131
+ tcp_url = "#{proto}://#{addr}:#{port}"
132
+ ssl_url = "ssl://#{addr}:#{port}"
133
+ ssl_url_prefix = "#{ssl_url}?"
134
+
135
+ existing = binds.find { |bind| bind == tcp_url || bind == ssl_url || bind.start_with?(ssl_url_prefix) }
136
+
137
+ activated_binds << (existing || tcp_url)
138
+ else
139
+ # TODO: can there be a SSL bind without a port?
140
+ activated_binds << "#{proto}://#{addr}"
141
+ end
142
+ end
143
+
144
+ if only_matching
145
+ activated_binds
146
+ else
147
+ binds | activated_binds
148
+ end
149
+ end
150
+
114
151
  def parse(binds, logger, log_msg = 'Listening')
115
152
  binds.each do |str|
116
153
  uri = URI.parse str
@@ -104,6 +104,10 @@ module Puma
104
104
  user_config.bind arg
105
105
  end
106
106
 
107
+ o.on "--bind-to-activated-sockets [only]", "Bind to all activated sockets" do |arg|
108
+ user_config.bind_to_activated_sockets(arg || true)
109
+ end
110
+
107
111
  o.on "-C", "--config PATH", "Load PATH as a config file" do |arg|
108
112
  file_config.load arg
109
113
  end
@@ -239,13 +239,8 @@ module Puma
239
239
  # @version 5.0.0
240
240
  #
241
241
  def can_close?
242
- # Allow connection to close if it's received at least one full request
243
- # and hasn't received any data for a future request.
244
- #
245
- # From RFC 2616 section 8.1.4:
246
- # Servers SHOULD always respond to at least one request per connection,
247
- # if at all possible.
248
- @requests_served > 0 && @parsed_bytes == 0
242
+ # Allow connection to close if we're not in the middle of parsing a request.
243
+ @parsed_bytes == 0
249
244
  end
250
245
 
251
246
  private
@@ -114,7 +114,7 @@ module Puma
114
114
  debug "Workers to cull: #{workers_to_cull.inspect}"
115
115
 
116
116
  workers_to_cull.each do |worker|
117
- log "- Worker #{worker.index} (pid: #{worker.pid}) terminating"
117
+ log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
118
118
  worker.term
119
119
  end
120
120
  end
@@ -329,15 +329,19 @@ module Puma
329
329
 
330
330
  output_header "cluster"
331
331
 
332
- log "* Process workers: #{@options[:workers]}"
332
+ # This is aligned with the output from Runner, see Runner#output_header
333
+ log "* Workers: #{@options[:workers]}"
333
334
 
334
- before = Thread.list
335
+ # Threads explicitly marked as fork safe will be ignored.
336
+ # Used in Rails, but may be used by anyone.
337
+ before = Thread.list.reject { |t| t.thread_variable_get(:fork_safe) }
335
338
 
336
339
  if preload?
340
+ log "* Restarts: (\u2714) hot (\u2716) phased"
337
341
  log "* Preloading application"
338
342
  load_and_bind
339
343
 
340
- after = Thread.list
344
+ after = Thread.list.reject { |t| t.thread_variable_get(:fork_safe) }
341
345
 
342
346
  if after.size > before.size
343
347
  threads = (after - before)
@@ -351,7 +355,7 @@ module Puma
351
355
  end
352
356
  end
353
357
  else
354
- log "* Phased restart available"
358
+ log "* Restarts: (\u2714) hot (\u2714) phased"
355
359
 
356
360
  unless @launcher.config.app_configured?
357
361
  error "No application configured, nothing to run"
@@ -430,7 +434,7 @@ module Puma
430
434
  case req
431
435
  when "b"
432
436
  w.boot!
433
- log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
437
+ log "- Worker #{w.index} (PID: #{pid}) booted, phase: #{w.phase}"
434
438
  @next_check = Time.now
435
439
  when "e"
436
440
  # external term, see worker method, Signal.trap "SIGTERM"
@@ -108,11 +108,17 @@ module Puma
108
108
  server_thread = server.run
109
109
  stat_thread ||= Thread.new(@worker_write) do |io|
110
110
  Puma.set_thread_name "stat payload"
111
+ base_payload = "p#{Process.pid}"
111
112
 
112
113
  while true
113
114
  begin
114
- require 'json'
115
- io << "p#{Process.pid}#{server.stats.to_json}\n"
115
+ b = server.backlog || 0
116
+ r = server.running || 0
117
+ t = server.pool_capacity || 0
118
+ m = server.max_threads || 0
119
+ rc = server.requests_count || 0
120
+ payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m}, "requests_count": #{rc} }\n!
121
+ io << payload
116
122
  rescue IOError
117
123
  Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
118
124
  break
@@ -42,8 +42,11 @@ module Puma
42
42
 
43
43
  def ping!(status)
44
44
  @last_checkin = Time.now
45
- require 'json'
46
- @last_status = JSON.parse(status, symbolize_names: true)
45
+ captures = status.match(/{ "backlog":(?<backlog>\d*), "running":(?<running>\d*), "pool_capacity":(?<pool_capacity>\d*), "max_threads": (?<max_threads>\d*), "requests_count": (?<requests_count>\d*) }/)
46
+ @last_status = captures.names.inject({}) do |hash, key|
47
+ hash[key.to_sym] = captures[key].to_i
48
+ hash
49
+ end
47
50
  end
48
51
 
49
52
  # @see Puma::Cluster#check_workers
@@ -92,6 +92,12 @@ module Puma
92
92
  end
93
93
  end
94
94
  end
95
+
96
+ def final_options
97
+ default_options
98
+ .merge(file_options)
99
+ .merge(user_options)
100
+ end
95
101
  end
96
102
 
97
103
  # The main configuration class of Puma.
@@ -198,7 +204,8 @@ module Puma
198
204
  :logger => STDOUT,
199
205
  :persistent_timeout => Const::PERSISTENT_TIMEOUT,
200
206
  :first_data_timeout => Const::FIRST_DATA_TIMEOUT,
201
- :raise_exception_on_sigterm => true
207
+ :raise_exception_on_sigterm => true,
208
+ :max_fast_inline => Const::MAX_FAST_INLINE
202
209
  }
203
210
  end
204
211
 
@@ -289,6 +296,10 @@ module Puma
289
296
  end
290
297
  end
291
298
 
299
+ def final_options
300
+ @options.final_options
301
+ end
302
+
292
303
  def self.temp_path
293
304
  require 'tmpdir'
294
305
 
@@ -100,8 +100,8 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "5.0.4".freeze
104
- CODE_NAME = "Spoony Bard".freeze
103
+ PUMA_VERSION = VERSION = "5.1.0".freeze
104
+ CODE_NAME = "At Your Service".freeze
105
105
 
106
106
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
107
107
 
@@ -228,7 +228,6 @@ module Puma
228
228
  COLON = ": ".freeze
229
229
 
230
230
  NEWLINE = "\n".freeze
231
- HTTP_INJECTION_REGEX = /[\r\n]/.freeze
232
231
 
233
232
  HIJACK_P = "rack.hijack?".freeze
234
233
  HIJACK = "rack.hijack".freeze
@@ -239,5 +238,14 @@ module Puma
239
238
  # Mininum interval to checks worker health
240
239
  WORKER_CHECK_INTERVAL = 5
241
240
 
241
+ # Illegal character in the key or value of response header
242
+ DQUOTE = "\"".freeze
243
+ HTTP_HEADER_DELIMITER = Regexp.escape("(),/:;<=>?@[]{}\\").freeze
244
+ ILLEGAL_HEADER_KEY_REGEX = /[\x00-\x20#{DQUOTE}#{HTTP_HEADER_DELIMITER}]/.freeze
245
+ # header values can contain HTAB?
246
+ ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze
247
+
248
+ # Banned keys of response header
249
+ BANNED_HEADER_KEY = /rack.|status/.freeze
242
250
  end
243
251
  end
@@ -11,10 +11,32 @@ require 'socket'
11
11
  module Puma
12
12
  class ControlCLI
13
13
 
14
- COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats thread-backtraces refork}
14
+ # values must be string or nil
15
+ # value of `nil` means command cannot be processed via signal
16
+ # @version 5.0.3
17
+ CMD_PATH_SIG_MAP = {
18
+ 'gc' => nil,
19
+ 'gc-stats' => nil,
20
+ 'halt' => 'SIGQUIT',
21
+ 'phased-restart' => 'SIGUSR1',
22
+ 'refork' => 'SIGURG',
23
+ 'reload-worker-directory' => nil,
24
+ 'restart' => 'SIGUSR2',
25
+ 'start' => nil,
26
+ 'stats' => nil,
27
+ 'status' => '',
28
+ 'stop' => 'SIGTERM',
29
+ 'thread-backtraces' => nil
30
+ }.freeze
31
+
32
+ # @deprecated 6.0.0
33
+ COMMANDS = CMD_PATH_SIG_MAP.keys.freeze
34
+
35
+ # commands that cannot be used in a request
36
+ NO_REQ_COMMANDS = %w{refork}.freeze
15
37
 
16
38
  # @version 5.0.0
17
- PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces}
39
+ PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces}.freeze
18
40
 
19
41
  def initialize(argv, stdout=STDOUT, stderr=STDERR)
20
42
  @state = nil
@@ -33,7 +55,7 @@ module Puma
33
55
  @cli_options = {}
34
56
 
35
57
  opts = OptionParser.new do |o|
36
- o.banner = "Usage: pumactl (-p PID | -P pidfile | -S status_file | -C url -T token | -F config.rb) (#{COMMANDS.join("|")})"
58
+ o.banner = "Usage: pumactl (-p PID | -P pidfile | -S status_file | -C url -T token | -F config.rb) (#{CMD_PATH_SIG_MAP.keys.join("|")})"
37
59
 
38
60
  o.on "-S", "--state PATH", "Where the state file to use is" do |arg|
39
61
  @state = arg
@@ -74,7 +96,7 @@ module Puma
74
96
  end
75
97
 
76
98
  o.on_tail("-V", "--version", "Show version") do
77
- puts Const::PUMA_VERSION
99
+ @stdout.puts Const::PUMA_VERSION
78
100
  exit
79
101
  end
80
102
  end
@@ -86,10 +108,10 @@ module Puma
86
108
 
87
109
  # check presence of command
88
110
  unless @command
89
- raise "Available commands: #{COMMANDS.join(", ")}"
111
+ raise "Available commands: #{CMD_PATH_SIG_MAP.keys.join(", ")}"
90
112
  end
91
113
 
92
- unless COMMANDS.include? @command
114
+ unless CMD_PATH_SIG_MAP.key? @command
93
115
  raise "Invalid command: #{@command}"
94
116
  end
95
117
 
@@ -134,7 +156,7 @@ module Puma
134
156
  @pid = sf.pid
135
157
  elsif @pidfile
136
158
  # get pid from pid_file
137
- File.open(@pidfile) { |f| @pid = f.read.to_i }
159
+ @pid = File.read(@pidfile, mode: 'rb:UTF-8').to_i
138
160
  end
139
161
  end
140
162
 
@@ -142,24 +164,27 @@ module Puma
142
164
  uri = URI.parse @control_url
143
165
 
144
166
  # create server object by scheme
145
- server = case uri.scheme
146
- when "ssl"
147
- require 'openssl'
148
- OpenSSL::SSL::SSLSocket.new(
149
- TCPSocket.new(uri.host, uri.port),
150
- OpenSSL::SSL::SSLContext.new)
151
- .tap { |ssl| ssl.sync_close = true } # default is false
152
- .tap(&:connect)
153
- when "tcp"
154
- TCPSocket.new uri.host, uri.port
155
- when "unix"
156
- UNIXSocket.new "#{uri.host}#{uri.path}"
157
- else
158
- raise "Invalid scheme: #{uri.scheme}"
159
- end
160
-
161
- if @command == "status"
162
- message "Puma is started"
167
+ server =
168
+ case uri.scheme
169
+ when 'ssl'
170
+ require 'openssl'
171
+ OpenSSL::SSL::SSLSocket.new(
172
+ TCPSocket.new(uri.host, uri.port),
173
+ OpenSSL::SSL::SSLContext.new)
174
+ .tap { |ssl| ssl.sync_close = true } # default is false
175
+ .tap(&:connect)
176
+ when 'tcp'
177
+ TCPSocket.new uri.host, uri.port
178
+ when 'unix'
179
+ UNIXSocket.new "#{uri.host}#{uri.path}"
180
+ else
181
+ raise "Invalid scheme: #{uri.scheme}"
182
+ end
183
+
184
+ if @command == 'status'
185
+ message 'Puma is started'
186
+ elsif NO_REQ_COMMANDS.include? @command
187
+ raise "Invalid request command: #{@command}"
163
188
  else
164
189
  url = "/#{@command}"
165
190
 
@@ -167,10 +192,10 @@ module Puma
167
192
  url = url + "?token=#{@control_auth_token}"
168
193
  end
169
194
 
170
- server << "GET #{url} HTTP/1.0\r\n\r\n"
195
+ server.syswrite "GET #{url} HTTP/1.0\r\n\r\n"
171
196
 
172
197
  unless data = server.read
173
- raise "Server closed connection before responding"
198
+ raise 'Server closed connection before responding'
174
199
  end
175
200
 
176
201
  response = data.split("\r\n")
@@ -179,13 +204,13 @@ module Puma
179
204
  raise "Server sent empty response"
180
205
  end
181
206
 
182
- (@http,@code,@message) = response.first.split(" ",3)
207
+ @http, @code, @message = response.first.split(' ',3)
183
208
 
184
- if @code == "403"
185
- raise "Unauthorized access to server (wrong auth token)"
186
- elsif @code == "404"
209
+ if @code == '403'
210
+ raise 'Unauthorized access to server (wrong auth token)'
211
+ elsif @code == '404'
187
212
  raise "Command error: #{response.last}"
188
- elsif @code != "200"
213
+ elsif @code != '200'
189
214
  raise "Bad response from server: #{@code}"
190
215
  end
191
216
 
@@ -194,7 +219,7 @@ module Puma
194
219
  end
195
220
  ensure
196
221
  if server
197
- if uri.scheme == "ssl"
222
+ if uri.scheme == 'ssl'
198
223
  server.sysclose
199
224
  else
200
225
  server.close unless server.closed?
@@ -204,51 +229,28 @@ module Puma
204
229
 
205
230
  def send_signal
206
231
  unless @pid
207
- raise "Neither pid nor control url available"
232
+ raise 'Neither pid nor control url available'
208
233
  end
209
234
 
210
235
  begin
236
+ sig = CMD_PATH_SIG_MAP[@command]
211
237
 
212
- case @command
213
- when "restart"
214
- Process.kill "SIGUSR2", @pid
215
-
216
- when "halt"
217
- Process.kill "QUIT", @pid
218
-
219
- when "stop"
220
- Process.kill "SIGTERM", @pid
221
-
222
- when "stats"
223
- puts "Stats not available via pid only"
224
- return
225
-
226
- when "reload-worker-directory"
227
- puts "reload-worker-directory not available via pid only"
238
+ if sig.nil?
239
+ @stdout.puts "'#{@command}' not available via pid only"
228
240
  return
229
-
230
- when "phased-restart"
231
- Process.kill "SIGUSR1", @pid
232
-
233
- when "status"
241
+ elsif sig.start_with? 'SIG'
242
+ Process.kill sig, @pid
243
+ elsif @command == 'status'
234
244
  begin
235
245
  Process.kill 0, @pid
236
- puts "Puma is started"
246
+ @stdout.puts 'Puma is started'
237
247
  rescue Errno::ESRCH
238
- raise "Puma is not running"
248
+ raise 'Puma is not running'
239
249
  end
240
-
241
- return
242
-
243
- when "refork"
244
- Process.kill "SIGURG", @pid
245
-
246
- else
247
250
  return
248
251
  end
249
-
250
252
  rescue SystemCallError
251
- if @command == "restart"
253
+ if @command == 'restart'
252
254
  start
253
255
  else
254
256
  raise "No pid '#{@pid}' found"
@@ -259,14 +261,13 @@ module Puma
259
261
  end
260
262
 
261
263
  def run
262
- return start if @command == "start"
263
-
264
+ return start if @command == 'start'
264
265
  prepare_configuration
265
266
 
266
- if Puma.windows?
267
+ if Puma.windows? || @control_url
267
268
  send_request
268
269
  else
269
- @control_url ? send_request : send_signal
270
+ send_signal
270
271
  end
271
272
 
272
273
  rescue => e