diff --git a/vertx-pg-client/README.adoc b/vertx-pg-client/README.adoc index 1d025898a..2c586afb6 100644 --- a/vertx-pg-client/README.adoc +++ b/vertx-pg-client/README.adoc @@ -307,7 +307,7 @@ mvn test -DcontainerFixedPort You can run tests with an external database: - the script `src/test/resources/create-postgres.sql` creates the test data -- the `TLSTest` expects the database to be configured with SSL with `src/test/resources/tls/server.key` / `src/test/resources/tls/server.cert`` +- the `TLSTest` expects the database to be configured with SSL with `src/test/resources/tls/server.key` / `src/test/resources/tls/server.cert` `src/test/resources/tls/pg_hba.conf` as an example how to force SSL You need to add some properties for testing: @@ -317,6 +317,7 @@ You need to add some properties for testing: - connection.uri(mandatory): configure the client to connect the specified database - tls.connection.uri(mandatory): configure the client to run `TLSTest` with the specified Postgres with SSL enabled +- tls.force.connection.uri(mandatory): configure the client to run `TLSTest` with the specified Postgres with SSL forced (only option) - unix.socket.directory(optional): the single unix socket directory(multiple socket directories are not supported) to test Unix domain socket with a specified database, domain socket tests will be skipped if this property is not specified (Note: Make sure you can access the unix domain socket with this directory under your host machine) - unix.socket.port(optional): unix socket file is named `.s.PGSQL.nnnn` and `nnnn` is the server's port number, @@ -335,5 +336,5 @@ Run the Postgres containers with `docker compose`: Run tests: ``` -> mvn test -Dconnection.uri=postgres://$username:$password@$host:$port/$database -Dtls.connection.uri=postgres://$username:$password@$host:$port/$database -Dunix.socket.directory=$path -Dunix.socket.port=$port +> mvn test -Dconnection.uri=postgres://$username:$password@$host:$port/$database -Dtls.connection.uri=postgres://$username:$password@$host:$port/$database -Dtls.force.connection.uri=postgres://$username:$password@$host:$port/$database -Dunix.socket.directory=$path -Dunix.socket.port=$port ``` diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgConnectionFactory.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgConnectionFactory.java index de3bec8ec..c6d2e9e3d 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgConnectionFactory.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgConnectionFactory.java @@ -70,49 +70,37 @@ protected Future doConnectInternal(PgConnectOptions options, Context } catch (Exception e) { return context.failedFuture(e); } - String username = options.getUser(); - String password = options.getPassword(); - String database = options.getDatabase(); SocketAddress server = options.getSocketAddress(); - Map properties = options.getProperties() != null ? Collections.unmodifiableMap(options.getProperties()) : null; - return doConnect(server, context, options).flatMap(conn -> { - PgSocketConnection socket = (PgSocketConnection) conn; - socket.init(); - return Future.future(p -> socket.sendStartupMessage(username, password, database, properties, p)) - .map(conn); - }); + return connect(server, context, true, options); } - public void cancelRequest(PgConnectOptions options, int processId, int secretKey, Handler> handler) { - doConnect(options.getSocketAddress(), vertx.createEventLoopContext(), options).onComplete(ar -> { - if (ar.succeeded()) { - PgSocketConnection conn = (PgSocketConnection) ar.result(); - conn.sendCancelRequestMessage(processId, secretKey, handler); - } else { - handler.handle(Future.failedFuture(ar.cause())); - } - }); + public Future cancelRequest(PgConnectOptions options, int processId, int secretKey) { + return connect(options.getSocketAddress(), vertx.createEventLoopContext(), false, options) + .compose(conn -> { + PgSocketConnection socket = (PgSocketConnection) conn; + return socket.sendCancelRequestMessage(processId, secretKey); + }); } - private Future doConnect(SocketAddress server, ContextInternal context, PgConnectOptions options) { + private Future connect(SocketAddress server, ContextInternal context, boolean sendStartupMessage, PgConnectOptions options) { SslMode sslMode = options.isUsingDomainSocket() ? SslMode.DISABLE : options.getSslMode(); ConnectOptions connectOptions = new ConnectOptions() .setRemoteAddress(server); Future connFuture; switch (sslMode) { case DISABLE: - connFuture = doConnect(connectOptions, context, false, options); + connFuture = connect(connectOptions, context, false, sendStartupMessage, options); break; case ALLOW: - connFuture = doConnect(connectOptions, context, false, options).recover(err -> doConnect(connectOptions, context, true, options)); + connFuture = connect(connectOptions, context, false, sendStartupMessage, options).recover(err -> connect(connectOptions, context, true, sendStartupMessage, options)); break; case PREFER: - connFuture = doConnect(connectOptions, context, true, options).recover(err -> doConnect(connectOptions, context, false, options)); + connFuture = connect(connectOptions, context, true, sendStartupMessage, options).recover(err -> connect(connectOptions, context, false, sendStartupMessage, options)); break; case REQUIRE: case VERIFY_CA: case VERIFY_FULL: - connFuture = doConnect(connectOptions, context, true, options); + connFuture = connect(connectOptions, context, true, sendStartupMessage, options); break; default: return context.failedFuture(new IllegalArgumentException("Unsupported SSL mode")); @@ -120,6 +108,23 @@ private Future doConnect(SocketAddress server, ContextInternal conte return connFuture; } + private Future connect(ConnectOptions connectOptions, ContextInternal context, boolean ssl, boolean sendStartupMessage, PgConnectOptions options) { + Future res = doConnect(connectOptions, context, ssl, options); + if (sendStartupMessage) { + return res.flatMap(conn -> { + PgSocketConnection socket = (PgSocketConnection) conn; + socket.init(); + String username = options.getUser(); + String password = options.getPassword(); + String database = options.getDatabase(); + Map properties = options.getProperties() != null ? Collections.unmodifiableMap(options.getProperties()) : null; + return socket.sendStartupMessage(username, password, database, properties); + }); + } else { + return res; + } + } + private Future doConnect(ConnectOptions connectOptions, ContextInternal context, boolean ssl, PgConnectOptions options) { Future soFut; try { diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgConnectionImpl.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgConnectionImpl.java index a83476e8d..4a5520a09 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgConnectionImpl.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgConnectionImpl.java @@ -123,7 +123,7 @@ public Future cancelRequest() { Promise promise = context.owner().getOrCreateContext().promise(); context.emit(promise, p -> { PgSocketConnection unwrap = (PgSocketConnection) conn.unwrap(); - ((PgConnectionFactory) factory).cancelRequest(unwrap.connectOptions(), this.processId(), this.secretKey(), p); + ((PgConnectionFactory) factory).cancelRequest(unwrap.connectOptions(), this.processId(), this.secretKey()).onComplete(p); }); return promise.future(); } diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgSocketConnection.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgSocketConnection.java index 4678cbf06..c5dde402e 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgSocketConnection.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/PgSocketConnection.java @@ -83,12 +83,12 @@ public void init() { } // TODO RETURN FUTURE ??? - void sendStartupMessage(String username, String password, String database, Map properties, Promise completionHandler) { + Future sendStartupMessage(String username, String password, String database, Map properties) { InitCommand cmd = new InitCommand(this, username, password, database, properties); - schedule(context, cmd).onComplete(completionHandler); + return schedule(context, cmd); } - void sendCancelRequestMessage(int processId, int secretKey, Handler> handler) { + Future sendCancelRequestMessage(int processId, int secretKey) { Buffer buffer = Buffer.buffer(16); buffer.appendInt(16); // cancel request code @@ -96,16 +96,13 @@ void sendCancelRequestMessage(int processId, int secretKey, Handler { + return socket.write(buffer).andThen(ar -> { if (ar.succeeded()) { // directly close this connection if (status == Status.CONNECTED) { status = Status.CLOSING; socket.close(); } - handler.handle(Future.succeededFuture()); - } else { - handler.handle(Future.failedFuture(ar.cause())); } }); } diff --git a/vertx-pg-client/src/test/java/io/vertx/pgclient/TLSTest.java b/vertx-pg-client/src/test/java/io/vertx/pgclient/TLSTest.java index bc455c428..4d23dcc58 100644 --- a/vertx-pg-client/src/test/java/io/vertx/pgclient/TLSTest.java +++ b/vertx-pg-client/src/test/java/io/vertx/pgclient/TLSTest.java @@ -35,7 +35,13 @@ public class TLSTest { @ClassRule - public static ContainerPgRule rule = new ContainerPgRule().ssl(true); + public static ContainerPgRule ruleOptionalSll = new ContainerPgRule().ssl(true); + + @ClassRule + public static ContainerPgRule ruleForceSsl = new ContainerPgRule().ssl(true).forceSsl(true); + + @ClassRule + public static ContainerPgRule ruleSllOff = new ContainerPgRule().ssl(false); private Vertx vertx; @@ -53,7 +59,7 @@ public void teardown(TestContext ctx) { public void testTLS(TestContext ctx) { Async async = ctx.async(); - PgConnectOptions options = new PgConnectOptions(rule.options()) + PgConnectOptions options = new PgConnectOptions(ruleOptionalSll.options()) .setSslMode(SslMode.REQUIRE) .setSslOptions(new ClientSSLOptions().setTrustOptions(new PemTrustOptions().addCertPath("tls/server.crt"))); PgConnection.connect(vertx, options.setSslMode(SslMode.REQUIRE)).onComplete(ctx.asyncAssertSuccess(conn -> { @@ -74,7 +80,7 @@ public void testTLS(TestContext ctx) { @Test public void testTLSTrustAll(TestContext ctx) { Async async = ctx.async(); - PgConnection.connect(vertx, rule.options().setSslMode(SslMode.REQUIRE).setSslOptions(new ClientSSLOptions().setTrustAll(true))).onComplete(ctx.asyncAssertSuccess(conn -> { + PgConnection.connect(vertx, ruleOptionalSll.options().setSslMode(SslMode.REQUIRE).setSslOptions(new ClientSSLOptions().setTrustAll(true))).onComplete(ctx.asyncAssertSuccess(conn -> { ctx.assertTrue(conn.isSSL()); async.complete(); })); @@ -83,7 +89,7 @@ public void testTLSTrustAll(TestContext ctx) { @Test public void testTLSInvalidCertificate(TestContext ctx) { Async async = ctx.async(); - PgConnection.connect(vertx, rule.options().setSslMode(SslMode.REQUIRE).setSslOptions(new ClientSSLOptions().setTrustOptions(new PemTrustOptions().addCertPath("tls/another.crt")))).onComplete(ctx.asyncAssertFailure(err -> { + PgConnection.connect(vertx, ruleOptionalSll.options().setSslMode(SslMode.REQUIRE).setSslOptions(new ClientSSLOptions().setTrustOptions(new PemTrustOptions().addCertPath("tls/another.crt")))).onComplete(ctx.asyncAssertFailure(err -> { // ctx.assertEquals(err.getClass(), VertxException.class); ctx.assertEquals(err.getMessage(), "SSL handshake failed"); async.complete(); @@ -93,7 +99,7 @@ public void testTLSInvalidCertificate(TestContext ctx) { @Test public void testSslModeDisable(TestContext ctx) { Async async = ctx.async(); - PgConnectOptions options = rule.options() + PgConnectOptions options = ruleOptionalSll.options() .setSslMode(SslMode.DISABLE); PgConnection.connect(vertx, new PgConnectOptions(options)).onComplete(ctx.asyncAssertSuccess(conn -> { ctx.assertFalse(conn.isSSL()); @@ -104,7 +110,7 @@ public void testSslModeDisable(TestContext ctx) { @Test public void testSslModeAllow(TestContext ctx) { Async async = ctx.async(); - PgConnectOptions options = rule.options() + PgConnectOptions options = ruleOptionalSll.options() .setSslMode(SslMode.ALLOW); PgConnection.connect(vertx, new PgConnectOptions(options)).onComplete(ctx.asyncAssertSuccess(conn -> { ctx.assertFalse(conn.isSSL()); @@ -112,10 +118,22 @@ public void testSslModeAllow(TestContext ctx) { })); } + @Test + public void testSslModeAllowFallback(TestContext ctx) { + Async async = ctx.async(); + PgConnectOptions options = ruleForceSsl.options() + .setSslMode(SslMode.ALLOW) + .setSslOptions(new ClientSSLOptions().setTrustAll(true)); + PgConnection.connect(vertx, new PgConnectOptions(options)).onComplete(ctx.asyncAssertSuccess(conn -> { + ctx.assertTrue(conn.isSSL()); + async.complete(); + })); + } + @Test public void testSslModePrefer(TestContext ctx) { Async async = ctx.async(); - PgConnectOptions options = rule.options() + PgConnectOptions options = ruleOptionalSll.options() .setSslMode(SslMode.PREFER) .setSslOptions(new ClientSSLOptions().setTrustAll(true)); PgConnection.connect(vertx, new PgConnectOptions(options)).onComplete(ctx.asyncAssertSuccess(conn -> { @@ -124,9 +142,21 @@ public void testSslModePrefer(TestContext ctx) { })); } + @Test + public void testSslModePreferFallback(TestContext ctx) { + Async async = ctx.async(); + PgConnectOptions options = ruleSllOff.options() + .setSslMode(SslMode.PREFER) + .setSslOptions(new ClientSSLOptions().setTrustAll(true)); + PgConnection.connect(vertx, options).onComplete(ctx.asyncAssertSuccess(conn -> { + ctx.assertFalse(conn.isSSL()); + async.complete(); + })); + } + @Test public void testSslModeVerifyCaConf(TestContext ctx) { - PgConnectOptions options = rule.options() + PgConnectOptions options = ruleOptionalSll.options() .setSslMode(SslMode.VERIFY_CA) .setSslOptions(new ClientSSLOptions().setTrustAll(true)); PgConnection.connect(vertx, new PgConnectOptions(options)).onComplete(ctx.asyncAssertFailure(error -> { @@ -136,7 +166,7 @@ public void testSslModeVerifyCaConf(TestContext ctx) { @Test public void testSslModeVerifyFullConf(TestContext ctx) { - PgConnectOptions options = rule.options() + PgConnectOptions options = ruleOptionalSll.options() .setSslOptions(new ClientSSLOptions().setTrustOptions(new PemTrustOptions().addCertPath("tls/another.crt"))) .setSslMode(SslMode.VERIFY_FULL); PgConnection.connect(vertx, new PgConnectOptions(options)).onComplete(ctx.asyncAssertFailure(error -> { diff --git a/vertx-pg-client/src/test/java/io/vertx/pgclient/junit/ContainerPgRule.java b/vertx-pg-client/src/test/java/io/vertx/pgclient/junit/ContainerPgRule.java index 8863b6e6e..24ee60dd0 100644 --- a/vertx-pg-client/src/test/java/io/vertx/pgclient/junit/ContainerPgRule.java +++ b/vertx-pg-client/src/test/java/io/vertx/pgclient/junit/ContainerPgRule.java @@ -36,11 +36,13 @@ public class ContainerPgRule extends ExternalResource { private static final String connectionUri = System.getProperty("connection.uri"); private static final String tlsConnectionUri = System.getProperty("tls.connection.uri"); + private static final String tlsForceConnectionUri = System.getProperty("tls.force.connection.uri"); private ServerContainer server; private PgConnectOptions options; private String databaseVersion; private boolean ssl; + private boolean forceSsl; private String user = "postgres"; public ContainerPgRule ssl(boolean ssl) { @@ -48,6 +50,11 @@ public ContainerPgRule ssl(boolean ssl) { return this; } + public ContainerPgRule forceSsl(boolean forceSsl) { + this.forceSsl = forceSsl; + return this; + } + public PgConnectOptions options() { return new PgConnectOptions(options); } @@ -75,6 +82,11 @@ private void initServer(String version) throws Exception { .withClasspathResourceMapping("tls/server.crt", "/server.crt", BindMode.READ_ONLY) .withClasspathResourceMapping("tls/server.key", "/server.key", BindMode.READ_ONLY) .withClasspathResourceMapping("tls/ssl.sh", "/docker-entrypoint-initdb.d/ssl.sh", BindMode.READ_ONLY); + if (forceSsl) { + server + .withClasspathResourceMapping("tls/pg_hba.conf", "/tmp/pg_hba.conf", BindMode.READ_ONLY) + .withClasspathResourceMapping("tls/force_ssl.sh", "/docker-entrypoint-initdb.d/force_ssl.sh", BindMode.READ_ONLY); + } } if (System.getProperties().containsKey("containerFixedPort")) { server.withFixedExposedPort(POSTGRESQL_PORT, POSTGRESQL_PORT); @@ -84,7 +96,7 @@ private void initServer(String version) throws Exception { } public static boolean isTestingWithExternalDatabase() { - return isSystemPropertyValid(connectionUri) || isSystemPropertyValid(tlsConnectionUri); + return isSystemPropertyValid(connectionUri) || isSystemPropertyValid(tlsConnectionUri) || isSystemPropertyValid(tlsForceConnectionUri); } private static boolean isSystemPropertyValid(String systemProperty) { @@ -133,7 +145,11 @@ protected void before() throws Throwable { if (isTestingWithExternalDatabase()) { if (ssl) { - options = PgConnectOptions.fromUri(tlsConnectionUri); + if (forceSsl) { + options = PgConnectOptions.fromUri(tlsForceConnectionUri); + } else { + options = PgConnectOptions.fromUri(tlsConnectionUri); + } } else { options = PgConnectOptions.fromUri(connectionUri); diff --git a/vertx-pg-client/src/test/resources/tls/force_ssl.sh b/vertx-pg-client/src/test/resources/tls/force_ssl.sh new file mode 100644 index 000000000..35ac9eddb --- /dev/null +++ b/vertx-pg-client/src/test/resources/tls/force_ssl.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# force ssl +cp /tmp/pg_hba.conf /var/lib/postgresql/data/pg_hba.conf diff --git a/vertx-pg-client/src/test/resources/tls/pg_hba.conf b/vertx-pg-client/src/test/resources/tls/pg_hba.conf new file mode 100644 index 000000000..676972c87 --- /dev/null +++ b/vertx-pg-client/src/test/resources/tls/pg_hba.conf @@ -0,0 +1,128 @@ +# PostgreSQL Client Authentication Configuration File +# =================================================== +# +# Refer to the "Client Authentication" section in the PostgreSQL +# documentation for a complete description of this file. A short +# synopsis follows. +# +# ---------------------- +# Authentication Records +# ---------------------- +# +# This file controls: which hosts are allowed to connect, how clients +# are authenticated, which PostgreSQL user names they can use, which +# databases they can access. Records take one of these forms: +# +# local DATABASE USER METHOD [OPTIONS] +# host DATABASE USER ADDRESS METHOD [OPTIONS] +# hostssl DATABASE USER ADDRESS METHOD [OPTIONS] +# hostnossl DATABASE USER ADDRESS METHOD [OPTIONS] +# hostgssenc DATABASE USER ADDRESS METHOD [OPTIONS] +# hostnogssenc DATABASE USER ADDRESS METHOD [OPTIONS] +# +# (The uppercase items must be replaced by actual values.) +# +# The first field is the connection type: +# - "local" is a Unix-domain socket +# - "host" is a TCP/IP socket (encrypted or not) +# - "hostssl" is a TCP/IP socket that is SSL-encrypted +# - "hostnossl" is a TCP/IP socket that is not SSL-encrypted +# - "hostgssenc" is a TCP/IP socket that is GSSAPI-encrypted +# - "hostnogssenc" is a TCP/IP socket that is not GSSAPI-encrypted +# +# DATABASE can be "all", "sameuser", "samerole", "replication", a +# database name, a regular expression (if it starts with a slash (/)) +# or a comma-separated list thereof. The "all" keyword does not match +# "replication". Access to replication must be enabled in a separate +# record (see example below). +# +# USER can be "all", a user name, a group name prefixed with "+", a +# regular expression (if it starts with a slash (/)) or a comma-separated +# list thereof. In both the DATABASE and USER fields you can also write +# a file name prefixed with "@" to include names from a separate file. +# +# ADDRESS specifies the set of hosts the record matches. It can be a +# host name, or it is made up of an IP address and a CIDR mask that is +# an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that +# specifies the number of significant bits in the mask. A host name +# that starts with a dot (.) matches a suffix of the actual host name. +# Alternatively, you can write an IP address and netmask in separate +# columns to specify the set of hosts. Instead of a CIDR-address, you +# can write "samehost" to match any of the server's own IP addresses, +# or "samenet" to match any address in any subnet that the server is +# directly connected to. +# +# METHOD can be "trust", "reject", "md5", "password", "scram-sha-256", +# "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert". +# Note that "password" sends passwords in clear text; "md5" or +# "scram-sha-256" are preferred since they send encrypted passwords. +# +# OPTIONS are a set of options for the authentication in the format +# NAME=VALUE. The available options depend on the different +# authentication methods -- refer to the "Client Authentication" +# section in the documentation for a list of which options are +# available for which authentication methods. +# +# Database and user names containing spaces, commas, quotes and other +# special characters must be quoted. Quoting one of the keywords +# "all", "sameuser", "samerole" or "replication" makes the name lose +# its special character, and just match a database or username with +# that name. +# +# --------------- +# Include Records +# --------------- +# +# This file allows the inclusion of external files or directories holding +# more records, using the following keywords: +# +# include FILE +# include_if_exists FILE +# include_dir DIRECTORY +# +# FILE is the file name to include, and DIR is the directory name containing +# the file(s) to include. Any file in a directory will be loaded if suffixed +# with ".conf". The files of a directory are ordered by name. +# include_if_exists ignores missing files. FILE and DIRECTORY can be +# specified as a relative or an absolute path, and can be double-quoted if +# they contain spaces. +# +# ------------- +# Miscellaneous +# ------------- +# +# This file is read on server startup and when the server receives a +# SIGHUP signal. If you edit the file on a running system, you have to +# SIGHUP the server for the changes to take effect, run "pg_ctl reload", +# or execute "SELECT pg_reload_conf()". +# +# ---------------------------------- +# Put your actual configuration here +# ---------------------------------- +# +# If you want to allow non-local connections, you need to add more +# "host" records. In that case you will also need to make PostgreSQL +# listen on a non-local interface via the listen_addresses +# configuration parameter, or via the -i or -h command line switches. + +# CAUTION: Configuring the system for local "trust" authentication +# allows any local user to connect as any PostgreSQL user, including +# the database superuser. If you do not trust all your local users, +# use another authentication method. + + +# TYPE DATABASE USER ADDRESS METHOD + +# "local" is for Unix domain socket connections only +local all all trust +# IPv4 local connections: +hostssl all all 127.0.0.1/32 trust +# IPv6 local connections: +hostssl all all ::1/128 trust +# Allow replication connections from localhost, by a user with the +# replication privilege. +local replication all trust +host replication all 127.0.0.1/32 trust +host replication all ::1/128 trust + +hostssl all all all md5