mirror of
https://github.com/puma/puma.git
synced 2022-11-09 13:48:40 -05:00
Automatic SSL certificate provisioning for localhost (#2610)
* Added puma to automatically use localhost gem to self signed https if defined. * Update files according to rubocop rules * Moved localhost authority from tcp_listener to ssl_listener Use MiniSSLContext and MiniSSLServer for localhost authority's self-signed ceritifcates * Reformatted codes according to rubocop rules * Fixed test case crashing in production env * Removed transform_keys to support ruby version < 2.5.0 * Changed wrong keystore_pass key given to context to keystore-pass * Remove accept_nonblock.rb since we are using MiniSSL Server * Removed localhost_authority test case running in JRUBY since localhost_authority doesn't suppot JKS yet. * Reload Localhost authority if not loaded runned from puma cli * Memorise localhost authority object on init * Added readme for self-signed certificates * Removed jruby version * Update readme.md * Added validations to check certificate * Remove ssl test running in no ssl implementations * Changed host in localhost authority * Update ssl events wrong arguments error * Update README.md * Update binder.rb * Update binder.rb * Update binder.rb * Update binder.rb * Update request.rb * Update binder * Removed running test in JRUBY * Removed testing localhost authority file while in JRUBY * Removed unused variables * Updated readme Co-authored-by: Nate Berkopec <nate.berkopec@gmail.com>
This commit is contained in:
parent
afc21c3bfc
commit
af3675b2ee
4 changed files with 151 additions and 1 deletions
1
Gemfile
1
Gemfile
|
@ -23,3 +23,4 @@ if %w(2.2.7 2.2.8 2.2.9 2.2.10 2.3.4 2.4.1).include? RUBY_VERSION
|
|||
end
|
||||
|
||||
gem 'm'
|
||||
gem "localhost", require: false
|
||||
|
|
15
README.md
15
README.md
|
@ -187,6 +187,21 @@ Need a bit of security? Use SSL sockets:
|
|||
```
|
||||
$ puma -b 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert'
|
||||
```
|
||||
#### Self-signed SSL certificates (via _localhost_ gem, for development use):
|
||||
|
||||
Puma supports [localhost](https://github.com/socketry/localhost) gem for self-signed certificates. This is particularly useful if you want to use Puma with SSL locally, and self-signed certificates will work for your use-case. Currently, `localhost-authority` can be used only in MRI. To use [localhost](https://github.com/socketry/localhost), you have to `require "localhost/authority"`:
|
||||
|
||||
```ruby
|
||||
# config.ru
|
||||
require './app'
|
||||
require 'localhost/authority'
|
||||
run Sinatra::Application
|
||||
|
||||
...
|
||||
|
||||
$ puma -b 'ssl://localhost:9292' config.ru
|
||||
```
|
||||
|
||||
|
||||
#### Controlling SSL Cipher Suites
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ module Puma
|
|||
|
||||
@envs = {}
|
||||
@ios = []
|
||||
localhost_authority
|
||||
end
|
||||
|
||||
attr_reader :ios
|
||||
|
@ -227,7 +228,13 @@ module Puma
|
|||
raise "Puma compiled without SSL support" unless HAS_SSL
|
||||
|
||||
params = Util.parse_query uri.query
|
||||
ctx = MiniSSL::ContextBuilder.new(params, @events).context
|
||||
|
||||
# If key and certs are not defined and localhost gem is required.
|
||||
# localhost gem will be used for self signed
|
||||
# Load localhost authority if not loaded.
|
||||
ctx = localhost_authority && localhost_authority_context if params.empty?
|
||||
|
||||
ctx ||= MiniSSL::ContextBuilder.new(params, @events).context
|
||||
|
||||
if fd = @inherited_fds.delete(str)
|
||||
logger.log "* Inherited #{str}"
|
||||
|
@ -285,6 +292,22 @@ module Puma
|
|||
end
|
||||
end
|
||||
|
||||
def localhost_authority
|
||||
@localhost_authority ||= Localhost::Authority.fetch if defined?(Localhost::Authority) && !Puma::IS_JRUBY
|
||||
end
|
||||
|
||||
def localhost_authority_context
|
||||
return unless localhost_authority
|
||||
|
||||
key_path, crt_path = if [:key_path, :certificate_path].all? { |m| localhost_authority.respond_to?(m) }
|
||||
[localhost_authority.key_path, localhost_authority.certificate_path]
|
||||
else
|
||||
local_certificates_path = File.expand_path("~/.localhost")
|
||||
[File.join(local_certificates_path, "localhost.key"), File.join(local_certificates_path, "localhost.crt")]
|
||||
end
|
||||
MiniSSL::ContextBuilder.new({ "key" => key_path, "cert" => crt_path }, @events).context
|
||||
end
|
||||
|
||||
# Tell the server to listen on host +host+, port +port+.
|
||||
# If +optimize_for_latency+ is true (the default) then clients connecting
|
||||
# will be optimized for latency over throughput.
|
||||
|
@ -302,6 +325,7 @@ module Puma
|
|||
|
||||
host = host[1..-2] if host and host[0..0] == '['
|
||||
tcp_server = TCPServer.new(host, port)
|
||||
|
||||
if optimize_for_latency
|
||||
tcp_server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
||||
end
|
||||
|
@ -323,6 +347,8 @@ module Puma
|
|||
optimize_for_latency=true, backlog=1024)
|
||||
|
||||
raise "Puma compiled without SSL support" unless HAS_SSL
|
||||
# Puma will try to use local authority context if context is supplied nil
|
||||
ctx ||= localhost_authority_context
|
||||
|
||||
if host == "localhost"
|
||||
loopback_addresses.each do |addr|
|
||||
|
@ -350,6 +376,8 @@ module Puma
|
|||
|
||||
def inherit_ssl_listener(fd, ctx)
|
||||
raise "Puma compiled without SSL support" unless HAS_SSL
|
||||
# Puma will try to use local authority context if context is supplied nil
|
||||
ctx ||= localhost_authority_context
|
||||
|
||||
s = fd.kind_of?(::TCPServer) ? fd : ::TCPServer.for_fd(fd)
|
||||
|
||||
|
|
106
test/test_puma_localhost_authority.rb
Normal file
106
test/test_puma_localhost_authority.rb
Normal file
|
@ -0,0 +1,106 @@
|
|||
# Nothing in this file runs if Puma isn't compiled with ssl support
|
||||
#
|
||||
# helper is required first since it loads Puma, which needs to be
|
||||
# loaded so HAS_SSL is defined
|
||||
require_relative "helper"
|
||||
require "localhost/authority"
|
||||
|
||||
if ::Puma::HAS_SSL && !Puma::IS_JRUBY
|
||||
require "puma/minissl"
|
||||
require "puma/events"
|
||||
require "net/http"
|
||||
|
||||
class SSLEventsHelper < ::Puma::Events
|
||||
attr_accessor :addr, :cert, :error
|
||||
|
||||
def ssl_error(error, ssl_socket)
|
||||
self.error = error
|
||||
self.addr = ssl_socket.peeraddr.last rescue "<unknown>"
|
||||
self.cert = ssl_socket.peercert
|
||||
end
|
||||
end
|
||||
|
||||
# net/http (loaded in helper) does not necessarily load OpenSSL
|
||||
require "openssl" unless Object.const_defined? :OpenSSL
|
||||
end
|
||||
|
||||
class TestPumaLocalhostAuthority < Minitest::Test
|
||||
parallelize_me!
|
||||
def setup
|
||||
@http = nil
|
||||
@server = nil
|
||||
end
|
||||
|
||||
def teardown
|
||||
@http.finish if @http && @http.started?
|
||||
@server.stop(true) if @server
|
||||
end
|
||||
|
||||
# yields ctx to block, use for ctx setup & configuration
|
||||
def start_server
|
||||
@host = "localhost"
|
||||
app = lambda { |env| [200, {}, [env['rack.url_scheme']]] }
|
||||
|
||||
@events = SSLEventsHelper.new STDOUT, STDERR
|
||||
@server = Puma::Server.new app, @events
|
||||
@server.app = app
|
||||
@server.add_ssl_listener @host, 0,nil
|
||||
@http = Net::HTTP.new @host, @server.connected_ports[0]
|
||||
|
||||
@http.use_ssl = true
|
||||
# Disabling verification since its self signed
|
||||
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||
# @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||
|
||||
@server.run
|
||||
end
|
||||
|
||||
def test_localhost_authority_file_generated
|
||||
# Initiate server to create localhost authority
|
||||
unless File.exist?(File.join(Localhost::Authority.path,"localhost.key"))
|
||||
start_server
|
||||
end
|
||||
assert_equal(File.exist?(File.join(Localhost::Authority.path,"localhost.key")), true)
|
||||
assert_equal(File.exist?(File.join(Localhost::Authority.path,"localhost.crt")), true)
|
||||
end
|
||||
|
||||
end if ::Puma::HAS_SSL && !Puma::IS_JRUBY
|
||||
|
||||
class TestPumaSSLLocalhostAuthority < Minitest::Test
|
||||
def test_self_signed_by_localhost_authority
|
||||
@host = "localhost"
|
||||
|
||||
app = lambda { |env| [200, {}, [env['rack.url_scheme']]] }
|
||||
|
||||
@events = SSLEventsHelper.new STDOUT, STDERR
|
||||
|
||||
@server = Puma::Server.new app, @events
|
||||
@server.app = app
|
||||
|
||||
@server.add_ssl_listener @host, 0,nil
|
||||
|
||||
@http = Net::HTTP.new @host, @server.connected_ports[0]
|
||||
@http.use_ssl = true
|
||||
|
||||
OpenSSL::PKey::RSA.new File.read(File.join(Localhost::Authority.path,"localhost.key"))
|
||||
local_authority_crt = OpenSSL::X509::Certificate.new File.read(File.join(Localhost::Authority.path,"localhost.crt"))
|
||||
|
||||
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||
@server.run
|
||||
@cert = nil
|
||||
begin
|
||||
@http.start do
|
||||
req = Net::HTTP::Get.new "/", {}
|
||||
@http.request(req)
|
||||
@cert = @http.peer_cert
|
||||
end
|
||||
rescue OpenSSL::SSL::SSLError, EOFError, Errno::ECONNRESET
|
||||
# Errno::ECONNRESET TruffleRuby
|
||||
# closes socket if open, may not close on error
|
||||
@http.send :do_finish
|
||||
end
|
||||
sleep 0.1
|
||||
|
||||
assert_equal(@cert.to_pem, local_authority_crt.to_pem)
|
||||
end
|
||||
end if ::Puma::HAS_SSL && !Puma::IS_JRUBY
|
Loading…
Reference in a new issue