puma 2.11.3-java → 2.12.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.

@@ -1,4 +1,3 @@
1
- require 'rack'
2
1
  require 'stringio'
3
2
 
4
3
  require 'puma/thread_pool'
@@ -13,8 +12,6 @@ require 'puma/delegation'
13
12
  require 'puma/accept_nonblock'
14
13
  require 'puma/util'
15
14
 
16
- require 'puma/rack_patch'
17
-
18
15
  require 'puma/puma_http11'
19
16
 
20
17
  unless Puma.const_defined? "IOBuffer"
@@ -39,6 +36,7 @@ module Puma
39
36
  attr_accessor :max_threads
40
37
  attr_accessor :persistent_timeout
41
38
  attr_accessor :auto_trim_time
39
+ attr_accessor :reaping_time
42
40
  attr_accessor :first_data_timeout
43
41
 
44
42
  # Create a server for the rack app +app+.
@@ -60,6 +58,7 @@ module Puma
60
58
  @min_threads = 0
61
59
  @max_threads = 16
62
60
  @auto_trim_time = 1
61
+ @reaping_time = 1
63
62
 
64
63
  @thread = nil
65
64
  @thread_pool = nil
@@ -250,6 +249,14 @@ module Puma
250
249
  client.finish
251
250
  process_now = true
252
251
  end
252
+ rescue MiniSSL::SSLError => e
253
+ ssl_socket = client.io
254
+ addr = ssl_socket.peeraddr.last
255
+ cert = ssl_socket.peercert
256
+
257
+ client.close
258
+
259
+ @events.ssl_error self, addr, cert, e
253
260
  rescue HttpParserError => e
254
261
  client.write_400
255
262
  client.close
@@ -274,6 +281,10 @@ module Puma
274
281
  @reactor.run_in_thread
275
282
  end
276
283
 
284
+ if @reaping_time
285
+ @thread_pool.auto_reap!(@reaping_time)
286
+ end
287
+
277
288
  if @auto_trim_time
278
289
  @thread_pool.auto_trim!(@auto_trim_time)
279
290
  end
@@ -395,6 +406,16 @@ module Puma
395
406
  rescue ConnectionError
396
407
  # Swallow them. The ensure tries to close +client+ down
397
408
 
409
+ # SSL handshake error
410
+ rescue MiniSSL::SSLError => e
411
+ ssl_socket = client.io
412
+ addr = ssl_socket.peeraddr.last
413
+ cert = ssl_socket.peercert
414
+
415
+ close_socket = true
416
+
417
+ @events.ssl_error self, addr, cert, e
418
+
398
419
  # The client doesn't know HTTP well
399
420
  rescue HttpParserError => e
400
421
  client.write_400
@@ -457,16 +478,23 @@ module Puma
457
478
  #
458
479
 
459
480
  unless env.key?(REMOTE_ADDR)
460
- addr = client.peeraddr.last
481
+ begin
482
+ addr = client.peeraddr.last
483
+ rescue Errno::ENOTCONN
484
+ # Client disconnects can result in an inability to get the
485
+ # peeraddr from the socket; default to localhost.
486
+ addr = LOCALHOST_IP
487
+ end
461
488
 
462
489
  # Set unix socket addrs to localhost
463
- addr = "127.0.0.1" if addr.empty?
490
+ addr = LOCALHOST_IP if addr.empty?
464
491
 
465
492
  env[REMOTE_ADDR] = addr
466
493
  end
467
494
  end
468
495
 
469
496
  def default_server_port(env)
497
+ return PORT_443 if env[HTTPS_KEY] == 'on' || env[HTTPS_KEY] == 'https'
470
498
  env['HTTP_X_FORWARDED_PROTO'] == 'https' ? PORT_443 : PORT_80
471
499
  end
472
500
 
@@ -487,6 +515,10 @@ module Puma
487
515
 
488
516
  env[PUMA_SOCKET] = client
489
517
 
518
+ if env[HTTPS_KEY] && client.peercert
519
+ env[PUMA_PEERCERT] = client.peercert
520
+ end
521
+
490
522
  env[HIJACK_P] = true
