diff --git a/core/src/main/java/com/google/cloud/sql/core/CloudSqlInstanceName.java b/core/src/main/java/com/google/cloud/sql/core/CloudSqlInstanceName.java index a207f320d..78a59e324 100644 --- a/core/src/main/java/com/google/cloud/sql/core/CloudSqlInstanceName.java +++ b/core/src/main/java/com/google/cloud/sql/core/CloudSqlInstanceName.java @@ -31,10 +31,42 @@ class CloudSqlInstanceName { // Some legacy project ids are domain-scoped (e.g. "example.com:PROJECT:REGION:INSTANCE") private static final Pattern CONNECTION_NAME = Pattern.compile("([^:]+(:[^:]+)?):([^:]+):([^:]+)"); + + /** + * The domain name pattern in accordance with RFC 1035, RFC 1123 and RFC 2181. + * + *

Explanation of the Regex: + * + *

^: Matches the beginning of the string.
+ * (?=.{1,255}$): Positive lookahead assertion to ensure the domain name is between 1 and 255 + * characters long.
+ * (?!-): Negative lookahead assertion to prevent hyphens at the beginning.
+ * [A-Za-z0-9-]+: Matches one or more alphanumeric characters or hyphens.
+ * (\\.[A-Za-z0-9-]+)*: Matches zero or more occurrences of a period followed by one or more + * alphanumeric characters or hyphens (for subdomains).
+ * \\.: Matches a period before the TLD.
+ * [A-Za-z]{2,}: Matches two or more letters for the TLD.
+ * $: Matches the end of the string.
+ */ + private static final Pattern DOMAIN_NAME = + Pattern.compile("^(?=.{1,255}$)(?!-)[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,}$"); + private final String projectId; private final String regionId; private final String instanceId; private final String connectionName; + private final String domainName; + + /** + * Validates that a string is a well-formed domain name. + * + * @param domain the domain name to check + * @return true if domain is a well-formed domain name. + */ + public static boolean isValidDomain(String domain) { + Matcher matcher = DOMAIN_NAME.matcher(domain); + return matcher.matches(); + } /** * Initializes a new CloudSqlInstanceName class. @@ -42,7 +74,16 @@ class CloudSqlInstanceName { * @param connectionName instance connection name in the format "PROJECT_ID:REGION_ID:INSTANCE_ID" */ CloudSqlInstanceName(String connectionName) { - this.connectionName = connectionName; + this(connectionName, null); + } + + /** + * Initializes a new CloudSqlInstanceName class containing the domain name. + * + * @param connectionName instance connection name in the format "PROJECT_ID:REGION_ID:INSTANCE_ID" + * @param domainName the domain name used to look up the instance, or null. + */ + CloudSqlInstanceName(String connectionName, String domainName) { Matcher matcher = CONNECTION_NAME.matcher(connectionName); checkArgument( matcher.matches(), @@ -50,9 +91,21 @@ class CloudSqlInstanceName { "[%s] Cloud SQL connection name is invalid, expected string in the form of" + " \"::\".", connectionName)); + this.connectionName = connectionName; this.projectId = matcher.group(1); this.regionId = matcher.group(3); this.instanceId = matcher.group(4); + + // Only set this.domainName when it is not empty + if (domainName != null && !domainName.isEmpty()) { + Matcher domainMatcher = DOMAIN_NAME.matcher(domainName); + checkArgument( + domainMatcher.matches(), + String.format("[%s] Domain name is invalid, expected a valid domain name", domainName)); + this.domainName = domainName; + } else { + this.domainName = null; + } } String getConnectionName() { @@ -71,6 +124,10 @@ String getInstanceId() { return instanceId; } + String getDomainName() { + return domainName; + } + @Override public String toString() { return connectionName; diff --git a/core/src/main/java/com/google/cloud/sql/core/Connector.java b/core/src/main/java/com/google/cloud/sql/core/Connector.java index 7d75ee63b..3de6e8d63 100644 --- a/core/src/main/java/com/google/cloud/sql/core/Connector.java +++ b/core/src/main/java/com/google/cloud/sql/core/Connector.java @@ -157,6 +157,13 @@ ConnectionInfoCache getConnection(final ConnectionConfig config) { return instance; } + /** + * Updates the ConnectionConfig to ensure that the cloudSqlInstance field is set, resolving the + * domainName using the InstanceNameResolver. + * + * @param config the configuration to resolve. + * @return a ConnectionConfig guaranteed to have the CloudSqlInstance field set. + */ private ConnectionConfig resolveConnectionName(ConnectionConfig config) { // If domainName is not set, return the original configuration unmodified. if (config.getDomainName() == null || config.getDomainName().isEmpty()) { diff --git a/core/src/main/java/com/google/cloud/sql/core/LazyRefreshConnectionInfoCache.java b/core/src/main/java/com/google/cloud/sql/core/LazyRefreshConnectionInfoCache.java index 424c50243..c5af65864 100644 --- a/core/src/main/java/com/google/cloud/sql/core/LazyRefreshConnectionInfoCache.java +++ b/core/src/main/java/com/google/cloud/sql/core/LazyRefreshConnectionInfoCache.java @@ -44,12 +44,15 @@ public LazyRefreshConnectionInfoCache( ConnectionInfoRepository connectionInfoRepository, CredentialFactory tokenSourceFactory, KeyPair keyPair) { + + CloudSqlInstanceName instanceName = + new CloudSqlInstanceName(config.getCloudSqlInstance(), config.getDomainName()); + this.config = config; - this.instanceName = new CloudSqlInstanceName(config.getCloudSqlInstance()); + this.instanceName = instanceName; AccessTokenSupplier accessTokenSupplier = DefaultAccessTokenSupplier.newInstance(config.getAuthType(), tokenSourceFactory); - CloudSqlInstanceName instanceName = new CloudSqlInstanceName(config.getCloudSqlInstance()); this.refreshStrategy = new LazyRefreshStrategy( diff --git a/core/src/main/java/com/google/cloud/sql/core/RefreshAheadConnectionInfoCache.java b/core/src/main/java/com/google/cloud/sql/core/RefreshAheadConnectionInfoCache.java index 0f8bf5063..c56c36a17 100644 --- a/core/src/main/java/com/google/cloud/sql/core/RefreshAheadConnectionInfoCache.java +++ b/core/src/main/java/com/google/cloud/sql/core/RefreshAheadConnectionInfoCache.java @@ -48,12 +48,15 @@ public RefreshAheadConnectionInfoCache( ListeningScheduledExecutorService executor, ListenableFuture keyPair, long minRefreshDelayMs) { + + CloudSqlInstanceName instanceName = + new CloudSqlInstanceName(config.getCloudSqlInstance(), config.getDomainName()); + this.config = config; - this.instanceName = new CloudSqlInstanceName(config.getCloudSqlInstance()); + this.instanceName = instanceName; AccessTokenSupplier accessTokenSupplier = DefaultAccessTokenSupplier.newInstance(config.getAuthType(), tokenSourceFactory); - CloudSqlInstanceName instanceName = new CloudSqlInstanceName(config.getCloudSqlInstance()); this.refreshStrategy = new RefreshAheadStrategy( diff --git a/core/src/test/java/com/google/cloud/sql/core/ConnectorTest.java b/core/src/test/java/com/google/cloud/sql/core/ConnectorTest.java index 9879bad04..dc4432384 100644 --- a/core/src/test/java/com/google/cloud/sql/core/ConnectorTest.java +++ b/core/src/test/java/com/google/cloud/sql/core/ConnectorTest.java @@ -154,6 +154,46 @@ public void create_successfulPublicConnectionWithDomainName() assertThat(readLine(socket)).isEqualTo(SERVER_MESSAGE); } + @Test + public void create_successfulPrivateConnection_UsesInstanceName_DomainNameIgnored() + throws IOException, InterruptedException { + FakeSslServer sslServer = new FakeSslServer(); + ConnectionConfig config = + new ConnectionConfig.Builder() + .withDomainName("db.example.com") + .withCloudSqlInstance("myProject:myRegion:myInstance") + .withIpTypes("PRIVATE") + .build(); + + int port = sslServer.start(PRIVATE_IP); + + Connector connector = newConnector(config.getConnectorConfig(), port); + + Socket socket = connector.connect(config, TEST_MAX_REFRESH_MS); + + assertThat(readLine(socket)).isEqualTo(SERVER_MESSAGE); + } + + @Test + public void create_successfulPrivateConnection_UsesInstanceName_EmptyDomainNameIgnored() + throws IOException, InterruptedException { + FakeSslServer sslServer = new FakeSslServer(); + ConnectionConfig config = + new ConnectionConfig.Builder() + .withDomainName("") + .withCloudSqlInstance("myProject:myRegion:myInstance") + .withIpTypes("PRIVATE") + .build(); + + int port = sslServer.start(PRIVATE_IP); + + Connector connector = newConnector(config.getConnectorConfig(), port); + + Socket socket = connector.connect(config, TEST_MAX_REFRESH_MS); + + assertThat(readLine(socket)).isEqualTo(SERVER_MESSAGE); + } + @Test public void create_throwsErrorForDomainNameWithNoResolver() throws IOException, InterruptedException {