Skip to content

Commit ae98043

Browse files
author
Brandon Black
committed
RUBY-565 adding support for ssl cert validation
* Added new SSL options to MongoClient, MongoReplicaSetClient and MongoShardedClient. * Added SSL certificate check to SSLSocket. * Updated pool tests to support these changes. The MongoDB auth spec explicity does not include any of these new SSL values (mostly because they're file paths) so there were no changes required the URI parser. SSL cert validation options must be set directly on the client.
1 parent 4b72d5d commit ae98043

11 files changed

+130
-56
lines changed

lib/mongo/mongo_client.rb

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ class MongoClient
3030
DEFAULT_HOST = 'localhost'
3131
DEFAULT_PORT = 27017
3232
DEFAULT_DB_NAME = 'test'
33-
GENERIC_OPTS = [:ssl, :auths, :logger, :connect]
33+
GENERIC_OPTS = [:auths, :logger, :connect]
3434
TIMEOUT_OPTS = [:timeout, :op_timeout, :connect_timeout]
35+
SSL_OPTS = [:ssl, :ssl_key, :ssl_cert, :ssl_verify, :ssl_ca_cert]
3536
POOL_OPTS = [:pool_size, :pool_timeout]
3637
READ_PREFERENCE_OPTS = [:read, :tag_sets, :secondary_acceptable_latency_ms]
3738
WRITE_CONCERN_OPTS = [:w, :j, :fsync, :wtimeout]
@@ -50,6 +51,7 @@ class MongoClient
5051
:pool_timeout,
5152
:primary_pool,
5253
:socket_class,
54+
:socket_opts,
5355
:op_timeout,
5456
:tag_sets,
5557
:acceptable_latency,
@@ -69,33 +71,43 @@ class MongoClient
6971
# MongoClient#arbiters. This is useful if your application needs to connect manually to nodes other
7072
# than the primary.
7173
#
72-
# @param [String] host
73-
# @param [Integer] port specify a port number here if only one host is being specified.
74-
#
75-
# @option opts [String, Integer, Symbol] :w (1) Set default number of nodes to which a write
76-
# should be acknowledged
77-
# @option opts [Boolean] :j (false) Set journal acknowledgement
78-
# @option opts [Integer] :wtimeout (nil) Set replica set acknowledgement timeout
79-
# @option opts [Boolean] :fsync (false) Set fsync acknowledgement.
80-
#
81-
# Notes about write concern options:
82-
# Write concern options are propagated to objects instantiated from this MongoClient.
83-
# These defaults can be overridden upon instantiation of any object by explicitly setting an options hash
84-
# on initialization.
85-
# @option opts [Boolean] :slave_ok (false) Must be set to +true+ when connecting
86-
# to a single, slave node.
87-
# @option opts [Logger, #debug] :logger (nil) A Logger instance for debugging driver ops. Note that
88-
# logging negatively impacts performance; therefore, it should not be used for high-performance apps.
89-
# @option opts [Integer] :pool_size (1) The maximum number of socket self.connections allowed per
90-
# connection pool. Note: this setting is relevant only for multi-threaded applications.
91-
# @option opts [Float] :timeout (5.0) When all of the self.connections a pool are checked out,
92-
# this is the number of seconds to wait for a new connection to be released before throwing an exception.
93-
# Note: this setting is relevant only for multi-threaded applications (which in Ruby are rare).
94-
# @option opts [Float] :op_timeout (nil) The number of seconds to wait for a read operation to time out.
95-
# Disabled by default.
96-
# @option opts [Float] :connect_timeout (nil) The number of seconds to wait before timing out a
97-
# connection attempt.
98-
# @option opts [Boolean] :ssl (false) If true, create the connection to the server using SSL.
74+
# @overload initialize(host, port, opts={})
75+
# @param [String] host hostname for the target MongoDB server.
76+
# @param [Integer] port specify a port number here if only one host is being specified.
77+
# @param [Hash] opts hash of optional settings and configuration values.
78+
#
79+
# @option opts [String, Integer, Symbol] :w (1) Set default number of nodes to which a write
80+
# should be acknowledged
81+
# @option opts [Boolean] :j (false) Set journal acknowledgement
82+
# @option opts [Integer] :wtimeout (nil) Set replica set acknowledgement timeout
83+
# @option opts [Boolean] :fsync (false) Set fsync acknowledgement.
84+
#
85+
# Notes about Write-Concern Options:
86+
# Write concern options are propagated to objects instantiated from this MongoClient.
87+
# These defaults can be overridden upon instantiation of any object by explicitly setting an options hash
88+
# on initialization.
89+
#
90+
# @option opts [Boolean] :ssl (false) If true, create the connection to the server using SSL.
91+
# @option opts [String] :ssl_cert (nil) The certificate file used to identify the local connection against MongoDB.
92+
# @option opts [String] :ssl_key (nil) The private keyfile used to identify the local connection against MongoDB.
93+
# If included with the :ssl_cert then only :ssl_cert is needed.
94+
# @option opts [Boolean] :ssl_verify (nil) Specifies whether or not peer certification validation should occur.
95+
# @option opts [String] :ssl_ca_cert (nil) The ca_certs file contains a set of concatenated "certification authority"
96+
# certificates, which are used to validate certificates passed from the other end of the connection.
97+
# Required for :ssl_verify.
98+
# @option opts [Boolean] :slave_ok (false) Must be set to +true+ when connecting
99+
# to a single, slave node.
100+
# @option opts [Logger, #debug] :logger (nil) A Logger instance for debugging driver ops. Note that
101+
# logging negatively impacts performance; therefore, it should not be used for high-performance apps.
102+
# @option opts [Integer] :pool_size (1) The maximum number of socket self.connections allowed per
103+
# connection pool. Note: this setting is relevant only for multi-threaded applications.
104+
# @option opts [Float] :timeout (5.0) When all of the self.connections a pool are checked out,
105+
# this is the number of seconds to wait for a new connection to be released before throwing an exception.
106+
# Note: this setting is relevant only for multi-threaded applications.
107+
# @option opts [Float] :op_timeout (nil) The number of seconds to wait for a read operation to time out.
108+
# Disabled by default.
109+
# @option opts [Float] :connect_timeout (nil) The number of seconds to wait before timing out a
110+
# connection attempt.
99111
#
100112
# @example localhost, 27017 (or <code>ENV["MONGODB_URI"]</code> if available)
101113
# MongoClient.new
@@ -459,9 +471,7 @@ def mongos?
459471
# @raise [ConnectionFailure] if unable to connect to any host or port.
460472
def connect
461473
close
462-
463474
config = check_is_master(host_port)
464-
465475
if config
466476
if config['ismaster'] == 1 || config['ismaster'] == true
467477
@read_primary = true
@@ -481,6 +491,7 @@ def connect
481491
if !connected?
482492
raise ConnectionFailure, "Failed to connect to a master node at #{host_port.join(":")}"
483493
end
494+
true
484495
end
485496
alias :reconnect :connect
486497

@@ -574,7 +585,7 @@ def check_is_master(node)
574585
begin
575586
host, port = *node
576587
config = nil
577-
socket = @socket_class.new(host, port, @op_timeout, @connect_timeout)
588+
socket = @socket_class.new(host, port, @op_timeout, @connect_timeout, @socket_opts)
578589
if @connect_timeout
579590
Timeout::timeout(@connect_timeout, OperationTimeout) do
580591
config = self['admin'].command({:ismaster => 1}, :socket => socket)
@@ -598,7 +609,8 @@ def valid_opts
598609
POOL_OPTS +
599610
READ_PREFERENCE_OPTS +
600611
WRITE_CONCERN_OPTS +
601-
TIMEOUT_OPTS
612+
TIMEOUT_OPTS +
613+
SSL_OPTS
602614
end
603615

604616
def check_opts(opts)
@@ -612,11 +624,32 @@ def check_opts(opts)
612624
# Parse option hash
613625
def setup(opts)
614626
@slave_ok = opts.delete(:slave_ok)
627+
@ssl = opts.delete(:ssl)
628+
@unix = @host ? @host.end_with?('.sock') : false
629+
630+
# if ssl options are present, but ssl is nil/false raise for misconfig
631+
ssl_opts = opts.keys.select { |k| k.to_s.start_with?('ssl') }
632+
if ssl_opts.size > 0 && !@ssl
633+
raise MongoArgumentError, "SSL has not been enabled (:ssl=false) " +
634+
"but the following SSL related options were " +
635+
"specified: #{ssl_opts.join(', ')}"
636+
end
615637

616-
@ssl = opts.delete(:ssl)
617-
@unix = @host ? @host.end_with?('.sock') : false
618-
638+
@socket_opts = {}
619639
if @ssl
640+
# construct ssl socket opts
641+
@socket_opts[:key] = opts.delete(:ssl_key)
642+
@socket_opts[:cert] = opts.delete(:ssl_cert)
643+
@socket_opts[:verify] = opts.delete(:ssl_verify)
644+
@socket_opts[:ca_cert] = opts.delete(:ssl_ca_cert)
645+
646+
# verify peer requires ca_cert, raise if only one is present
647+
if @socket_opts[:verify] && !@socket_opts[:ca_cert]
648+
raise MongoArgumentError,
649+
"If :ssl_verify_mode has been specified, then you must include " +
650+
":ssl_ca_cert in order to perform server validation."
651+
end
652+
620653
@socket_class = Mongo::SSLSocket
621654
elsif @unix
622655
@socket_class = Mongo::UNIXSocket
@@ -631,7 +664,8 @@ def setup(opts)
631664
@pool_size = opts.delete(:pool_size) || 1
632665
if opts[:timeout]
633666
warn "The :timeout option has been deprecated " +
634-
"and will be removed in the 2.0 release. Use :pool_timeout instead."
667+
"and will be removed in the 2.0 release. " +
668+
"Use :pool_timeout instead."
635669
end
636670
@pool_timeout = opts.delete(:pool_timeout) || opts.delete(:timeout) || 5.0
637671

lib/mongo/mongo_replica_set_client.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ class MongoReplicaSetClient < MongoClient
8383
# @option opts [Float] :connect_timeout (30) The number of seconds to wait before timing out a
8484
# connection attempt.
8585
# @option opts [Boolean] :ssl (false) If true, create the connection to the server using SSL.
86+
# @option opts [String] :ssl_cert (nil) The certificate file used to identify the local connection against MongoDB.
87+
# @option opts [String] :ssl_key (nil) The private keyfile used to identify the local connection against MongoDB.
88+
# If included with the :ssl_cert then only :ssl_cert is needed.
89+
# @option opts [Boolean] :ssl_verify (nil) Specifies whether or not peer certification validation should occur.
90+
# @option opts [String] :ssl_ca_cert (nil) The ca_certs file contains a set of concatenated "certification authority"
91+
# certificates, which are used to validate certificates passed from the other end of the connection.
92+
# Required for :ssl_verify.
8693
# @option opts [Boolean] :refresh_mode (false) Set this to :sync to periodically update the
8794
# state of the connection every :refresh_interval seconds. Replica set connection failures
8895
# will always trigger a complete refresh. This option is useful when you want to add new nodes

lib/mongo/mongo_sharded_client.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def initialize(*args)
6666
end
6767

6868
def valid_opts
69-
GENERIC_OPTS + SHARDED_CLUSTER_OPTS + READ_PREFERENCE_OPTS + WRITE_CONCERN_OPTS
69+
super + SHARDED_CLUSTER_OPTS
7070
end
7171

7272
def inspect

lib/mongo/util/node.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ def connect
6262
@node_mutex.synchronize do
6363
begin
6464
@socket = @client.socket_class.new(@host, @port,
65-
@client.op_timeout, @client.connect_timeout
66-
)
65+
@client.op_timeout,
66+
@client.connect_timeout,
67+
@client.socket_opts)
6768
rescue OperationTimeout, ConnectionFailure, OperationFailure, SocketError, SystemCallError, IOError => ex
6869
@client.log(:debug, "Failed connection to #{host_string} with #{ex.class}, #{ex.message}.")
6970
close