491
523
  env[HIJACK] = req
492
524
 
@@ -610,15 +642,13 @@ module Puma
610
642
  fast_write client, lines.to_s
611
643
  return keep_alive
612
644
  end
613
-
614
- unless response_hijack
615
- if content_length
616
- lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
617
- chunked = false
618
- elsif allow_chunked
619
- lines << TRANSFER_ENCODING_CHUNKED
620
- chunked = true
621
- end
645
+
646
+ if content_length
647
+ lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
648
+ chunked = false
649
+ elsif !response_hijack and allow_chunked
650
+ lines << TRANSFER_ENCODING_CHUNKED
651
+ chunked = true
622
652
  end
623
653
 
624
654
  lines << line_ending
@@ -33,6 +33,7 @@ module Puma
33
33
  @workers = []
34
34
 
35
35
  @auto_trim = nil
36
+ @reaper = nil
36
37
 
37
38
  @mutex.synchronize do
38
39
  @min.times { spawn_thread }
@@ -101,7 +102,10 @@ module Puma
101
102
  end
102
103
  end
103
104
 
104
- block.call(work, *extra)
105
+ begin
106
+ block.call(work, *extra)
107
+ rescue Exception
108
+ end
105
109
  end
106
110
 
107
111
  mutex.synchronize do
@@ -155,6 +159,21 @@ module Puma
155
159
  end
156
160
  end
157
161
 
162
+ # If there are dead threads in the pool make them go away while decreasing
163
+ # spwaned counter so that new healty threads could be created again.
164
+ def reap
165
+ @mutex.synchronize do
166
+ dead_workers = @workers.reject(&:alive?)
167
+
168
+ dead_workers.each do |worker|
169
+ worker.kill
170
+ @spawned -= 1
171
+ end
172
+
173
+ @workers -= dead_workers
174
+ end
175
+ end
176
+
158
177
  class AutoTrim
159
178
  def initialize(pool, timeout)
160
179
  @pool = pool
@@ -184,6 +203,35 @@ module Puma
184
203
  @auto_trim.start!
185
204
  end
186
205
 
206
+ class Reaper
207
+ def initialize(pool, timeout)
208
+ @pool = pool
209
+ @timeout = timeout
210
+ @running = false
211
+ end
212
+
213
+ def start!
214
+ @running = true
215
+
216
+ @thread = Thread.new do
217
+ while @running
218
+ @pool.reap
219
+ sleep @timeout
220
+ end
221
+ end
222
+ end
223
+
224
+ def stop
225
+ @running = false
226
+ @thread.wakeup
227
+ end
228
+ end
229
+
230
+ def auto_reap!(timeout=5)
231
+ @reaper = Reaper.new(self, timeout)
232
+ @reaper.start!
233
+ end
234
+
187
235
  # Tell all threads in the pool to exit and wait for them to finish.
188
236
  #
189
237
  def shutdown
@@ -193,6 +241,7 @@ module Puma
193
241
  @not_full.broadcast
194
242
 
195
243
  @auto_trim.stop if @auto_trim
244
+ @reaper.stop if @reaper
196
245
  end
197
246
 
198
247
  # Use this instead of #each so that we don't stop in the middle
@@ -1,3 +1,15 @@
1
+ major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
2
+
3
+ if major == 1 && minor < 9
4
+ require 'puma/rack/backports/uri/common_18'
5
+ elsif major == 1 && minor == 9 && patch == 2 && RUBY_PATCHLEVEL <= 328 && RUBY_ENGINE != 'jruby'
6
+ require 'puma/rack/backports/uri/common_192'
7
+ elsif major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
8
+ require 'puma/rack/backports/uri/common_193'
9
+ else
10
+ require 'uri/common'
11
+ end
12
+
1
13
  module Puma
2
14
  module Util
3
15
  module_function
@@ -5,5 +17,116 @@ module Puma
5
17
  def pipe
6
18
  IO.pipe
7
19
  end
