puma 6.0.2 → 6.2.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.

@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../plugin'
4
+
5
+ # Puma's systemd integration allows Puma to inform systemd:
6
+ # 1. when it has successfully started
7
+ # 2. when it is starting shutdown
8
+ # 3. periodically for a liveness check with a watchdog thread
9
+ # 4. periodically set the status
10
+ Puma::Plugin.create do
11
+ def start(launcher)
12
+ require_relative '../sd_notify'
13
+
14
+ launcher.log_writer.log "* Enabling systemd notification integration"
15
+
16
+ # hook_events
17
+ launcher.events.on_booted { Puma::SdNotify.ready }
18
+ launcher.events.on_stopped { Puma::SdNotify.stopping }
19
+ launcher.events.on_restart { Puma::SdNotify.reloading }
20
+
21
+ # start watchdog
22
+ if Puma::SdNotify.watchdog?
23
+ ping_f = watchdog_sleep_time
24
+
25
+ in_background do
26
+ launcher.log_writer.log "Pinging systemd watchdog every #{ping_f.round(1)} sec"
27
+ loop do
28
+ sleep ping_f
29
+ Puma::SdNotify.watchdog
30
+ end
31
+ end
32
+ end
33
+
34
+ # start status loop
35
+ instance = self
36
+ sleep_time = 1.0
37
+ in_background do
38
+ launcher.log_writer.log "Sending status to systemd every #{sleep_time.round(1)} sec"
39
+
40
+ loop do
41
+ sleep sleep_time
42
+ # TODO: error handling?
43
+ Puma::SdNotify.status(instance.status)
44
+ end
45
+ end
46
+ end
47
+
48
+ def status
49
+ if clustered?
50
+ messages = stats[:worker_status].map do |worker|
51
+ common_message(worker[:last_status])
52
+ end.join(',')
53
+
54
+ "Puma #{Puma::Const::VERSION}: cluster: #{booted_workers}/#{workers}, worker_status: [#{messages}]"
55
+ else
56
+ "Puma #{Puma::Const::VERSION}: worker: #{common_message(stats)}"
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def watchdog_sleep_time
63
+ usec = Integer(ENV["WATCHDOG_USEC"])
64
+
65
+ sec_f = usec / 1_000_000.0
66
+ # "It is recommended that a daemon sends a keep-alive notification message
67
+ # to the service manager every half of the time returned here."
68
+ sec_f / 2
69
+ end
70
+
71
+ def stats
72
+ Puma.stats_hash
73
+ end
74
+
75
+ def clustered?
76
+ stats.has_key?(:workers)
77
+ end
78
+
79
+ def workers
80
+ stats.fetch(:workers, 1)
81
+ end
82
+
83
+ def booted_workers
84
+ stats.fetch(:booted_workers, 1)
85
+ end
86
+
87
+ def common_message(stats)
88
+ "{ #{stats[:running]}/#{stats[:max_threads]} threads, #{stats[:pool_capacity]} available, #{stats[:backlog]} backlog }"
89
+ end
90
+ end
@@ -2,8 +2,23 @@
2
2
 
3
3
  require_relative '../rack/handler/puma'
4
4
 
5
- module Rack::Handler
6
- def self.default(options = {})
7
- Rack::Handler::Puma
5
+ # rackup was removed in Rack 3, it is now a separate gem
6
+ if Object.const_defined? :Rackup
7
+ module Rackup
8
+ module Handler
9
+ def self.default(options = {})
10
+ ::Rackup::Handler::Puma
11
+ end
12
+ end
8
13
  end
14
+ elsif Object.const_defined?(:Rack) && Rack::RELEASE < '3'
15
+ module Rack
16
+ module Handler
17
+ def self.default(options = {})
18
+ ::Rack::Handler::Puma
19
+ end
20
+ end
21
+ end
22
+ else
23
+ raise "Rack 3 must be used with the Rackup gem"
9
24
  end
