puma 3.11.3 → 4.0.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.

Files changed (54) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +61 -0
  3. data/README.md +41 -11
  4. data/docs/architecture.md +2 -1
  5. data/docs/deployment.md +24 -4
  6. data/docs/restart.md +5 -3
  7. data/docs/systemd.md +37 -9
  8. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  9. data/ext/puma_http11/mini_ssl.c +42 -5
  10. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  11. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +17 -4
  12. data/lib/puma.rb +8 -0
  13. data/lib/puma/app/status.rb +3 -2
  14. data/lib/puma/binder.rb +22 -10
  15. data/lib/puma/cli.rb +18 -7
  16. data/lib/puma/client.rb +54 -22
  17. data/lib/puma/cluster.rb +54 -15
  18. data/lib/puma/commonlogger.rb +2 -0
  19. data/lib/puma/configuration.rb +4 -1
  20. data/lib/puma/const.rb +8 -2
  21. data/lib/puma/control_cli.rb +23 -11
  22. data/lib/puma/convenient.rb +2 -0
  23. data/lib/puma/daemon_ext.rb +2 -0
  24. data/lib/puma/delegation.rb +2 -0
  25. data/lib/puma/detect.rb +2 -0
  26. data/lib/puma/dsl.rb +63 -11
  27. data/lib/puma/events.rb +2 -0
  28. data/lib/puma/io_buffer.rb +3 -6
  29. data/lib/puma/jruby_restart.rb +2 -0
  30. data/lib/puma/launcher.rb +15 -13
  31. data/lib/puma/minissl.rb +20 -4
  32. data/lib/puma/null_io.rb +2 -0
  33. data/lib/puma/plugin.rb +2 -0
  34. data/lib/puma/rack/builder.rb +2 -1
  35. data/lib/puma/reactor.rb +215 -30
  36. data/lib/puma/runner.rb +11 -2
  37. data/lib/puma/server.rb +63 -26
  38. data/lib/puma/single.rb +14 -3
  39. data/lib/puma/state_file.rb +2 -0
  40. data/lib/puma/tcp_logger.rb +2 -0
  41. data/lib/puma/thread_pool.rb +50 -5
  42. data/lib/puma/util.rb +2 -6
  43. data/lib/rack/handler/puma.rb +4 -0
  44. data/tools/jungle/README.md +10 -4
  45. data/tools/jungle/init.d/README.md +2 -0
  46. data/tools/jungle/init.d/puma +7 -7
  47. data/tools/jungle/init.d/run-puma +1 -1
  48. data/tools/jungle/rc.d/README.md +74 -0
  49. data/tools/jungle/rc.d/puma +61 -0
  50. data/tools/jungle/rc.d/puma.conf +10 -0
  51. metadata +23 -9
  52. data/lib/puma/compat.rb +0 -14
  53. data/lib/puma/java_io_buffer.rb +0 -45
  54. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
@@ -0,0 +1,72 @@
1
+ package org.jruby.puma;
2
+
3
+ import org.jruby.*;
4
+ import org.jruby.anno.JRubyMethod;
5
+ import org.jruby.runtime.ObjectAllocator;
6
+ import org.jruby.runtime.ThreadContext;
7
+ import org.jruby.runtime.builtin.IRubyObject;
8
+ import org.jruby.util.ByteList;
9
+
10
+ /**
11
+ * @author kares
12
+ */
13
+ public class IOBuffer extends RubyObject {
14
+
15
+ private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() {
16
+ public IRubyObject allocate(Ruby runtime, RubyClass klass) {
17
+ return new IOBuffer(runtime, klass);
18
+ }
19
+ };
20
+
21
+ public static void createIOBuffer(Ruby runtime) {
22
+ RubyModule mPuma = runtime.defineModule("Puma");
23
+ RubyClass cIOBuffer = mPuma.defineClassUnder("IOBuffer", runtime.getObject(), ALLOCATOR);
24
+ cIOBuffer.defineAnnotatedMethods(IOBuffer.class);
25
+ }
26
+
27
+ private static final int DEFAULT_SIZE = 4096;
28
+
29
+ final ByteList buffer = new ByteList(DEFAULT_SIZE);
30
+
31
+ IOBuffer(Ruby runtime, RubyClass klass) {
32
+ super(runtime, klass);
33
+ }
34
+
35
+ @JRubyMethod
36
+ public RubyInteger used(ThreadContext context) {
37
+ return context.runtime.newFixnum(buffer.getRealSize());
38
+ }
39
+
40
+ @JRubyMethod
41
+ public RubyInteger capacity(ThreadContext context) {
42
+ return context.runtime.newFixnum(buffer.unsafeBytes().length);
43
+ }
44
+
45
+ @JRubyMethod
46
+ public IRubyObject reset() {
47
+ buffer.setRealSize(0);
48
+ return this;
49
+ }
50
+
51
+ @JRubyMethod(name = { "to_s", "to_str" })
52
+ public RubyString to_s(ThreadContext context) {
53
+ return RubyString.newStringShared(context.runtime, buffer.unsafeBytes(), 0, buffer.getRealSize());
54
+ }
55
+
56
+ @JRubyMethod(name = "<<")
57
+ public IRubyObject add(IRubyObject str) {
58
+ addImpl(str.convertToString());
59
+ return this;
60
+ }
61
+
62
+ @JRubyMethod(rest = true)
63
+ public IRubyObject append(IRubyObject[] strs) {
64
+ for (IRubyObject str : strs) addImpl(str.convertToString());
65
+ return this;
66
+ }
67
+
68
+ private void addImpl(RubyString str) {
69
+ buffer.append(str.getByteList());
70
+ }
71
+
72
+ }
@@ -23,6 +23,7 @@ import javax.net.ssl.SSLPeerUnverifiedException;
23
23
  import javax.net.ssl.SSLSession;
