Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.apache.v5.Apache5HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.gax.rpc.TransportChannelProvider;
import com.google.auth.http.HttpTransportFactory;
import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException;
Expand Down Expand Up @@ -58,6 +59,7 @@ final class BigQueryJdbcProxyUtility {
new BigQueryJdbcCustomLogger(BigQueryJdbcProxyUtility.class.getName());
static final String validPortRegex =
"^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$";
private static final HttpTransport DEFAULT_TRANSPORT = new NetHttpTransport.Builder().build();

private BigQueryJdbcProxyUtility() {}

Expand Down Expand Up @@ -136,17 +138,14 @@ static HttpTransportOptions getHttpTransportOptions(
boolean hasProxyOrSsl =
proxyProperties.containsKey(BigQueryJdbcUrlUtility.PROXY_HOST_PROPERTY_NAME)
|| sslTrustStorePath != null;
boolean hasTimeoutConfig = connectTimeout != null || readTimeout != null;

if (!hasProxyOrSsl && !hasTimeoutConfig) {
return null;
}

HttpTransportOptions.Builder httpTransportOptionsBuilder = HttpTransportOptions.newBuilder();
if (hasProxyOrSsl) {
httpTransportOptionsBuilder.setHttpTransportFactory(
getHttpTransportFactory(
proxyProperties, sslTrustStorePath, sslTrustStorePassword, callerClassName));
} else {
httpTransportOptionsBuilder.setHttpTransportFactory(() -> DEFAULT_TRANSPORT);
}

if (connectTimeout != null) {
Expand Down Expand Up @@ -178,9 +177,8 @@ private static HttpTransportFactory getHttpTransportFactory(
HttpRoutePlanner httpRoutePlanner = new DefaultProxyRoutePlanner(proxyHostDetails);
httpClientBuilder.setRoutePlanner(httpRoutePlanner);
addAuthToProxyIfPresent(proxyProperties, httpClientBuilder, callerClassName);
} else {
httpClientBuilder.useSystemProperties();
}
httpClientBuilder.useSystemProperties();

if (sslTrustStorePath != null) {
try (FileInputStream trustStoreStream = new FileInputStream(sslTrustStorePath)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ public void testGetHttpTransportOptionsWithNonAuthenticatedProxy() {
}

@Test
public void testGetHttpTransportOptionsWithNoProxySettingsReturnsNull() {
public void testGetHttpTransportOptionsWithNoProxySettingsReturnsDefaultOptions() {
String connection_uri =
"jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;"
+ "ProjectId=TestProject"
Expand All @@ -172,7 +172,8 @@ public void testGetHttpTransportOptionsWithNoProxySettingsReturnsNull() {
HttpTransportOptions result =
BigQueryJdbcProxyUtility.getHttpTransportOptions(
proxyProperties, null, null, null, null, "TestClass");
assertNull(result);
assertNotNull(result);
assertNotNull(result.getHttpTransportFactory());
}

private String getTestResourcePath(String resourceName) throws URISyntaxException {
Expand Down Expand Up @@ -299,11 +300,12 @@ public void testGetTransportChannelProvider_noProxyNoSsl_returnsNull() {
}

@Test
public void testGetHttpTransportOptions_noProxyNoSsl_returnsNull() {
public void testGetHttpTransportOptions_noProxyNoSsl_returnsDefaultOptions() {
HttpTransportOptions options =
BigQueryJdbcProxyUtility.getHttpTransportOptions(
Collections.<String, String>emptyMap(), null, null, null, null, "TestClass");
assertNull(options);
assertNotNull(options);
assertNotNull(options.getHttpTransportFactory());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
/*
* Copyright 2026 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
*
* http://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.bigquery.jdbc.it;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.cloud.bigquery.jdbc.utils.URIBuilder;
import com.google.common.io.CharStreams;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsParameters;
import com.sun.net.httpserver.HttpsServer;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

public class ITLocalSslValidationTest {
private static final String HOST = "localhost";
private static final String PASSWORD = "changeit";
private static final String KEYSTORE_RESOURCE = "/localhost-keystore.jks";
private static final String TRUSTSTORE_RESOURCE = "/localhost-truststore.jks";
private static final String SUCCESS_MARKER = "SUBPROCESS_RESULT: SUCCESS";
private static final String FAILURE_MARKER_PREFIX = "SUBPROCESS_RESULT: FAILURE - ";
private static final String PKIX_ERROR_MSG = "PKIX path building failed";

private static MockHttpsServer mockServer;
private static int port;

public static class MockHttpsServer {
private final HttpsServer server;

public MockHttpsServer(int port) throws Exception {
server = HttpsServer.create(new InetSocketAddress(HOST, port), 0);
SSLContext sslContext = SSLContext.getInstance("TLS");

KeyStore ks = KeyStore.getInstance("JKS");
try (InputStream stream = getClass().getResourceAsStream(KEYSTORE_RESOURCE)) {
if (stream == null) {
throw new IllegalStateException(
"Keystore resource " + KEYSTORE_RESOURCE + " not found on classpath!");
}
ks.load(stream, PASSWORD.toCharArray());
}

KeyManagerFactory kmf =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, PASSWORD.toCharArray());

sslContext.init(kmf.getKeyManagers(), null, null);

server.setHttpsConfigurator(
new HttpsConfigurator(sslContext) {
@Override
public void configure(HttpsParameters params) {
try {
SSLContext context = getSSLContext();
SSLParameters sslParams = context.getDefaultSSLParameters();
params.setSSLParameters(sslParams);
} catch (Exception ex) {
ex.printStackTrace();
}
}
});

server.createContext(
"/",
exchange -> {
String path = exchange.getRequestURI().getPath();
String response;
if (path.contains("/queries")) {
response =
"{\n"
+ " \"kind\": \"bigquery#queryResponse\",\n"
+ " \"jobComplete\": true,\n"
+ " \"rows\": [],\n"
+ " \"totalRows\": \"0\",\n"
+ " \"schema\": {\n"
+ " \"fields\": []\n"
+ " }\n"
+ "}";
} else {
response =
"{\n"
+ " \"kind\": \"bigquery#job\",\n"
+ " \"status\": {\n"
+ " \"state\": \"DONE\"\n"
+ " },\n"
+ " \"jobReference\": {\n"
+ " \"projectId\": \"dummy\",\n"
+ " \"jobId\": \"dummy-job\"\n"
+ " },\n"
+ " \"configuration\": {\n"
+ " \"query\": {\n"
+ " \"query\": \"SELECT 1\"\n"
+ " }\n"
+ " },\n"
+ " \"statistics\": {\n"
+ " \"query\": {\n"
+ " \"statementType\": \"SELECT\"\n"
+ " }\n"
+ " }\n"
+ "}";
}
byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, responseBytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(responseBytes);
}
Comment thread
keshavdandeva marked this conversation as resolved.
});
}

public void start() {
server.start();
}

public void stop() {
server.stop(0);
}

public int getPort() {
return server.getAddress().getPort();
}
}

private static class ProcessResult {
final int exitCode;
final String stdout;

ProcessResult(int exitCode, String stdout) {
this.exitCode = exitCode;
this.stdout = stdout;
}
}

@BeforeAll
public static void setUp() throws Exception {
mockServer = new MockHttpsServer(0);
mockServer.start();
port = mockServer.getPort();
}

@AfterAll
public static void tearDown() {
if (mockServer == null) {
return;
}
mockServer.stop();
}

private ProcessResult runSubprocess(String trustStore, String password) throws Exception {
String javaHome = System.getProperty("java.home");
String javaBin = javaHome + File.separator + "bin" + File.separator + "java";
String classpath = System.getProperty("java.class.path");
String className = ITLocalSslValidationTest.class.getCanonicalName();

List<String> command = new ArrayList<>();
command.add(javaBin);
if (trustStore != null) {
command.add("-Djavax.net.ssl.trustStore=" + trustStore);
}
if (password != null) {
command.add("-Djavax.net.ssl.trustStorePassword=" + password);
}
command.add("-cp");
command.add(classpath);
command.add(className);
command.add(String.valueOf(port));

ProcessBuilder builder = new ProcessBuilder(command);
builder.redirectErrorStream(true);
Process process = builder.start();

String output = "";
boolean finished = false;
try {
try (InputStreamReader reader =
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
output = CharStreams.toString(reader);
}
finished = process.waitFor(10, TimeUnit.SECONDS);
if (!finished) {
throw new TimeoutException("Subprocess timed out after 10 seconds");
}
int exitCode = process.exitValue();
return new ProcessResult(exitCode, output);
} finally {
if (!finished && process.isAlive()) {
process.destroyForcibly();
}
}
}

@Test
public void testDefaultSslFailsForSelfSigned() throws Exception {
ProcessResult result = runSubprocess(null, null);
assertEquals(1, result.exitCode, "Subprocess should fail. Output:\n" + result.stdout);
assertTrue(result.stdout.contains(PKIX_ERROR_MSG));
}

@Test
public void testCustomTrustStoreSucceeds() throws Exception {
URL trustStoreUrl = getClass().getResource(TRUSTSTORE_RESOURCE);
if (trustStoreUrl == null) {
throw new IllegalStateException(
"Truststore resource " + TRUSTSTORE_RESOURCE + " not found on classpath!");
}
String trustStorePath = new File(trustStoreUrl.toURI()).getAbsolutePath();
ProcessResult result = runSubprocess(trustStorePath, PASSWORD);

Comment thread
keshavdandeva marked this conversation as resolved.
assertEquals(0, result.exitCode, "Subprocess failed. Output:\n" + result.stdout);
assertTrue(result.stdout.contains(SUCCESS_MARKER));
assertFalse(
result.stdout.contains(PKIX_ERROR_MSG),
"Handshake failed with SSL error: " + result.stdout);
}

public static void main(String[] args) {
int port = Integer.parseInt(args[0]);
String baseUri = "jdbc:bigquery://https://" + HOST + ":" + port + ";";
String url =
new URIBuilder(baseUri)
.append("EndpointOverrides", "BIGQUERY=https://" + HOST + ":" + port)
.append("ProjectId", "dummy")
.append("OAuthType", 2)
.append("OAuthAccessToken", "dummy-token")
.toString();
try (Connection connection = DriverManager.getConnection(url);
Statement statement = connection.createStatement()) {
statement.execute("SELECT 1");
System.out.println(SUCCESS_MARKER);
System.exit(0);
} catch (Throwable e) {
System.out.println(FAILURE_MARKER_PREFIX + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.google.cloud.bigquery.jdbc.it.ITConnectionTest;
import com.google.cloud.bigquery.jdbc.it.ITDatabaseMetadataTest;
import com.google.cloud.bigquery.jdbc.it.ITDriverTest;
import com.google.cloud.bigquery.jdbc.it.ITLocalSslValidationTest;
import com.google.cloud.bigquery.jdbc.it.ITResultSetMetadataTest;
import com.google.cloud.bigquery.jdbc.it.ITStatementTest;
import org.junit.platform.suite.api.SelectClasses;
Expand All @@ -37,6 +38,7 @@
ITConnectionPoolingTest.class,
ITDatabaseMetadataTest.class,
ITDriverTest.class,
ITLocalSslValidationTest.class,
ITResultSetMetadataTest.class,
ITStatementTest.class
})
Expand Down
Binary file not shown.
Binary file not shown.
Loading