20
+
21
+ # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
22
+ # target encoding of the string returned, and it defaults to UTF-8
23
+ if defined?(::Encoding)
24
+ def unescape(s, encoding = Encoding::UTF_8)
25
+ URI.decode_www_form_component(s, encoding)
26
+ end
27
+ else
28
+ def unescape(s, encoding = nil)
29
+ URI.decode_www_form_component(s, encoding)
30
+ end
31
+ end
32
+ module_function :unescape
33
+
34
+ DEFAULT_SEP = /[&;] */n
35
+
36
+ # Stolen from Mongrel, with some small modifications:
37
+ # Parses a query string by breaking it up at the '&'
38
+ # and ';' characters. You can also use this to parse
39
+ # cookies by changing the characters used in the second
40
+ # parameter (which defaults to '&;').
41
+ def parse_query(qs, d = nil, &unescaper)
42
+ unescaper ||= method(:unescape)
43
+
44
+ params = {}
45
+
46
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
47
+ next if p.empty?
48
+ k, v = p.split('=', 2).map(&unescaper)
49
+
50
+ if cur = params[k]
51
+ if cur.class == Array
52
+ params[k] << v
53
+ else
54
+ params[k] = [cur, v]
55
+ end
56
+ else
57
+ params[k] = v
58
+ end
59
+ end
60
+
61
+ return params
62
+ end
63
+
64
+ # A case-insensitive Hash that preserves the original case of a
65
+ # header when set.
66
+ class HeaderHash < Hash
67
+ def self.new(hash={})
68
+ HeaderHash === hash ? hash : super(hash)
69
+ end
70
+
71
+ def initialize(hash={})
72
+ super()
73
+ @names = {}
74
+ hash.each { |k, v| self[k] = v }
75
+ end
76
+
77
+ def each
78
+ super do |k, v|
79
+ yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
80
+ end
81
+ end
82
+
83
+ def to_hash
84
+ hash = {}
85
+ each { |k,v| hash[k] = v }
86
+ hash
87
+ end
88
+
89
+ def [](k)
90
+ super(k) || super(@names[k.downcase])
91
+ end
92
+
93
+ def []=(k, v)
94
+ canonical = k.downcase
95
+ delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
96
+ @names[k] = @names[canonical] = k
97
+ super k, v
98
+ end
99
+
100
+ def delete(k)
101
+ canonical = k.downcase
102
+ result = super @names.delete(canonical)
103
+ @names.delete_if { |name,| name.downcase == canonical }
104
+ result
105
+ end
106
+
107
+ def include?(k)
108
+ @names.include?(k) || @names.include?(k.downcase)
109
+ end
110
+
111
+ alias_method :has_key?, :include?
112
+ alias_method :member?, :include?
113
+ alias_method :key?, :include?
114
+
115
+ def merge!(other)
116
+ other.each { |k, v| self[k] = v }
117
+ self
118
+ end
119
+
120
+ def merge(other)
121
+ hash = dup
122
+ hash.merge! other
123
+ end
124
+
125
+ def replace(other)
126
+ clear
127
+ other.each { |k, v| self[k] = v }
128
+ self
129
+ end
130
+ end
8
131
  end
9
132
  end
@@ -36,18 +36,15 @@ Gem::Specification.new do |s|
36
36
  s.specification_version = 3
37
37
 
38
38
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
39
- s.add_runtime_dependency(%q<rack>, ["< 2.0", ">= 1.1"])
40
39
  s.add_development_dependency(%q<rdoc>, ["~> 4.0"])
41
40
  s.add_development_dependency(%q<rake-compiler>, ["~> 0.8.0"])
42
41
  s.add_development_dependency(%q<hoe>, ["~> 3.6"])
43
42
  else
44
- s.add_dependency(%q<rack>, ["< 2.0", ">= 1.1"])
45
43
  s.add_dependency(%q<rdoc>, ["~> 4.0"])
46
44
  s.add_dependency(%q<rake-compiler>, ["~> 0.8.0"])
47
45
  s.add_dependency(%q<hoe>, ["~> 3.6"])
48
46
  end
49
47
  else
50
- s.add_dependency(%q<rack>, ["< 2.0", ">= 1.1"])
51
48
  s.add_dependency(%q<rdoc>, ["~> 4.0"])