data/lib/puma/request.rb CHANGED
@@ -53,8 +53,13 @@ module Puma
53
53
  socket = client.io # io may be a MiniSSL::Socket
54
54
  app_body = nil
55
55
 
56
+
56
57
  return false if closed_socket?(socket)
57
58
 
59
+ if client.http_content_length_limit_exceeded
60
+ return prepare_response(413, {}, ["Payload Too Large"], requests, client)
61
+ end
62
+
58
63
  normalize_env env, client
59
64
 
60
65
  env[PUMA_SOCKET] = socket
@@ -101,6 +106,7 @@ module Puma
101
106
  # is called
102
107
  res_body = app_body
103
108
 
109
+ # full hijack, app called env['rack.hijack']
104
110
  return :async if client.hijacked
105
111
 
106
112
  status = status.to_i
@@ -164,78 +170,87 @@ module Puma
164
170
  resp_info = str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
165
171
 
166
172
  close_body = false
173
+ response_hijack = nil
174
+ content_length = resp_info[:content_length]
175
+ keep_alive = resp_info[:keep_alive]
167
176
 
168
- # below converts app_body into body, dependent on app_body's characteristics, and
169
- # resp_info[:content_length] will be set if it can be determined
170
- if !resp_info[:content_length] && !resp_info[:transfer_encoding] && status != 204
171
- if res_body.respond_to?(:to_ary) && (array_body = res_body.to_ary)
172
- body = array_body
173
- resp_info[:content_length] = body.sum(&:bytesize)
174
- elsif res_body.is_a?(File) && res_body.respond_to?(:size)
175
- body = res_body
176
- resp_info[:content_length] = body.size
177
- elsif res_body.respond_to?(:to_path) && res_body.respond_to?(:each) &&
177
+ if res_body.respond_to?(:each) && !resp_info[:response_hijack]
178
+ # below converts app_body into body, dependent on app_body's characteristics, and
179
+ # content_length will be set if it can be determined
180
+ if !content_length && !resp_info[:transfer_encoding] && status != 204
181
+ if res_body.respond_to?(:to_ary) && (array_body = res_body.to_ary) &&
182
+ array_body.is_a?(Array)
183
+ body = array_body.compact
184
+ content_length = body.sum(&:bytesize)
185
+ elsif res_body.is_a?(File) && res_body.respond_to?(:size)
186
+ body = res_body
187
+ content_length = body.size
188
+ elsif res_body.respond_to?(:to_path) && File.readable?(fn = res_body.to_path)
189
+ body = File.open fn, 'rb'
190
+ content_length = body.size
191
+ close_body = true
192
+ else
193
+ body = res_body
194
+ end
195
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:to_path) &&
178
196
  File.readable?(fn = res_body.to_path)
179
197
  body = File.open fn, 'rb'
180
- resp_info[:content_length] = body.size
198
+ content_length = body.size
181
199
  close_body = true
200
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:filename) &&
201
+ res_body.respond_to?(:bytesize) && File.readable?(fn = res_body.filename)
202
+ # Sprockets::Asset
203
+ content_length = res_body.bytesize unless content_length
204
+ if (body_str = res_body.to_hash[:source])
205
+ body = [body_str]
206
+ else # avoid each and use a File object
207
+ body = File.open fn, 'rb'
208
+ close_body = true
209
+ end
182
210
  else
183
211
  body = res_body
184
212
  end
185
- elsif !res_body.is_a?(::File) && res_body.respond_to?(:to_path) && res_body.respond_to?(:each) &&
186
- File.readable?(fn = res_body.to_path)
187
- body = File.open fn, 'rb'
188
- resp_info[:content_length] = body.size
189
- close_body = true
190
- elsif !res_body.is_a?(::File) && res_body.respond_to?(:filename) && res_body.respond_to?(:each) &&
191
- res_body.respond_to?(:bytesize) && File.readable?(fn = res_body.filename)
192
- # Sprockets::Asset
193
- resp_info[:content_length] = res_body.bytesize unless resp_info[:content_length]
194
- if res_body.to_hash[:source] # use each to return @source
195
- body = res_body
196
- else # avoid each and use a File object
197
- body = File.open fn, 'rb'
198
- close_body = true
199
- end
200
213
  else
