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 {