lib/mongo/util/pool.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,9 @@ def checkin(socket)
175175
# therefore, it runs within a mutex.
176176
def checkout_new_socket
177177
begin
178-
socket = @client.socket_class.new(@host, @port, @client.op_timeout, @client.connect_timeout)
178+
socket = @client.socket_class.new(@host, @port, @client.op_timeout,
179+
@client.connect_timeout,
180+
@client.socket_opts)
179181
socket.pool = self
180182
rescue => ex
181183
socket.close if socket

lib/mongo/util/ssl_socket.rb

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,45 @@ module Mongo
2424
class SSLSocket
2525
include SocketUtil
2626

27-
def initialize(host, port, op_timeout=nil, connect_timeout=nil)
28-
@op_timeout = op_timeout
27+
def initialize(host, port, op_timeout=nil, connect_timeout=nil, opts={})
28+
@pid = Process.pid
29+
@op_timeout = op_timeout
2930
@connect_timeout = connect_timeout
30-
@pid = Process.pid
3131

3232
@tcp_socket = ::TCPSocket.new(host, port)
3333
@tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
3434

35-
@socket = OpenSSL::SSL::SSLSocket.new(@tcp_socket)
36-
@socket.sync_close = true
35+
@context = OpenSSL::SSL::SSLContext.new
3736

