Skip to content

Commit e76518d

Browse files
authored
feat: enable setting ipType configuration option for SQL Server connector (#936)
1 parent e545f3e commit e76518d

File tree

4 files changed

+130
-5
lines changed

4 files changed

+130
-5
lines changed

docs/jdbc-sqlserver.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ compile 'com.google.cloud.sql:cloud-sql-connector-jdbc-sqlserver:1.7.2'
2121
```
2222
*Note*: Also include the JDBC Driver for SQL Server, `com.microsoft.sqlserver:mssql-jdbc:<LATEST-VERSION>`.
2323

24-
### Creating theJDBC URL
24+
### Creating the JDBC URL
2525

2626
Base JDBC URL: `jdbc:sqlserver://localhost;databaseName=<DATABASE_NAME>`
2727

@@ -42,6 +42,18 @@ jdbc:sqlserver://localhost;databaseName=<DATABASE_NAME>;socketFactoryClass=com.g
4242

4343
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.
4444

45+
### Specifying IP Types
46+
47+
"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`.
48+
49+
IP types can be specified by appending the ipTypes argument to `socketFactoryConstructorArg` using query syntax, such as:
50+
51+
```
52+
jdbc:sqlserver://localhost;databaseName=<DATABASE_NAME>;socketFactoryClass=com.google.cloud.sql.sqlserver.SocketFactory;socketFactoryConstructorArg=<INSTANCE_CONNECTION_NAME>?ipTypes=PRIVATE;user=<USER_NAME>;password=<PASSWORD>
53+
```
54+
55+
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).
56+
4557
## Examples
4658

4759
Examples for using the Cloud SQL JDBC Connector for SQL Server can be found by looking at the integration tests in this repository.

jdbc/sqlserver/src/main/java/com/google/cloud/sql/sqlserver/SocketFactory.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717
package com.google.cloud.sql.sqlserver;
1818

1919
import com.google.cloud.sql.core.CoreSocketFactory;
20+
import com.google.common.annotations.VisibleForTesting;
2021
import java.io.IOException;
22+
import java.io.UnsupportedEncodingException;
2123
import java.net.InetAddress;
2224
import java.net.Socket;
25+
import java.net.URLDecoder;
26+
import java.nio.charset.StandardCharsets;
2327
import java.util.Properties;
2428
import java.util.logging.Logger;
2529

