puma 5.0.0-java → 5.1.0-java

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1190 -574
  3. data/README.md +28 -20
  4. data/bin/puma-wild +3 -9
  5. data/docs/compile_options.md +19 -0
  6. data/docs/deployment.md +5 -6
  7. data/docs/fork_worker.md +2 -0
  8. data/docs/jungle/README.md +0 -4
  9. data/docs/jungle/rc.d/puma +2 -2
  10. data/docs/nginx.md +1 -1
  11. data/docs/restart.md +46 -23
  12. data/docs/systemd.md +25 -3
  13. data/ext/puma_http11/ext_help.h +1 -1
  14. data/ext/puma_http11/extconf.rb +4 -5
  15. data/ext/puma_http11/http11_parser.c +64 -64
  16. data/ext/puma_http11/mini_ssl.c +39 -37
  17. data/ext/puma_http11/puma_http11.c +25 -12
  18. data/lib/puma.rb +7 -4
  19. data/lib/puma/app/status.rb +44 -46
  20. data/lib/puma/binder.rb +48 -1
  21. data/lib/puma/cli.rb +4 -0
  22. data/lib/puma/client.rb +31 -80
  23. data/lib/puma/cluster.rb +39 -202
  24. data/lib/puma/cluster/worker.rb +176 -0
  25. data/lib/puma/cluster/worker_handle.rb +86 -0
  26. data/lib/puma/configuration.rb +20 -8
  27. data/lib/puma/const.rb +11 -3
  28. data/lib/puma/control_cli.rb +71 -70
  29. data/lib/puma/dsl.rb +67 -19
  30. data/lib/puma/error_logger.rb +2 -2
  31. data/lib/puma/events.rb +21 -3
  32. data/lib/puma/json.rb +96 -0
  33. data/lib/puma/launcher.rb +61 -12
  34. data/lib/puma/minissl.rb +8 -0
  35. data/lib/puma/puma_http11.jar +0 -0
  36. data/lib/puma/queue_close.rb +26 -0
  37. data/lib/puma/reactor.rb +79 -373
  38. data/lib/puma/request.rb +451 -0
  39. data/lib/puma/runner.rb +15 -21
  40. data/lib/puma/server.rb +193 -508
  41. data/lib/puma/single.rb +3 -2
  42. data/lib/puma/state_file.rb +5 -3
  43. data/lib/puma/systemd.rb +46 -0
  44. data/lib/puma/thread_pool.rb +22 -2
  45. data/lib/puma/util.rb +12 -0
  46. metadata +9 -6
  47. data/docs/jungle/upstart/README.md +0 -61
  48. data/docs/jungle/upstart/puma-manager.conf +0 -31
  49. data/docs/jungle/upstart/puma.conf +0 -69
  50. data/lib/puma/accept_nonblock.rb +0 -29
