Skip to content

Commit 97fc297

Browse files
isabekmp911de
authored andcommitted
Accept SSL certificates by providing a resource path.
We now try to resolve SSL certificates first from the class path and then fall back to files when providing a path as string. [resolves #313][closes #318] Signed-off-by: Mark Paluch <[email protected]>
1 parent d418b86 commit 97fc297

5 files changed

+243
-27
lines changed

README.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,17 @@ Mono<Connection> connectionMono = Mono.from(connectionFactory.create());
7979
| `autodetectExtensions` | Whether to auto-detect and register `Extension`s from the class path. Defaults to `true`. _(Optional)_
8080
| `compatibilityMode` | Enable compatibility mode for cursored fetching. Required when using newer pgpool versions. Defaults to `false`. _(Optional)_
8181
| `fetchSize` | The default number of rows to return when fetching results. Defaults to `0` for unlimited. _(Optional)_
82-
| `forceBinary` | Whether to force binary transfer. Defaults to `false`. _(Optional)_
82+
| `forceBinary` | Whether to force binary transfer. Defaults to `false`. _(Optional)_
8383
| `loopResources` | TCP/Socket LoopResources (depends on the endpoint connection type). _(Optional)_
8484
| `preferAttachedBuffers` |Configure whether codecs should prefer attached data buffers. The default is `false`, meaning that codecs will copy data from the input buffer into a byte array. Enabling attached buffers requires consumption of values such as `Json` to avoid memory leaks.
8585
| `preparedStatementCacheQueries` | Determine the number of queries that are cached in each connection. The default is `-1`, meaning there's no limit. The value of `0` disables the cache. Any other value specifies the cache size.
86-
| `options` | A `Map<String, String>` of connection parameters. These are applied to each database connection created by the `ConnectionFactory`. Useful for setting generic [PostgreSQL connection parameters][psql-runtime-config]. _(Optional)_
86+
| `options` | A `Map<String, String>` of connection parameters. These are applied to each database connection created by the `ConnectionFactory`. Useful for setting generic [PostgreSQL connection parameters][psql-runtime-config]. _(
87+
Optional)_
8788
| `schema` | The search path to set. _(Optional)_
8889
| `sslMode` | SSL mode to use, see `SSLMode` enum. Supported values: `DISABLE`, `ALLOW`, `PREFER`, `REQUIRE`, `VERIFY_CA`, `VERIFY_FULL`, `TUNNEL`. _(Optional)_
89-
| `sslRootCert` | Path to SSL CA certificate in PEM format. _(Optional)_
90-
| `sslKey` | Path to SSL key for TLS authentication in PEM format. _(Optional)_
91-
| `sslCert` | Path to SSL certificate for TLS authentication in PEM format. _(Optional)_
90+
| `sslRootCert` | Path to SSL CA certificate in PEM format. Can be also a resource path. _(Optional)_
91+
| `sslKey` | Path to SSL key for TLS authentication in PEM format. Can be also a resource path. _(Optional)_
92+
| `sslCert` | Path to SSL certificate for TLS authentication in PEM format. Can be also a resource path. _(Optional)_
9293
| `sslPassword` | Key password to decrypt SSL key. _(Optional)_
9394
| `sslHostnameVerifier` | `javax.net.ssl.HostnameVerifier` implementation. _(Optional)_
9495
| `tcpNoDelay` | Enable/disable TCP NoDelay. Enabled by default. _(Optional)_

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

+107-19
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@
3333

3434
import javax.net.ssl.HostnameVerifier;
3535
import java.io.File;
36+
import java.io.IOException;
37+
import java.io.InputStream;
38+
import java.net.MalformedURLException;
3639
import java.net.Socket;
40+
import java.net.URL;
3741
import java.time.Duration;
3842
import java.util.ArrayList;
3943
import java.util.Collections;
@@ -338,20 +342,20 @@ public static final class Builder {
338342
private String socket;
339343

340344
@Nullable
341-
private String sslCert = null;
345+
private URL sslCert = null;
342346

343347
private HostnameVerifier sslHostnameVerifier = DefaultHostnameVerifier.INSTANCE;
344348

345349
@Nullable
346-
private String sslKey = null;
350+
private URL sslKey = null;
347351

348352
private SSLMode sslMode = SSLMode.DISABLE;
349353

350354
@Nullable
351355
private CharSequence sslPassword = null;
352356

353357
@Nullable
354-
private String sslRootCert = null;
358+
private URL sslRootCert = null;
355359

356360
private Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer = Function.identity();
357361

@@ -657,13 +661,24 @@ public Builder sslContextBuilderCustomizer(Function<SslContextBuilder, SslContex
657661
}
658662

659663
/**
660-
* Configure ssl cert for client certificate authentication.
664+
* Configure ssl cert for client certificate authentication. Can point to either a resource within the classpath or a file.
661665
*
662666
* @param sslCert an X.509 certificate chain file in PEM format
663667
* @return this {@link Builder}
664668
*/
665669
public Builder sslCert(String sslCert) {
666-
this.sslCert = Assert.requireFileExistsOrNull(sslCert, "sslCert must not be null and must exist");
670+
return sslCert(requireExistingFilePath(sslCert, "sslCert must not be null and must exist"));
671+
}
672+
673+
/**
674+
* Configure ssl cert for client certificate authentication.
675+
*
676+
* @param sslCert an X.509 certificate chain file in PEM format
677+
* @return this {@link Builder}
678+
* @since 0.8.7
679+
*/
680+
public Builder sslCert(URL sslCert) {
681+
this.sslCert = Assert.requireNonNull(sslCert, "sslCert must not be null");
667682
return this;
668683
}
669684

@@ -679,13 +694,24 @@ public Builder sslHostnameVerifier(HostnameVerifier sslHostnameVerifier) {
679694
}
680695

681696
/**
682-
* Configure ssl key for client certificate authentication.
697+
* Configure ssl key for client certificate authentication. Can point to either a resource within the classpath or a file.
683698
*
684699
* @param sslKey a PKCS#8 private key file in PEM format
685700
* @return this {@link Builder}
686701
*/
687702
public Builder sslKey(String sslKey) {
688-
this.sslKey = Assert.requireFileExistsOrNull(sslKey, "sslKey must not be null and must exist");
703+
return sslKey(requireExistingFilePath(sslKey, "sslKey must not be null and must exist"));
704+
}
705+
706+
/**
707+
* Configure ssl key for client certificate authentication.
708+
*
709+
* @param sslKey a PKCS#8 private key file in PEM format
710+
* @return this {@link Builder}
711+
* @since 0.8.7
712+
*/
713+
public Builder sslKey(URL sslKey) {
714+
this.sslKey = Assert.requireNonNull(sslKey, "sslKey must not be null");
689715
return this;
690716
}
691717

@@ -712,13 +738,24 @@ public Builder sslPassword(@Nullable CharSequence sslPassword) {
712738
}
713739

714740
/**
715-
* Configure ssl root cert for server certificate validation.
741+
* Configure ssl root cert for server certificate validation. Can point to either a resource within the classpath or a file.
716742
*
717743
* @param sslRootCert an X.509 certificate chain file in PEM format
718744
* @return this {@link Builder}
719745
*/
720746
public Builder sslRootCert(String sslRootCert) {
721-
this.sslRootCert = Assert.requireFileExistsOrNull(sslRootCert, "sslRootCert must not be null and must exist");
747+
return sslRootCert(requireExistingFilePath(sslRootCert, "sslRootCert must not be null and must exist"));
748+
}
749+
750+
/**
751+
* Configure ssl root cert for server certificate validation.
752+
*
753+
* @param sslRootCert an X.509 certificate chain file in PEM format
754+
* @return this {@link Builder}
755+
* @since 0.8.7
756+
*/
757+
public Builder sslRootCert(URL sslRootCert) {
758+
this.sslRootCert = Assert.requireNonNull(sslRootCert, "sslRootCert must not be null and must exist");
722759
return this;
723760
}
724761

@@ -804,14 +841,14 @@ private Supplier<SslProvider> createSslProvider() {
804841
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
805842
if (this.sslMode.verifyCertificate()) {
806843
if (this.sslRootCert != null) {
807-
sslContextBuilder.trustManager(new File(this.sslRootCert));
844+
doWithStream(this.sslRootCert, sslContextBuilder::trustManager);
808845
}
809846
} else {
810847
sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
811848
}
812849

813-
String sslKey = this.sslKey;
814-
String sslCert = this.sslCert;
850+
URL sslKey = this.sslKey;
851+
URL sslCert = this.sslCert;
815852

816853
// Emulate Libpq behavior
817854
// Determining the default file location
@@ -825,21 +862,24 @@ private Supplier<SslProvider> createSslProvider() {
825862

826863
if (sslCert == null) {
827864
String pathname = defaultDir + "postgresql.crt";
828-
if (new File(pathname).exists()) {
829-
sslCert = pathname;
830-
}
865+
sslCert = resolveUrlFromFile(pathname);
831866
}
832867

833868
if (sslKey == null) {
834869
String pathname = defaultDir + "postgresql.pk8";
835-
if (new File(pathname).exists()) {
836-
sslKey = pathname;
837-
}
870+
sslKey = resolveUrlFromFile(pathname);
838871
}
839872

873+
URL sslKeyToUse = sslKey;
874+
840875
if (sslKey != null && sslCert != null) {
841876
String sslPassword = this.sslPassword == null ? null : this.sslPassword.toString();
842-
sslContextBuilder.keyManager(new File(sslCert), new File(sslKey), sslPassword);
877+
878+
doWithStream(sslCert, certStream -> {
879+
doWithStream(sslKeyToUse, keyStream -> {
880+
sslContextBuilder.keyManager(certStream, keyStream, sslPassword);
881+
});
882+
});
843883
}
844884

845885
return () -> SslProvider.builder()
@@ -848,6 +888,54 @@ private Supplier<SslProvider> createSslProvider() {
848888
.build();
849889
}
850890

891+
interface StreamConsumer {
892+
893+
void doWithStream(InputStream is) throws IOException;
894+
895+
}
896+
897+
private void doWithStream(URL url, StreamConsumer consumer) {
898+
899+
try (InputStream is = url.openStream()) {
900+
901+
consumer.doWithStream(is);
902+
} catch (IOException e) {
903+
throw new IllegalStateException("Error while reading " + url, e);
904+
}
905+
}
906+
907+
private URL requireExistingFilePath(String path, String message) {
908+
909+
Assert.requireNonNull(path, message);
910+
911+
URL resource = getClass().getClassLoader().getResource(path);
912+
913+
if (resource != null) {
914+
return resource;
915+
}
916+
917+
if (!new File(path).exists()) {
918+
throw new IllegalArgumentException(message);
919+
}
920+
921+
return resolveUrlFromFile(path);
922+
}
923+
924+
private URL resolveUrlFromFile(String pathname) {
925+
926+
File file = new File(pathname);
927+
928+
if (file.exists()) {
929+
try {
930+
return file.toURI().toURL();
931+
} catch (MalformedURLException e) {
932+
throw new IllegalArgumentException(String.format("Malformed error occurred during creating URL from %s", pathname));
933+
}
934+
}
935+
936+
return null;
937+
}
938+
851939
}
852940

853941
static class FixedFetchSize implements ToIntFunction<String> {

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public final class PostgresqlConnectionFactoryProvider implements ConnectionFact
133133
public static final Option<Function<SslContextBuilder, SslContextBuilder>> SSL_CONTEXT_BUILDER_CUSTOMIZER = Option.valueOf("sslContextBuilderCustomizer");
134134

135135
/**
136-
* Full path for the certificate file.
136+
* Path for the certificate file. Can point to either a resource within the classpath or a file.
137137
*/
138138
public static final Option<String> SSL_CERT = Option.valueOf("sslCert");
139139

@@ -143,7 +143,7 @@ public final class PostgresqlConnectionFactoryProvider implements ConnectionFact
143143
public static final Option<HostnameVerifier> SSL_HOSTNAME_VERIFIER = Option.valueOf("sslHostnameVerifier");
144144

145145
/**
146-
* Full path for the key file.
146+
* File path for the key file. Can point to either a resource within the classpath or a file.
147147
*/
148148
public static final Option<String> SSL_KEY = Option.valueOf("sslKey");
149149

@@ -158,7 +158,7 @@ public final class PostgresqlConnectionFactoryProvider implements ConnectionFact
158158
public static final Option<String> SSL_PASSWORD = Option.valueOf("sslPassword");
159159

160160
/**
161-
* File name of the SSL root certificate.
161+
* File path of the SSL root certificate. Can point to either a resource within the classpath or a file.
162162
*/
163163
public static final Option<String> SSL_ROOT_CERT = Option.valueOf("sslRootCert");
164164

src/test/java/io/r2dbc/postgresql/PostgresqlConnectionConfigurationUnitTests.java

+92
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,96 @@ void constructorNoSslCustomizer() {
190190
.withMessage("sslContextBuilderCustomizer must not be null");
191191
}
192192

193+
@Test
194+
void constructorNoSslCert() {
195+
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder()
196+
.host("test-host")
197+
.password("test-password")
198+
.sslCert((String) null)
199+
.build())
200+
.withMessage("sslCert must not be null and must exist");
201+
}
202+
203+
@Test
204+
void constructorNotExistSslCert() {
205+
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder()
206+
.host("test-host")
207+
.username("test-username")
208+
.password("test-password")
209+
.sslCert("no-client.crt")
210+
.build())
211+
.withMessage("sslCert must not be null and must exist");
212+
}
213+
214+
@Test
215+
void constructorSslCert() {
216+
PostgresqlConnectionConfiguration.builder()
217+
.host("test-host")
218+
.username("test-username")
219+
.password("test-password")
220+
.sslCert("client.crt")
221+
.build();
222+
}
223+
224+
@Test
225+
void constructorNoSslKey() {
226+
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder()
227+
.host("test-host")
228+
.password("test-password")
229+
.sslKey((String) null)
230+
.build())
231+
.withMessage("sslKey must not be null and must exist");
232+
}
233+
234+
@Test
235+
void constructorNotExistSslKey() {
236+
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder()
237+
.host("test-host")
238+
.username("test-username")
239+
.password("test-password")
240+
.sslKey("no-client.key")
241+
.build())
242+
.withMessage("sslKey must not be null and must exist");
243+
}
244+
245+
@Test
246+
void constructorSslKey() {
247+
PostgresqlConnectionConfiguration.builder()
248+
.host("test-host")
249+
.username("test-username")
250+
.password("test-password")
251+
.sslKey("client.key")
252+
.build();
253+
}
254+
255+
@Test
256+
void constructorNullSslRootCert() {
257+
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder()
258+
.host("test-host")
259+
.password("test-password")
260+
.sslRootCert((String) null)
261+
.build())
262+
.withMessage("sslRootCert must not be null and must exist");
263+
}
264+
265+
@Test
266+
void constructorNotExistSslRootCert() {
267+
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder()
268+
.host("test-host")
269+
.password("test-password")
270+
.sslRootCert("no-server.crt")
271+
.build())
272+
.withMessage("sslRootCert must not be null and must exist");
273+
}
274+
275+
@Test
276+
void constructorSslRootCert() {
277+
PostgresqlConnectionConfiguration.builder()
278+
.host("test-host")
279+
.username("test-username")
280+
.password("test-password")
281+
.sslRootCert("client.crt")
282+
.build();
283+
}
284+
193285
}

0 commit comments

Comments
 (0)