Skip to content

Commit 8a525a2

Browse files
committed
Add support for sslnegotation=direct.
[resolves #651]
1 parent bc8af68 commit 8a525a2

19 files changed

+313
-105
lines changed

README.md

Lines changed: 41 additions & 40 deletions
Large diffs are not rendered by default.

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import io.r2dbc.postgresql.client.MultiHostConfiguration;
2727
import io.r2dbc.postgresql.client.SSLConfig;
2828
import io.r2dbc.postgresql.client.SSLMode;
29+
import io.r2dbc.postgresql.client.SSLNegotiation;
2930
import io.r2dbc.postgresql.client.SingleHostConfiguration;
3031
import io.r2dbc.postgresql.codec.Codec;
3132
import io.r2dbc.postgresql.codec.Codecs;
@@ -406,6 +407,8 @@ public static final class Builder {
406407

407408
private SSLMode sslMode = SSLMode.DISABLE;
408409

410+
private SSLNegotiation sslNegotiation = SSLNegotiation.POSTGRES;
411+
409412
@Nullable
410413
private CharSequence sslPassword = null;
411414

@@ -971,6 +974,18 @@ public Builder sslMode(SSLMode sslMode) {
971974
return this;
972975
}
973976

977+
/**
978+
* Configure ssl negotiation. Useful if the server is known to support SSL directly (e.g. Postgres 17+ or a SSL tunnel).
979+
*
980+
* @param sslNegotiation the SSL negotiation mechanism to use.
981+
* @return this {@link Builder}
982+
* @since 1.1
983+
*/
984+
public Builder sslNegotiation(SSLNegotiation sslNegotiation) {
985+
this.sslNegotiation = Assert.requireNonNull(sslNegotiation, "sslNegotiation must be not be null");
986+
return this;
987+
}
988+
974989
/**
975990
* Configure ssl password.
976991
*
@@ -1168,19 +1183,23 @@ private SSLConfig createSslConfig(boolean sslSni) {
11681183
return SSLConfig.disabled();
11691184
}
11701185

1171-
Function<SocketAddress, SSLParameters> sslParametersFunctionToUse = getSslParametersFactory(sslSni, this.sslParametersFactory);
1172-
return new SSLConfig(this.sslMode, createSslProvider(), this.sslEngineCustomizer, sslParametersFunctionToUse, this.sslHostnameVerifier);
1186+
Function<SocketAddress, SSLParameters> sslParametersFunctionToUse = getSslParametersFactory(sslSni, this.sslNegotiation, this.sslParametersFactory);
1187+
return new SSLConfig(this.sslNegotiation, this.sslMode, createSslProvider(), this.sslEngineCustomizer, sslParametersFunctionToUse, this.sslHostnameVerifier);
11731188
}
11741189

1175-
private static Function<SocketAddress, SSLParameters> getSslParametersFactory(boolean sslSni, Function<SocketAddress, SSLParameters> sslParametersFunction) {
1176-
if (!sslSni) {
1190+
private static Function<SocketAddress, SSLParameters> getSslParametersFactory(boolean sslSni, SSLNegotiation sslNegotiation, Function<SocketAddress, SSLParameters> sslParametersFunction) {
1191+
if (!sslSni && sslNegotiation != SSLNegotiation.DIRECT) {
11771192
return sslParametersFunction;
11781193
}
11791194

11801195
return socket -> {
11811196

11821197
SSLParameters sslParameters = sslParametersFunction.apply(socket);
11831198

1199+
if (sslNegotiation == SSLNegotiation.DIRECT) {
1200+
sslParameters.setApplicationProtocols(new String[]{"postgresql"});
1201+
}
1202+
11841203
if (socket instanceof InetSocketAddress) {
11851204

11861205
InetSocketAddress inetSocketAddress = (InetSocketAddress) socket;

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.netty.handler.ssl.SslContextBuilder;
2020
import io.r2dbc.postgresql.client.DefaultHostnameVerifier;
2121
import io.r2dbc.postgresql.client.SSLMode;
22+
import io.r2dbc.postgresql.client.SSLNegotiation;
2223
import io.r2dbc.postgresql.codec.Codecs;
2324
import io.r2dbc.postgresql.codec.Json;
2425
import io.r2dbc.postgresql.extension.Extension;
@@ -211,6 +212,20 @@ public final class PostgresqlConnectionFactoryProvider implements ConnectionFact
211212
*/
212213
public static final Option<SSLMode> SSL_MODE_ALIAS = Option.valueOf("sslmode");
213214

215+
/**
216+
* Ssl negotiation mechanism. Default: Postgres
217+
*
218+
* @since 1.1
219+
*/
220+
public static final Option<SSLNegotiation> SSL_NEGOTIATION = Option.valueOf("sslNegotiation");
221+
222+
/**
223+
* Ssl negotiation mechanism alias. Default: Postgres
224+
*
225+
* @since 1.1
226+
*/
227+
public static final Option<SSLNegotiation> SSL_NEGOTIATION_ALIAS = Option.valueOf("sslnegotiation");
228+
214229
/**
215230
* SSL key password
216231
*/
@@ -408,6 +423,10 @@ private static void setupSsl(PostgresqlConnectionConfiguration.Builder builder,
408423
mapper.from(SSL_MODE_ALIAS).map(PostgresqlConnectionFactoryProvider::toSSLMode).to(builder::sslMode);
409424
});
410425

426+
mapper.from(SSL_NEGOTIATION).map(PostgresqlConnectionFactoryProvider::toSSLNegotiation).to(builder::sslNegotiation).otherwise(() -> {
427+
mapper.from(SSL_NEGOTIATION_ALIAS).map(PostgresqlConnectionFactoryProvider::toSSLNegotiation).to(builder::sslNegotiation);
428+
});
429+
411430
mapper.fromTyped(SSL_CERT).to(builder::sslCert);
412431
mapper.fromTyped(SSL_CONTEXT_BUILDER_CUSTOMIZER).to(builder::sslContextBuilderCustomizer);
413432
mapper.fromTyped(SSL_KEY).to(builder::sslKey);
@@ -441,6 +460,14 @@ private static SSLMode toSSLMode(Object it) {
441460
return (SSLMode) it;
442461
}
443462

463+
private static SSLNegotiation toSSLNegotiation(Object it) {
464+
if (it instanceof String) {
465+
return SSLNegotiation.fromValue(it.toString());
466+
}
467+
468+
return (SSLNegotiation) it;
469+
}
470+
444471
@SuppressWarnings("unchecked")
445472
private static Map<String, String> convertToMap(Object options) {
446473
if (options instanceof Map) {

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

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,14 @@
1717
package io.r2dbc.postgresql;
1818

1919
import io.r2dbc.postgresql.authentication.AuthenticationHandler;
20-
import io.r2dbc.postgresql.authentication.PasswordAuthenticationHandler;
21-
import io.r2dbc.postgresql.authentication.SASLAuthenticationHandler;
20+
import io.r2dbc.postgresql.authentication.UsernameAndPassword;
2221
import io.r2dbc.postgresql.client.Client;
2322
import io.r2dbc.postgresql.client.ConnectionContext;
2423
import io.r2dbc.postgresql.client.ConnectionSettings;
2524
import io.r2dbc.postgresql.client.PostgresStartupParameterProvider;
2625
import io.r2dbc.postgresql.client.StartupMessageFlow;
2726
import io.r2dbc.postgresql.message.backend.AuthenticationMessage;
28-
import io.r2dbc.postgresql.util.Assert;
2927
import reactor.core.publisher.Mono;
30-
import reactor.util.annotation.Nullable;
3128

3229
import java.net.SocketAddress;
3330

@@ -57,15 +54,7 @@ private static PostgresStartupParameterProvider getParameterProvider(PostgresqlC
5754
}
5855

5956
protected AuthenticationHandler getAuthenticationHandler(AuthenticationMessage message, UsernameAndPassword usernameAndPassword, ConnectionContext context) {
60-
if (PasswordAuthenticationHandler.supports(message)) {
61-
CharSequence password = Assert.requireNonNull(usernameAndPassword.getPassword(), "Password must not be null");
62-
return new PasswordAuthenticationHandler(password, usernameAndPassword.getUsername());
63-
} else if (SASLAuthenticationHandler.supports(message)) {
64-
CharSequence password = Assert.requireNonNull(usernameAndPassword.getPassword(), "Password must not be null");
65-
return new SASLAuthenticationHandler(password, usernameAndPassword.getUsername(), context);
66-
} else {
67-
throw new IllegalStateException(String.format("Unable to provide AuthenticationHandler capable of handling %s", message));
68-
}
57+
return AuthenticationHandler.getAuthenticationHandler(message, usernameAndPassword, context);
6958
}
7059

7160
Mono<UsernameAndPassword> getCredentials() {
@@ -75,25 +64,4 @@ Mono<UsernameAndPassword> getCredentials() {
7564
});
7665
}
7766

78-
static class UsernameAndPassword {
79-
80-
final String username;
81-
82-
final @Nullable CharSequence password;
83-
84-
public UsernameAndPassword(String username, @Nullable CharSequence password) {
85-
this.username = username;
86-
this.password = password;
87-
}
88-
89-
public String getUsername() {
90-
return this.username;
91-
}
92-
93-
@Nullable
94-
public CharSequence getPassword() {
95-
return this.password;
96-
}
97-
}
98-
9967
}

src/main/java/io/r2dbc/postgresql/authentication/AuthenticationHandler.java

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

1717
package io.r2dbc.postgresql.authentication;
1818

19+
import io.r2dbc.postgresql.client.ConnectionContext;
1920
import io.r2dbc.postgresql.message.backend.AuthenticationMessage;
2021
import io.r2dbc.postgresql.message.frontend.FrontendMessage;
22+
import io.r2dbc.postgresql.util.Assert;
2123
import reactor.util.annotation.Nullable;
2224

2325
/**
@@ -36,4 +38,24 @@ public interface AuthenticationHandler {
3638
@Nullable
3739
FrontendMessage handle(AuthenticationMessage message);
3840

41+
/**
42+
* Return a suitable {@link AuthenticationHandler} for the given {@link AuthenticationMessage} and {@link UsernameAndPassword}.
43+
*
44+
* @param message the message to handle
45+
* @param usernameAndPassword authentication credentials
46+
* @param context connection context
47+
* @return the authentication handler
48+
* @since 1.1
49+
*/
50+
static AuthenticationHandler getAuthenticationHandler(AuthenticationMessage message, UsernameAndPassword usernameAndPassword, ConnectionContext context) {
51+
if (PasswordAuthenticationHandler.supports(message)) {
52+
CharSequence password = Assert.requireNonNull(usernameAndPassword.getPassword(), "Password must not be null");
53+
return new PasswordAuthenticationHandler(password, usernameAndPassword.getUsername());
54+
} else if (SASLAuthenticationHandler.supports(message)) {
55+
CharSequence password = Assert.requireNonNull(usernameAndPassword.getPassword(), "Password must not be null");
56+
return new SASLAuthenticationHandler(password, usernameAndPassword.getUsername(), context);
57+
} else {
58+
throw new IllegalStateException(String.format("Unable to provide AuthenticationHandler capable of handling %s", message));
59+
}
60+
}
3961
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
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+
* https://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+
package io.r2dbc.postgresql.authentication;
18+
19+
import reactor.util.annotation.Nullable;
20+
21+
public class UsernameAndPassword {
22+
23+
private final String username;
24+
25+
private final @Nullable CharSequence password;
26+
27+
public UsernameAndPassword(String username, @Nullable CharSequence password) {
28+
this.username = username;
29+
this.password = password;
30+
}
31+
32+
public String getUsername() {
33+
return this.username;
34+
}
35+
36+
@Nullable
37+
public CharSequence getPassword() {
38+
return this.password;
39+
}
40+
41+
}

src/main/java/io/r2dbc/postgresql/client/SSLTunnelHandlerAdapter.java renamed to src/main/java/io/r2dbc/postgresql/client/DirectSSLHandlerAdapter.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222
import java.net.SocketAddress;
2323

2424
/**
25-
* SSL handler assuming the endpoint is a SSL tunnel and not a Postgres endpoint.
25+
* SSL handler assuming the endpoint uses SSL directly (Postgres 17+ endpoint or an SSL tunnel).
2626
*/
27-
final class SSLTunnelHandlerAdapter extends AbstractPostgresSSLHandlerAdapter {
27+
final class DirectSSLHandlerAdapter extends AbstractPostgresSSLHandlerAdapter {
2828

2929
private final SSLConfig sslConfig;
3030

31-
SSLTunnelHandlerAdapter(ByteBufAllocator alloc, SocketAddress socketAddress, SSLConfig sslConfig) {
31+
DirectSSLHandlerAdapter(ByteBufAllocator alloc, SocketAddress socketAddress, SSLConfig sslConfig) {
3232
super(alloc, socketAddress, sslConfig);
3333
this.sslConfig = sslConfig;
3434
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,8 +434,8 @@ private static void registerSslHandler(SocketAddress socketAddress, SSLConfig ss
434434
if (sslConfig.getSslMode().startSsl()) {
435435

436436
AbstractPostgresSSLHandlerAdapter sslAdapter;
437-
if (sslConfig.getSslMode() == SSLMode.TUNNEL) {
438-
sslAdapter = new SSLTunnelHandlerAdapter(channel.alloc(), socketAddress, sslConfig);
437+
if (sslConfig.isDirectSsl()) {
438+
sslAdapter = new DirectSSLHandlerAdapter(channel.alloc(), socketAddress, sslConfig);
439439
} else {
440440
sslAdapter = new SSLSessionHandlerAdapter(channel.alloc(), socketAddress, sslConfig);
441441
}

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public final class SSLConfig {
3232
@Nullable
3333
private final HostnameVerifier hostnameVerifier;
3434

35+
private final SSLNegotiation sslNegotiation;
36+
3537
private final SSLMode sslMode;
3638

3739
@Nullable
@@ -42,11 +44,13 @@ public final class SSLConfig {
4244
private final Function<SocketAddress, SSLParameters> sslParametersFactory;
4345

4446
public SSLConfig(SSLMode sslMode, @Nullable Supplier<SslContext> sslProvider, @Nullable HostnameVerifier hostnameVerifier) {
45-
this(sslMode, sslProvider, Function.identity(), it -> new SSLParameters(), hostnameVerifier);
47+
this(SSLNegotiation.POSTGRES, sslMode, sslProvider, Function.identity(), it -> new SSLParameters(), hostnameVerifier);
4648
}
4749

48-
public SSLConfig(SSLMode sslMode, @Nullable Supplier<SslContext> sslProvider, Function<SSLEngine, SSLEngine> sslEngineCustomizer, Function<SocketAddress, SSLParameters> sslParametersFactory,
50+
public SSLConfig(SSLNegotiation sslNegotiation, SSLMode sslMode, @Nullable Supplier<SslContext> sslProvider, Function<SSLEngine, SSLEngine> sslEngineCustomizer, Function<SocketAddress,
51+
SSLParameters> sslParametersFactory,
4952
@Nullable HostnameVerifier hostnameVerifier) {
53+
this.sslNegotiation = sslNegotiation;
5054
if (sslMode != SSLMode.DISABLE) {
5155
Assert.requireNonNull(sslProvider, "SslContext provider is required for ssl mode " + sslMode);
5256
}
@@ -68,6 +72,15 @@ HostnameVerifier getHostnameVerifier() {
6872
return this.hostnameVerifier;
6973
}
7074

75+
@SuppressWarnings("deprecation")
76+
public boolean isDirectSsl() {
77+
return getSslMode() == SSLMode.TUNNEL || getSslNegotiation() == SSLNegotiation.DIRECT || getSslNegotiation() == SSLNegotiation.TUNNEL;
78+
}
79+
80+
public SSLNegotiation getSslNegotiation() {
81+
return this.sslNegotiation;
82+
}
83+
7184
public SSLMode getSslMode() {
7285
return this.sslMode;
7386
}
@@ -83,13 +96,13 @@ public Function<SSLEngine, SSLEngine> getSslEngineCustomizer() {
8396
return this.sslEngineCustomizer;
8497
}
8598

86-
8799
public Function<SocketAddress, SSLParameters> getSslParametersFactory() {
88100
return this.sslParametersFactory;
89101
}
90102

91103
public SSLConfig mutateMode(SSLMode newMode) {
92104
return new SSLConfig(
105+
this.sslNegotiation,
93106
newMode,
94107
this.sslProvider,
95108
this.sslEngineCustomizer,

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

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

1717
package io.r2dbc.postgresql.client;
1818

19+
import java.util.Arrays;
20+
1921
public enum SSLMode {
2022
/**
2123
* I don't care about security and don't want to pay the overhead for encryption
@@ -48,8 +50,10 @@ public enum SSLMode {
4850
VERIFY_FULL("verify-full"),
4951

5052
/**
51-
* I want to use a SSL tunnel instead of following Postgres SSL handshake protocol.
53+
* I want to use an SSL tunnel instead of following Postgres SSL handshake protocol.
54+
* @deprecated since 1.1, use {@link SSLNegotiation#TUNNEL} and configure your own SSL tunnel.
5255
*/
56+
@Deprecated
5357
TUNNEL("tunnel");
5458

5559
private final String value;
@@ -68,7 +72,7 @@ public boolean startSsl() {
6872

6973
@Override
7074
public String toString() {
71-
return value;
75+
return this.value;
7276
}
7377

7478
public boolean verifyCertificate() {
@@ -85,6 +89,6 @@ public static SSLMode fromValue(String sslModeString) {
8589
return sslMode;
8690
}
8791
}
88-
throw new IllegalArgumentException("Invalid ssl mode value: " + sslModeString);
92+
throw new IllegalArgumentException("Invalid ssl mode value: " + sslModeString + ". Supported values are: " + Arrays.toString(values()));
8993
}
9094
}

0 commit comments

Comments
 (0)