201
- body = res_body
214
+ # partial hijack, from Rack spec:
215
+ # Servers must ignore the body part of the response tuple when the
216
+ # rack.hijack response header is present.
217
+ response_hijack = resp_info[:response_hijack] || res_body
202
218
  end
203
219
 
204
220
  line_ending = LINE_END
205
221
 
206
- content_length = resp_info[:content_length]
207
- keep_alive = resp_info[:keep_alive]
208
-
209
- if res_body && !res_body.respond_to?(:each)
210
- response_hijack = res_body
211
- else
212
- response_hijack = resp_info[:response_hijack]
213
- end
214
-
215
222
  cork_socket socket
216
223
 
217
224
  if resp_info[:no_body]
218
- if content_length and status != 204
225
+ # 101 (Switching Protocols) doesn't return here or have content_length,
226
+ # it should be using `response_hijack`
227
+ unless status == 101
228
+ if content_length && status != 204
229
+ io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
230
+ end
231
+
232
+ io_buffer << LINE_END
233
+ fast_write_str socket, io_buffer.read_and_reset
234
+ socket.flush
235
+ return keep_alive
236
+ end
237
+ else
238
+ if content_length
219
239
  io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
240
+ chunked = false
241
+ elsif !response_hijack && resp_info[:allow_chunked]
242
+ io_buffer << TRANSFER_ENCODING_CHUNKED
243
+ chunked = true
220
244
  end
221
-
222
- io_buffer << LINE_END
223
- fast_write_str socket, io_buffer.read_and_reset
224
- socket.flush
225
- return keep_alive
226
- end
227
- if content_length
228
- io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
229
- chunked = false
230
- elsif !response_hijack and resp_info[:allow_chunked]
231
- io_buffer << TRANSFER_ENCODING_CHUNKED
232
- chunked = true
233
245
  end
234
246
 
235
247
  io_buffer << line_ending
236
248
 
249
+ # partial hijack, we write headers, then hand the socket to the app via
250
+ # response_hijack.call
237
251
  if response_hijack
238
252
  fast_write_str socket, io_buffer.read_and_reset
253
+ uncork_socket socket
239
254
  response_hijack.call socket
240
255
  return :async
241
256
  end
@@ -295,8 +310,8 @@ module Puma
295
310
  def fast_write_response(socket, body, io_buffer, chunked, content_length)
296
311
  if body.is_a?(::File) && body.respond_to?(:read)
297
312
  if chunked # would this ever happen?
298
- while part = body.read(BODY_LEN_MAX)
299
- io_buffer.append part.bytesize.to_s(16), LINE_END, part, LINE_END
313
+ while chunk = body.read(BODY_LEN_MAX)
314
+ io_buffer.append chunk.bytesize.to_s(16), LINE_END, chunk, LINE_END
300
315
  end
301
316
  fast_write_str socket, CLOSE_CHUNKED
302
317
  else
@@ -347,16 +362,22 @@ module Puma
347
362
  fast_write_str(socket, io_buffer.read_and_reset) unless io_buffer.length.zero?
348
363
  else
349
364
  # for enum bodies
350
- fast_write_str socket, io_buffer.read_and_reset
351
365
  if chunked
366
+ empty_body = true
352
367
  body.each do |part|
353
- next if (byte_size = part.bytesize).zero?
354
- fast_write_str socket, (byte_size.to_s(16) << LINE_END)
355
- fast_write_str socket, part
356
- fast_write_str socket, LINE_END
368
+ next if part.nil? || (byte_size = part.bytesize).zero?
369
+ empty_body = false
370
+ io_buffer.append byte_size.to_s(16), LINE_END, part, LINE_END
371
+ fast_write_str socket, io_buffer.read_and_reset
372
+ end
373
+ if empty_body
374
+ io_buffer << CLOSE_CHUNKED
375
+ fast_write_str socket, io_buffer.read_and_reset
376
+ else
377
+ fast_write_str socket, CLOSE_CHUNKED
357
378
  end
