Skip to content

Commit

Permalink
Add SSL support for the control app (#2046)
Browse files Browse the repository at this point in the history
* Extract class for building SSL context

This commit extracts the `MiniSSL::Context` creation into its own
`MiniSSL::ContextBuilder` class along the same lines as in [#1989].

This will allow us to reuse this code for adding SSL support to the
control app (issue [#2015]). Since we will need the `MiniSSL` require
and check in both places, I moved that into the `ContextBuilder` class
as well.

[#1989]: #1989
[#2015]: #2015

* Add SSL support for the control app

This starts to address [#2015]. I think we will need to add SSL support
to the control cli as well.

[#2015]: #2015
  • Loading branch information
composerinteralia authored and nateberkopec committed Oct 21, 2019
1 parent b484bda commit f5ccd03
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 70 deletions.
1 change: 1 addition & 0 deletions History.md
Expand Up @@ -3,6 +3,7 @@
* Features
* Strip whitespace at end of HTTP headers (#2010)
* Optimize HTTP parser for JRuby (#2012)
* Add SSL support for the control app (#2046)

* Bugfixes
* Fix Errno::EINVAL when SSL is enabled and browser rejects cert (#1564)
Expand Down
60 changes: 2 additions & 58 deletions lib/puma/binder.rb
Expand Up @@ -5,6 +5,7 @@

require 'puma/const'
require 'puma/util'
require 'puma/minissl/context_builder'

module Puma
class Binder
Expand Down Expand Up @@ -154,64 +155,7 @@ def parse(binds, logger)
@listeners << [str, io]
when "ssl"
params = Util.parse_query uri.query
require 'puma/minissl'

MiniSSL.check

ctx = MiniSSL::Context.new

if defined?(JRUBY_VERSION)
unless params['keystore']
@events.error "Please specify the Java keystore via 'keystore='"
end

ctx.keystore = params['keystore']

unless params['keystore-pass']
@events.error "Please specify the Java keystore password via 'keystore-pass='"
end

ctx.keystore_pass = params['keystore-pass']
ctx.ssl_cipher_list = params['ssl_cipher_list'] if params['ssl_cipher_list']
else
unless params['key']
@events.error "Please specify the SSL key via 'key='"
end

ctx.key = params['key']

unless params['cert']
@events.error "Please specify the SSL cert via 'cert='"
end

ctx.cert = params['cert']

if ['peer', 'force_peer'].include?(params['verify_mode'])
unless params['ca']
@events.error "Please specify the SSL ca via 'ca='"
end
end

ctx.ca = params['ca'] if params['ca']
ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
end

ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
ctx.no_tlsv1_1 = true if params['no_tlsv1_1'] == 'true'

if params['verify_mode']
ctx.verify_mode = case params['verify_mode']
when "peer"
MiniSSL::VERIFY_PEER
when "force_peer"
MiniSSL::VERIFY_PEER | MiniSSL::VERIFY_FAIL_IF_NO_PEER_CERT
when "none"
MiniSSL::VERIFY_NONE
else
@events.error "Please specify a valid verify_mode="
MiniSSL::VERIFY_NONE
end
end
ctx = MiniSSL::ContextBuilder.new(params, @events).context

if fd = @inherited_fds.delete(str)
logger.log "* Inherited #{str}"
Expand Down
76 changes: 76 additions & 0 deletions lib/puma/minissl/context_builder.rb
@@ -0,0 +1,76 @@
module Puma
module MiniSSL
class ContextBuilder
def initialize(params, events)
require 'puma/minissl'
MiniSSL.check

@params = params
@events = events
end

def context
ctx = MiniSSL::Context.new

if defined?(JRUBY_VERSION)
unless params['keystore']
events.error "Please specify the Java keystore via 'keystore='"
end

ctx.keystore = params['keystore']

unless params['keystore-pass']
events.error "Please specify the Java keystore password via 'keystore-pass='"
end

ctx.keystore_pass = params['keystore-pass']
ctx.ssl_cipher_list = params['ssl_cipher_list'] if params['ssl_cipher_list']
else
unless params['key']
events.error "Please specify the SSL key via 'key='"
end

ctx.key = params['key']

unless params['cert']
events.error "Please specify the SSL cert via 'cert='"
end

ctx.cert = params['cert']

if ['peer', 'force_peer'].include?(params['verify_mode'])
unless params['ca']
events.error "Please specify the SSL ca via 'ca='"
end
end

ctx.ca = params['ca'] if params['ca']
ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
end

ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
ctx.no_tlsv1_1 = true if params['no_tlsv1_1'] == 'true'

if params['verify_mode']
ctx.verify_mode = case params['verify_mode']
when "peer"
MiniSSL::VERIFY_PEER
when "force_peer"
MiniSSL::VERIFY_PEER | MiniSSL::VERIFY_FAIL_IF_NO_PEER_CERT
when "none"
MiniSSL::VERIFY_NONE
else
events.error "Please specify a valid verify_mode="
MiniSSL::VERIFY_NONE
end
end

ctx
end

private

attr_reader :params, :events
end
end
end
7 changes: 7 additions & 0 deletions lib/puma/runner.rb
Expand Up @@ -2,6 +2,7 @@

require 'puma/server'
require 'puma/const'
require 'puma/minissl/context_builder'

module Puma
# Generic class that is used by `Puma::Cluster` and `Puma::Single` to
Expand Down Expand Up @@ -64,6 +65,12 @@ def start_control
control.max_threads = 1

case uri.scheme
when "ssl"
log "* Starting control server on #{str}"
params = Util.parse_query uri.query
ctx = MiniSSL::ContextBuilder.new(params, @events).context

control.add_ssl_listener uri.host, uri.port, ctx
when "tcp"
log "* Starting control server on #{str}"
control.add_tcp_listener uri.host, uri.port
Expand Down
13 changes: 13 additions & 0 deletions test/helpers/ssl.rb
@@ -0,0 +1,13 @@
module SSLHelper
def ssl_query
@ssl_query ||= if Puma.jruby?
@keystore = File.expand_path "../../../examples/puma/keystore.jks", __FILE__
@ssl_cipher_list = "TLS_DHE_RSA_WITH_DES_CBC_SHA,TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA"
"keystore=#{@keystore}&keystore-pass=pswd&ssl_cipher_list=#{@ssl_cipher_list}"
else
@cert = File.expand_path "../../../examples/puma/cert_puma.pem", __FILE__
@key = File.expand_path "../../../examples/puma/puma_keypair.pem", __FILE__
"key=#{@key}&cert=#{@cert}"
end
end
end
15 changes: 3 additions & 12 deletions test/test_binder.rb
@@ -1,11 +1,14 @@
# frozen_string_literal: true

require_relative "helper"
require_relative "helpers/ssl"

require "puma/binder"
require "puma/puma_http11"

class TestBinderBase < Minitest::Test
include SSLHelper

def setup
@events = Puma::Events.strings
@binder = Puma::Binder.new(@events)
Expand All @@ -16,18 +19,6 @@ def setup
def ssl_context_for_binder(binder = @binder)
binder.ios[0].instance_variable_get(:@ctx)
end

def ssl_query
@ssl_query ||= if Puma.jruby?
@keystore = File.expand_path "../../examples/puma/keystore.jks", __FILE__
@ssl_cipher_list = "TLS_DHE_RSA_WITH_DES_CBC_SHA,TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA"
"keystore=#{@keystore}&keystore-pass=pswd&ssl_cipher_list=#{@ssl_cipher_list}"
else
@cert = File.expand_path "../../examples/puma/cert_puma.pem", __FILE__
@key = File.expand_path "../../examples/puma/puma_keypair.pem", __FILE__
"key=#{@key}&cert=#{@cert}"
end
end
end

class TestBinder < TestBinderBase
Expand Down
39 changes: 39 additions & 0 deletions test/test_cli.rb
@@ -1,9 +1,12 @@
require_relative "helper"
require_relative "helpers/ssl"

require "puma/cli"
require "json"

class TestCLI < Minitest::Test
include SSLHelper

def setup
@environment = 'production'
@tmp_file = Tempfile.new("puma-test")
Expand Down Expand Up @@ -62,6 +65,42 @@ def test_control_for_tcp
t.join
end

def test_control_for_ssl
app_port = UniquePort.call
control_port = UniquePort.call
control_host = "127.0.0.1"
control_url = "ssl://#{control_host}:#{control_port}?#{ssl_query}"
token = "token"

cli = Puma::CLI.new ["-b", "tcp://127.0.0.1:#{app_port}",
"--control-url", control_url,
"--control-token", token,
"test/rackup/lobster.ru"], @events

t = Thread.new do
cli.run
end

wait_booted

body = ""
http = Net::HTTP.new control_host, control_port
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
http.start do
req = Net::HTTP::Get.new "/stats?token=#{token}", {}
body = http.request(req).body
end

expected_stats = /{ "started_at": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", "backlog": 0, "running": 0, "pool_capacity": 16, "max_threads": 16 }/
assert_match(expected_stats, body.split(/\r?\n/).last)
assert_match(expected_stats, Puma.stats)

ensure
cli.launcher.stop
t.join
end

def test_control_clustered
skip NO_FORK_MSG unless HAS_FORK
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
Expand Down

0 comments on commit f5ccd03

Please sign in to comment.