Skip to content

Commit ebc67bc

Browse files
committed
JAVA-2229: Separate the sslEnabled and socketFactory properties in MongoClientOptions.
The socketFactory should only get a default value based on the sslEnabled property if it wasn't already explicitly set. Consider it an error if the socket factory doesn't produce an instance of SSLSocket when SSL is set to enabled
1 parent ec10aec commit ebc67bc

File tree

4 files changed

+183
-66
lines changed

4 files changed

+183
-66
lines changed

driver-core/src/main/com/mongodb/connection/SocketStreamHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ static void initialize(final Socket socket, final ServerAddress address, final S
4040
if (settings.getSendBufferSize() > 0) {
4141
socket.setSendBufferSize(settings.getSendBufferSize());
4242
}
43-
if (sslSettings.isEnabled()) {
43+
if (sslSettings.isEnabled() || socket instanceof SSLSocket) {
4444
if (!(socket instanceof SSLSocket)) {
4545
throw new MongoInternalException("SSL is enabled but the socket is not an instance of javax.net.ssl.SSLSocket");
4646
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2016 MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package com.mongodb.connection
19+
20+
import com.mongodb.ClusterFixture
21+
import com.mongodb.MongoInternalException
22+
import spock.lang.IgnoreIf
23+
import spock.lang.Specification
24+
25+
import javax.net.SocketFactory
26+
import javax.net.ssl.SNIHostName
27+
import javax.net.ssl.SSLSocket
28+
import javax.net.ssl.SSLSocketFactory
29+
30+
import static com.mongodb.ClusterFixture.getPrimary
31+
import static java.util.concurrent.TimeUnit.MILLISECONDS
32+
import static java.util.concurrent.TimeUnit.SECONDS
33+
34+
class SocketStreamHelperSpecification extends Specification {
35+
36+
def 'should configure socket with settings()'() {
37+
given:
38+
Socket socket = SocketFactory.default.createSocket()
39+
def socketSettings = SocketSettings.builder()
40+
.readTimeout(10, SECONDS)
41+
.keepAlive(true)
42+
.build()
43+
44+
when:
45+
SocketStreamHelper.initialize(socket, getPrimary(), socketSettings, SslSettings.builder().build())
46+
47+
then:
48+
socket.getTcpNoDelay()
49+
socket.getKeepAlive()
50+
socket.getSoTimeout() == socketSettings.getReadTimeout(MILLISECONDS)
51+
52+
cleanup:
53+
socket?.close()
54+
}
55+
56+
def 'should connect socket()'() {
57+
given:
58+
Socket socket = SocketFactory.default.createSocket()
59+
60+
when:
61+
SocketStreamHelper.initialize(socket, getPrimary(), SocketSettings.builder().build(), SslSettings.builder().build())
62+
63+
then:
64+
socket.isConnected()
65+
66+
cleanup:
67+
socket?.close()
68+
}
69+
70+
@IgnoreIf({ !ClusterFixture.sslSettings.enabled || System.getProperty('java.version').startsWith('1.6.') })
71+
def 'should enable SSL options if socket is an instance of SSLSocket'() {
72+
given:
73+
SSLSocket socket = SSLSocketFactory.default.createSocket()
74+
75+
when:
76+
SocketStreamHelper.initialize(socket, getPrimary(), SocketSettings.builder().build(), sslSettings)
77+
78+
then:
79+
socket.getSSLParameters().endpointIdentificationAlgorithm == (sslSettings.invalidHostNameAllowed ? null : 'HTTPS')
80+
socket.getSSLParameters().getServerNames() == [new SNIHostName(getPrimary().getHost())]
81+
82+
cleanup:
83+
socket?.close()
84+
85+
where:
86+
sslSettings << [SslSettings.builder().enabled(true).build(),
87+
SslSettings.builder().enabled(false).build(),
88+
SslSettings.builder().enabled(true).invalidHostNameAllowed(true).build()]
89+
}
90+
91+
def 'should throw MongoInternalException is ssl is enabled and the socket is not an instance of SSLSocket'() {
92+
given:
93+
Socket socket = SocketFactory.default.createSocket()
94+
95+
when:
96+
SocketStreamHelper.initialize(socket, getPrimary(), SocketSettings.builder().build(), SslSettings.builder().enabled(true).build())
97+
98+
then:
99+
thrown(MongoInternalException)
100+
101+
cleanup:
102+
socket?.close()
103+
}
104+
}

driver/src/main/com/mongodb/MongoClientOptions.java

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2008-2015 MongoDB, Inc.
2+
* Copyright (c) 2008-2016 MongoDB, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -51,6 +51,9 @@
5151
@Immutable
5252
public class MongoClientOptions {
5353

54+
private static final SocketFactory DEFAULT_SSL_SOCKET_FACTORY = SSLSocketFactory.getDefault();
55+
private static final SocketFactory DEFAULT_SOCKET_FACTORY = SocketFactory.getDefault();
56+
5457
private final String description;
5558
private final String applicationName;
5659
private final ReadPreference readPreference;
@@ -572,7 +575,13 @@ public boolean isAlwaysUseMBeans() {
572575
* @return the socket factory
573576
*/
574577
public SocketFactory getSocketFactory() {
575-
return socketFactory;
578+
if (socketFactory != null) {
579+
return socketFactory;
580+
} else if (getSslSettings().isEnabled()) {
581+
return DEFAULT_SSL_SOCKET_FACTORY;
582+
} else {
583+
return DEFAULT_SOCKET_FACTORY;
584+
}
576585
}
577586

578587
/**
@@ -717,7 +726,7 @@ public boolean equals(final Object o) {
717726
: that.requiredReplicaSetName != null) {
718727
return false;
719728
}
720-
if (!socketFactory.getClass().equals(that.socketFactory.getClass())) {
729+
if (socketFactory != null ? !socketFactory.equals(that.socketFactory) : that.socketFactory != null) {
721730
return false;
722731
}
723732

@@ -758,7 +767,7 @@ public int hashCode() {
758767
result = 31 * result + (dbDecoderFactory != null ? dbDecoderFactory.hashCode() : 0);
759768
result = 31 * result + (dbEncoderFactory != null ? dbEncoderFactory.hashCode() : 0);
760769
result = 31 * result + (cursorFinalizerEnabled ? 1 : 0);
761-
result = 31 * result + socketFactory.getClass().hashCode();
770+
result = 31 * result + (socketFactory != null ? socketFactory.hashCode() : 0);
762771
return result;
763772
}
764773

@@ -846,7 +855,7 @@ public static class Builder {
846855
private String requiredReplicaSetName;
847856
private DBDecoderFactory dbDecoderFactory = DefaultDBDecoder.FACTORY;
848857
private DBEncoderFactory dbEncoderFactory = DefaultDBEncoder.FACTORY;
849-
private SocketFactory socketFactory = SocketFactory.getDefault();
858+
private SocketFactory socketFactory;
850859
private boolean cursorFinalizerEnabled = true;
851860

852861
/**
@@ -893,7 +902,7 @@ public Builder(final MongoClientOptions options) {
893902
requiredReplicaSetName = options.getRequiredReplicaSetName();
894903
dbDecoderFactory = options.getDbDecoderFactory();
895904
dbEncoderFactory = options.getDbEncoderFactory();
896-
socketFactory = options.getSocketFactory();
905+
socketFactory = options.socketFactory;
897906
cursorFinalizerEnabled = options.isCursorFinalizerEnabled();
898907
commandListeners.addAll(options.getCommandListeners());
899908
clusterListeners.addAll(options.getClusterListeners());
@@ -1072,17 +1081,23 @@ public Builder socketKeepAlive(final boolean socketKeepAlive) {
10721081
}
10731082

10741083
/**
1075-
* Sets whether to use SSL. Setting this to true will also set the socketFactory to {@code SSLSocketFactory.getDefault()} and
1076-
* setting it to false will set the socketFactory to {@code SocketFactory.getDefault()}
1084+
* Sets whether to use SSL.
1085+
*
1086+
* <p>If the socketFactory is unset, setting this to true will also set the socketFactory to
1087+
* {@link SSLSocketFactory#getDefault()} and setting it to false will set the socketFactory to
1088+
* {@link SocketFactory#getDefault()}</p>
1089+
*
1090+
* <p>If the socket factory is set and sslEnabled is also set, the socket factory must create instances of
1091+
* {@link javax.net.ssl.SSLSocket}. Otherwise, MongoClient will refuse to connect.</p>
10771092
*
10781093
* @param sslEnabled set to true if using SSL
10791094
* @return {@code this}
10801095
* @see MongoClientOptions#isSslEnabled()
1096+
* @see MongoClientOptions#getSocketFactory()
10811097
* @since 3.0
10821098
*/
10831099
public Builder sslEnabled(final boolean sslEnabled) {
10841100
this.sslEnabled = sslEnabled;
1085-
this.socketFactory(sslEnabled ? SSLSocketFactory.getDefault() : SocketFactory.getDefault());
10861101
return this;
10871102
}
10881103

@@ -1216,9 +1231,6 @@ public Builder addServerMonitorListener(final ServerMonitorListener serverMonito
12161231
* @see MongoClientOptions#getSocketFactory()
12171232
*/
12181233
public Builder socketFactory(final SocketFactory socketFactory) {
1219-
if (socketFactory == null) {
1220-
throw new IllegalArgumentException("null is not a legal value");
1221-
}
12221234
this.socketFactory = socketFactory;
12231235
return this;
12241236
}

driver/src/test/unit/com/mongodb/MongoClientOptionsSpecification.groovy

Lines changed: 54 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2008-2015 MongoDB, Inc.
2+
* Copyright (c) 2008-2016 MongoDB, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the 'License');
55
* you may not use this file except in compliance with the License.
@@ -142,6 +142,7 @@ class MongoClientOptionsSpecification extends Specification {
142142
def 'should build with set options'() {
143143
given:
144144
def encoderFactory = new MyDBEncoderFactory()
145+
def socketFactory = SSLSocketFactory.getDefault()
145146
def options = MongoClientOptions.builder()
146147
.description('test')
147148
.applicationName('appName')
@@ -157,6 +158,7 @@ class MongoClientOptionsSpecification extends Specification {
157158
.maxConnectionLifeTime(400)
158159
.threadsAllowedToBlockForConnectionMultiplier(2)
159160
.socketKeepAlive(true)
161+
.socketFactory(socketFactory)
160162
.sslEnabled(true)
161163
.sslInvalidHostNameAllowed(true)
162164
.dbDecoderFactory(LazyDBDecoder.FACTORY)
@@ -185,6 +187,7 @@ class MongoClientOptionsSpecification extends Specification {
185187
options.getSocketTimeout() == 700
186188
options.getThreadsAllowedToBlockForConnectionMultiplier() == 2
187189
options.isSocketKeepAlive()
190+
options.socketFactory == socketFactory
188191
options.isSslEnabled()
189192
options.isSslInvalidHostNameAllowed()
190193
options.getDbDecoderFactory() == LazyDBDecoder.FACTORY
@@ -212,23 +215,25 @@ class MongoClientOptionsSpecification extends Specification {
212215
}
213216

214217
@IgnoreIf({ System.getProperty('java.version').startsWith('1.6.') })
215-
def 'should set sslEnabled based on socketFactory'() {
216-
given:
218+
def 'should get socketFactory based on sslEnabled'() {
219+
when:
217220
MongoClientOptions.Builder builder = MongoClientOptions.builder()
218-
SocketFactory socketFactory = SSLSocketFactory.getDefault()
219221

220-
when:
221-
builder.socketFactory(socketFactory)
222222
then:
223-
builder.build().getSocketFactory() == socketFactory
224-
builder.sslEnabled(false)
225-
builder.build().getSocketFactory() != null
226-
!(builder.build().getSocketFactory() instanceof SSLSocketFactory)
223+
builder.build().getSocketFactory() == MongoClientOptions.DEFAULT_SOCKET_FACTORY
227224

228225
when:
229226
builder.sslEnabled(true)
227+
230228
then:
231-
builder.build().getSocketFactory() instanceof SSLSocketFactory
229+
builder.build().getSocketFactory() == MongoClientOptions.DEFAULT_SSL_SOCKET_FACTORY
230+
231+
when:
232+
def socketFactory = Mock(SocketFactory)
233+
builder.socketFactory(socketFactory)
234+
235+
then:
236+
builder.build().getSocketFactory() == socketFactory
232237
}
233238

234239
def 'should be easy to create new options from existing'() {
@@ -493,48 +498,44 @@ class MongoClientOptionsSpecification extends Specification {
493498

494499
def 'builder should copy all values from the existing MongoClientOptions'() {
495500
given:
496-
def options = Mock(MongoClientOptions)
497-
498-
499-
when:
500-
MongoClientOptions.builder(options)
501-
502-
then:
503-
1 * options.isAlwaysUseMBeans()
504-
1 * options.getCodecRegistry()
505-
1 * options.getConnectionsPerHost()
506-
1 * options.getConnectTimeout()
507-
1 * options.isCursorFinalizerEnabled()
508-
1 * options.getDbDecoderFactory()
509-
1 * options.getDbEncoderFactory()
510-
1 * options.getDescription()
511-
1 * options.getApplicationName()
512-
1 * options.getHeartbeatConnectTimeout()
513-
1 * options.getHeartbeatFrequency()
514-
1 * options.getHeartbeatSocketTimeout()
515-
1 * options.getLocalThreshold()
516-
1 * options.getMaxConnectionIdleTime()
517-
1 * options.getMaxConnectionLifeTime()
518-
1 * options.getMaxWaitTime()
519-
1 * options.getMinConnectionsPerHost()
520-
1 * options.getMinHeartbeatFrequency()
521-
1 * options.getReadPreference()
522-
1 * options.getRequiredReplicaSetName()
523-
1 * options.getServerSelectionTimeout()
524-
1 * options.getSocketFactory()
525-
1 * options.isSocketKeepAlive()
526-
1 * options.getSocketTimeout()
527-
1 * options.isSslEnabled()
528-
1 * options.isSslInvalidHostNameAllowed()
529-
1 * options.getThreadsAllowedToBlockForConnectionMultiplier()
530-
1 * options.getWriteConcern()
531-
1 * options.getReadConcern()
532-
1 * options.getCommandListeners() >> Collections.unmodifiableList(Collections.emptyList())
533-
1 * options.getClusterListeners() >> Collections.unmodifiableList(Collections.emptyList())
534-
1 * options.getServerListeners() >> Collections.unmodifiableList(Collections.emptyList())
535-
1 * options.getServerMonitorListeners() >> Collections.unmodifiableList(Collections.emptyList())
536-
537-
0 * options._ // Ensure no other interactions
501+
def options = MongoClientOptions.builder()
502+
.description('test')
503+
.applicationName('appName')
504+
.readPreference(ReadPreference.secondary())
505+
.writeConcern(WriteConcern.JOURNALED)
506+
.minConnectionsPerHost(30)
507+
.connectionsPerHost(500)
508+
.connectTimeout(100)
509+
.socketTimeout(700)
510+
.serverSelectionTimeout(150)
511+
.maxWaitTime(200)
512+
.maxConnectionIdleTime(300)
513+
.maxConnectionLifeTime(400)
514+
.threadsAllowedToBlockForConnectionMultiplier(2)
515+
.socketKeepAlive(true)
516+
.sslEnabled(true)
517+
.sslInvalidHostNameAllowed(true)
518+
.socketFactory(SSLSocketFactory.getDefault())
519+
.dbDecoderFactory(LazyDBDecoder.FACTORY)
520+
.heartbeatFrequency(5)
521+
.minHeartbeatFrequency(11)
522+
.heartbeatConnectTimeout(15)
523+
.heartbeatSocketTimeout(20)
524+
.localThreshold(25)
525+
.requiredReplicaSetName('test')
526+
.cursorFinalizerEnabled(false)
527+
.dbEncoderFactory(new MyDBEncoderFactory())
528+
.addCommandListener(Mock(CommandListener))
529+
.addClusterListener(Mock(ClusterListener))
530+
.addServerListener(Mock(ServerListener))
531+
.addServerMonitorListener(Mock(ServerMonitorListener))
532+
.build()
533+
534+
when:
535+
def copy = MongoClientOptions.builder(options).build()
536+
537+
then:
538+
copy == options
538539
}
539540

540541
def 'should only have the following fields in the builder'() {

0 commit comments

Comments
 (0)