From 8fff660ee105fd60e39eabec1030e7c944bddbf8 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Wed, 3 Aug 2022 13:37:31 -0700 Subject: [PATCH 1/5] feat: enable setting ipType configuration option for SQL Server connector --- .../cloud/sql/sqlserver/SocketFactory.java | 15 ++++-- .../JdbcSqlServerIntegrationTests.java | 1 - .../sql/sqlserver/JdbcSqlServerUnitTests.java | 49 +++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerUnitTests.java diff --git a/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java b/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java index 8eb88dc23..b06d34c32 100644 --- a/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java +++ b/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java @@ -26,7 +26,8 @@ public class SocketFactory extends javax.net.SocketFactory { private static final Logger logger = Logger.getLogger(SocketFactory.class.getName()); - private Properties props = new Properties(); + // props are protected, not private, so that they can be accessed from unit tests + protected Properties props = new Properties(); static { CoreSocketFactory.addArtifactId("cloud-sql-connector-jdbc-sqlserver"); @@ -36,8 +37,16 @@ public class SocketFactory extends javax.net.SocketFactory { * Implements the {@link SocketFactory} constructor, which can be used to create authenticated * connections to a Cloud SQL instance. */ - public SocketFactory(String instanceName) { - this.props.setProperty(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY, instanceName); + public SocketFactory(String socketFactoryConstructorArg) { + String[] s = socketFactoryConstructorArg.split("\\?"); + this.props.setProperty(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY, s[0]); + if (s.length == 2) { + String[] queryParams = s[1].split("&"); + for (String param : queryParams) { + String[] splitParam = param.split("="); + this.props.setProperty(splitParam[0], splitParam[1]); + } + } } @Override diff --git a/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerIntegrationTests.java b/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerIntegrationTests.java index ef7450854..d56f5eb12 100644 --- a/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerIntegrationTests.java +++ b/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerIntegrationTests.java @@ -29,7 +29,6 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; -import java.util.Properties; import java.util.UUID; import java.util.concurrent.TimeUnit; import org.junit.After; diff --git a/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerUnitTests.java b/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerUnitTests.java new file mode 100644 index 000000000..449e37a6c --- /dev/null +++ b/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerUnitTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.sql.sqlserver; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.sql.core.CoreSocketFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class JdbcSqlServerUnitTests { + + private static final String CONNECTION_NAME = "my-project:my-region:my-instance"; + + @Test + public void checkConnectionStringNoQueryParams() { + String socketFactoryConstructorArg = CONNECTION_NAME; + SocketFactory socketFactory = new SocketFactory(socketFactoryConstructorArg); + assertThat(socketFactory.props.get(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY)).isEqualTo( + CONNECTION_NAME); + } + + @Test + public void checkConnectionStringWithQueryParam() { + String socketFactoryConstructorArg = String.format("%s?%s=%s", CONNECTION_NAME, "ipTypes", "PRIVATE"); + SocketFactory socketFactory = new SocketFactory(socketFactoryConstructorArg); + assertThat(socketFactory.props.get(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY)).isEqualTo( + CONNECTION_NAME); + assertThat(socketFactory.props.get("ipTypes")).isEqualTo( + "PRIVATE"); + } + +} From 53b5936e5fd56f03fc0fb6f9dceeb9d2b2bf63cc Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Wed, 10 Aug 2022 13:31:50 -0700 Subject: [PATCH 2/5] use built in Java URI/URL parsing and decoding --- .../cloud/sql/sqlserver/SocketFactory.java | 17 +++++++++++------ .../sql/sqlserver/JdbcSqlServerUnitTests.java | 5 +++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java b/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java index b06d34c32..8feb82a24 100644 --- a/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java +++ b/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java @@ -18,8 +18,12 @@ import com.google.cloud.sql.core.CoreSocketFactory; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.Socket; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.Properties; import java.util.logging.Logger; @@ -37,14 +41,15 @@ public class SocketFactory extends javax.net.SocketFactory { * Implements the {@link SocketFactory} constructor, which can be used to create authenticated * connections to a Cloud SQL instance. */ - public SocketFactory(String socketFactoryConstructorArg) { - String[] s = socketFactoryConstructorArg.split("\\?"); - this.props.setProperty(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY, s[0]); - if (s.length == 2) { - String[] queryParams = s[1].split("&"); + public SocketFactory(String socketFactoryConstructorArg) throws UnsupportedEncodingException { + URI uri = URI.create(socketFactoryConstructorArg); + this.props.setProperty(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY, uri.getPath()); + if (uri.getQuery() != null) { + String[] queryParams = uri.getQuery().split("&"); for (String param : queryParams) { String[] splitParam = param.split("="); - this.props.setProperty(splitParam[0], splitParam[1]); + this.props.setProperty(URLDecoder.decode(splitParam[0], StandardCharsets.UTF_8.name()), + URLDecoder.decode(splitParam[1], StandardCharsets.UTF_8.name())); } } } diff --git a/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerUnitTests.java b/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerUnitTests.java index 449e37a6c..d457ff1d2 100644 --- a/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerUnitTests.java +++ b/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerUnitTests.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import com.google.cloud.sql.core.CoreSocketFactory; +import java.io.UnsupportedEncodingException; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -29,7 +30,7 @@ public class JdbcSqlServerUnitTests { private static final String CONNECTION_NAME = "my-project:my-region:my-instance"; @Test - public void checkConnectionStringNoQueryParams() { + public void checkConnectionStringNoQueryParams() throws UnsupportedEncodingException { String socketFactoryConstructorArg = CONNECTION_NAME; SocketFactory socketFactory = new SocketFactory(socketFactoryConstructorArg); assertThat(socketFactory.props.get(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY)).isEqualTo( @@ -37,7 +38,7 @@ public void checkConnectionStringNoQueryParams() { } @Test - public void checkConnectionStringWithQueryParam() { + public void checkConnectionStringWithQueryParam() throws UnsupportedEncodingException { String socketFactoryConstructorArg = String.format("%s?%s=%s", CONNECTION_NAME, "ipTypes", "PRIVATE"); SocketFactory socketFactory = new SocketFactory(socketFactoryConstructorArg); assertThat(socketFactory.props.get(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY)).isEqualTo( From 75c06e719646ec244e4e99169e6c4ae301a69743 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Wed, 10 Aug 2022 13:33:09 -0700 Subject: [PATCH 3/5] Use VisibleForTesting annotation --- .../main/java/com/google/cloud/sql/sqlserver/SocketFactory.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java b/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java index 8feb82a24..09fa97fd2 100644 --- a/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java +++ b/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java @@ -17,6 +17,7 @@ package com.google.cloud.sql.sqlserver; import com.google.cloud.sql.core.CoreSocketFactory; +import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.InetAddress; @@ -31,6 +32,7 @@ public class SocketFactory extends javax.net.SocketFactory { private static final Logger logger = Logger.getLogger(SocketFactory.class.getName()); // props are protected, not private, so that they can be accessed from unit tests + @VisibleForTesting protected Properties props = new Properties(); static { From 11fbfbea80035bcd3015eba903aa34b622749a94 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Wed, 10 Aug 2022 16:53:24 -0700 Subject: [PATCH 4/5] revert URI back to string and add more tests --- .../cloud/sql/sqlserver/SocketFactory.java | 19 ++++--- .../sql/sqlserver/JdbcSqlServerUnitTests.java | 49 +++++++++++++++++-- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java b/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java index 09fa97fd2..6c0b364f7 100644 --- a/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java +++ b/jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java @@ -22,7 +22,6 @@ import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.Socket; -import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Properties; @@ -43,16 +42,24 @@ public class SocketFactory extends javax.net.SocketFactory { * Implements the {@link SocketFactory} constructor, which can be used to create authenticated * connections to a Cloud SQL instance. */ - public SocketFactory(String socketFactoryConstructorArg) throws UnsupportedEncodingException { - URI uri = URI.create(socketFactoryConstructorArg); - this.props.setProperty(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY, uri.getPath()); - if (uri.getQuery() != null) { - String[] queryParams = uri.getQuery().split("&"); + public SocketFactory(String socketFactoryConstructorArg) + throws UnsupportedEncodingException { + String[] s = socketFactoryConstructorArg.split("\\?"); + this.props.setProperty(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY, s[0]); + if (s.length == 2 && s[1].length() > 0) { + String[] queryParams = s[1].split("&"); for (String param : queryParams) { String[] splitParam = param.split("="); + if (splitParam.length != 2 || splitParam[0].length() == 0 || splitParam[1].length() == 0) { + throw new IllegalArgumentException(String.format( + "Malformed query param in socketFactoryConstructorArg : %s", param)); + } this.props.setProperty(URLDecoder.decode(splitParam[0], StandardCharsets.UTF_8.name()), URLDecoder.decode(splitParam[1], StandardCharsets.UTF_8.name())); } + } else if (s.length > 2) { + throw new IllegalArgumentException( + "Only one query string allowed in socketFactoryConstructorArg"); } } diff --git a/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerUnitTests.java b/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerUnitTests.java index d457ff1d2..49ce92196 100644 --- a/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerUnitTests.java +++ b/jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerUnitTests.java @@ -27,10 +27,11 @@ @RunWith(JUnit4.class) public class JdbcSqlServerUnitTests { - private static final String CONNECTION_NAME = "my-project:my-region:my-instance"; + private static final String CONNECTION_NAME = "my-projectmy-regionmy-instance"; @Test - public void checkConnectionStringNoQueryParams() throws UnsupportedEncodingException { + public void checkConnectionStringNoQueryParams() + throws UnsupportedEncodingException { String socketFactoryConstructorArg = CONNECTION_NAME; SocketFactory socketFactory = new SocketFactory(socketFactoryConstructorArg); assertThat(socketFactory.props.get(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY)).isEqualTo( @@ -38,8 +39,10 @@ public void checkConnectionStringNoQueryParams() throws UnsupportedEncodingExcep } @Test - public void checkConnectionStringWithQueryParam() throws UnsupportedEncodingException { - String socketFactoryConstructorArg = String.format("%s?%s=%s", CONNECTION_NAME, "ipTypes", "PRIVATE"); + public void checkConnectionStringWithQueryParam() + throws UnsupportedEncodingException { + String socketFactoryConstructorArg = String.format("%s?%s=%s", CONNECTION_NAME, "ipTypes", + "PRIVATE"); SocketFactory socketFactory = new SocketFactory(socketFactoryConstructorArg); assertThat(socketFactory.props.get(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY)).isEqualTo( CONNECTION_NAME); @@ -47,4 +50,42 @@ public void checkConnectionStringWithQueryParam() throws UnsupportedEncodingExce "PRIVATE"); } + @Test + public void checkConnectionStringWithEmptyQueryParam() + throws UnsupportedEncodingException { + String socketFactoryConstructorArg = String.format("%s?", CONNECTION_NAME); + SocketFactory socketFactory = new SocketFactory(socketFactoryConstructorArg); + assertThat(socketFactory.props.get(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY)).isEqualTo( + CONNECTION_NAME); + assertThat(socketFactory.props.get("ipTypes")).isEqualTo( + null); + } + + @Test + public void checkConnectionStringWithUrlEncodedParam() + throws UnsupportedEncodingException { + String socketFactoryConstructorArg = String.format("%s?token=%s", CONNECTION_NAME, + "abc%20def%20xyz%2F%26%3D"); + SocketFactory socketFactory = new SocketFactory(socketFactoryConstructorArg); + assertThat(socketFactory.props.get(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY)).isEqualTo( + CONNECTION_NAME); + assertThat(socketFactory.props.get("token")).isEqualTo( + "abc def xyz/&="); + } + + @Test(expected = IllegalArgumentException.class) + public void checkConnectionStringWithParamMissingKey() + throws UnsupportedEncodingException { + String socketFactoryConstructorArg = String.format("%s?=%s", CONNECTION_NAME, "PRIVATE"); + new SocketFactory(socketFactoryConstructorArg); + } + + @Test(expected = IllegalArgumentException.class) + public void checkConnectionStringWithParamMissingValue() + throws UnsupportedEncodingException { + String socketFactoryConstructorArg = String.format("%s?enableIamAuth=true&%s", CONNECTION_NAME, + "ipTypes"); + new SocketFactory(socketFactoryConstructorArg); + } + } From 43cf62363c877f5af42dd5cd1efe6b44ccef2c85 Mon Sep 17 00:00:00 2001 From: Shubha Rajan Date: Wed, 7 Dec 2022 01:06:27 -0800 Subject: [PATCH 5/5] document query string syntax --- docs/jdbc-sqlserver.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/jdbc-sqlserver.md b/docs/jdbc-sqlserver.md index addded1cf..02bbfa147 100644 --- a/docs/jdbc-sqlserver.md +++ b/docs/jdbc-sqlserver.md @@ -21,7 +21,7 @@ compile 'com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver:1.7.2' ``` *Note*: Also include the JDBC Driver for SQL Server, `com.microsoft.sqlserver:mssql-jdbc:`. -### Creating theJDBC URL +### Creating the JDBC URL Base JDBC URL: `jdbc:sqlserver://localhost;databaseName=` @@ -42,6 +42,18 @@ jdbc:sqlserver://localhost;databaseName=;socketFactoryClass=com.g Note: The host portion of the JDBC URL is currently unused, and has no effect on the connection process. The SocketFactory will get your instances IP address based on the provided `socketFactoryConstructorArg` arg. +### Specifying IP Types + +"The `ipTypes` argument is used to specify a preferred order of IP types used to connect via a comma delimited list. For example, `ipTypes=PUBLIC,PRIVATE` will use the instance's Public IP if it exists, otherwise private. The value `ipTypes=PRIVATE` will force the Cloud SQL instance to connect via it's private IP. If not specified, the default used is `ipTypes=PUBLIC,PRIVATE`. + +IP types can be specified by appending the ipTypes argument to `socketFactoryConstructorArg` using query syntax, such as: + +``` +jdbc:sqlserver://localhost;databaseName=;socketFactoryClass=com.google.cloud.sql.sqlserver.SocketFactory;socketFactoryConstructorArg=?ipTypes=PRIVATE;user=;password= +``` + +For more info on connecting using a private IP address, see [Requirements for Private IP](https://cloud.google.com/sql/docs/mysql/private-ip#requirements_for_private_ip). + ## Examples Examples for using the Cloud SQL JDBC Connector for SQL Server can be found by looking at the integration tests in this repository.