Skip to content

Commit 9931b99

Browse files
committed
Set SNI on SSL connections.
[resolves pgjdbc#634]
1 parent 2ca5519 commit 9931b99

9 files changed

+209
-14
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Mono<Connection> connectionMono = Mono.from(connectionFactory.create());
102102
| `sslCert` | Path to SSL certificate for TLS authentication in PEM format. Can be also a resource path. _(Optional)_
103103
| `sslPassword` | Key password to decrypt SSL key. _(Optional)_
104104
| `sslHostnameVerifier` | `javax.net.ssl.HostnameVerifier` implementation. _(Optional)_
105+
| `sslSni` | Enable/disable SNI to send the configured `host` name during the SSL handshake. Defaults to `true`. _(Optional)_
105106
| `statementTimeout` | Statement timeout. _(Optional)_
106107
| `targetServerType` | Type of server to use when using multi-host operations. Supported values: `ANY`, `PRIMARY`, `SECONDARY`, `PREFER_SECONDARY`. Defaults to `ANY`. _(Optional)_
107108
| `tcpNoDelay` | Enable/disable TCP NoDelay. Enabled by default. _(Optional)_

src/main/java/io/r2dbc/postgresql/PostgresqlConnectionConfiguration.java

+92-4
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,18 @@
4242
import reactor.util.annotation.Nullable;
4343

4444
import javax.net.ssl.HostnameVerifier;
45+
import javax.net.ssl.SNIHostName;
46+
import javax.net.ssl.SNIServerName;
47+
import javax.net.ssl.SSLEngine;
4548
import javax.net.ssl.SSLException;
49+
import javax.net.ssl.SSLParameters;
4650
import java.io.File;
4751
import java.io.IOException;
4852
import java.io.InputStream;
53+
import java.net.InetSocketAddress;
4954
import java.net.MalformedURLException;
5055
import java.net.Socket;
56+
import java.net.SocketAddress;
5157
import java.net.URL;
5258
import java.time.Duration;
5359
import java.util.ArrayList;
@@ -411,6 +417,12 @@ public static final class Builder {
411417

412418
private Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer = Function.identity();
413419

420+
private Function<SSLEngine, SSLEngine> sslEngineCustomizer = Function.identity();
421+
422+
private Function<SocketAddress, SSLParameters> sslParametersFactory = it -> new SSLParameters();
423+
424+
private boolean sslSni = true;
425+
414426
private boolean tcpKeepAlive = false;
415427

416428
private boolean tcpNoDelay = true;
@@ -476,7 +488,7 @@ public PostgresqlConnectionConfiguration build() {
476488
this.extensions, this.fetchSize, this.forceBinary, this.lockWaitTimeout, this.loopResources, multiHostConfiguration,
477489
this.noticeLogLevel, this.options, this.password, this.preferAttachedBuffers,
478490
this.preparedStatementCacheQueries, this.schema, singleHostConfiguration,
479-
this.createSslConfig(), this.statementTimeout, this.tcpKeepAlive, this.tcpNoDelay, this.timeZone, this.username);
491+
this.createSslConfig(this.sslSni), this.statementTimeout, this.tcpKeepAlive, this.tcpNoDelay, this.timeZone, this.username);
480492
}
481493