38-
connect
37+
if opts[:cert]
38+
@context.cert = OpenSSL::X509::Certificate.new(File.open(opts[:cert]))
39+
end
40+
41+
if opts[:key]
42+
@context.key = OpenSSL::PKey::RSA.new(File.open(opts[:key]))
43+
end
44+
45+
if opts[:verify]
46+
@context.ca_file = opts[:ca_cert]
47+
@context.verify_mode = OpenSSL::SSL::VERIFY_PEER
48+
end
49+
50+
begin
51+
@socket = OpenSSL::SSL::SSLSocket.new(@tcp_socket, @context)
52+
@socket.sync_close = true
53+
connect
54+
rescue SSLError
55+
raise ConnectionFailure, "SSL handshake failed. MongoDB may " +
56+
"not be configured with SSL support."
57+
end
58+
59+
if opts[:verify]
60+
unless OpenSSL::SSL.verify_certificate_identity(@socket.peer_cert, host)
61+
raise ConnectionFailure, "SSL handshake failed. Hostname mismatch."
62+
end
63+
end
64+
65+
self
3966
end
4067

4168
def connect

lib/mongo/util/tcp_socket.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ module Mongo
2424
class TCPSocket
2525
include SocketUtil
2626

27-
def initialize(host, port, op_timeout=nil, connect_timeout=nil)
28-
@op_timeout = op_timeout
27+
def initialize(host, port, op_timeout=nil, connect_timeout=nil, opts={})
28+
@op_timeout = op_timeout
2929
@connect_timeout = connect_timeout
30-
@pid = Process.pid
30+
@pid = Process.pid
3131