358
- fast_write_str socket, CLOSE_CHUNKED
359
379
  else
380
+ fast_write_str socket, io_buffer.read_and_reset
360
381
  body.each do |part|
361
382
  next if part.bytesize.zero?
362
383
  fast_write_str socket, part
@@ -476,7 +497,7 @@ module Puma
476
497
  to_add = nil
477
498
 
478
499
  env.each do |k,v|
479
- if k.start_with?("HTTP_") and k.include?(",") and k != "HTTP_TRANSFER,ENCODING"
500
+ if k.start_with?("HTTP_") && k.include?(",") && k != "HTTP_TRANSFER,ENCODING"
480
501
  if to_delete
481
502
  to_delete << k
482
503
  else
data/lib/puma/runner.rb CHANGED
@@ -198,5 +198,12 @@ module Puma
198
198
  }
199
199
  }
200
200
  end
201
+
202
+ # this method call should always be guarded by `@log_writer.debug?`
203
+ def debug_loaded_extensions(str)
204
+ @log_writer.debug "────────────────────────────────── #{str}"
205
+ re_ext = /\.#{RbConfig::CONFIG['DLEXT']}\z/i
206
+ $LOADED_FEATURES.grep(re_ext).each { |f| @log_writer.debug(" #{f}") }
207
+ end
201
208
  end
202
209
  end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module Puma