482494
/**
@@ -852,6 +864,47 @@ public Builder sslContextBuilderCustomizer(Function<SslContextBuilder, SslContex
852864
return this;
853865
}
854866

867+
/**
868+
* Configure a {@link SSLEngine} customizer. The customizer gets applied on each SSL connection attempt to allow for just-in-time configuration updates. The {@link Function} gets
869+
* called with a {@link SSLEngine} instance that has all configuration options applied. The customizer may return the same builder or return a new builder instance to be used to
870+
* build the SSL context.
871+
*
872+
* @param sslEngineCustomizer customizer function
873+
* @return this {@link Builder}
874+
* @throws IllegalArgumentException if {@code sslEngineCustomizer} is {@code null}
875+
*/
876+
public Builder sslEngineCustomizer(Function<SSLEngine, SSLEngine> sslEngineCustomizer) {
877+
this.sslEngineCustomizer = Assert.requireNonNull(sslEngineCustomizer, "sslEngineCustomizer must not be null");
878+
return this;
879+
}
880+
881+
/**
882+
* Configure a {@link SSLParameters} provider for a given {@link SocketAddress}. The provider gets applied on each SSL connection attempt to allow for just-in-time configuration updates.
883+
* Typically used to configure SSL protocols
884+
*
885+
* @param sslParametersFactory customizer function
886+
* @return this {@link Builder}
887+
* @throws IllegalArgumentException if {@code sslParametersFactory} is {@code null}
888+
* @since 1.0.4
889+
*/
890+
public Builder sslParameters(Function<SocketAddress, SSLParameters> sslParametersFactory) {
891+
this.sslParametersFactory = Assert.requireNonNull(sslParametersFactory, "sslParametersFactory must not be null");
892+
return this;
893+
}
894+
895+
/**
896+
* Configure whether to indicate the hostname and port via SNI to the server. Enabled by default.
897+
*
898+
* @param sslSni whether to indicate the hostname and port via SNI. Sets {@link SSLParameters#setServerNames(List)} on the {@link SSLParameters} instance provided by
899+
* {@link #sslParameters(Function)}.
900+
* @return this {@link Builder}
901+
* @since 1.0.4
902+
*/
903+
public Builder sslSni(boolean sslSni) {
904+
this.sslSni = sslSni;
905+
return this;
906+
}
907+
855908
/**
856909
* Configure ssl cert for client certificate authentication. Can point to either a resource within the classpath or a file.
857910
*
@@ -1094,10 +1147,13 @@ public String toString() {
10941147
", schema='" + this.schema + '\'' +
10951148
", singleHostConfiguration='" + this.singleHostConfiguration + '\'' +
10961149
", sslContextBuilderCustomizer='" + this.sslContextBuilderCustomizer + '\'' +
1150+
", sslEngineCustomizer='" + this.sslEngineCustomizer + '\'' +
1151+
", sslParametersFactory='" + this.sslParametersFactory + '\'' +
10971152
", sslMode='" + this.sslMode + '\'' +
10981153
", sslRootCert='" + this.sslRootCert + '\'' +
10991154
", sslCert='" + this.sslCert + '\'' +
11001155
", sslKey='" + this.sslKey + '\'' +
1156+
", sslSni='" + this.sslSni + '\'' +
11011157
", statementTimeout='" + this.statementTimeout + '\'' +
11021158
", sslHostnameVerifier='" + this.sslHostnameVerifier + '\'' +
11031159
", tcpKeepAlive='" + this.tcpKeepAlive + '\'' +
@@ -1107,15 +1163,47 @@ public String toString() {
11071163
'}';
11081164
}
11091165

1110-
private SSLConfig createSslConfig() {
1166+
private SSLConfig createSslConfig(boolean sslSni) {
11111167
if (this.singleHostConfiguration != null && this.singleHostConfiguration.getSocket() != null || this.sslMode == SSLMode.DISABLE) {
11121168
return SSLConfig.disabled();
11131169
}
11141170

1115-
HostnameVerifier hostnameVerifier = this.sslHostnameVerifier;
1116-
return new SSLConfig(this.sslMode, createSslProvider(), hostnameVerifier);
1171+
Function<SocketAddress, SSLParameters> sslParametersFunctionToUse = getSslParametersFactory(sslSni, this.sslParametersFactory);
1172+
return new SSLConfig(this.sslMode, createSslProvider(), this.sslEngineCustomizer, sslParametersFunctionToUse, this.sslHostnameVerifier);
1173+
}
1174+
1175+
private static Function<SocketAddress, SSLParameters> getSslParametersFactory(boolean sslSni, Function<SocketAddress, SSLParameters> sslParametersFunction) {
1176+
if (!sslSni) {
1177+
return sslParametersFunction;
1178+
}
1179+
1180+
return socket -> {
1181+
1182+
SSLParameters sslParameters = sslParametersFunction.apply(socket);
1183+
1184+
if (socket instanceof InetSocketAddress) {
1185+
1186+
InetSocketAddress inetSocketAddress = (InetSocketAddress) socket;
1187+
String hostString = inetSocketAddress.getHostString();
1188+
if (SSLConfig.isValidSniHostname(hostString)) {
1189+
appendSniHost(sslParameters, hostString);
1190+
}
1191+
}
1192+
1193+
return sslParameters;
1194+
};
11171195
}
11181196

1197+
private static void appendSniHost(SSLParameters sslParameters, String hostString) {
1198+
1199+
List<SNIServerName> existingServerNames = sslParameters.getServerNames();
1200+
List<SNIServerName> serverNames = existingServerNames == null ? new ArrayList<>() : new ArrayList<>(existingServerNames);
1201+
serverNames.add(new SNIHostName(hostString));
1202+
1203+
sslParameters.setServerNames(serverNames);
1204+
}
1205+
1206+
11191207
private Supplier<SslContext> createSslProvider() {
11201208
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
11211209
if (this.sslMode.verifyCertificate()) {

src/main/java/io/r2dbc/postgresql/PostgresqlConnectionFactoryProvider.java

+8
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,13 @@ public final class PostgresqlConnectionFactoryProvider implements ConnectionFact
221221
*/
222222
public static final Option<String> SSL_ROOT_CERT = Option.valueOf("sslRootCert");
223223