3232
# TODO: Prefer ipv6 if server is ipv6 enabled
3333
@address = Socket.getaddrinfo(host, nil, Socket::AF_INET).first[3]
34-
@port = port
34+
@port = port
3535

3636
@socket_address = Socket.pack_sockaddr_in(@port, @address)
3737
@socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)

lib/mongo/util/unix_socket.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ module Mongo
2121
# sans Timeout::timeout
2222
#
2323
class UNIXSocket < TCPSocket
24-
def initialize(socket_path, port=:socket, op_timeout=nil, connect_timeout=nil)
25-
@op_timeout = op_timeout
24+
def initialize(socket_path, port=:socket, op_timeout=nil, connect_timeout=nil, opts={})
25+
@op_timeout = op_timeout
2626
@connect_timeout = connect_timeout
2727

28-
@address = socket_path
29-
@port = :socket # purposely override input
28+
@address = socket_path
29+
@port = :socket # purposely override input
3030

31-
@socket_address = Socket.pack_sockaddr_un(@address)
32-
@socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_STREAM, 0)
31+
@socket_address = Socket.pack_sockaddr_un(@address)
32+
@socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_STREAM, 0)
3333
connect
3434
end
3535
end

test/unit/node_test.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def setup
3636
@client.stubs(:connect_timeout).returns(nil)
3737
@client.expects(:log)
3838
@client.expects(:mongos?).returns(false)
39+
@client.stubs(:socket_opts)
3940

4041
assert node.connect
4142
node.config

test/unit/pool_manager_test.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class PoolManagerTest < Test::Unit::TestCase
3232
@client.stubs(:socket_class).returns(TCPSocket)
3333
@client.stubs(:mongos?).returns(false)
3434
@client.stubs(:[]).returns(@db)
35+
@client.stubs(:socket_opts)
3536

3637
@client.stubs(:replica_set_name).returns(nil)
3738
@client.stubs(:log)

0 commit comments

Comments
 (0)