1
0
Fork 0
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:
Y 2021-08-18 21:33:11 +07:00 committed by GitHub
parent afc21c3bfc
commit af3675b2ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 151 additions and 1 deletions

View file

@ -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

View file

@ -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

View file

@ -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)

View 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