From 8defee1da8b257d17314e01ee4b1c5229c14ebf9 Mon Sep 17 00:00:00 2001 From: Bruce Irschick Date: Tue, 6 Dec 2022 13:43:26 -0800 Subject: [PATCH 01/23] [AD-1017] Implement single-instance SSH tunnel. * Handle late-bound invocation (DbVisualizer) * Ensure support for multiple hash algorithms. * Properly embed new SSH tunnel component. --- NOTICE | 12 +- build.gradle | 31 +- config/spotbugs/spotbugs-exclude.xml | 8 +- .../documentdb/jdbc/DocumentDbConnection.java | 246 +----- .../jdbc/DocumentDbConnectionProperties.java | 411 +++++++--- .../jdbc/DocumentDbConnectionProperty.java | 11 +- .../documentdb/jdbc/DocumentDbMain.java | 51 +- .../jdbc/common/DatabaseMetaData.java | 4 +- .../jdbc/common/utilities/SqlError.java | 12 +- .../DocumentDbMultiThreadFileChannel.java | 143 ++++ .../sshtunnel/DocumentDbSshTunnelClient.java | 221 ++++++ .../sshtunnel/DocumentDbSshTunnelLock.java | 207 +++++ .../sshtunnel/DocumentDbSshTunnelServer.java | 736 ++++++++++++++++++ .../sshtunnel/DocumentDbSshTunnelService.java | 562 +++++++++++++ src/main/resources/jdbc.properties | 9 +- .../scalardatatypepromotion-opaque.png | Bin 50358 -> 48344 bytes .../scalardatatypepromotion-transparent.png | Bin 53611 -> 51303 bytes src/markdown/support/troubleshooting-guide.md | 14 + .../DocumentDbConnectionPropertiesTest.java | 143 ++-- .../jdbc/DocumentDbConnectionTest.java | 106 ++- .../documentdb/jdbc/DocumentDbMainTest.java | 64 +- .../jdbc/common/DatabaseMetaDataTest.java | 2 +- ...ocumentDbQueryMappingServiceBasicTest.java | 4 +- ...mentDbQueryMappingServiceDateTimeTest.java | 6 +- .../DocumentDbSshTunnelClientRunner.java | 145 ++++ .../DocumentDbSshTunnelClientTest.java | 146 ++++ .../DocumentDbSshTunnelServerTest.java | 59 ++ .../DocumentDbSshTunnelServiceTest.java | 246 ++++++ .../DocumentDbAbstractTestEnvironment.java | 22 +- .../jdbc/common/test/DocumentDbTest.java | 13 +- 30 files changed, 3172 insertions(+), 462 deletions(-) create mode 100644 src/main/java/software/amazon/documentdb/jdbc/sshtunnel/DocumentDbMultiThreadFileChannel.java create mode 100644 src/main/java/software/amazon/documentdb/jdbc/sshtunnel/DocumentDbSshTunnelClient.java create mode 100644 src/main/java/software/amazon/documentdb/jdbc/sshtunnel/DocumentDbSshTunnelLock.java create mode 100644 src/main/java/software/amazon/documentdb/jdbc/sshtunnel/DocumentDbSshTunnelServer.java create mode 100644 src/main/java/software/amazon/documentdb/jdbc/sshtunnel/DocumentDbSshTunnelService.java create mode 100644 src/test/java/software/amazon/documentdb/jdbc/sshtunnel/DocumentDbSshTunnelClientRunner.java create mode 100644 src/test/java/software/amazon/documentdb/jdbc/sshtunnel/DocumentDbSshTunnelClientTest.java create mode 100644 src/test/java/software/amazon/documentdb/jdbc/sshtunnel/DocumentDbSshTunnelServerTest.java create mode 100644 src/test/java/software/amazon/documentdb/jdbc/sshtunnel/DocumentDbSshTunnelServiceTest.java diff --git a/NOTICE b/NOTICE index 5b173ffb..cda25688 100644 --- a/NOTICE +++ b/NOTICE @@ -348,12 +348,12 @@ Package Artifact: https://repo.maven.apache.org/maven2/com/google/protobuf/proto Package Artifact: https://repo.maven.apache.org/maven2/com/google/protobuf/protobuf-java/3.6.1/protobuf-java-3.6.1-sources.jar Package Artifact: https://github.com/google/protobuf.git ----------------------- -Package ID: com.jcraft:jsch:0.1.55 -Package Homepage: http://www.jcraft.com/jsch/ +Package ID: com.github.mwiede:jsch:0.2.4 +Package Homepage: https://github.com/mwiede/jsch Package SPDX-License-Identifier: BSD-3-Clause -Package Artifact: https://repo.maven.apache.org/maven2/com/jcraft/jsch/0.1.55/jsch-0.1.55.jar -Package Artifact: https://repo.maven.apache.org/maven2/com/jcraft/jsch/0.1.55/jsch-0.1.55-sources.jar -Package Artifact: http://git.jcraft.com/jsch.git +Package Artifact: https://repo.maven.org/maven2/com/github/mwiede/jsch/0.2.4/jsch-0.2.4.jar +Package Artifact: https://repo.maven.org/maven2/com/github/mwiede/jsch/0.2.4/jsch-0.2.4-sources.jar +Package Artifact: https://github.com/mwiede/jsch.git ----------------------- Package ID: org.codehaus.janino:commons-compiler:3.1.6 Package Homepage: http://janino-compiler.github.io/commons-compiler/ @@ -16570,7 +16570,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ----------------------- -com.jcraft/jsch/0.1.55 LICENSE +github.com/mwiede/jsch/0.2.4 LICENSE JSch 0.0.* was released under the GNU LGPL license. Later, we have switched over to a BSD-style license. diff --git a/build.gradle b/build.gradle index 57296949..4101e60b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'com.github.spotbugs' version '5.0.10' + id 'com.github.spotbugs' version '5.0.13' id 'checkstyle' id 'jacoco' id 'com.github.hierynomus.license' version '0.16.1' @@ -92,7 +92,7 @@ shadowJar { minimize { exclude(dependency('org.apache.calcite::')) exclude(dependency('org.slf4j.*::')) - exclude(dependency('com.jcraft::')) + exclude(dependency('com.github.mwiede::')) } } @@ -145,24 +145,32 @@ spotbugsMain { // Configure HTML report reports { xml { - enabled = true + required.set(true) destination = file("$buildDir/reports/spotbugs/main.xml") } + html { + required.set(true) + destination = file("$buildDir/reports/spotbugs/main.html") } + } } spotbugsTest { // Configure HTML report reports { xml { - enabled = true + required.set(true) destination = file("$buildDir/reports/spotbugs/test.xml") } + html { + required.set(true) + destination = file("$buildDir/reports/spotbugs/test.html") } + } } task checkSpotBugsMainReport { doLast { - def xmlReport = spotbugsMain.reports.getByName("XML") + def xmlReport = spotbugsMain.reports.getByName("xml") def slurped = new groovy.xml.XmlSlurper().parse(xmlReport.destination) def bugsFound = slurped.BugInstance.size() slurped.BugInstance.each { @@ -179,7 +187,7 @@ task checkSpotBugsMainReport { task checkSpotBugsTestReport { doLast { - def xmlReport = spotbugsTest.reports.getByName("XML") + def xmlReport = spotbugsTest.reports.getByName("xml") def slurped = new XmlSlurper().parse(xmlReport.destination) def bugsFound = slurped.BugInstance.size() slurped.BugInstance.each { @@ -320,13 +328,14 @@ dependencies { implementation group: 'com.google.guava', name: 'guava', version: '31.1-jre' implementation group: 'org.slf4j', name: 'slf4j-log4j12', version: '2.0.5' implementation group: 'org.apache.commons', name: 'commons-text', version: '1.10.0' + implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.19.0' implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.19.0' - implementation group: 'org.mongodb', name: 'mongodb-driver-sync', version: '4.7.2' - implementation group: 'com.jcraft', name: 'jsch', version: '0.1.55' + implementation group: 'org.mongodb', name: 'mongodb-driver-sync', version: '4.8.0' + implementation group: 'com.github.mwiede', name: 'jsch', version: '0.2.4' implementation group: 'org.apache.calcite', name: 'calcite-core', version: '1.32.0' implementation group: 'commons-beanutils', name: 'commons-beanutils', version: '1.9.4' - implementation 'io.github.hakky54:sslcontext-kickstart:7.4.7' + implementation 'io.github.hakky54:sslcontext-kickstart:7.4.8' compileOnly group: 'org.immutables', name: 'value', version: '2.9.2' compileOnly group: 'com.puppycrawl.tools', name: 'checkstyle', version: '10.5.0' @@ -339,7 +348,7 @@ dependencies { testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.24' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.9.1' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.9.1' - testImplementation group: 'org.mockito', name: 'mockito-core', version: '4.8.0' + testImplementation group: 'org.mockito', name: 'mockito-core', version: '4.9.0' testRuntimeOnly group: 'de.flapdoodle.embed', name: 'de.flapdoodle.embed.mongo', version: '3.5.3' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-api:5.9.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' @@ -350,7 +359,7 @@ dependencies { testFixturesCompileOnly group: 'de.flapdoodle.embed', name: 'de.flapdoodle.embed.mongo', version: '3.5.3' testFixturesCompileOnly 'org.junit.jupiter:junit-jupiter-api:5.9.1' testFixturesImplementation group: 'com.google.guava', name: 'guava', version: '29.0-jre' - testFixturesImplementation group: 'org.mongodb', name: 'mongodb-driver-sync', version: '4.7.2' + testFixturesImplementation group: 'org.mongodb', name: 'mongodb-driver-sync', version: '4.8.0' testFixturesImplementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.2' testFixturesImplementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.2' } diff --git a/config/spotbugs/spotbugs-exclude.xml b/config/spotbugs/spotbugs-exclude.xml index 8b08357e..a618e626 100644 --- a/config/spotbugs/spotbugs-exclude.xml +++ b/config/spotbugs/spotbugs-exclude.xml @@ -90,7 +90,10 @@ - + + + + @@ -98,6 +101,9 @@ + + + diff --git a/src/main/java/software/amazon/documentdb/jdbc/DocumentDbConnection.java b/src/main/java/software/amazon/documentdb/jdbc/DocumentDbConnection.java index 38fb8ed4..8dae5df0 100644 --- a/src/main/java/software/amazon/documentdb/jdbc/DocumentDbConnection.java +++ b/src/main/java/software/amazon/documentdb/jdbc/DocumentDbConnection.java @@ -16,22 +16,13 @@ package software.amazon.documentdb.jdbc; -import com.jcraft.jsch.HostKey; -import com.jcraft.jsch.HostKeyRepository; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.Session; import com.mongodb.MongoClientSettings; import com.mongodb.MongoCommandException; import com.mongodb.MongoSecurityException; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.MongoDatabase; -import lombok.AllArgsConstructor; -import lombok.Getter; import lombok.SneakyThrows; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; import org.bson.Document; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,20 +30,20 @@ import software.amazon.documentdb.jdbc.common.utilities.SqlError; import software.amazon.documentdb.jdbc.common.utilities.SqlState; import software.amazon.documentdb.jdbc.metadata.DocumentDbDatabaseSchemaMetadata; +import software.amazon.documentdb.jdbc.sshtunnel.DocumentDbSshTunnelClient; -import java.nio.file.Files; -import java.nio.file.Path; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; -import static software.amazon.documentdb.jdbc.DocumentDbConnectionProperties.getDocumentDbSearchPaths; -import static software.amazon.documentdb.jdbc.DocumentDbConnectionProperties.getPath; -import static software.amazon.documentdb.jdbc.DocumentDbConnectionProperties.isNullOrWhitespace; import static software.amazon.documentdb.jdbc.DocumentDbConnectionProperty.REFRESH_SCHEMA; import static software.amazon.documentdb.jdbc.metadata.DocumentDbDatabaseSchemaMetadata.VERSION_LATEST_OR_NEW; import static software.amazon.documentdb.jdbc.metadata.DocumentDbDatabaseSchemaMetadata.VERSION_NEW; @@ -65,22 +56,17 @@ public class DocumentDbConnection extends Connection private static final Logger LOGGER = LoggerFactory.getLogger(DocumentDbConnection.class.getName()); - public static final String SSH_KNOWN_HOSTS_FILE = "~/.ssh/known_hosts"; - public static final String STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking"; - public static final String HASH_KNOWN_HOSTS = "HashKnownHosts"; - public static final String SERVER_HOST_KEY = "server_host_key"; - public static final String YES = "yes"; - public static final String NO = "no"; - public static final String LOCALHOST = "localhost"; - public static final int DEFAULT_DOCUMENTDB_PORT = 27017; - public static final int DEFAULT_SSH_PORT = 22; + private static final Set SECRET_PROPERTIES = + Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + DocumentDbConnectionProperty.PASSWORD.getName(), + DocumentDbConnectionProperty.PASSWORD.getName()))); private final DocumentDbConnectionProperties connectionProperties; private DocumentDbDatabaseMetaData metadata; private DocumentDbDatabaseSchemaMetadata databaseMetadata; private MongoClient mongoClient = null; private MongoDatabase mongoDatabase = null; - private SshPortForwardingSession session; + private DocumentDbSshTunnelClient sshTunnelClient; /** * DocumentDbConnection constructor, initializes super class. @@ -93,53 +79,37 @@ public class DocumentDbConnection extends Connection final StringBuilder sb = new StringBuilder(); sb.append("Creating connection with following properties:"); for (String propertyName : connectionProperties.stringPropertyNames()) { - if (!DocumentDbConnectionProperty.PASSWORD.getName().equals(propertyName)) { + if (!SECRET_PROPERTIES.contains(propertyName)) { sb.append(String.format("%n Connection property %s=%s", propertyName, connectionProperties.get(propertyName).toString())); } } LOGGER.debug(sb.toString()); } - this.session = createSshTunnel(connectionProperties); + if (connectionProperties.enableSshTunnel()) { + ensureSshTunnel(connectionProperties); + } else { + LOGGER.debug("Internal SSH tunnel not used."); + } initializeClients(connectionProperties); } /** - * Initializes the SSH session and creates a port forwarding tunnel. + * Ensures an SSH Tunnel service is started for this set of SSH Tunnel properties, or confirms + * an SSH Tunnel is already running. It ensures an SSH Tunnel client session is active and also ensures the + * SSH Tunnel's listening port is valid. * - * @param connectionProperties the {@link DocumentDbConnectionProperties} connection properties. - * @return a {@link Session} session. This session must be closed by calling the - * {@link Session#disconnect()} method. - * @throws SQLException if unable to create SSH session or create the port forwarding tunnel. + * @param connectionProperties the connection properties to use for the SSH Tunnel. + * @throws SQLException when unable to ensure an SSH Tunnel is started. */ - public static SshPortForwardingSession createSshTunnel( - final DocumentDbConnectionProperties connectionProperties) throws SQLException { - if (!connectionProperties.enableSshTunnel()) { - LOGGER.info("Internal SSH tunnel not started."); - return null; - } else if (!connectionProperties.isSshPrivateKeyFileExists()) { - throw SqlError.createSQLException( - LOGGER, - SqlState.CONNECTION_EXCEPTION, - SqlError.SSH_PRIVATE_KEY_FILE_NOT_FOUND, - connectionProperties.getSshPrivateKeyFile()); - } - - LOGGER.info("Internal SSH tunnel starting."); + private void ensureSshTunnel(final DocumentDbConnectionProperties connectionProperties) throws SQLException { try { - final JSch jSch = new JSch(); - addIdentity(connectionProperties, jSch); - final Session session = createSession(connectionProperties, jSch); - connectSession(connectionProperties, jSch, session); - final SshPortForwardingSession portForwardingSession = getPortForwardingSession( - connectionProperties, session); - LOGGER.info("Internal SSH tunnel started on local port '{}'.", - portForwardingSession.localPort); - return portForwardingSession; + this.sshTunnelClient = new DocumentDbSshTunnelClient(connectionProperties); } catch (SQLException e) { throw e; } catch (Exception e) { - throw new SQLException(e.getMessage(), e); + throw SqlError.createSQLException(LOGGER, SqlState.CONNECTION_EXCEPTION, e, + SqlError.SSH_TUNNEL_ERROR, e.getMessage()); } } @@ -151,7 +121,7 @@ public static SshPortForwardingSession createSshTunnel( public int getSshLocalPort() { // Get the port from the SSH tunnel session, if it exists. if (isSshTunnelActive()) { - return session.localPort; + return sshTunnelClient.getServiceListeningPort(); } return 0; } @@ -161,9 +131,10 @@ public int getSshLocalPort() { * * @return returns {@code true} if the SSH tunnel is active, {@code false}, otherwise. */ + @SneakyThrows public boolean isSshTunnelActive() { // indicate whether the SSH tunnel is enabled - return session != null; + return sshTunnelClient != null && sshTunnelClient.getServiceListeningPort() > 0; } @Override @@ -177,7 +148,7 @@ public boolean isValid(final int timeout) throws SQLException { if (mongoDatabase != null) { try { // Convert to milliseconds - final int maxTimeMS = timeout + 1000; + final long maxTimeMS = TimeUnit.SECONDS.toMillis(timeout); pingDatabase(maxTimeMS); return true; } catch (Exception e) { @@ -188,7 +159,7 @@ public boolean isValid(final int timeout) throws SQLException { } @Override - public void doClose() { + public void doClose() throws SQLException { if (mongoDatabase != null) { mongoDatabase = null; } @@ -196,9 +167,17 @@ public void doClose() { mongoClient.close(); mongoClient = null; } - if (session != null) { - session.session.disconnect(); - session = null; + if (sshTunnelClient != null) { + try { + sshTunnelClient.close(); + } catch (SQLException e) { + throw e; + } catch (Exception e) { + throw SqlError.createSQLException(LOGGER, SqlState.CONNECTION_EXCEPTION, e, + SqlError.SSH_TUNNEL_ERROR, e.getMessage()); + } finally { + sshTunnelClient = null; + } } } @@ -313,7 +292,7 @@ private void pingDatabase() throws SQLException { pingDatabase(0); } - private void pingDatabase(final int maxTimeMS) throws SQLException { + private void pingDatabase(final long maxTimeMS) throws SQLException { try { final String maxTimeMSOption = (maxTimeMS > 0) ? String.format(", \"maxTimeMS\" : %d", maxTimeMS) @@ -345,145 +324,4 @@ private void pingDatabase(final int maxTimeMS) throws SQLException { throw new SQLException(e.getMessage(), e); } } - - private static SshPortForwardingSession getPortForwardingSession( - final DocumentDbConnectionProperties connectionProperties, - final Session session) throws JSchException { - final Pair clusterHostAndPort = getHostAndPort( - connectionProperties.getHostname(), DEFAULT_DOCUMENTDB_PORT); - final int localPort = session.setPortForwardingL( - LOCALHOST, 0, clusterHostAndPort.getLeft(), clusterHostAndPort.getRight()); - return new SshPortForwardingSession(session, localPort); - } - - private static Pair getHostAndPort( - final String hostname, - final int defaultPort) { - final String clusterHost; - final int clusterPort; - final int portSeparatorIndex = hostname.indexOf(':'); - if (portSeparatorIndex >= 0) { - clusterHost = hostname.substring(0, portSeparatorIndex); - clusterPort = Integer.parseInt( - hostname.substring(portSeparatorIndex + 1)); - } else { - clusterHost = hostname; - clusterPort = defaultPort; - } - return new ImmutablePair<>(clusterHost, clusterPort); - } - - private static void connectSession( - final DocumentDbConnectionProperties connectionProperties, - final JSch jSch, - final Session session) throws SQLException { - setSecurityConfig(connectionProperties, jSch, session); - try { - session.connect(); - } catch (JSchException e) { - throw new SQLException(e.getMessage(), e); - } - } - - private static void addIdentity( - final DocumentDbConnectionProperties connectionProperties, - final JSch jSch) throws JSchException { - final String privateKeyFileName = getPath(connectionProperties.getSshPrivateKeyFile(), - getDocumentDbSearchPaths()).toString(); - LOGGER.debug("SSH private key file resolved to '{}'.", privateKeyFileName); - // If passPhrase protected, will need to provide this, too. - final String passPhrase = !isNullOrWhitespace(connectionProperties.getSshPrivateKeyPassphrase()) - ? connectionProperties.getSshPrivateKeyPassphrase() - : null; - jSch.addIdentity(privateKeyFileName, passPhrase); - } - - private static Session createSession( - final DocumentDbConnectionProperties connectionProperties, - final JSch jSch) throws SQLException { - final String sshUsername = connectionProperties.getSshUser(); - final Pair sshHostAndPort = getHostAndPort( - connectionProperties.getSshHostname(), DEFAULT_SSH_PORT); - setKnownHostsFile(connectionProperties, jSch); - try { - return jSch.getSession(sshUsername, sshHostAndPort.getLeft(), sshHostAndPort.getRight()); - } catch (JSchException e) { - throw new SQLException(e.getMessage(), e); - } - } - - private static void setSecurityConfig( - final DocumentDbConnectionProperties connectionProperties, - final JSch jSch, - final Session session) { - if (!connectionProperties.getSshStrictHostKeyChecking()) { - session.setConfig(STRICT_HOST_KEY_CHECKING, NO); - return; - } - setHostKeyType(connectionProperties, jSch, session); - } - - private static void setHostKeyType( - final DocumentDbConnectionProperties connectionProperties, - final JSch jSch, final Session session) { - final HostKeyRepository keyRepository = jSch.getHostKeyRepository(); - final HostKey[] hostKeys = keyRepository.getHostKey(); - final Pair sshHostAndPort = getHostAndPort( - connectionProperties.getSshHostname(), DEFAULT_SSH_PORT); - final HostKey hostKey = Arrays.stream(hostKeys) - .filter(hk -> hk.getHost().equals(sshHostAndPort.getLeft())) - .findFirst().orElse(null); - // This will ensure a match between how the host key was hashed in the known_hosts file. - final String hostKeyType = (hostKey != null) ? hostKey.getType() : null; - // Set the hash algorithm - if (hostKeyType != null) { - session.setConfig(SERVER_HOST_KEY, hostKeyType); - } - // The default behaviour of `ssh-keygen` is to hash known hosts keys - session.setConfig(HASH_KNOWN_HOSTS, YES); - } - - private static void setKnownHostsFile( - final DocumentDbConnectionProperties connectionProperties, - final JSch jSch) throws SQLException { - if (!connectionProperties.getSshStrictHostKeyChecking()) { - return; - } - final String knowHostsFilename; - if (!isNullOrWhitespace(connectionProperties.getSshKnownHostsFile())) { - final Path knownHostsPath = getPath(connectionProperties.getSshKnownHostsFile()); - if (Files.exists(knownHostsPath)) { - knowHostsFilename = knownHostsPath.toString(); - } else { - throw SqlError.createSQLException( - LOGGER, - SqlState.INVALID_PARAMETER_VALUE, - SqlError.KNOWN_HOSTS_FILE_NOT_FOUND, - connectionProperties.getSshKnownHostsFile()); - } - } else { - knowHostsFilename = getPath(SSH_KNOWN_HOSTS_FILE).toString(); - } - try { - jSch.setKnownHosts(knowHostsFilename); - } catch (JSchException e) { - throw new SQLException(e.getMessage(), e); - } - } - - /** - * Container for the SSH port forwarding tunnel session. - */ - @Getter - @AllArgsConstructor - public static class SshPortForwardingSession { - /** - * Gets the SSH session. - */ - private final Session session; - /** - * Gets the local port for the port forwarding tunnel. - */ - private final int localPort; - } } diff --git a/src/main/java/software/amazon/documentdb/jdbc/DocumentDbConnectionProperties.java b/src/main/java/software/amazon/documentdb/jdbc/DocumentDbConnectionProperties.java index ce4e3170..a86bf7fe 100644 --- a/src/main/java/software/amazon/documentdb/jdbc/DocumentDbConnectionProperties.java +++ b/src/main/java/software/amazon/documentdb/jdbc/DocumentDbConnectionProperties.java @@ -41,6 +41,8 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; @@ -50,6 +52,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Properties; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; @@ -61,20 +64,21 @@ public class DocumentDbConnectionProperties extends Properties { public static final String USER_HOME_PROPERTY = "user.home"; private static final Logger LOGGER = LoggerFactory.getLogger(DocumentDbConnectionProperties.class.getName()); - private static final String USER_HOME_PATH_NAME = System.getProperty(USER_HOME_PROPERTY); - private static final String DOCUMENTDB_HOME_PATH_NAME = Paths.get( - USER_HOME_PATH_NAME, ".documentdb").toString(); private static final Pattern WHITE_SPACE_PATTERN = Pattern.compile("^\\s*$"); private static final String ROOT_PEM_RESOURCE_FILE_NAME = "/rds-ca-2019-root.pem"; public static final String HOME_PATH_PREFIX_REG_EXPR = "^~[/\\\\].*$"; public static final int FETCH_SIZE_DEFAULT = 2000; public static final String DOCUMENTDB_CUSTOM_OPTIONS = "DOCUMENTDB_CUSTOM_OPTIONS"; - private static String classPathLocationName = null; private static String[] documentDbSearchPaths = null; private static final String DEFAULT_APPLICATION_NAME_KEY = "default.application.name"; private static final String PROPERTIES_FILE_PATH = "/documentdb-jdbc.properties"; static final String DEFAULT_APPLICATION_NAME; + public static final String USER_HOME_PATH_NAME = System.getProperty(USER_HOME_PROPERTY); + public static final String DOCUMENTDB_HOME_PATH_NAME = Paths.get( + USER_HOME_PATH_NAME, ".documentdb").toString(); + public static final String CONNECTION_STRING_TEMPLATE = "//%s%s/%s%s"; + static { String defaultAppName = ""; try (InputStream is = DocumentDbConnectionProperties.class.getResourceAsStream(PROPERTIES_FILE_PATH)) { @@ -87,6 +91,24 @@ public class DocumentDbConnectionProperties extends Properties { DEFAULT_APPLICATION_NAME = defaultAppName; } + /** + * Enumeration of type of validation. + */ + public enum ValidationType { + /** + * No validation. + */ + NONE, + /** + * Validate client connection required properties. + */ + CLIENT, + /** + * Validate SSH tunnel required properties. + */ + SSH_TUNNEL, + } + /** * Constructor for DocumentDbConnectionProperties, initializes with given properties. * @@ -121,36 +143,11 @@ public static String[] getDocumentDbSearchPaths() { } /** - * Gets the user's home path name. - * - * @return the user's home path name. - */ - static String getUserHomePathName() { - return USER_HOME_PATH_NAME; - } - - /** - * Gets the ~/.documentdb path name. + * Gets the parent folder location of the current class. * - * @return the ~/.documentdb path name. + * @return a string representing the parent folder location of the current class. */ - static String getDocumentdbHomePathName() { - return DOCUMENTDB_HOME_PATH_NAME; - } - - /** - * Gets the class path's location name. - * - * @return the class path's location name. - */ - static String getClassPathLocationName() { - if (classPathLocationName == null) { - classPathLocationName = getClassPathLocation(); - } - return classPathLocationName; - } - - private static String getClassPathLocation() { + public static String getClassPathLocation() { String classPathLocation = null; final URL classPathLocationUrl = DocumentDbConnectionProperties.class .getProtectionDomain() @@ -771,46 +768,151 @@ public MongoClientSettings buildMongoClientSettings( * * @return a {@link String} with the sanitized connection properties. */ - public String buildSanitizedConnectionString() { - final String connectionStringTemplate = "//%s%s/%s%s"; - final String loginInfo = isNullOrWhitespace(getUser()) ? "" : getUser() + "@"; - final String hostInfo = isNullOrWhitespace(getHostname()) ? "" : getHostname(); - final String databaseInfo = isNullOrWhitespace(getDatabase()) ? "" : getDatabase(); + public @NonNull String buildSanitizedConnectionString() { + final String loginInfo = buildLoginInfo(getUser(), null); + final String hostInfo = buildHostInfo(getHostname()); + final String databaseInfo = buildDatabaseInfo(getDatabase()); final StringBuilder optionalInfo = new StringBuilder(); - if (!getApplicationName() - .equals(DocumentDbConnectionProperty.APPLICATION_NAME.getDefaultValue())) { - appendOption(optionalInfo, DocumentDbConnectionProperty.APPLICATION_NAME, getApplicationName()); - } - if (getLoginTimeout() != Integer.parseInt(DocumentDbConnectionProperty.LOGIN_TIMEOUT_SEC.getDefaultValue())) { - appendOption(optionalInfo, DocumentDbConnectionProperty.LOGIN_TIMEOUT_SEC, getLoginTimeout()); - } - if (getMetadataScanLimit() != Integer.parseInt(DocumentDbConnectionProperty.METADATA_SCAN_LIMIT.getDefaultValue())) { - appendOption(optionalInfo, DocumentDbConnectionProperty.METADATA_SCAN_LIMIT, getMetadataScanLimit()); - } - if (getMetadataScanMethod() != DocumentDbMetadataScanMethod.fromString(DocumentDbConnectionProperty.METADATA_SCAN_METHOD.getDefaultValue())) { - appendOption(optionalInfo, DocumentDbConnectionProperty.METADATA_SCAN_METHOD, getMetadataScanMethod().getName()); - } - if (getRetryReadsEnabled() != Boolean.parseBoolean(DocumentDbConnectionProperty.RETRY_READS_ENABLED.getDefaultValue())) { - appendOption(optionalInfo, DocumentDbConnectionProperty.RETRY_READS_ENABLED, getRetryReadsEnabled()); + buildSanitizedOptionalInfo(optionalInfo, this); + return buildConnectionString(loginInfo, hostInfo, databaseInfo, optionalInfo.toString()); + } + + @NonNull static String buildDatabaseInfo(final @Nullable String database) { + return isNullOrWhitespace(database) ? "" : encodeValue(database); + } + + @NonNull static String buildHostInfo(final @Nullable String hostname) { + return isNullOrWhitespace(hostname) ? "" : hostname; + } + + @NonNull static String buildLoginInfo(final @Nullable String user, final @Nullable String password) { + final String userString = isNullOrWhitespace(user) + ? "" + : encodeValue(user); + final String passwordString = isNullOrWhitespace(password) + ? "" + : ":" + encodeValue(password); + final String userInfo = isNullOrWhitespace(userString) && isNullOrWhitespace(passwordString) + ? "" + : "@"; + return userString + passwordString + userInfo; + } + + static @NonNull String buildConnectionString( + final String loginInfo, + final String hostInfo, + final String databaseInfo, + final String optionalInfo) { + return String.format(CONNECTION_STRING_TEMPLATE, + loginInfo, + hostInfo, + databaseInfo, + optionalInfo); + } + + /** + * Builds the sanitized optional info connection string. I does not include + * sensitive options like SSH_PRIVATE_KEY_PASSPHRASE. + * + * @param optionalInfo the connection string to build. + */ + static void buildSanitizedOptionalInfo( + final StringBuilder optionalInfo, + final DocumentDbConnectionProperties properties) { + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.APPLICATION_NAME, properties.getApplicationName()); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.LOGIN_TIMEOUT_SEC, properties.getLoginTimeout()); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.METADATA_SCAN_LIMIT, properties.getMetadataScanLimit()); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.METADATA_SCAN_METHOD, properties.getMetadataScanMethod()); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.RETRY_READS_ENABLED, properties.getRetryReadsEnabled()); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.READ_PREFERENCE, properties.getReadPreference()); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.REPLICA_SET, properties.getReplicaSet(), null); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.TLS_ENABLED, properties.getTlsEnabled()); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.TLS_ALLOW_INVALID_HOSTNAMES, properties.getTlsAllowInvalidHostnames()); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.TLS_CA_FILE, properties.getTlsCAFilePath(), null); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.SCHEMA_NAME, properties.getSchemaName()); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.SSH_USER, properties.getSshUser(), null); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.SSH_HOSTNAME, properties.getSshHostname(), null); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.SSH_PRIVATE_KEY_FILE, properties.getSshPrivateKeyFile(), null); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.SSH_STRICT_HOST_KEY_CHECKING, properties.getSshStrictHostKeyChecking()); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.SSH_KNOWN_HOSTS_FILE, properties.getSshKnownHostsFile(), null); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.DEFAULT_FETCH_SIZE, properties.getDefaultFetchSize()); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.REFRESH_SCHEMA, properties.getRefreshSchema()); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.DEFAULT_AUTH_DB, properties.getDefaultAuthenticationDatabase()); + maybeAppendOptionalValue(optionalInfo, DocumentDbConnectionProperty.ALLOW_DISK_USE, properties.getAllowDiskUseOption()); + } + + static void maybeAppendOptionalValue(final StringBuilder optionalInfo, + final DocumentDbConnectionProperty property, + final String value) { + if (!property.getDefaultValue().equals(value)) { + appendOption(optionalInfo, property, value); } - if (getReadPreference() != null) { - appendOption(optionalInfo, DocumentDbConnectionProperty.READ_PREFERENCE, getReadPreference().getName()); + } + + static void maybeAppendOptionalValue(final StringBuilder optionalInfo, + final DocumentDbConnectionProperty property, + final String value, + final String defaultValue) { + if (!Objects.equals(defaultValue, value)) { + appendOption(optionalInfo, property, value); } - if (getReplicaSet() != null) { - appendOption(optionalInfo, DocumentDbConnectionProperty.REPLICA_SET, getReplicaSet()); + } + + static void maybeAppendOptionalValue(final StringBuilder optionalInfo, + final DocumentDbConnectionProperty property, + final int value) { + if (value != Integer.parseInt(property.getDefaultValue())) { + appendOption(optionalInfo, property, value); } - if (getTlsEnabled() != Boolean.parseBoolean(DocumentDbConnectionProperty.TLS_ENABLED.getDefaultValue())) { - appendOption(optionalInfo, DocumentDbConnectionProperty.TLS_ENABLED, getTlsEnabled()); + } + + static void maybeAppendOptionalValue(final StringBuilder optionalInfo, + final DocumentDbConnectionProperty property, + final boolean value) { + if (value != Boolean.parseBoolean(property.getDefaultValue())) { + appendOption(optionalInfo, property, value); } - if (getTlsAllowInvalidHostnames() != Boolean.parseBoolean(DocumentDbConnectionProperty.TLS_ALLOW_INVALID_HOSTNAMES.getDefaultValue())) { - appendOption(optionalInfo, DocumentDbConnectionProperty.TLS_ALLOW_INVALID_HOSTNAMES, getTlsAllowInvalidHostnames()); + } + + static void maybeAppendOptionalValue(final StringBuilder optionalInfo, + final DocumentDbConnectionProperty property, + final DocumentDbMetadataScanMethod value) { + if (value != DocumentDbMetadataScanMethod.fromString(property.getDefaultValue())) { + appendOption(optionalInfo, property, value.getName()); } - if (getTlsCAFilePath() != null) { - appendOption(optionalInfo, DocumentDbConnectionProperty.TLS_CA_FILE, getTlsCAFilePath()); + } + + static void maybeAppendOptionalValue(final StringBuilder optionalInfo, + final DocumentDbConnectionProperty property, + final DocumentDbReadPreference value) { + if (value != null) { + appendOption(optionalInfo, property, value.getName()); } - if (!DocumentDbConnectionProperty.SCHEMA_NAME.getDefaultValue().equals(getSchemaName())) { - appendOption(optionalInfo, DocumentDbConnectionProperty.SCHEMA_NAME, getSchemaName()); + } + + static void maybeAppendOptionalValue(final StringBuilder optionalInfo, + final DocumentDbConnectionProperty property, + final DocumentDbAllowDiskUseOption value) { + if (value != DocumentDbAllowDiskUseOption.fromString(property.getDefaultValue())) { + appendOption(optionalInfo, property, value.getName()); } + } + + /** + * Builds the connection string for SSH properties. + * + * @return a connection string with SSH properties. + */ + public String buildSshConnectionString() { + final String loginInfo = ""; + final String hostInfo = buildHostInfo(getHostname()); + final String databaseInfo = ""; + final StringBuilder optionalInfo = new StringBuilder(); + buildSshOptionalInfo(optionalInfo); + return buildConnectionString(loginInfo, hostInfo, databaseInfo, optionalInfo.toString()); + } + + private void buildSshOptionalInfo(final StringBuilder optionalInfo) { if (getSshUser() != null) { appendOption(optionalInfo, DocumentDbConnectionProperty.SSH_USER, getSshUser()); } @@ -820,36 +922,45 @@ public String buildSanitizedConnectionString() { if (getSshPrivateKeyFile() != null) { appendOption(optionalInfo, DocumentDbConnectionProperty.SSH_PRIVATE_KEY_FILE, getSshPrivateKeyFile()); } + if (getSshPrivateKeyPassphrase() != null && !DocumentDbConnectionProperty.SSH_PRIVATE_KEY_PASSPHRASE.getDefaultValue().equals(getSshPrivateKeyPassphrase())) { + appendOption(optionalInfo, DocumentDbConnectionProperty.SSH_PRIVATE_KEY_PASSPHRASE, getSshPrivateKeyPassphrase()); + } if (getSshStrictHostKeyChecking() != Boolean.parseBoolean(DocumentDbConnectionProperty.SSH_STRICT_HOST_KEY_CHECKING.getDefaultValue())) { appendOption(optionalInfo, DocumentDbConnectionProperty.SSH_STRICT_HOST_KEY_CHECKING, getSshStrictHostKeyChecking()); } if (getSshKnownHostsFile() != null && !DocumentDbConnectionProperty.SSH_KNOWN_HOSTS_FILE.getDefaultValue().equals(getSshKnownHostsFile())) { appendOption(optionalInfo, DocumentDbConnectionProperty.SSH_KNOWN_HOSTS_FILE, getSshKnownHostsFile()); } - if (getDefaultFetchSize() != Integer.parseInt(DocumentDbConnectionProperty.DEFAULT_FETCH_SIZE.getDefaultValue())) { - appendOption(optionalInfo, DocumentDbConnectionProperty.DEFAULT_FETCH_SIZE, getDefaultFetchSize()); - } - if (getRefreshSchema() != Boolean.parseBoolean(DocumentDbConnectionProperty.REFRESH_SCHEMA.getDefaultValue())) { - appendOption(optionalInfo, DocumentDbConnectionProperty.REFRESH_SCHEMA, getRefreshSchema()); - } - if (getDefaultAuthenticationDatabase() != null && !DocumentDbConnectionProperty.DEFAULT_AUTH_DB.getDefaultValue().equals(getDefaultAuthenticationDatabase())) { - appendOption(optionalInfo, DocumentDbConnectionProperty.DEFAULT_AUTH_DB, getDefaultAuthenticationDatabase()); - } - if (getAllowDiskUseOption() != DocumentDbAllowDiskUseOption.fromString(DocumentDbConnectionProperty.ALLOW_DISK_USE.getDefaultValue())) { - appendOption(optionalInfo, DocumentDbConnectionProperty.ALLOW_DISK_USE, getAllowDiskUseOption().getName()); - } - return String.format(connectionStringTemplate, - loginInfo, - hostInfo, - databaseInfo, - optionalInfo); } - private void appendOption(final StringBuilder optionInfo, + /** + * Appends an option and value to the string. + * + * @param optionInfo the connection string to build. + * @param option the option to add. + * @param optionValue the option value to set. + */ + public static void appendOption(final StringBuilder optionInfo, final DocumentDbConnectionProperty option, final Object optionValue) { optionInfo.append(optionInfo.length() == 0 ? "?" : "&"); - optionInfo.append(option.getName()).append("=").append(optionValue); + optionInfo.append(option.getName()) + .append("=") + .append(optionValue == null ? "" : encodeValue(optionValue.toString())); + } + + /** + * Encodes a value into URL encoded value. + * + * @param value the value to encode. + * @return the encoded value. + */ + public static String encodeValue(final String value) { + try { + return URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + return value; + } } /** @@ -857,28 +968,60 @@ private void appendOption(final StringBuilder optionInfo, * @throws SQLException if the required properties are not correctly set. */ public void validateRequiredProperties() throws SQLException { - if (isNullOrWhitespace(getUser()) - || isNullOrWhitespace(getPassword())) { + validateRequiredProperties(ValidationType.CLIENT); + } + + /** + * Validates the existing properties. + * @param validationType Which validation type to perform. + * @throws SQLException if the required properties are not correctly set. + */ + public void validateRequiredProperties(final ValidationType validationType) throws SQLException { + if ((isNullOrWhitespace(getUser()) + || isNullOrWhitespace(getPassword())) && validationType == ValidationType.CLIENT) { throw SqlError.createSQLException( LOGGER, SqlState.INVALID_PARAMETER_VALUE, SqlError.MISSING_USER_PASSWORD ); } - if (isNullOrWhitespace(getDatabase())) { + if (isNullOrWhitespace(getDatabase()) && validationType == ValidationType.CLIENT) { throw SqlError.createSQLException( LOGGER, SqlState.INVALID_PARAMETER_VALUE, SqlError.MISSING_DATABASE ); } - if (isNullOrWhitespace(getHostname())) { + if (isNullOrWhitespace(getHostname()) + && (validationType == ValidationType.CLIENT || validationType == ValidationType.SSH_TUNNEL)) { throw SqlError.createSQLException( LOGGER, SqlState.INVALID_PARAMETER_VALUE, SqlError.MISSING_HOSTNAME ); } + + if (isNullOrWhitespace(getSshUser()) && validationType == ValidationType.SSH_TUNNEL) { + throw SqlError.createSQLException( + LOGGER, + SqlState.INVALID_PARAMETER_VALUE, + SqlError.MISSING_SSH_USER + ); + } + if (isNullOrWhitespace(getSshHostname()) && validationType == ValidationType.SSH_TUNNEL) { + throw SqlError.createSQLException( + LOGGER, + SqlState.INVALID_PARAMETER_VALUE, + SqlError.MISSING_SSH_HOSTNAME + ); + } + if (isNullOrWhitespace(getSshPrivateKeyFile()) && validationType == ValidationType.SSH_TUNNEL) { + throw SqlError.createSQLException( + LOGGER, + SqlState.INVALID_PARAMETER_VALUE, + SqlError.MISSING_SSH_PRIVATE_KEY_FILE + ); + } } /** @@ -893,6 +1036,19 @@ public static DocumentDbConnectionProperties getPropertiesFromConnectionString(f return getPropertiesFromConnectionString(new Properties(), documentDbUrl, DOCUMENT_DB_SCHEME); } + /** + * Gets the connection properties from the connection string. + * + * @param documentDbUrl the given properties. + * @param validationType Which properties to validate. + * @return a {@link DocumentDbConnectionProperties} with the properties set. + * @throws SQLException if connection string is invalid. + */ + public static DocumentDbConnectionProperties getPropertiesFromConnectionString( + final String documentDbUrl, + final ValidationType validationType) throws SQLException { + return getPropertiesFromConnectionString(new Properties(), documentDbUrl, DOCUMENT_DB_SCHEME, validationType); + } /** * Gets the connection properties from the connection string. @@ -906,6 +1062,22 @@ public static DocumentDbConnectionProperties getPropertiesFromConnectionString(f public static DocumentDbConnectionProperties getPropertiesFromConnectionString( final Properties info, final String documentDbUrl, final String connectionStringPrefix) throws SQLException { + return getPropertiesFromConnectionString(info, documentDbUrl, connectionStringPrefix, ValidationType.CLIENT); + } + + /** + * Gets the connection properties from the connection string. + * + * @param info the given properties. + * @param documentDbUrl the connection string. + * @param connectionStringPrefix the connection string prefix. + * @param validationType Which validation to perform. + * @return a {@link DocumentDbConnectionProperties} with the properties set. + * @throws SQLException if connection string is invalid. + */ + public static DocumentDbConnectionProperties getPropertiesFromConnectionString( + final Properties info, final String documentDbUrl, final String connectionStringPrefix, + final ValidationType validationType) throws SQLException { final DocumentDbConnectionProperties properties = new DocumentDbConnectionProperties(info); final String postSchemeSuffix = documentDbUrl.substring(connectionStringPrefix.length()); @@ -913,11 +1085,11 @@ public static DocumentDbConnectionProperties getPropertiesFromConnectionString( try { final URI uri = new URI(postSchemeSuffix); - setHostName(properties, uri); + setHostName(properties, uri, validationType); - setUserPassword(properties, uri); + setUserPassword(properties, uri, validationType); - setDatabase(properties, uri); + setDatabase(properties, uri, validationType); setOptionalProperties(properties, uri); @@ -927,15 +1099,17 @@ public static DocumentDbConnectionProperties getPropertiesFromConnectionString( throw SqlError.createSQLException( LOGGER, SqlState.CONNECTION_FAILURE, + e, SqlError.INVALID_CONNECTION_PROPERTIES, - documentDbUrl + documentDbUrl + " : '" + e.getMessage() + "'" ); } catch (UnsupportedEncodingException e) { throw new SQLException(e.getMessage(), e); } } - properties.validateRequiredProperties(); + properties.validateRequiredProperties(validationType); + return properties; } @@ -954,11 +1128,13 @@ static void setCustomOptions(final DocumentDbConnectionProperties properties) { } } - private static void setDatabase(final Properties properties, final URI mongoUri) - throws SQLException { + private static void setDatabase( + final Properties properties, + final URI mongoUri, + final ValidationType validationType) throws SQLException { if (isNullOrWhitespace(mongoUri.getPath())) { - if (properties.getProperty( - DocumentDbConnectionProperty.DATABASE.getName(), null) == null) { + if (properties.getProperty(DocumentDbConnectionProperty.DATABASE.getName(), null) == null + && validationType == ValidationType.CLIENT) { throw SqlError.createSQLException( LOGGER, SqlState.CONNECTION_FAILURE, @@ -988,13 +1164,15 @@ private static void setOptionalProperties(final Properties properties, final URI } } - private static void setUserPassword(final Properties properties, final URI mongoUri) - throws UnsupportedEncodingException, SQLException { + private static void setUserPassword( + final Properties properties, + final URI mongoUri, + final ValidationType validationType) throws UnsupportedEncodingException, SQLException { if (mongoUri.getUserInfo() == null) { - if (properties.getProperty( - DocumentDbConnectionProperty.USER.getName(), null) == null - || properties.getProperty( - DocumentDbConnectionProperty.PASSWORD.getName(), null) == null) { + if ((properties.getProperty(DocumentDbConnectionProperty.USER.getName(), null) == null + && validationType == ValidationType.CLIENT) + || (properties.getProperty(DocumentDbConnectionProperty.PASSWORD.getName(), null) == null + && validationType == ValidationType.CLIENT)) { throw SqlError.createSQLException( LOGGER, SqlState.CONNECTION_FAILURE, @@ -1018,11 +1196,14 @@ private static void setUserPassword(final Properties properties, final URI mongo } } - private static void setHostName(final Properties properties, final URI mongoUri) throws SQLException { + private static void setHostName( + final Properties properties, + final URI mongoUri, + final ValidationType validationType) throws SQLException { String hostName = mongoUri.getHost(); if (hostName == null) { - if (properties.getProperty( - DocumentDbConnectionProperty.HOSTNAME.getName(), null) == null) { + if (properties.getProperty(DocumentDbConnectionProperty.HOSTNAME.getName(), null) == null + && (validationType == ValidationType.CLIENT || validationType == ValidationType.SSH_TUNNEL)) { throw SqlError.createSQLException( LOGGER, SqlState.CONNECTION_FAILURE, @@ -1229,31 +1410,31 @@ void appendEmbeddedAndOptionalCaCertificates(final List caCertifica * Gets an absolute path from the given file path. It performs the substitution for a leading * '~' to be replaced by the user's home directory. * - * @param filePath the given file path to process. + * @param filePathString the given file path to process. * @param searchFolders list of folders * @return a {@link Path} for the absolution path for the given file path. */ - public static Path getPath(final String filePath, final String... searchFolders) { - if (filePath.matches(HOME_PATH_PREFIX_REG_EXPR)) { - final String fromHomePath = filePath.replaceFirst("~", + public static Path getPath(final String filePathString, final String... searchFolders) { + final Path filePath = Paths.get(filePathString); + if (filePathString.matches(HOME_PATH_PREFIX_REG_EXPR)) { + final String fromHomePath = filePathString.replaceFirst("~", Matcher.quoteReplacement(USER_HOME_PATH_NAME)); return Paths.get(fromHomePath).toAbsolutePath(); } else { - final Path origFilePath = Paths.get(filePath); - if (origFilePath.isAbsolute()) { - return origFilePath; + if (filePath.isAbsolute()) { + return filePath; } for (String searchFolder : searchFolders) { if (searchFolder == null) { continue; } - final Path testPath = Paths.get(searchFolder, filePath); + final Path testPath = Paths.get(searchFolder, filePathString); if (testPath.toAbsolutePath().toFile().exists()) { return testPath; } } } - return Paths.get(filePath).toAbsolutePath(); + return filePath.toAbsolutePath(); } /** @@ -1288,7 +1469,7 @@ private DocumentDbMetadataScanMethod getPropertyAsScanMethod(@NonNull final Stri if (getProperty(key) != null) { property = DocumentDbMetadataScanMethod.fromString(getProperty(key)); } else if (DocumentDbConnectionProperty.getPropertyFromKey(key) != null) { - property = DocumentDbMetadataScanMethod.fromString( + property = DocumentDbMetadataScanMethod.fromString( DocumentDbConnectionProperty.getPropertyFromKey(key).getDefaultValue()); } } catch (IllegalArgumentException e) { diff --git a/src/main/java/software/amazon/documentdb/jdbc/DocumentDbConnectionProperty.java b/src/main/java/software/amazon/documentdb/jdbc/DocumentDbConnectionProperty.java index 9c674f6d..849e31ed 100644 --- a/src/main/java/software/amazon/documentdb/jdbc/DocumentDbConnectionProperty.java +++ b/src/main/java/software/amazon/documentdb/jdbc/DocumentDbConnectionProperty.java @@ -16,6 +16,7 @@ package software.amazon.documentdb.jdbc; +import org.checkerframework.checker.nullness.qual.NonNull; import software.amazon.documentdb.jdbc.common.utilities.ConnectionProperty; import java.util.Arrays; @@ -130,9 +131,9 @@ public enum DocumentDbConnectionProperty implements ConnectionProperty { * @param description Description of the property. */ DocumentDbConnectionProperty( - final String connectionProperty, - final String defaultValue, - final String description) { + final @NonNull String connectionProperty, + final @NonNull String defaultValue, + final @NonNull String description) { this.connectionProperty = connectionProperty; this.defaultValue = defaultValue; this.description = description; @@ -143,7 +144,7 @@ public enum DocumentDbConnectionProperty implements ConnectionProperty { * * @return the connection property. */ - public String getName() { + public @NonNull String getName() { return connectionProperty; } @@ -152,7 +153,7 @@ public String getName() { * * @return the default value of the connection property. */ - public String getDefaultValue() { + public @NonNull String getDefaultValue() { return defaultValue; } diff --git a/src/main/java/software/amazon/documentdb/jdbc/DocumentDbMain.java b/src/main/java/software/amazon/documentdb/jdbc/DocumentDbMain.java index 5d2d33a1..9fc88c61 100644 --- a/src/main/java/software/amazon/documentdb/jdbc/DocumentDbMain.java +++ b/src/main/java/software/amazon/documentdb/jdbc/DocumentDbMain.java @@ -58,6 +58,7 @@ import software.amazon.documentdb.jdbc.metadata.DocumentDbSchemaColumn; import software.amazon.documentdb.jdbc.metadata.DocumentDbSchemaTable; import software.amazon.documentdb.jdbc.persist.DocumentDbSchemaSecurityException; +import software.amazon.documentdb.jdbc.sshtunnel.DocumentDbSshTunnelService; import java.io.Console; import java.io.File; @@ -118,6 +119,7 @@ public class DocumentDbMain { private static final Options HELP_VERSION_OPTIONS; private static final Option HELP_OPTION; private static final Option VERSION_OPTION; + private static final Options SSH_TUNNEL_SERVICE_OPTIONS; private static final OptionGroup COMMAND_OPTIONS; private static final List