Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 85 additions & 27 deletions lib/drb/ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,79 @@ module DRb
#
# The URI for a DRb socket over SSL is:
# <code>drbssl://<host>:<port>?<option></code>. The option is optional
#
# SSL is configured by the :SSL* config options. See
# DRb::DRbSSLSocket::SSLConfig.new for them.
#
# You can also specify an OpenSSL::SSL::SSLContext by the :SSLContext
# config option. If the :SSLContext config option is specified, it's
# used as-is and the other :SSL* config options are ignored. This is
# useful to use features that aren't covered by the :SSL* config
# options:
#
# require 'drb/ssl'
# ctx = OpenSSL::SSL::SSLContext.new
# ctx.add_certificate(cert, key)
# ctx.groups = "X25519MLKEM768:X25519"
# DRb.start_service('drbssl://localhost:0', front, SSLContext: ctx)
class DRbSSLSocket < DRbTCPSocket

# BasicSSLConfig uses the given OpenSSL::SSL::SSLContext as-is for
# establishing a DRbSSLSocket connection.
#
# This is used when the :SSLContext config option is specified. An
# instance of this config can also be passed to DRbSSLSocket.new,
# DRbSSLSocket.open and DRbSSLSocket.open_server
class BasicSSLConfig
# Create a new DRb::DRbSSLSocket::BasicSSLConfig instance.
#
# +config+ is a Hash. +ssl_ctx+ is an OpenSSL::SSL::SSLContext
# used for connections.
def initialize(config, ssl_ctx)
@config = config
@ssl_ctx = ssl_ctx
end

# A convenience method to access the values like a Hash
def [](key)
@config[key]
end

# Connect to IO +tcp+, with context of the current certificate
# configuration
def connect(tcp)
ssl = ::OpenSSL::SSL::SSLSocket.new(tcp, @ssl_ctx)
ssl.sync = true
ssl.connect
ssl
end

# Accept connection to IO +tcp+, with context of the current certificate
# configuration
def accept(tcp)
ssl = ::OpenSSL::SSL::SSLSocket.new(tcp, @ssl_ctx)
ssl.sync = true
ssl.accept
ssl
end

# Does nothing. The certificate must be set in +ssl_ctx+.
def setup_certificate
end

# Does nothing. +ssl_ctx+ is used as-is.
def setup_ssl_context
end
end

# SSLConfig handles the needed SSL information for establishing a
# DRbSSLSocket connection, including generating the X509 / RSA pair.
#
# An instance of this config can be passed to DRbSSLSocket.new,
# DRbSSLSocket.open and DRbSSLSocket.open_server
#
# See DRb::DRbSSLSocket::SSLConfig.new for more details
class SSLConfig
class SSLConfig < BasicSSLConfig

# Default values for a SSLConfig instance.
#
Expand Down Expand Up @@ -133,33 +196,14 @@ class SSLConfig
# c.setup_certificate
#
def initialize(config)
@config = config
super(config, nil)
@cert = config[:SSLCertificate]
@pkey = config[:SSLPrivateKey]
@ssl_ctx = nil
end

# A convenience method to access the values like a Hash
def [](key);
@config[key] || DEFAULT[key]
end

# Connect to IO +tcp+, with context of the current certificate
# configuration
def connect(tcp)
ssl = ::OpenSSL::SSL::SSLSocket.new(tcp, @ssl_ctx)
ssl.sync = true
ssl.connect
ssl
end

# Accept connection to IO +tcp+, with context of the current certificate
# configuration
def accept(tcp)
ssl = OpenSSL::SSL::SSLSocket.new(tcp, @ssl_ctx)
ssl.sync = true
ssl.accept
ssl
super || DEFAULT[key]
end

# Ensures that :SSLCertificate and :SSLPrivateKey have been provided
Expand Down Expand Up @@ -218,6 +262,17 @@ def setup_ssl_context
end
end

def self.ensure_ssl_config(config)
return config if config.is_a?(BasicSSLConfig)

if ssl_ctx = config[:SSLContext]
BasicSSLConfig.new(config, ssl_ctx)
else
SSLConfig.new(config)
end
end
private_class_method :ensure_ssl_config