@@ -32,7 +36,9 @@
3236
public class SocketFactory extends javax.net.SocketFactory {
3337

3438
private static final Logger logger = Logger.getLogger(SocketFactory.class.getName());
35-
private Properties props = new Properties();
39+
// props are protected, not private, so that they can be accessed from unit tests
40+
@VisibleForTesting
41+
protected Properties props = new Properties();
3642

3743
static {
3844
CoreSocketFactory.addArtifactId("cloud-sql-connector-jdbc-sqlserver");
@@ -42,8 +48,25 @@ public class SocketFactory extends javax.net.SocketFactory {
4248
* Implements the {@link SocketFactory} constructor, which can be used to create authenticated
4349
* connections to a Cloud SQL instance.
4450
*/
45-
public SocketFactory(String instanceName) {
46-
this.props.setProperty(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY, instanceName);
51+
public SocketFactory(String socketFactoryConstructorArg)
52+
throws UnsupportedEncodingException {
53+
String[] s = socketFactoryConstructorArg.split("\\?");
54+
this.props.setProperty(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY, s[0]);
55+
if (s.length == 2 && s[1].length() > 0) {
56+
String[] queryParams = s[1].split("&");
57+
for (String param : queryParams) {
58+
String[] splitParam = param.split("=");
59+
if (splitParam.length != 2 || splitParam[0].length() == 0 || splitParam[1].length() == 0) {
60+
throw new IllegalArgumentException(String.format(
61+
"Malformed query param in socketFactoryConstructorArg : %s", param));
62+
}
63+
this.props.setProperty(URLDecoder.decode(splitParam[0], StandardCharsets.UTF_8.name()),
64+
URLDecoder.decode(splitParam[1], StandardCharsets.UTF_8.name()));
65+
}
66+
} else if (s.length > 2) {
67+
throw new IllegalArgumentException(
68+
"Only one query string allowed in socketFactoryConstructorArg");
69+
}
4770
}
4871

4972
@Override

jdbc/sqlserver/src/test/java/com/google/cloud/sql/sqlserver/JdbcSqlServerIntegrationTests.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import java.sql.SQLException;
3030
import java.util.ArrayList;
3131
import java.util.List;
32-
import java.util.Properties;
3332
import java.util.UUID;
3433
import java.util.concurrent.TimeUnit;
3534
import org.junit.After;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.sql.sqlserver;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
21+
import com.google.cloud.sql.core.CoreSocketFactory;
22+
import java.io.UnsupportedEncodingException;
23+
import org.junit.Test;
24+
import org.junit.runner.RunWith;
25+
import org.junit.runners.JUnit4;
26+
27+
@RunWith(JUnit4.class)
28+
public class JdbcSqlServerUnitTests {
29+
30+
private static final String CONNECTION_NAME = "my-projectmy-regionmy-instance";
31+
32+
@Test
33+
public void checkConnectionStringNoQueryParams()
34+
throws UnsupportedEncodingException {
35+
String socketFactoryConstructorArg = CONNECTION_NAME;
36+
SocketFactory socketFactory = new SocketFactory(socketFactoryConstructorArg);
37+
assertThat(socketFactory.props.get(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY)).isEqualTo(
38+
CONNECTION_NAME);
39+
}
40+
41+
@Test
42+
public void checkConnectionStringWithQueryParam()
43+
throws UnsupportedEncodingException {
44+
String socketFactoryConstructorArg = String.format("%s?%s=%s", CONNECTION_NAME, "ipTypes",
45+
"PRIVATE");
46+
SocketFactory socketFactory = new SocketFactory(socketFactoryConstructorArg);
47+
assertThat(socketFactory.props.get(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY)).isEqualTo(
48+
CONNECTION_NAME);
49+
assertThat(socketFactory.props.get("ipTypes")).isEqualTo(
50+
"PRIVATE");
51+
}
52+
53+
@Test
54+
public void checkConnectionStringWithEmptyQueryParam()
55+
throws UnsupportedEncodingException {
56+
String socketFactoryConstructorArg = String.format("%s?", CONNECTION_NAME);
57+
SocketFactory socketFactory = new SocketFactory(socketFactoryConstructorArg);
58+
assertThat(socketFactory.props.get(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY)).isEqualTo(
59+
CONNECTION_NAME);
60+
assertThat(socketFactory.props.get("ipTypes")).isEqualTo(
61+
null);
62+
}
63+
64+
@Test
65+
public void checkConnectionStringWithUrlEncodedParam()
66+
throws UnsupportedEncodingException {
67+
String socketFactoryConstructorArg = String.format("%s?token=%s", CONNECTION_NAME,
68+
"abc%20def%20xyz%2F%26%3D");
69+
SocketFactory socketFactory = new SocketFactory(socketFactoryConstructorArg);
70+
assertThat(socketFactory.props.get(CoreSocketFactory.CLOUD_SQL_INSTANCE_PROPERTY)).isEqualTo(
71+
CONNECTION_NAME);
72+
assertThat(socketFactory.props.get("token")).isEqualTo(
73+
"abc def xyz/&=");
74+
}
75+
76+
@Test(expected = IllegalArgumentException.class)
77+
public void checkConnectionStringWithParamMissingKey()
78+
throws UnsupportedEncodingException {
79+
String socketFactoryConstructorArg = String.format("%s?=%s", CONNECTION_NAME, "PRIVATE");
80+
new SocketFactory(socketFactoryConstructorArg);
81+
}
82+
83+
@Test(expected = IllegalArgumentException.class)
84+
public void checkConnectionStringWithParamMissingValue()
85+
throws UnsupportedEncodingException {
86+
String socketFactoryConstructorArg = String.format("%s?enableIamAuth=true&%s", CONNECTION_NAME,
87+
"ipTypes");
88+
new SocketFactory(socketFactoryConstructorArg);
89+
}
90+
91+
}

0 commit comments

Comments
 (0)