Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SSL support for the control app #2046

Merged
merged 2 commits into from Oct 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice call. I'm not gonna review that commit because it looks like a straight copy-paste into the new class.


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