52
49
  s.add_dependency(%q<rake-compiler>, ["~> 0.8.0"])
53
50
  s.add_dependency(%q<hoe>, ["~> 3.6"])
@@ -4,6 +4,16 @@ require 'puma'
4
4
  require 'puma/configuration'
5
5
 
6
6
  class TestConfigFile < Test::Unit::TestCase
7
+ def test_app_from_rackup
8
+ opts = {:rackup => "test/hello-bind.ru"}
9
+ conf = Puma::Configuration.new opts
10
+ conf.load
11
+
12
+ conf.app
13
+
14
+ assert_equal ["tcp://127.0.0.1:9292"], conf.options[:binds]
15
+ end
16
+
7
17
  def test_app_from_app_DSL
8
18
  opts = { :config_file => "test/config/app.rb" }
9
19
  conf = Puma::Configuration.new opts
@@ -94,7 +94,7 @@ class Http11ParserTest < Test::Unit::TestCase
94
94
  parser.reset
95
95
 
96
96
  # Raise exception if URI path length > 2048
97
- path = "/" + rand_data(2049, 100)
97
+ path = "/" + rand_data(3000, 100)
98
98
  http = "GET #{path} HTTP/1.1\r\n\r\n"
99
99
  assert_raises Puma::HttpParserError do
100
100
  parser.execute(req, http, 0)
@@ -369,7 +369,30 @@ class TestPumaServer < Test::Unit::TestCase
369
369
 
370
370
  assert_equal "HTTP/1.0 200 OK\r\nContent-Type: plain/text\r\nContent-Length: 5\r\n\r\nhello", data
371
371
  end
372
+
373
+ def test_http_10_partial_hijack_with_content_length
374
+ body_parts = ['abc', 'de']
375
+
376
+ @server.app = proc do |env|
377
+ hijack_lambda = proc do | io |
378
+ io.write(body_parts[0])
379
+ io.write(body_parts[1])
380
+ io.close
381
+ end
382
+ [200, {"Content-Length" => "5", 'rack.hijack' => hijack_lambda}, nil]
383
+ end
384
+
385
+ @server.add_tcp_listener @host, @port
386
+ @server.run
387
+
388
+ sock = TCPSocket.new @host, @port
389
+ sock << "GET / HTTP/1.0\r\nConnection: close\r\n\r\n"
372
390
 
391
+ data = sock.read
392
+
393
+ assert_equal "HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\nabcde", data
394
+ end
395
+
373
396
  def test_http_10_keep_alive_without_body
374
397
  @server.app = proc { |env| [204, {}, []] }
375
398
 
@@ -8,6 +8,16 @@ require 'puma/server'
8
8
 
9
9
  require 'net/https'
10
10
 
11
+ class SSLEventsHelper < ::Puma::Events
12
+ attr_accessor :addr, :cert, :error
13
+
14
+ def ssl_error(server, peeraddr, peercert, error)
15
+ self.addr = peeraddr
16
+ self.cert = peercert
17
+ self.error = error
18
+ end
19
+ end
20
+
11
21
  class TestPumaServerSSL < Test::Unit::TestCase
12
22
 
13
23
  def setup
@@ -28,7 +38,7 @@ class TestPumaServerSSL < Test::Unit::TestCase
28
38
 
29
39
  @ctx.verify_mode = Puma::MiniSSL::VERIFY_NONE
30
40
 
31
- @events = Puma::Events.new STDOUT, STDERR
41
+ @events = SSLEventsHelper.new STDOUT, STDERR
32
42
  @server = Puma::Server.new @app, @events
33
43
  @server.add_ssl_listener @host, @port, @ctx
34
44
  @server.run
@@ -95,6 +105,94 @@ class TestPumaServerSSL < Test::Unit::TestCase
95
105
  Net::HTTP::Get.new '/'
96
106
  end
97
107
  end
108
+ unless defined?(JRUBY_VERSION)
109
+ assert_match("wrong version number", @events.error.message) if @events.error
110
+ end
98
111
  end