# Parse the dRuby +uri+ for an SSL connection.
#
# Expects drbssl://...
Expand All @@ -243,11 +298,12 @@ def self.parse_uri(uri) # :nodoc:
#
# +uri+ is the URI we are connected to,
# <code>'drbssl://localhost:0'</code> above, +config+ is our
# configuration. Either a Hash or DRb::DRbSSLSocket::SSLConfig
# configuration. Either a Hash, DRb::DRbSSLSocket::BasicSSLConfig or
# DRb::DRbSSLSocket::SSLConfig
def self.open(uri, config)
host, port, = parse_uri(uri)
soc = TCPSocket.open(host, port)
ssl_conf = SSLConfig::new(config)
ssl_conf = ensure_ssl_config(config)
ssl_conf.setup_ssl_context
ssl = ssl_conf.connect(soc)
self.new(uri, ssl, ssl_conf, true)
Expand All @@ -261,7 +317,8 @@ def self.open(uri, config)
#
# +uri+ is the URI we are connected to,
# <code>'drbssl://localhost:0'</code> above, +config+ is our
# configuration. Either a Hash or DRb::DRbSSLSocket::SSLConfig
# configuration. Either a Hash, DRb::DRbSSLSocket::BasicSSLConfig or
# DRb::DRbSSLSocket::SSLConfig
def self.open_server(uri, config)
uri = 'drbssl://:0' unless uri
host, port, = parse_uri(uri)
Expand All @@ -274,7 +331,7 @@ def self.open_server(uri, config)
port = soc.addr[1] if port == 0
@uri = "drbssl://#{host}:#{port}"

ssl_conf = SSLConfig.new(config)
ssl_conf = ensure_ssl_config(config)
ssl_conf.setup_certificate
ssl_conf.setup_ssl_context
self.new(@uri, soc, ssl_conf, false)
Expand All @@ -295,7 +352,8 @@ def self.uri_option(uri, config) # :nodoc:
#
# +uri+ is the URI we are connected to.
# +soc+ is the tcp socket we are bound to.
# +config+ is our configuration. Either a Hash or SSLConfig
# +config+ is our configuration. Either a Hash, BasicSSLConfig or
# SSLConfig
# +is_established+ is a boolean of whether +soc+ is currently established
#
# This is called automatically based on the DRb protocol.
Expand Down
54 changes: 54 additions & 0 deletions test/drb/test_drbssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,60 @@ def setup
end


class TestDRbSSLContext < Test::Unit::TestCase
def setup
if RUBY_PLATFORM.match?(/mswin|mingw/)
omit 'This test seems to randomly hang on Windows'
end
@cert, key = generate_certificate
server_ctx = OpenSSL::SSL::SSLContext.new
server_ctx.add_certificate(@cert, key)
@server = DRb::DRbServer.new('drbssl://localhost:0', nil,
{SSLContext: server_ctx})
begin
yield
ensure
@server.stop_service
end
end

def test_ssl_context
ctx = OpenSSL::SSL::SSLContext.new
client = DRb::DRbSSLSocket.open(@server.uri, {SSLContext: ctx})
begin
assert_equal([ctx, @cert.to_der],
[client.stream.context, client.stream.peer_cert.to_der])
ensure
client.close
end
end

def test_basic_ssl_config
ctx = OpenSSL::SSL::SSLContext.new
config = DRb::DRbSSLSocket::BasicSSLConfig.new({}, ctx)
client = DRb::DRbSSLSocket.open(@server.uri, config)
begin
assert_same(ctx, client.stream.context)
ensure
client.close
end
end

private
def generate_certificate
key = OpenSSL::PKey::RSA.new(2048)
cert = OpenSSL::X509::Certificate.new
name = OpenSSL::X509::Name.new([["CN", "localhost"]])
cert.subject = name
cert.issuer = name
cert.not_before = Time.now
cert.not_after = Time.now + 3600
cert.public_key = key
cert.sign(key, "SHA256")
[cert, key]
end
end

end

end
Loading