Skip to content

Commit b8b7f19

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 5bc0379 commit b8b7f19

5 files changed

+243
-27
lines changed

README.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,18 @@ Mono<Connection> connectionMono = Mono.from(connectionFactory.create());
8282
| `compatibilityMode` | Enable compatibility mode for cursored fetching. Required when using newer pgpool versions. Defaults to `false`. _(Optional)_
8383
| `errorResponseLogLevel` | Log level for error responses. Any of `OFF`, `DEBUG`, `INFO`, `WARN` or `ERROR` Defaults to `DEBUG`. _(Optional)_
8484
| `fetchSize` | The default number of rows to return when fetching results. Defaults to `0` for unlimited. _(Optional)_
85-
| `forceBinary` | Whether to force binary transfer. Defaults to `false`. _(Optional)_
85+
| `forceBinary` | Whether to force binary transfer. Defaults to `false`. _(Optional)_
8686
| `loopResources` | TCP/Socket LoopResources (depends on the endpoint connection type). _(Optional)_
8787
| `noticeLogLevel` | Log level for error responses. Any of `OFF`, `DEBUG`, `INFO`, `WARN` or `ERROR` Defaults to `DEBUG`. _(Optional)_
8888
| `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.
8989
| `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.
90-
| `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)_
90+
| `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]. _(
91+
Optional)_
9192
| `schema` | The search path to set. _(Optional)_
9293
| `sslMode` | SSL mode to use, see `SSLMode` enum. Supported values: `DISABLE`, `ALLOW`, `PREFER`, `REQUIRE`, `VERIFY_CA`, `VERIFY_FULL`, `TUNNEL`. _(Optional)_
93-
| `sslRootCert` | Path to SSL CA certificate in PEM format. _(Optional)_
94-
| `sslKey` | Path to SSL key for TLS authentication in PEM format. _(Optional)_
95-
| `sslCert` | Path to SSL certificate for TLS authentication in PEM format. _(Optional)_
94+
| `sslRootCert` | Path to SSL CA certificate in PEM format. Can be also a resource path. _(Optional)_
95+
| `sslKey` | Path to SSL key for TLS authentication in PEM format. Can be also a resource path. _(Optional)_
96+
| `sslCert` | Path to SSL certificate for TLS authentication in PEM format. Can be also a resource path. _(Optional)_
9697
| `sslPassword` | Key password to decrypt SSL key. _(Optional)_
9798
| `sslHostnameVerifier` | `javax.net.ssl.HostnameVerifier` implementation. _(Optional)_
9899
| `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
@@ -37,7 +37,11 @@
3737

3838
import javax.net.ssl.HostnameVerifier;
3939
import java.io.File;
40+
import java.io.IOException;
41+
import java.io.InputStream;
42+
import java.net.MalformedURLException;
4043
import java.net.Socket;
44+
import java.net.URL;
4145
import java.time.Duration;
4246
import java.util.ArrayList;
4347
import java.util.Collections;
@@ -360,20 +364,20 @@ public static final class Builder {
360364
private String socket;
361365

362366
@Nullable
363-
private String sslCert = null;
367+
private URL sslCert = null;
364368

365369
private HostnameVerifier sslHostnameVerifier = DefaultHostnameVerifier.INSTANCE;
366370

367371
@Nullable
368-
private String sslKey = null;
372+
private URL sslKey = null;
369373

370374
private SSLMode sslMode = SSLMode.DISABLE;
371375

372376
@Nullable
373377
private CharSequence sslPassword = null;
374378

375379
@Nullable
376-
private String sslRootCert = null;
380+
private URL sslRootCert = null;
377381

378382
private Function<SslContextBuilder, SslContextBuilder> sslContextBuilderCustomizer = Function.identity();
379383

@@ -702,13 +706,24 @@ public Builder sslContextBuilderCustomizer(Function<SslContextBuilder, SslContex
702706
}
703707

704708
/**
705-
* Configure ssl cert for client certificate authentication.
709+
* Configure ssl cert for client certificate authentication. Can point to either a resource within the classpath or a file.
706710
*
707711
* @param sslCert an X.509 certificate chain file in PEM format
708712
* @return this {@link Builder}
709713
*/
710714
public Builder sslCert(String sslCert) {
711-
this.sslCert = Assert.requireFileExistsOrNull(sslCert, "sslCert must not be null and must exist");
715+
return sslCert(requireExistingFilePath(sslCert, "sslCert must not be null and must exist"));
716+
}
717+
718+
/**
719+
* Configure ssl cert for client certificate authentication.
720+
*
721+
* @param sslCert an X.509 certificate chain file in PEM format
722+
* @return this {@link Builder}
723+
* @since 0.8.7
724+
*/
725+
public Builder sslCert(URL sslCert) {
726+
this.sslCert = Assert.requireNonNull(sslCert, "sslCert must not be null");
712727
return this;
713728
}
714729

@@ -724,13 +739,24 @@ public Builder sslHostnameVerifier(HostnameVerifier sslHostnameVerifier) {
724739
}
725740

726741
/**
727-
* Configure ssl key for client certificate authentication.
742+
* Configure ssl key for client certificate authentication. Can point to either a resource within the classpath or a file.
728743
*
729744
* @param sslKey a PKCS#8 private key file in PEM format
730745
* @return this {@link Builder}
731746
*/
732747
public Builder sslKey(String sslKey) {
733-
this.sslKey = Assert.requireFileExistsOrNull(sslKey, "sslKey must not be null and must exist");
748+
return sslKey(requireExistingFilePath(sslKey, "sslKey must not be null and must exist"));
749+
}
750+
751+
/**
752+
* Configure ssl key for client certificate authentication.
753+
*
754+
* @param sslKey a PKCS#8 private key file in PEM format
755+
* @return this {@link Builder}
756+
* @since 0.8.7
757+
*/
758+
public Builder sslKey(URL sslKey) {
759+
this.sslKey = Assert.requireNonNull(sslKey, "sslKey must not be null");
734760
return this;
735761
}
736762

@@ -757,13 +783,24 @@ public Builder sslPassword(@Nullable CharSequence sslPassword) {
757783
}
758784

759785
/**
760-
* Configure ssl root cert for server certificate validation.
786+
* Configure ssl root cert for server certificate validation. Can point to either a resource within the classpath or a file.
761787
*
762788
* @param sslRootCert an X.509 certificate chain file in PEM format
763789
* @return this {@link Builder}
764790
*/
765791
public Builder sslRootCert(String sslRootCert) {
766-
this.sslRootCert = Assert.requireFileExistsOrNull(sslRootCert, "sslRootCert must not be null and must exist");
792+
return sslRootCert(requireExistingFilePath(sslRootCert, "sslRootCert must not be null and must exist"));
793+
}
794+
795+
/**
796+
* Configure ssl root cert for server certificate validation.
797+
*
798+
* @param sslRootCert an X.509 certificate chain file in PEM format
799+
* @return this {@link Builder}
800+
* @since 0.8.7
801+
*/
802+
public Builder sslRootCert(URL sslRootCert) {
803+
this.sslRootCert = Assert.requireNonNull(sslRootCert, "sslRootCert must not be null and must exist");
767804
return this;
768805
}
769806

@@ -851,14 +888,14 @@ private Supplier<SslProvider> createSslProvider() {
851888
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
852889
if (this.sslMode.verifyCertificate()) {
853890
if (this.sslRootCert != null) {
854-
sslContextBuilder.trustManager(new File(this.sslRootCert));
891+
doWithStream(this.sslRootCert, sslContextBuilder::trustManager);
855892
}
856893
} else {
857894
sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
858895
}
859896

860-
String sslKey = this.sslKey;
861-
String sslCert = this.sslCert;
897+
URL sslKey = this.sslKey;
898+
URL sslCert = this.sslCert;
862899

863900
// Emulate Libpq behavior
864901
// Determining the default file location
@@ -872,21 +909,24 @@ private Supplier<SslProvider> createSslProvider() {
872909

873910
if (sslCert == null) {
874911
String pathname = defaultDir + "postgresql.crt";
875-
if (new File(pathname).exists()) {
876-
sslCert = pathname;
877-
}
912+
sslCert = resolveUrlFromFile(pathname);
878913
}
879914

880915
if (sslKey == null) {
881916
String pathname = defaultDir + "postgresql.pk8";
882-
if (new File(pathname).exists()) {
883-
sslKey = pathname;
884-
}
917+
sslKey = resolveUrlFromFile(pathname);
885918
}
886919

920+
URL sslKeyToUse = sslKey;
921+
887922
if (sslKey != null && sslCert != null) {
888923
String sslPassword = this.sslPassword == null ? null : this.sslPassword.toString();
889-
sslContextBuilder.keyManager(new File(sslCert), new File(sslKey), sslPassword);
924+
925+
doWithStream(sslCert, certStream -> {
926+
doWithStream(sslKeyToUse, keyStream -> {
927+
sslContextBuilder.keyManager(certStream, keyStream, sslPassword);
928+
});
929+
});
890930
}
891931

892932
return () -> SslProvider.builder()
@@ -895,6 +935,54 @@ private Supplier<SslProvider> createSslProvider() {
895935
.build();
896936
}
897937

938+
interface StreamConsumer {
939+
940+
void doWithStream(InputStream is) throws IOException;
941+
942+
}
943+
944+
private void doWithStream(URL url, StreamConsumer consumer) {
945+
946+
try (InputStream is = url.openStream()) {
947+
948+
consumer.doWithStream(is);
949+
} catch (IOException e) {
950+
throw new IllegalStateException("Error while reading " + url, e);
951+
}
952+
}
953+
954+
private URL requireExistingFilePath(String path, String message) {
955+
956+
Assert.requireNonNull(path, message);
957+
958+
URL resource = getClass().getClassLoader().getResource(path);
959+
960+
if (resource != null) {
961+
return resource;
962+
}
963+
964+
if (!new File(path).exists()) {
965+
throw new IllegalArgumentException(message);
966+
}
967+
968+
return resolveUrlFromFile(path);
969+
}
970+
971+
private URL resolveUrlFromFile(String pathname) {
972+
973+
File file = new File(pathname);
974+
975+
if (file.exists()) {
976+
try {
977+
return file.toURI().toURL();
978+
} catch (MalformedURLException e) {
979+
throw new IllegalArgumentException(String.format("Malformed error occurred during creating URL from %s", pathname));
980+
}
981+
}
982+
983+
return null;
984+
}
985+
898986
}
899987

900988
static class FixedFetchSize implements ToIntFunction<String> {

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

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

146146
/**
147-
* Full path for the certificate file.
147+
* Path for the certificate file. Can point to either a resource within the classpath or a file.
148148
*/
149149
public static final Option<String> SSL_CERT = Option.valueOf("sslCert");
150150

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

156156
/**
157-
* Full path for the key file.
157+
* File path for the key file. Can point to either a resource within the classpath or a file.
158158
*/
159159
public static final Option<String> SSL_KEY = Option.valueOf("sslKey");
160160

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

171171
/**
172-
* File name of the SSL root certificate.
172+
* File path of the SSL root certificate. Can point to either a resource within the classpath or a file.
173173
*/
174174
public static final Option<String> SSL_ROOT_CERT = Option.valueOf("sslRootCert");
175175

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)