224+
/**
225+
* Configure whether to use SNI on SSL connections. Enabled by default.
226+
*
227+
* @since 1.0.4
228+
*/
229+
public static final Option<Boolean> SSL_SNI = Option.valueOf("sslSni");
230+
224231
/**
225232
* Statement timeout.
226233
*
@@ -406,6 +413,7 @@ private static void setupSsl(PostgresqlConnectionConfiguration.Builder builder,
406413
mapper.fromTyped(SSL_KEY).to(builder::sslKey);
407414
mapper.fromTyped(SSL_ROOT_CERT).to(builder::sslRootCert);
408415
mapper.fromTyped(SSL_PASSWORD).to(builder::sslPassword);
416+
mapper.fromTyped(SSL_SNI).map(OptionMapper::toBoolean).to(builder::sslSni);
409417

410418
mapper.from(SSL_HOSTNAME_VERIFIER).map(it -> {
411419

src/main/java/io/r2dbc/postgresql/client/AbstractPostgresSSLHandlerAdapter.java

+9-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
import reactor.core.publisher.Mono;
2929

3030
import javax.net.ssl.SSLEngine;
31+
import javax.net.ssl.SSLParameters;
3132
import java.net.InetSocketAddress;
33+
import java.net.SocketAddress;
3234
import java.util.concurrent.CompletableFuture;
3335

3436
abstract class AbstractPostgresSSLHandlerAdapter extends ChannelInboundHandlerAdapter implements GenericFutureListener<Future<Channel>> {
@@ -41,9 +43,14 @@ abstract class AbstractPostgresSSLHandlerAdapter extends ChannelInboundHandlerAd
4143

4244
private final CompletableFuture<Void> handshakeFuture;
4345

44-
AbstractPostgresSSLHandlerAdapter(ByteBufAllocator alloc, SSLConfig sslConfig) {
46+
AbstractPostgresSSLHandlerAdapter(ByteBufAllocator alloc, SocketAddress socketAddress, SSLConfig sslConfig) {
4547
this.sslConfig = sslConfig;
46-
this.sslEngine = sslConfig.getSslProvider().get().newEngine(alloc);
48+
49+
SSLEngine sslEngine = sslConfig.getSslProvider().get().newEngine(alloc);
50+
SSLParameters sslParameters = sslConfig.getSslParametersFactory().apply(socketAddress);
51+
sslEngine.setSSLParameters(sslParameters);
52+
53+
this.sslEngine = sslConfig.getSslEngineCustomizer().apply(sslEngine);
4754
this.handshakeFuture = new CompletableFuture<>();
4855
this.sslHandler = new SslHandler(this.sslEngine);
4956
this.sslHandler.handshakeFuture().addListener(this);

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -399,21 +399,21 @@ public static Mono<ReactorNettyClient> connect(SocketAddress socketAddress, Conn
399399
new LoggingHandler(ReactorNettyClient.class, LogLevel.TRACE));
400400
}
401401

402-
registerSslHandler(settings.getSslConfig(), channel);
402+
registerSslHandler(socketAddress, settings.getSslConfig(), channel);
403403
}).connect().flatMap(it ->
404404
getSslHandshake(it.channel()).thenReturn(new ReactorNettyClient(it, settings))
405405
);
406406
}
407407

408-
private static void registerSslHandler(SSLConfig sslConfig, Channel channel) {
408+
private static void registerSslHandler(SocketAddress socketAddress, SSLConfig sslConfig, Channel channel) {
409409
try {
410410
if (sslConfig.getSslMode().startSsl()) {
411411

412412
AbstractPostgresSSLHandlerAdapter sslAdapter;
413413
if (sslConfig.getSslMode() == SSLMode.TUNNEL) {
414-
sslAdapter = new SSLTunnelHandlerAdapter(channel.alloc(), sslConfig);
414+
sslAdapter = new SSLTunnelHandlerAdapter(channel.alloc(), socketAddress, sslConfig);
415415
} else {
416-
sslAdapter = new SSLSessionHandlerAdapter(channel.alloc(), sslConfig);
416+
sslAdapter = new SSLSessionHandlerAdapter(channel.alloc(), socketAddress, sslConfig);
417417
}
418418

419419
channel.pipeline().addFirst(sslAdapter);

src/main/java/io/r2dbc/postgresql/client/SSLConfig.java

+63
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
import reactor.util.annotation.Nullable;
2222

2323
import javax.net.ssl.HostnameVerifier;
24+
import javax.net.ssl.SSLEngine;
25+
import javax.net.ssl.SSLParameters;
26+
import java.net.SocketAddress;
27+
import java.util.function.Function;
2428
import java.util.function.Supplier;
2529

2630
public final class SSLConfig {
@@ -33,7 +37,16 @@ public final class SSLConfig {
3337
@Nullable
3438
private final Supplier<SslContext> sslProvider;
3539

40+
private final Function<SSLEngine, SSLEngine> sslEngineCustomizer;
41+
42+
private final Function<SocketAddress, SSLParameters> sslParametersFactory;
43+
3644
public SSLConfig(SSLMode sslMode, @Nullable Supplier<SslContext> sslProvider, @Nullable HostnameVerifier hostnameVerifier) {
45+
this(sslMode, sslProvider, Function.identity(), it -> new SSLParameters(), hostnameVerifier);
46+
}
47+
48+
public SSLConfig(SSLMode sslMode, @Nullable Supplier<SslContext> sslProvider, Function<SSLEngine, SSLEngine> sslEngineCustomizer, Function<SocketAddress, SSLParameters> sslParametersFactory,
49+
@Nullable HostnameVerifier hostnameVerifier) {
3750
if (sslMode != SSLMode.DISABLE) {
3851
Assert.requireNonNull(sslProvider, "SslContext provider is required for ssl mode " + sslMode);
3952
}
@@ -42,6 +55,8 @@ public SSLConfig(SSLMode sslMode, @Nullable Supplier<SslContext> sslProvider, @N
4255
}
4356
this.sslMode = sslMode;
4457
this.sslProvider = sslProvider;
58+
this.sslEngineCustomizer = sslEngineCustomizer;
59+
this.sslParametersFactory = sslParametersFactory;
4560
this.hostnameVerifier = hostnameVerifier;
4661
}
4762

@@ -64,12 +79,60 @@ public Supplier<SslContext> getSslProvider() {
6479
return this.sslProvider;
6580
}
6681

82+
public Function<SSLEngine, SSLEngine> getSslEngineCustomizer() {
83+
return this.sslEngineCustomizer;
84+
}
85+
86+
87+
public Function<SocketAddress, SSLParameters> getSslParametersFactory() {
88+
return this.sslParametersFactory;
89+
}
90+
6791
public SSLConfig mutateMode(SSLMode newMode) {
6892
return new SSLConfig(
6993
newMode,
7094
this.sslProvider,
95+
this.sslEngineCustomizer,
96+
this.sslParametersFactory,
7197
this.hostnameVerifier
7298
);
7399
}
74100

101+
public static boolean isValidSniHostname(String input) {
102+
for (int i = 0; i < input.length(); i++) {
103+
char c = input.charAt(i);
104+
if (isLabelSeparator(c)) {
105+
continue;
106+
}
107+
if (isNonLDHAsciiCodePoint(c)) {
108+
return false;
109+
}
110+
}
111+
return true;
112+
}
113+
114+
//
115+
// LDH stands for "letter/digit/hyphen", with characters restricted to the
116+
// 26-letter Latin alphabet <A-Z a-z>, the digits <0-9>, and the hyphen
117+
// <->.
118+
// Non LDH refers to characters in the ASCII range, but which are not
119+
// letters, digits or the hypen.
120+
//
121+
// non-LDH = 0..0x2C, 0x2E..0x2F, 0x3A..0x40, 0x5B..0x60, 0x7B..0x7F
122+
//
123+
private static boolean isNonLDHAsciiCodePoint(char ch) {
124+
return (0x0000 <= ch && ch <= 0x002C) ||
125+
(0x002E <= ch && ch <= 0x002F) ||
126+
(0x003A <= ch && ch <= 0x0040) ||
127+
(0x005B <= ch && ch <= 0x0060) ||
128+
(0x007B <= ch && ch <= 0x007F);
129+
}
130+
131+
//
132+
// to check if a character is a label separator, i.e. a dot character.
133+
//
134+
private static boolean isLabelSeparator(char c) {
135+
return (c == '.' || c == '\u3002' || c == '\uFF0E' || c == '\uFF61');
136+
}
137+
75138
}

src/main/java/io/r2dbc/postgresql/client/SSLSessionHandlerAdapter.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import io.r2dbc.postgresql.message.frontend.SSLRequest;
2323
import reactor.core.publisher.Mono;
2424

25+
import java.net.SocketAddress;
26+
2527
/**
2628
* SSL handler assuming the endpoint a Postgres endpoint following the {@link SSLRequest} flow.
2729
*
@@ -35,8 +37,8 @@ final class SSLSessionHandlerAdapter extends AbstractPostgresSSLHandlerAdapter {
3537

3638
private boolean negotiating = true;
3739

38-
SSLSessionHandlerAdapter(ByteBufAllocator alloc, SSLConfig sslConfig) {
39-
super(alloc, sslConfig);
40+
SSLSessionHandlerAdapter(ByteBufAllocator alloc, SocketAddress socketAddress, SSLConfig sslConfig) {
41+
super(alloc, socketAddress, sslConfig);
4042
this.alloc = alloc;
4143
this.sslConfig = sslConfig;
4244
}

src/main/java/io/r2dbc/postgresql/client/SSLTunnelHandlerAdapter.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@
1919
import io.netty.buffer.ByteBufAllocator;
2020
import io.netty.channel.ChannelHandlerContext;
2121

22+
import java.net.SocketAddress;
23+
2224
/**
2325
* SSL handler assuming the endpoint is a SSL tunnel and not a Postgres endpoint.
2426
*/
2527
final class SSLTunnelHandlerAdapter extends AbstractPostgresSSLHandlerAdapter {
2628

2729
private final SSLConfig sslConfig;
2830

29-
SSLTunnelHandlerAdapter(ByteBufAllocator alloc, SSLConfig sslConfig) {
30-
super(alloc, sslConfig);
31+
SSLTunnelHandlerAdapter(ByteBufAllocator alloc, SocketAddress socketAddress, SSLConfig sslConfig) {
32+
super(alloc, socketAddress, sslConfig);
3133
this.sslConfig = sslConfig;
3234
}
3335

0 commit comments

Comments
 (0)