6
+ # The MIT License
7
+ #
8
+ # Copyright (c) 2017-2022 Agis Anastasopoulos
9
+ #
10
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
11
+ # this software and associated documentation files (the "Software"), to deal in
12
+ # the Software without restriction, including without limitation the rights to
13
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
14
+ # the Software, and to permit persons to whom the Software is furnished to do so,
15
+ # subject to the following conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be included in all
18
+ # copies or substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
22
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
23
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
24
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
25
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
+ #
27
+ # This is a copy of https://github.com/agis/ruby-sdnotify as of commit cca575c
28
+ # The only changes made was "rehoming" it within the Puma module to avoid
29
+ # namespace collisions and applying standard's code formatting style.
30
+ #
31
+ # SdNotify is a pure-Ruby implementation of sd_notify(3). It can be used to
32
+ # notify systemd about state changes. Methods of this package are no-op on
33
+ # non-systemd systems (eg. Darwin).
34
+ #
35
+ # The API maps closely to the original implementation of sd_notify(3),
36
+ # therefore be sure to check the official man pages prior to using SdNotify.
37
+ #
38
+ # @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
39
+ module SdNotify
40
+ # Exception raised when there's an error writing to the notification socket
41
+ class NotifyError < RuntimeError; end
42
+
43
+ READY = "READY=1"
44
+ RELOADING = "RELOADING=1"
45
+ STOPPING = "STOPPING=1"
46
+ STATUS = "STATUS="
47
+ ERRNO = "ERRNO="
48
+ MAINPID = "MAINPID="
49
+ WATCHDOG = "WATCHDOG=1"
50
+ FDSTORE = "FDSTORE=1"
51
+
52
+ def self.ready(unset_env=false)
53
+ notify(READY, unset_env)
54
+ end
55
+
56
+ def self.reloading(unset_env=false)
57
+ notify(RELOADING, unset_env)
58
+ end
59
+
60
+ def self.stopping(unset_env=false)
61
+ notify(STOPPING, unset_env)
62
+ end
63
+
64
+ # @param status [String] a custom status string that describes the current
65
+ # state of the service
66
+ def self.status(status, unset_env=false)
67
+ notify("#{STATUS}#{status}", unset_env)
68
+ end
69
+
70
+ # @param errno [Integer]
71
+ def self.errno(errno, unset_env=false)
72
+ notify("#{ERRNO}#{errno}", unset_env)
73
+ end
74
+
75
+ # @param pid [Integer]
76
+ def self.mainpid(pid, unset_env=false)
77
+ notify("#{MAINPID}#{pid}", unset_env)
78
+ end
79
+
80
+ def self.watchdog(unset_env=false)
81
+ notify(WATCHDOG, unset_env)
82
+ end
83
+
84
+ def self.fdstore(unset_env=false)
85
+ notify(FDSTORE, unset_env)
86
+ end
87
+
88
+ # @param [Boolean] true if the service manager expects watchdog keep-alive
89
+ # notification messages to be sent from this process.
90
+ #
91
+ # If the $WATCHDOG_USEC environment variable is set,
92
+ # and the $WATCHDOG_PID variable is unset or set to the PID of the current
93
+ # process
94
+ #
95
+ # @note Unlike sd_watchdog_enabled(3), this method does not mutate the
96
+ # environment.
97
+ def self.watchdog?
98
+ wd_usec = ENV["WATCHDOG_USEC"]
99
+ wd_pid = ENV["WATCHDOG_PID"]
100
+
101
+ return false if !wd_usec
102
+
103
+ begin
104
+ wd_usec = Integer(wd_usec)
105
+ rescue
106
+ return false
107
+ end
108
+
109
+ return false if wd_usec <= 0
110
+ return true if !wd_pid || wd_pid == $$.to_s
111
+
112
+ false
113
+ end
114
+
115
+ # Notify systemd with the provided state, via the notification socket, if
116
+ # any.
117
+ #
118
+ # Generally this method will be used indirectly through the other methods
119
+ # of the library.
120
+ #
121
+ # @param state [String]
122
+ # @param unset_env [Boolean]
123
+ #
124
+ # @return [Fixnum, nil] the number of bytes written to the notification
125
+ # socket or nil if there was no socket to report to (eg. the program wasn't
126
+ # started by systemd)
127
+ #
128
+ # @raise [NotifyError] if there was an error communicating with the systemd
129
+ # socket
130
+ #
131
+ # @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
132
+ def self.notify(state, unset_env=false)
133
+ sock = ENV["NOTIFY_SOCKET"]
134
+
135
+ return nil if !sock
136
+
137
+ ENV.delete("NOTIFY_SOCKET") if unset_env
138
+
139
+ begin
140
+ Addrinfo.unix(sock, :DGRAM).connect do |s|
141
+ s.close_on_exec = true
142
+ s.write(state)
143
+ end
144
+ rescue StandardError => e
145
+ raise NotifyError, "#{e.class}: #{e.message}", e.backtrace
146
+ end
147
+ end
148
+ end
149
+ end
data/lib/puma/server.rb CHANGED
@@ -95,6 +95,7 @@ module Puma
95
95
  @queue_requests = @options[:queue_requests]
96
96
  @max_fast_inline = @options[:max_fast_inline]
97
97
  @io_selector_backend = @options[:io_selector_backend]
98
+ @http_content_length_limit = @options[:http_content_length_limit]
98
99
 
99
100
  temp = !!(@options[:environment] =~ /\A(development|test)\z/)
100
101
  @leak_stack_on_error = @options[:environment] ? temp : true
@@ -334,6 +335,7 @@ module Puma
334
335
  drain += 1 if shutting_down?
335
336
  pool << Client.new(io, @binder.env(sock)).tap { |c|
336
337
  c.listener = sock
338
+ c.http_content_length_limit = @http_content_length_limit
337
339
  c.send(addr_send_name, addr_value) if addr_value
338
340
  }
339
341
  end
data/lib/puma/single.rb CHANGED
@@ -57,6 +57,8 @@ module Puma
57
57
 
58
58
  @events.fire_on_booted!
59
59
 
60
+ debug_loaded_extensions("Loaded Extensions:") if @log_writer.debug?
61
+
60
62
  begin
61
63
  server_thread.join
62
64
  rescue Interrupt