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

Automatic SSL certificate provisioning for localhost #2610

Merged
merged 32 commits into from Aug 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
90353b0
Added puma to automatically use localhost gem to self signed https if…
Apr 24, 2021
5c1b82f
Update files according to rubocop rules
ye-lin-aung Apr 24, 2021
0c5ff76
Moved localhost authority from tcp_listener to ssl_listener
ye-lin-aung Apr 25, 2021
4b448ce
Reformatted codes according to rubocop rules
ye-lin-aung Apr 25, 2021
e3cace7
Fixed test case crashing in production env
ye-lin-aung Apr 25, 2021
944cc0d
Removed transform_keys to support ruby version < 2.5.0
ye-lin-aung Apr 25, 2021
0f33961
Changed wrong keystore_pass key given to context to keystore-pass
ye-lin-aung Apr 25, 2021
cc359e6
Remove accept_nonblock.rb since we are using MiniSSL Server
ye-lin-aung Apr 25, 2021
014c703
Removed localhost_authority test case running in JRUBY since localhos…
ye-lin-aung Apr 25, 2021
c72f8de
Reload Localhost authority if not loaded runned from puma cli
ye-lin-aung Apr 27, 2021
c44c1df
Memorise localhost authority object on init
ye-lin-aung Apr 27, 2021
c459993
Added readme for self-signed certificates
ye-lin-aung Apr 27, 2021
f3126e3
Removed jruby version
ye-lin-aung Jun 20, 2021
d9b9e20
Update readme.md
ye-lin-aung Jun 20, 2021
29c5763
Merged with master
ye-lin-aung Jun 20, 2021
05c4926
Added validations to check certificate
ye-lin-aung Jun 21, 2021
f3ced89
Remove ssl test running in no ssl implementations
ye-lin-aung Jun 21, 2021
54f07d1
Changed host in localhost authority
ye-lin-aung Jun 21, 2021
345e6cc
Update ssl events wrong arguments error
ye-lin-aung Jun 22, 2021
525b288
Update README.md
nateberkopec Jun 27, 2021
522c286
Update binder.rb
nateberkopec Jun 27, 2021
de84ffd
Update binder.rb
nateberkopec Jun 27, 2021
d8b0451
Update binder.rb
nateberkopec Jun 27, 2021
0699664
Update binder.rb
nateberkopec Jun 27, 2021
b07434c
Update request.rb
nateberkopec Jun 27, 2021
540a971
Update binder
ye-lin-aung Jun 28, 2021
c71b062
Removed running test in JRUBY
ye-lin-aung Jun 28, 2021
1ec13b6
Removed test for jruby
ye-lin-aung Jun 28, 2021
c09b3dc
Merge branch 'master' of github.com:puma/puma
ye-lin-aung Jun 28, 2021
fe16b29
Removed testing localhost authority file while in JRUBY
ye-lin-aung Jun 28, 2021
ca27c11
Removed unused variables
ye-lin-aung Jun 29, 2021
ca3fa6e
Updated readme
ye-lin-aung Jun 29, 2021
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 Gemfile
Expand Up @@ -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 changes: 15 additions & 0 deletions README.md
Expand Up @@ -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'
Copy link
Member

Choose a reason for hiding this comment

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

can you gem 'localhost', require: 'localhost/authority'? Does that work?

Copy link
Contributor

Choose a reason for hiding this comment

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

presumably in my app I should also put localhost in the development group in my Gemfile since the release doesn't depend on the gem... :)

run Sinatra::Application

...

$ puma -b 'ssl://localhost:9292' config.ru
```


#### Controlling SSL Cipher Suites

Expand Down
30 changes: 29 additions & 1 deletion lib/puma/binder.rb
Expand Up @@ -57,6 +57,7 @@ def initialize(events, conf = Configuration.new)

@envs = {}
@ios = []
localhost_authority
end

attr_reader :ios
Expand Down Expand Up @@ -227,7 +228,13 @@ def parse(binds, logger, log_msg = 'Listening')
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}"
Expand Down Expand Up @@ -285,6 +292,22 @@ def parse(binds, logger, log_msg = 'Listening')
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")
ye-lin-aung marked this conversation as resolved.
Show resolved Hide resolved
[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.
Expand All @@ -302,6 +325,7 @@ def add_tcp_listener(host, port, optimize_for_latency=true, backlog=1024)

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
Expand All @@ -323,6 +347,8 @@ def add_ssl_listener(host, port, ctx,
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|
Expand Down Expand Up @@ -350,6 +376,8 @@ def add_ssl_listener(host, port, ctx,

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)

Expand Down
106 changes: 106 additions & 0 deletions test/test_puma_localhost_authority.rb
@@ -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
nateberkopec marked this conversation as resolved.
Show resolved Hide resolved
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