99
112
 
100
- end
113
+ end
114
+
115
+ # client-side TLS authentication tests
116
+ unless defined?(JRUBY_VERSION)
117
+ class TestPumaServerSSLClient < Test::Unit::TestCase
118
+
119
+ def assert_ssl_client_error_match(error, subject=nil, &blk)
120
+ @port = 3212
121
+ @host = "127.0.0.1"
122
+
123
+ @app = lambda { |env| [200, {}, [env['rack.url_scheme']]] }
124
+
125
+ @ctx = Puma::MiniSSL::Context.new
126
+ @ctx.key = File.expand_path "../../examples/puma/client-certs/server.key", __FILE__
127
+ @ctx.cert = File.expand_path "../../examples/puma/client-certs/server.crt", __FILE__
128
+ @ctx.ca = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__
129
+ @ctx.verify_mode = Puma::MiniSSL::VERIFY_PEER | Puma::MiniSSL::VERIFY_FAIL_IF_NO_PEER_CERT
130
+
131
+ events = SSLEventsHelper.new STDOUT, STDERR
132
+ @server = Puma::Server.new @app, events
133
+ @server.add_ssl_listener @host, @port, @ctx
134
+ @server.run
135
+
136
+ @http = Net::HTTP.new @host, @port
137
+ @http.use_ssl = true
138
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
139
+
140
+ blk.call(@http)
141
+
142
+ client_error = false
143
+ begin
144
+ @http.start do
145
+ req = Net::HTTP::Get.new "/", {}
146
+ @http.request(req)
147
+ end
148
+ rescue OpenSSL::SSL::SSLError
149
+ client_error = true
150
+ end
151
+
152
+ sleep 0.1
153
+ assert_equal !!error, client_error
154
+ assert_match error, events.error.message if error
155
+ assert_equal @host, events.addr if error
156
+ assert_equal subject, events.cert.subject.to_s if subject
157
+ ensure
158
+ @server.stop(true)
159
+ end
160
+
161
+ def test_verify_fail_if_no_client_cert
162
+ assert_ssl_client_error_match 'peer did not return a certificate' do |http|
163
+ # nothing
164
+ end
165
+ end
166
+
167
+ def test_verify_fail_if_client_unknown_ca
168
+ assert_ssl_client_error_match('self signed certificate in certificate chain', '/DC=net/DC=puma/CN=ca-unknown') do |http|
169
+ key = File.expand_path "../../examples/puma/client-certs/client_unknown.key", __FILE__
170
+ crt = File.expand_path "../../examples/puma/client-certs/client_unknown.crt", __FILE__
171
+ http.key = OpenSSL::PKey::RSA.new File.read(key)
172
+ http.cert = OpenSSL::X509::Certificate.new File.read(crt)
173
+ http.ca_file = File.expand_path "../../examples/puma/client-certs/unknown_ca.crt", __FILE__
174
+ end
175
+ end
176
+
177
+ def test_verify_fail_if_client_expired_cert
178
+ assert_ssl_client_error_match('certificate has expired', '/DC=net/DC=puma/CN=client-expired') do |http|
179
+ key = File.expand_path "../../examples/puma/client-certs/client_expired.key", __FILE__
180
+ crt = File.expand_path "../../examples/puma/client-certs/client_expired.crt", __FILE__
181
+ http.key = OpenSSL::PKey::RSA.new File.read(key)
182
+ http.cert = OpenSSL::X509::Certificate.new File.read(crt)
183
+ http.ca_file = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__
184
+ end
185
+ end
186
+
187
+ def test_verify_client_cert
188
+ assert_ssl_client_error_match(nil) do |http|
189
+ key = File.expand_path "../../examples/puma/client-certs/client.key", __FILE__
190
+ crt = File.expand_path "../../examples/puma/client-certs/client.crt", __FILE__
191
+ http.key = OpenSSL::PKey::RSA.new File.read(key)
192
+ http.cert = OpenSSL::X509::Certificate.new File.read(crt)
193
+ http.ca_file = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__
194
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
195
+ end
196
+ end
197
+ end
198
+ end