24
24
  import java.io.FileInputStream;
25
25
  import java.io.IOException;
26
+ import java.nio.Buffer;
26
27
  import java.nio.ByteBuffer;
27
28
  import java.security.KeyManagementException;
28
29
  import java.security.KeyStore;
@@ -65,7 +66,7 @@ public class MiniSSL extends RubyObject {
65
66
 
66
67
  public void clear() { buffer.clear(); }
67
68
  public void compact() { buffer.compact(); }
68
- public void flip() { buffer.flip(); }
69
+ public void flip() { ((Buffer) buffer).flip(); }
69
70
  public boolean hasRemaining() { return buffer.hasRemaining(); }
70
71
  public int position() { return buffer.position(); }
71
72
 
@@ -89,7 +90,7 @@ public class MiniSSL extends RubyObject {
89
90
  public void resize(int newCapacity) {
90
91
  if (newCapacity > buffer.capacity()) {
91
92
  ByteBuffer dstTmp = ByteBuffer.allocate(newCapacity);
92
- buffer.flip();
93
+ flip();
93
94
  dstTmp.put(buffer);
94
95
  buffer = dstTmp;
95
96
  } else {
@@ -101,7 +102,7 @@ public class MiniSSL extends RubyObject {
101
102
  * Drains the buffer to a ByteList, or returns null for an empty buffer
102
103
  */
103
104
  public ByteList asByteList() {
104
- buffer.flip();
105
+ flip();
105
106
  if (!buffer.hasRemaining()) {
106
107
  buffer.clear();
107
108
  return null;
@@ -158,7 +159,13 @@ public class MiniSSL extends RubyObject {
158
159
  sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
159
160
  engine = sslCtx.createSSLEngine();
160
161
 
161
- String[] protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" };
162
+ String[] protocols;
163
+ if(miniSSLContext.callMethod(threadContext, "no_tlsv1").isTrue()) {
164
+ protocols = new String[] { "TLSv1.1", "TLSv1.2" };
165
+ } else {
166
+ protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" };
167
+ }
168
+
162
169
  engine.setEnabledProtocols(protocols);
163
170
  engine.setUseClientMode(false);
164
171
 
@@ -170,6 +177,12 @@ public class MiniSSL extends RubyObject {
170
177
  engine.setNeedClientAuth(true);
171
178
  }
172
179
 
180
+ IRubyObject sslCipherListObject = miniSSLContext.callMethod(threadContext, "ssl_cipher_list");
181
+ if (!sslCipherListObject.isNil()) {
182
+ String[] sslCipherList = sslCipherListObject.convertToString().asJavaString().split(",");
183
+ engine.setEnabledCipherSuites(sslCipherList);
184
+ }
185
+
173
186
  SSLSession session = engine.getSession();
174
187
  inboundNetData = new MiniSSLBuffer(session.getPacketBufferSize());
175
188
  outboundAppData = new MiniSSLBuffer(session.getApplicationBufferSize());
data/lib/puma.rb CHANGED
@@ -12,4 +12,12 @@ module Puma
12
12
  autoload :Const, 'puma/const'
13
13
  autoload :Server, 'puma/server'
14
14
  autoload :Launcher, 'puma/launcher'
15
+
16
+ def self.stats_object=(val)
17
+ @get_stats = val
18
+ end
19
+
20
+ def self.stats
21
+ @get_stats.stats
22
+ end
15
23
  end
@@ -1,3 +1,5 @@
1
+ require 'json'
2
+
1
3
  module Puma
2
4
  module App
3
5
  class Status
@@ -60,8 +62,7 @@ module Puma
60
62
  return rack_response(200, OK_STATUS)
61
63
 
62
64
  when /\/gc-stats$/
63
- json = "{" + GC.stat.map { |k, v| "\"#{k}\": #{v}" }.join(",") + "}"
64
- return rack_response(200, json)
65
+ return rack_response(200, GC.stat.to_json)
65
66
 
66
67
  when /\/stats$/
67
68
  return rack_response(200, @cli.stats)
data/lib/puma/binder.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
  require 'socket'
3
5
 
@@ -48,7 +50,13 @@ module Puma
48
50
 
49
51
  def close
50
52
  @ios.each { |i| i.close }
51
- @unix_paths.each { |i| File.unlink i }
53
+ @unix_paths.each do |i|
54
+ # Errno::ENOENT is intermittently raised
55
+ begin
56
+ File.unlink i
57
+ rescue Errno::ENOENT
58
+ end
59
+ end
52
60
  end
53
61
 
54
62
  def import_from_env
@@ -90,19 +98,19 @@ module Puma
90
98
  case uri.scheme
91
99
  when "tcp"
92
100
  if fd = @inherited_fds.delete(str)
93
- logger.log "* Inherited #{str}"
94
101
  io = inherit_tcp_listener uri.host, uri.port, fd
102
+ logger.log "* Inherited #{str}"
95
103
  elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
96
- logger.log "* Activated #{str}"
97
104
  io = inherit_tcp_listener uri.host, uri.port, sock
105
+ logger.log "* Activated #{str}"
98
106
  else
99
107
  params = Util.parse_query uri.query
100
108
 
101
109
  opt = params.key?('low_latency')
102
110
  bak = params.fetch('backlog', 1024).to_i
103
111
 
104
- logger.log "* Listening on #{str}"
105
112
  io = add_tcp_listener uri.host, uri.port, opt, bak
113
+ logger.log "* Listening on #{str}"
106
114
  end
107
115
 
108
116
  @listeners << [str, io] if io
@@ -110,14 +118,12 @@ module Puma
110
118
  path = "#{uri.host}#{uri.path}".gsub("%20", " ")
111
119
 
112
120
  if fd = @inherited_fds.delete(str)
113
- logger.log "* Inherited #{str}"
114
121
  io = inherit_unix_listener path, fd
122
+ logger.log "* Inherited #{str}"
115
123
  elsif sock = @activated_sockets.delete([ :unix, path ])
116
- logger.log "* Activated #{str}"
117
124
  io = inherit_unix_listener path, sock
125
+ logger.log "* Activated #{str}"
118
126
  else
119
- logger.log "* Listening on #{str}"
120
-
121
127
  umask = nil
122
128
  mode = nil
123
129
  backlog = 1024
@@ -139,6 +145,7 @@ module Puma
139
145
  end
140
146
 
141
147
  io = add_unix_listener path, umask, mode, backlog
148
+ logger.log "* Listening on #{str}"
142
149
  end
143
150
 
144
151
  @listeners << [str, io]
@@ -162,6 +169,7 @@ module Puma
162
169
  end
163
170
 
164
171
  ctx.keystore_pass = params['keystore-pass']
172
+ ctx.ssl_cipher_list = params['ssl_cipher_list'] if params['ssl_cipher_list']
165
173
  else
166
174
  unless params['key']
167
175
  @events.error "Please specify the SSL key via 'key='"
@@ -182,8 +190,11 @@ module Puma
182
190
  end
183
191
 
184
192
  ctx.ca = params['ca'] if params['ca']
193
+ ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
185
194
  end
186
195
 
196
+ ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
197
+
187
198
  if params['verify_mode']
188
199
  ctx.verify_mode = case params['verify_mode']
189
200
  when "peer"
@@ -202,11 +213,11 @@ module Puma
202
213
  logger.log "* Inherited #{str}"
203
214
  io = inherit_ssl_listener fd, ctx
204
215
  elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
205
- logger.log "* Activated #{str}"
206
216
  io = inherit_ssl_listener sock, ctx
217
+ logger.log "* Activated #{str}"
207
218
  else
208
- logger.log "* Listening on #{str}"
209
219
  io = add_ssl_listener uri.host, uri.port, ctx
220
+ logger.log "* Listening on #{str}"
210
221
  end
211
222
 
212
223
  @listeners << [str, io] if io
@@ -313,6 +324,7 @@ module Puma
313
324
  s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
314
325
  s.listen backlog
315
326
 
327
+
316
328
  ssl = MiniSSL::Server.new s, ctx
317
329
  env = @proto_env.dup
318
330
  env[HTTPS_KEY] = HTTPS
data/lib/puma/cli.rb CHANGED
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'optparse'
2
4
  require 'uri'
3
5
 
6
+ require 'puma'
4
7
  require 'puma/configuration'
5
8
  require 'puma/launcher'
6
9
  require 'puma/const'
@@ -83,6 +86,14 @@ module Puma
83
86
  raise UnsupportedOption
84
87
  end
85
88
 
89
+ def configure_control_url(command_line_arg)
90
+ if command_line_arg
91
+ @control_url = command_line_arg
92
+ elsif Puma.jruby?
93
+ unsupported "No default url available on JRuby"
94
+ end
95
+ end
96
+
86
97
  # Build the OptionParser object to handle the available options.
87
98
  #
88
99
 
@@ -97,13 +108,13 @@ module Puma
97
108
  file_config.load arg
98
109
  end
99
110
 
100
- o.on "--control URL", "The bind url to use for the control server",
101
- "Use 'auto' to use temp unix server" do |arg|
102
- if arg
103
- @control_url = arg
104
- elsif Puma.jruby?
105
- unsupported "No default url available on JRuby"
106
- end
111
+ o.on "--control-url URL", "The bind url to use for the control server. Use 'auto' to use temp unix server" do |arg|
112
+ configure_control_url(arg)
113
+ end
114
+
115
+ # alias --control-url for backwards-compatibility
116
+ o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
117
+ configure_control_url(arg)
107
118
  end
108
119
 
109
120
  o.on "--control-token TOKEN",
data/lib/puma/client.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class IO
2
4
  # We need to use this for a jruby work around on both 1.8 and 1.9.
3
5
  # So this either creates the constant (on 1.8), or harmlessly
@@ -21,6 +23,18 @@ module Puma
21
23
 
22
24
  class ConnectionError < RuntimeError; end
23
25
 
26
+ # An instance of this class represents a unique request from a client.
27
+ # For example a web request from a browser or from CURL. This
28
+ #
29
+ # An instance of `Puma::Client` can be used as if it were an IO object
30
+ # by the reactor, that's because the latter is expected to call `#to_io`
31
+ # on any non-IO objects it polls. For example nio4r internally calls
32
+ # `IO::try_convert` (which may call `#to_io`) when a new socket is
33
+ # registered.
34
+ #
35
+ # Instances of this class are responsible for knowing if
36
+ # the header and body are fully buffered via the `try_to_finish` method.
37
+ # They can be used to "time out" a response via the `timeout_at` reader.
24
38
  class Client
25
39
  include Puma::Const
26
40
  extend Puma::Delegation
@@ -41,6 +55,7 @@ module Puma
41
55
  @ready = false
42
56
 
43
57
  @body = nil
58
+ @body_read_start = nil
44
59
  @buffer = nil
45
60
  @tempfile = nil
46
61
 
@@ -51,6 +66,8 @@ module Puma
51
66
 
52
67
  @peerip = nil
53
68
  @remote_addr_header = nil
69
+
70
+ @body_remain = 0
54
71
  end
55
72
 
56
73
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
@@ -89,6 +106,8 @@ module Puma
89
106
  @tempfile = nil
90
107
  @parsed_bytes = 0
91
108
  @ready = false
109
+ @body_remain = 0
110
+ @peerip = nil
92
111
 
93
112
  if @buffer
94
113
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
@@ -101,9 +120,16 @@ module Puma
101
120
  end
102
121
 
103
122
  return false
104
- elsif fast_check &&
105
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
106
- return try_to_finish
123
+ else
124
+ begin
125
+ if fast_check &&
126
+ IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
127
+ return try_to_finish
128
+ end
129
+ rescue IOError
130
+ # swallow it
131
+ end
132
+
107
133
  end
108
134
  end
109
135
 
@@ -134,8 +160,11 @@ module Puma
134
160
  def decode_chunk(chunk)
135
161
  if @partial_part_left > 0
136
162
  if @partial_part_left <= chunk.size
137
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
163
+ if @partial_part_left > 2
164
+ @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
165
+ end
138
166
  chunk = chunk[@partial_part_left..-1]
167
+ @partial_part_left = 0
139
168
  else
140
169
  @body << chunk
141
170
  @partial_part_left -= chunk.size
@@ -157,9 +186,9 @@ module Puma
157
186
  if len == 0
158
187
  @body.rewind
159
188
  rest = io.read
189
+ rest = rest[2..-1] if rest.start_with?("\r\n")
160
190
  @buffer = rest.empty? ? nil : rest
161
- @requests_served += 1
162
- @ready = true
191
+ set_ready
163
192
  return true
164
193
  end
165
194
 
@@ -197,7 +226,7 @@ module Puma
197
226
  while true
198
227
  begin
199
228
  chunk = @io.read_nonblock(4096)
200
- rescue Errno::EAGAIN
229
+ rescue IO::WaitReadable
201
230
  return false
202
231
  rescue SystemCallError, IOError
203
232
  raise ConnectionError, "Connection error detected during read"
@@ -207,8 +236,7 @@ module Puma
207
236
  unless chunk
208
237
  @body.close
209
238
  @buffer = nil
210
- @requests_served += 1
211
- @ready = true
239
+ set_ready
212
240
  raise EOFError
213
241
  end
214
242
 
@@ -217,6 +245,8 @@ module Puma
217
245
  end
218
246
 
219
247
  def setup_body
248
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
249
+
220
250
  if @env[HTTP_EXPECT] == CONTINUE
221
251
  # TODO allow a hook here to check the headers before
222
252
  # going forward
@@ -241,8 +271,7 @@ module Puma
241
271
  unless cl
242
272
  @buffer = body.empty? ? nil : body
243
273
  @body = EmptyBody
244
- @requests_served += 1
245
- @ready = true
274
+ set_ready
246
275
  return true
247
276
  end
248
277
 
@@ -251,8 +280,7 @@ module Puma
251
280
  if remain <= 0
252
281
  @body = StringIO.new(body)
253
282
  @buffer = nil
254
- @requests_served += 1
255
- @ready = true
283
+ set_ready
256
284
  return true
257
285
  end
258
286
 
@@ -280,15 +308,14 @@ module Puma
280
308
  data = @io.read_nonblock(CHUNK_SIZE)
281
309
  rescue Errno::EAGAIN
282
310
  return false
283
- rescue SystemCallError, IOError
311
+ rescue SystemCallError, IOError, EOFError
284
312
  raise ConnectionError, "Connection error detected during read"
285
313
  end
286
314
 
287
315
  # No data means a closed socket
288
316
  unless data
289
317
  @buffer = nil
290
- @requests_served += 1
291
- @ready = true
318
+ set_ready
292
319
  raise EOFError
293
320
  end
294
321
 
@@ -324,8 +351,7 @@ module Puma
324
351
  # No data means a closed socket
325
352
  unless data
326
353
  @buffer = nil
327
- @requests_served += 1
328
- @ready = true
354
+ set_ready
329
355
  raise EOFError
330
356
  end
331
357
 
@@ -402,8 +428,7 @@ module Puma
402
428
  unless chunk
403
429
  @body.close
404
430
  @buffer = nil
405
- @requests_served += 1
406
- @ready = true
431
+ set_ready
407
432
  raise EOFError
408
433
  end
409
434
 
@@ -412,8 +437,7 @@ module Puma
412
437
  if remain <= 0
413
438
  @body.rewind
414
439
  @buffer = nil
415
- @requests_served += 1
416
- @ready = true
440
+ set_ready
417
441
  return true
418
442
  end
419
443
 
@@ -422,6 +446,14 @@ module Puma
422
446
  false
423
447
  end
424
448
 
449
+ def set_ready
450
+ if @body_read_start
451
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
452
+ end
453
+ @requests_served += 1
454
+ @ready = true
455
+ end
456
+
425
457
  def write_400
426
458
  begin
427
459
  @io << ERROR_400_RESPONSE