@@ -0,0 +1,451 @@
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_key [#to_s]
286
+ # @return [Boolean]
287
+ #
288
+ def illegal_header_key?(header_key)
289
+ !!(ILLEGAL_HEADER_KEY_REGEX =~ header_key.to_s)
290
+ end
291
+
292
+ # @param header_value [#to_s]
293
+ # @return [Boolean]
294
+ #
295
+ def illegal_header_value?(header_value)
296
+ !!(ILLEGAL_HEADER_VALUE_REGEX =~ header_value.to_s)
297
+ end
298
+ private :illegal_header_key?, :illegal_header_value?
299
+
300
+ # Fixup any headers with `,` in the name to have `_` now. We emit
301
+ # headers with `,` in them during the parse phase to avoid ambiguity
302
+ # with the `-` to `_` conversion for critical headers. But here for
303
+ # compatibility, we'll convert them back. This code is written to
304
+ # avoid allocation in the common case (ie there are no headers
305
+ # with `,` in their names), that's why it has the extra conditionals.
306
+ # @param env [Hash] see Puma::Client#env, from request, modifies in place
307
+ # @version 5.0.3
308
+ #
309
+ def req_env_post_parse(env)
310
+ to_delete = nil
311
+ to_add = nil
312
+
313
+ env.each do |k,v|
314
+ if k.start_with?("HTTP_") and k.include?(",") and k != "HTTP_TRANSFER,ENCODING"
315
+ if to_delete
316
+ to_delete << k
317
+ else
318
+ to_delete = [k]
319
+ end
320
+
321
+ unless to_add
322
+ to_add = {}
323
+ end
324
+
325
+ to_add[k.tr(",", "_")] = v
326
+ end
327
+ end
328
+
329
+ if to_delete
330
+ to_delete.each { |k| env.delete(k) }
331
+ env.merge! to_add
332
+ end
333
+ end
334
+ private :req_env_post_parse
335
+
336
+ # Used in the lambda for env[ `Puma::Const::EARLY_HINTS` ]
337
+ # @param headers [Hash] the headers returned by the Rack application
338
+ # @return [String]
339
+ # @version 5.0.3
340
+ #
341
+ def str_early_hints(headers)
342
+ eh_str = "HTTP/1.1 103 Early Hints\r\n".dup
343
+ headers.each_pair do |k, vs|
344
+ next if illegal_header_key?(k)
345
+
346
+ if vs.respond_to?(:to_s) && !vs.to_s.empty?
347
+ vs.to_s.split(NEWLINE).each do |v|
348
+ next if illegal_header_value?(v)
349
+ eh_str << "#{k}: #{v}\r\n"
350
+ end
351
+ else
352
+ eh_str << "#{k}: #{vs}\r\n"
353
+ end
354
+ end
355
+ "#{eh_str}\r\n".freeze
356
+ end
357
+ private :str_early_hints
358
+
359
+ # Processes and write headers to the IOBuffer.
360
+ # @param env [Hash] see Puma::Client#env, from request
361
+ # @param status [Integer] the status returned by the Rack application
362
+ # @param headers [Hash] the headers returned by the Rack application
363
+ # @param res_info [Hash] used to pass info between this method and #handle_request
364
+ # @param lines [Puma::IOBuffer] modified inn place
365
+ # @version 5.0.3
366
+ #
367
+ def str_headers(env, status, headers, res_info, lines)
368
+ line_ending = LINE_END
369
+ colon = COLON
370
+
371
+ http_11 = env[HTTP_VERSION] == HTTP_11
372
+ if http_11
373
+ res_info[:allow_chunked] = true
374
+ res_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase != CLOSE
375
+
376
+ # An optimization. The most common response is 200, so we can
377
+ # reply with the proper 200 status without having to compute
378
+ # the response header.
379
+ #
380
+ if status == 200
381
+ lines << HTTP_11_200
382
+ else
383
+ lines.append "HTTP/1.1 ", status.to_s, " ",
384
+ fetch_status_code(status), line_ending
385
+
386
+ res_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
387
+ end
388
+ else
389
+ res_info[:allow_chunked] = false
390
+ res_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase == KEEP_ALIVE
391
+
392
+ # Same optimization as above for HTTP/1.1
393
+ #
394
+ if status == 200
395
+ lines << HTTP_10_200
396
+ else
397
+ lines.append "HTTP/1.0 ", status.to_s, " ",
398
+ fetch_status_code(status), line_ending
399
+
400
+ res_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
401
+ end
402
+ end
403
+
404
+ # regardless of what the client wants, we always close the connection
405
+ # if running without request queueing
406
+ res_info[:keep_alive] &&= @queue_requests
407
+
408
+ res_info[:response_hijack] = nil
409
+
410
+ headers.each do |k, vs|
411
+ next if illegal_header_key?(k)
412
+
413
+ case k.downcase
414
+ when CONTENT_LENGTH2
415
+ next if illegal_header_value?(vs)
416
+ res_info[:content_length] = vs
417
+ next
418
+ when TRANSFER_ENCODING
419
+ res_info[:allow_chunked] = false
420
+ res_info[:content_length] = nil
421
+ when HIJACK
422
+ res_info[:response_hijack] = vs
423
+ next
424
+ when BANNED_HEADER_KEY
425
+ next
426
+ end
427
+
428
+ if vs.respond_to?(:to_s) && !vs.to_s.empty?
429
+ vs.to_s.split(NEWLINE).each do |v|
430
+ next if illegal_header_value?(v)
431
+ lines.append k, colon, v, line_ending
432
+ end
433
+ else
434
+ lines.append k, colon, line_ending
435
+ end
436
+ end
437
+
438
+ # HTTP/1.1 & 1.0 assume different defaults:
439
+ # - HTTP 1.0 assumes the connection will be closed if not specified
440
+ # - HTTP 1.1 assumes the connection will be kept alive if not specified.
441
+ # Only set the header if we're doing something which is not the default
442
+ # for this protocol version
443
+ if http_11
444
+ lines << CONNECTION_CLOSE if !res_info[:keep_alive]
445
+ else
446
+ lines << CONNECTION_KEEP_ALIVE if res_info[:keep_alive]
447
+ end
448
+ end
449
+ private :str_headers
450
+ end
451
+ end
@@ -54,13 +54,12 @@ module Puma
54
54
 
55
55
  app = Puma::App::Status.new @launcher, token
56
56
 
57
- control = Puma::Server.new app, @launcher.events
58
- control.min_threads = 0
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
 
63
- control.run
62
+ control.run thread_name: 'control'
64
63
  @control = control
65
64
  end
66
65
 
@@ -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}"
@@ -86,9 +86,16 @@ module Puma
86
86
  max_t = @options[:max_threads]
87
87
 
88
88
  log "Puma starting in #{mode} mode..."
89
- log "* Version #{Puma::Const::PUMA_VERSION} (#{ruby_engine}), codename: #{Puma::Const::CODE_NAME}"
90
- log "* Min threads: #{min_t}, max threads: #{max_t}"
91
- log "* Environment: #{ENV['RACK_ENV']}"
89
+ log "* Puma version: #{Puma::Const::PUMA_VERSION} (#{ruby_engine}) (\"#{Puma::Const::CODE_NAME}\")"
90
+ log "* Min threads: #{min_t}"
91
+ log "* Max threads: #{max_t}"
92
+ log "* Environment: #{ENV['RACK_ENV']}"
93
+
94
+ if mode == "cluster"
95
+ log "* Master PID: #{Process.pid}"
96
+ else
97
+ log "* PID: #{Process.pid}"
98
+ end
92
99
  end
93
100
 
94
101
  def redirected_io?
@@ -137,27 +144,14 @@ module Puma
137
144
  @launcher.binder.parse @options[:binds], self
138
145
  end
139
146
 
147
+ # @!attribute [r] app
140
148
  def app
141
149
  @app ||= @launcher.config.app
142
150
  end
143
151
 
144
152
  def start_server
145
- min_t = @options[:min_threads]
146
- max_t = @options[:max_threads]
147
-
148
153
  server = Puma::Server.new app, @launcher.events, @options
149
- server.min_threads = min_t
150
- server.max_threads = max_t
151
154
  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
155
  server
162
156
  end
163
157
  end