diff --git a/lib/drb/ssl.rb b/lib/drb/ssl.rb
index 096f355..7f53818 100644
--- a/lib/drb/ssl.rb
+++ b/lib/drb/ssl.rb
@@ -10,8 +10,71 @@ module DRb
#
# The URI for a DRb socket over SSL is:
# drbssl://:?. 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.
#
@@ -19,7 +82,7 @@ class DRbSSLSocket < DRbTCPSocket
# 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.
#
@@ -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
@@ -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://...
@@ -243,11 +298,12 @@ def self.parse_uri(uri) # :nodoc:
#
# +uri+ is the URI we are connected to,
# 'drbssl://localhost:0' 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)
@@ -261,7 +317,8 @@ def self.open(uri, config)
#
# +uri+ is the URI we are connected to,
# 'drbssl://localhost:0' 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)
@@ -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)
@@ -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.
diff --git a/test/drb/test_drbssl.rb b/test/drb/test_drbssl.rb
index f2d9a20..720e02f 100644
--- a/test/drb/test_drbssl.rb
+++ b/test/drb/test_drbssl.rb
@@ -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