Skip to content

Commit 33b0223

Browse files
committed
Add sslContextBuilderCustomizer(Function<SslContextBuilder, SslContextBuilder>)
We now accept a customizer function to customize the SSL configuration through SslContextBuilder. The customizer is applied each time a connection gets established. [resolves #152]
1 parent f385598 commit 33b0223

10 files changed

+132
-43
lines changed

src/main/java/io/r2dbc/mssql/MssqlConnectionConfiguration.java

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,33 @@
1616

1717
package io.r2dbc.mssql;
1818

19+
import io.netty.handler.ssl.SslContextBuilder;
1920
import io.r2dbc.mssql.client.ClientConfiguration;
21+
import io.r2dbc.mssql.client.ssl.ExpectedHostnameX509TrustManager;
22+
import io.r2dbc.mssql.client.ssl.TrustAllTrustManager;
2023
import io.r2dbc.mssql.codec.DefaultCodecs;
2124
import io.r2dbc.mssql.message.tds.Redirect;
2225
import io.r2dbc.mssql.util.Assert;
2326
import io.r2dbc.mssql.util.StringUtils;
2427
import reactor.netty.resources.ConnectionProvider;
28+
import reactor.netty.tcp.SslProvider;
2529
import reactor.util.annotation.Nullable;
2630

31+
import javax.net.ssl.TrustManager;
32+
import javax.net.ssl.TrustManagerFactory;
33+
import javax.net.ssl.X509TrustManager;
2734
import java.net.InetAddress;
2835
import java.net.UnknownHostException;
36+
import java.security.GeneralSecurityException;
37+
import java.security.KeyStore;
2938
import java.time.Duration;
3039
import java.util.Optional;
3140
import java.util.UUID;
41+
import java.util.function.Function;
3242
import java.util.function.Predicate;
3343

44+
import static reactor.netty.tcp.SslProvider.DefaultConfigurationType.TCP;
45+
3446
/**
3547
* Connection configuration information for connecting to a Microsoft SQL database.
3648
* Allows configuration of the connection endpoint, login credentials, database and trace details such as application name and connection Id.
@@ -73,10 +85,13 @@ public final class MssqlConnectionConfiguration {
7385

7486
private final boolean ssl;
7587

88+
private final Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer;
89+
7690
private final String username;
7791

7892
private MssqlConnectionConfiguration(@Nullable String applicationName, @Nullable UUID connectionId, Duration connectTimeout, @Nullable String database, String host, String hostNameInCertificate
79-
, CharSequence password, Predicate<String> preferCursoredExecution, int port, boolean sendStringParametersAsUnicode, boolean ssl, String username) {
93+
, CharSequence password, Predicate<String> preferCursoredExecution, int port, boolean sendStringParametersAsUnicode, boolean ssl,
94+
Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer, String username) {
8095

8196
this.applicationName = applicationName;
8297
this.connectionId = connectionId;
@@ -89,6 +104,7 @@ private MssqlConnectionConfiguration(@Nullable String applicationName, @Nullable
89104
this.port = port;
90105
this.sendStringParametersAsUnicode = sendStringParametersAsUnicode;
91106
this.ssl = ssl;
107+
this.sslContextBuilderCustomizer = sslContextBuilderCustomizer;
92108
this.username = Assert.requireNonNull(username, "username must not be null");
93109
}
94110

@@ -126,11 +142,11 @@ MssqlConnectionConfiguration withRedirect(Redirect redirect) {
126142
}
127143

128144
return new MssqlConnectionConfiguration(this.applicationName, this.connectionId, this.connectTimeout, this.database, redirectServerName, hostNameInCertificate, this.password,
129-
this.preferCursoredExecution, redirect.getPort(), this.sendStringParametersAsUnicode, this.ssl, this.username);
145+
this.preferCursoredExecution, redirect.getPort(), this.sendStringParametersAsUnicode, this.ssl, this.sslContextBuilderCustomizer, this.username);
130146
}
131147

132148
ClientConfiguration toClientConfiguration() {
133-
return new DefaultClientConfiguration(this.connectTimeout, this.host, this.hostNameInCertificate, this.port, this.ssl);
149+
return new DefaultClientConfiguration(this.connectTimeout, this.host, this.hostNameInCertificate, this.port, this.ssl, sslContextBuilderCustomizer);
134150
}
135151

136152
ConnectionOptions toConnectionOptions() {
@@ -152,6 +168,7 @@ public String toString() {
152168
sb.append(", port=").append(this.port);
153169
sb.append(", sendStringParametersAsUnicode=").append(this.sendStringParametersAsUnicode);
154170
sb.append(", ssl=").append(this.ssl);
171+
sb.append(", sslContextBuilderCustomizer=").append(this.sslContextBuilderCustomizer);
155172
sb.append(", username=\"").append(this.username).append('\"');
156173
sb.append(']');
157174
return sb.toString();
@@ -280,6 +297,8 @@ public static final class Builder {
280297

281298
private boolean ssl;
282299

300+
private Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer = Function.identity();
301+
283302
private String username;
284303

285304
private Builder() {
@@ -428,6 +447,21 @@ public Builder sendStringParametersAsUnicode(boolean sendStringParametersAsUnico
428447
return this;
429448
}
430449

450+
/**
451+
* Configure a {@link SslContextBuilder} customizer. The customizer gets applied on each SSL connection attempt to allow for just-in-time configuration updates. The {@link Function} gets
452+
* called with the prepared {@link SslContextBuilder} that has all configuration options applied. The customizer may return the same builder or return a new builder instance to be used to
453+
* build the SSL context.
454+
*
455+
* @param sslContextBuilderCustomizer customizer function
456+
* @return this {@link Builder}
457+
* @throws IllegalArgumentException if {@code sslContextBuilderCustomizer} is {@code null}
458+
* @since 0.8.3
459+
*/
460+
public Builder sslContextBuilderCustomizer(Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer) {
461+
this.sslContextBuilderCustomizer = Assert.requireNonNull(sslContextBuilderCustomizer, "sslContextBuilderCustomizer must not be null");
462+
return this;
463+
}
464+
431465
/**
432466
* Configure the username.
433467
*
@@ -453,7 +487,7 @@ public MssqlConnectionConfiguration build() {
453487

454488
return new MssqlConnectionConfiguration(this.applicationName, this.connectionId, this.connectTimeout, this.database, this.host, this.hostNameInCertificate, this.password,
455489
this.preferCursoredExecution, this.port,
456-
this.sendStringParametersAsUnicode, this.ssl, this.username);
490+
this.sendStringParametersAsUnicode, this.ssl, this.sslContextBuilderCustomizer, this.username);
457491
}
458492
}
459493

@@ -469,13 +503,17 @@ private static class DefaultClientConfiguration implements ClientConfiguration {
469503

470504
private final boolean ssl;
471505

472-
DefaultClientConfiguration(Duration connectTimeout, String host, String hostNameInCertificate, int port, boolean ssl) {
506+
private final Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer;
507+
508+
DefaultClientConfiguration(Duration connectTimeout, String host, String hostNameInCertificate, int port, boolean ssl,
509+
Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer) {
473510

474511
this.connectTimeout = connectTimeout;
475512
this.host = host;
476513
this.hostNameInCertificate = hostNameInCertificate;
477514
this.port = port;
478515
this.ssl = ssl;
516+
this.sslContextBuilderCustomizer = sslContextBuilderCustomizer;
479517
}
480518

481519
@Override
@@ -504,8 +542,29 @@ public boolean isSslEnabled() {
504542
}
505543

506544
@Override
507-
public String getHostNameInCertificate() {
508-
return this.hostNameInCertificate;
545+
public SslProvider getSslProvider() throws GeneralSecurityException {
546+
547+
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
548+
549+
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
550+
KeyStore ks = null;
551+
tmf.init(ks);
552+
553+
TrustManager[] trustManagers = tmf.getTrustManagers();
554+
TrustManager result;
555+
556+
if (isSslEnabled()) {
557+
result = new ExpectedHostnameX509TrustManager((X509TrustManager) trustManagers[0], this.hostNameInCertificate);
558+
} else {
559+
result = TrustAllTrustManager.INSTANCE;
560+
}
561+
562+
sslContextBuilder.trustManager(result);
563+
564+
return SslProvider.builder()
565+
.sslContext(this.sslContextBuilderCustomizer.apply(sslContextBuilder))
566+
.defaultConfiguration(TCP)
567+
.build();
509568
}
510569
}
511570
}

src/main/java/io/r2dbc/mssql/MssqlConnectionFactoryProvider.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package io.r2dbc.mssql;
1818

19+
import io.netty.handler.ssl.SslContextBuilder;
1920
import io.r2dbc.mssql.util.Assert;
2021
import io.r2dbc.spi.ConnectionFactoryOptions;
2122
import io.r2dbc.spi.ConnectionFactoryProvider;
@@ -25,6 +26,7 @@
2526

2627
import java.time.Duration;
2728
import java.util.UUID;
29+
import java.util.function.Function;
2830
import java.util.function.Predicate;
2931

3032
import static io.r2dbc.spi.ConnectionFactoryOptions.CONNECT_TIMEOUT;
@@ -73,6 +75,13 @@ public final class MssqlConnectionFactoryProvider implements ConnectionFactoryPr
7375
*/
7476
public static final Option<Boolean> SEND_STRING_PARAMETERS_AS_UNICODE = Option.valueOf("sendStringParametersAsUnicode");
7577

78+
/**
79+
* Customizer {@link Function} for {@link SslContextBuilder}.
80+
*
81+
* @since 0.8.3
82+
*/
83+
public static final Option<Function<SslContextBuilder, SslContextBuilder>> SSL_CONTEXT_BUILDER_CUSTOMIZER = Option.valueOf("sslContextBuilderCustomizer");
84+
7685
/**
7786
* Driver option value.
7887
*/
@@ -167,6 +176,10 @@ public MssqlConnectionFactory create(ConnectionFactoryOptions connectionFactoryO
167176
builder.username(connectionFactoryOptions.getRequiredValue(USER));
168177
builder.applicationName(connectionFactoryOptions.getRequiredValue(USER));
169178

179+
if (connectionFactoryOptions.hasOption(SSL_CONTEXT_BUILDER_CUSTOMIZER)) {
180+
builder.sslContextBuilderCustomizer(connectionFactoryOptions.getRequiredValue(SSL_CONTEXT_BUILDER_CUSTOMIZER));
181+
}
182+
170183
MssqlConnectionConfiguration configuration = builder.build();
171184
if (this.logger.isDebugEnabled()) {
172185
this.logger.debug(String.format("Creating MssqlConnectionFactory with configuration [%s] from options [%s]", configuration, connectionFactoryOptions));

src/main/java/io/r2dbc/mssql/client/ReactorNettyClient.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.netty.channel.ChannelPipeline;
2525
import io.netty.handler.logging.LogLevel;
2626
import io.netty.handler.logging.LoggingHandler;
27+
import io.netty.handler.ssl.SslContextBuilder;
2728
import io.netty.util.internal.logging.InternalLogger;
2829
import io.netty.util.internal.logging.InternalLoggerFactory;
2930
import io.r2dbc.mssql.client.ssl.TdsSslHandler;
@@ -52,6 +53,7 @@
5253
import reactor.core.publisher.SynchronousSink;
5354
import reactor.netty.Connection;
5455
import reactor.netty.resources.ConnectionProvider;
56+
import reactor.netty.tcp.SslProvider;
5557
import reactor.netty.tcp.TcpClient;
5658
import reactor.util.Logger;
5759
import reactor.util.Loggers;
@@ -70,6 +72,8 @@
7072
import java.util.function.Predicate;
7173
import java.util.function.Supplier;
7274

75+
import static reactor.netty.tcp.SslProvider.DefaultConfigurationType.TCP;
76+
7377
/**
7478
* An implementation of a TDS client based on the Reactor Netty project.
7579
*
@@ -383,8 +387,11 @@ public boolean isSslEnabled() {
383387
}
384388

385389
@Override
386-
public String getHostNameInCertificate() {
387-
return host;
390+
public SslProvider getSslProvider() {
391+
return SslProvider.builder()
392+
.sslContext(SslContextBuilder.forClient())
393+
.defaultConfiguration(TCP)
394+
.build();
388395
}
389396
}, null, null);
390397
}

src/main/java/io/r2dbc/mssql/client/ssl/ExpectedHostnameX509TrustManager.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
*
3333
* @author Mark Paluch
3434
*/
35-
final class ExpectedHostnameX509TrustManager implements X509TrustManager {
35+
public final class ExpectedHostnameX509TrustManager implements X509TrustManager {
3636

3737
private static final Logger logger = Loggers.getLogger(TdsSslHandler.class);
3838

@@ -42,7 +42,7 @@ final class ExpectedHostnameX509TrustManager implements X509TrustManager {
4242

4343
private final Predicate<String> matcher;
4444

45-
ExpectedHostnameX509TrustManager(X509TrustManager tm, String expectedHostName) {
45+
public ExpectedHostnameX509TrustManager(X509TrustManager tm, String expectedHostName) {
4646

4747
this.defaultTrustManager = tm;
4848
this.expectedHostName = expectedHostName;

src/main/java/io/r2dbc/mssql/client/ssl/SslConfiguration.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package io.r2dbc.mssql.client.ssl;
1818

19+
import reactor.netty.tcp.SslProvider;
20+
21+
import java.security.GeneralSecurityException;
22+
1923
/**
2024
* SSL Configuration for SQL Server connections.
2125
* <p>Microsoft SQL server supports various SSL setups:
@@ -28,7 +32,7 @@
2832
* <p>
2933
* Supported mode uses SSL during login to encrypt login credentials. SSL is disabled after login.
3034
* The client supports login-time SSL even when {@link #isSslEnabled()} is {@code false}. This mode does not validate certificates.
31-
* <p>Enabling {@link #isSslEnabled() SSL} enables also SSL certificate validation using {@link #getHostNameInCertificate()}.
35+
* <p>Enabling {@link #isSslEnabled() SSL} enables also SSL certificate validation.
3236
*
3337
* @author Mark Paluch
3438
*/
@@ -40,7 +44,8 @@ public interface SslConfiguration {
4044
boolean isSslEnabled();
4145

4246
/**
43-
* @return expected hostname in the SSL certificate.
47+
* @return the {@link SslProvider}.
48+
* @since 0.8.3
4449
*/
45-
String getHostNameInCertificate();
50+
SslProvider getSslProvider() throws GeneralSecurityException;
4651
}

src/main/java/io/r2dbc/mssql/client/ssl/TdsSslHandler.java

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,8 @@
3939
import reactor.util.Loggers;
4040
import reactor.util.annotation.Nullable;
4141

42-
import javax.net.ssl.SSLContext;
4342
import javax.net.ssl.SSLEngine;
44-
import javax.net.ssl.TrustManager;
45-
import javax.net.ssl.TrustManagerFactory;
46-
import javax.net.ssl.X509TrustManager;
4743
import java.security.GeneralSecurityException;
48-
import java.security.KeyStore;
4944

5045
/**
5146
* SSL handling for TDS connections.
@@ -118,31 +113,14 @@ void setState(SslState state) {
118113
* @return the configured {@link SslHandler}.
119114
* @throws GeneralSecurityException thrown on security API errors.
120115
*/
121-
private static SslHandler createSslHandler(SslConfiguration sslConfiguration) throws GeneralSecurityException {
116+
private static SslHandler createSslHandler(SslConfiguration sslConfiguration, ByteBufAllocator allocator) throws GeneralSecurityException {
122117

123-
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
124-
SSLContext sslContext = SSLContext.getInstance("TLS");
125-
KeyStore ks = null;
126-
tmf.init(ks);
118+
SSLEngine sslEngine = sslConfiguration.getSslProvider().getSslContext()
119+
.newEngine(allocator);
127120

128-
TrustManager[] trustManagers = tmf.getTrustManagers();
129-
TrustManager[] tms = new TrustManager[]{getTrustManager(sslConfiguration, trustManagers[0])};
130-
sslContext.init(null, tms, null);
131-
132-
SSLEngine sslEngine = sslContext.createSSLEngine();
133-
sslEngine.setUseClientMode(true);
134121
return new SslHandler(sslEngine);
135122
}
136123

137-
private static TrustManager getTrustManager(SslConfiguration sslConfiguration, TrustManager trustManager) {
138-
139-
if (sslConfiguration.isSslEnabled()) {
140-
return new ExpectedHostnameX509TrustManager((X509TrustManager) trustManager, sslConfiguration.getHostNameInCertificate());
141-
}
142-
143-
return TrustAllTrustManager.INSTANCE;
144-
}
145-
146124
/**
147125
* Lazily register {@link SslHandler} if needed.
148126
*
@@ -156,7 +134,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
156134
if (evt == SslState.LOGIN_ONLY || evt == SslState.CONNECTION) {
157135

158136
this.state = (SslState) evt;
159-
this.sslHandler = createSslHandler(this.sslConfiguration);
137+
this.sslHandler = createSslHandler(this.sslConfiguration, ctx.alloc());
160138

161139
LOGGER.debug(this.connectionContext.getMessage("Registering Context Proxy and SSL Event Handlers to propagate SSL events to channelRead()"));
162140
ctx.pipeline().addAfter(getClass().getName(), ContextProxy.class.getName(), new ContextProxy());

src/main/java/io/r2dbc/mssql/client/ssl/TrustAllTrustManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
*
2727
* @author Mark Paluch
2828
*/
29-
enum TrustAllTrustManager implements X509TrustManager {
29+
public enum TrustAllTrustManager implements X509TrustManager {
3030

3131
INSTANCE;
3232

src/test/java/io/r2dbc/mssql/MssqlConnectionConfigurationUnitTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ void constructorNoUsername() {
135135
.withMessage("username must not be null");
136136
}
137137

138+
@Test
139+
void constructorNoSslCustomizer() {
140+
assertThatIllegalArgumentException().isThrownBy(() -> MssqlConnectionConfiguration.builder()
141+
.sslContextBuilderCustomizer(null)
142+
.build())
143+
.withMessage("sslContextBuilderCustomizer must not be null");
144+
}
145+
138146
@Test
139147
void redirect() {
140148
MssqlConnectionConfiguration configuration = MssqlConnectionConfiguration.builder()

0 commit comments

Comments
 (0)