diff --git a/docs/source/flight_sql_jdbc_driver.rst b/docs/source/flight_sql_jdbc_driver.rst index 6d40434a22..ca52d0a157 100644 --- a/docs/source/flight_sql_jdbc_driver.rst +++ b/docs/source/flight_sql_jdbc_driver.rst @@ -93,6 +93,10 @@ case-sensitive. The supported parameters are: - false - When TLS is enabled, whether to verify the server certificate + * - hostnameOverride + - null + - When TLS is enabled, the hostname used for server certificate verification and SNI instead of the host in the JDBC URL + * - password - null - The password for user/password authentication diff --git a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java index 623c2b81be..d5fb2105e5 100644 --- a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java +++ b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java @@ -118,6 +118,7 @@ private static ArrowFlightSqlClientHandler createNewClientHandler( .withClientKey(config.getClientKeyPath()) .withBufferAllocator(allocator) .withEncryption(config.useEncryption()) + .withHostnameOverride(config.getHostnameOverride()) .withDisableCertificateVerification(config.getDisableCertificateVerification()) .withToken(config.getToken()) .withCallOptions(config.toCallOption()) diff --git a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java index 719cc38a2b..66239dfc31 100644 --- a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java +++ b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java @@ -188,6 +188,13 @@ public List getStreams(final FlightInfo flightInfo) .withHost(endpointUri.getHost()) .withPort(endpointUri.getPort()) .withEncryption(endpointUri.getScheme().equals(LocationSchemes.GRPC_TLS)) + // An override belongs to the original connection host, not arbitrary endpoints. + .withHostnameOverride( + endpointUri.getHost() != null + && this.builder.host != null + && endpointUri.getHost().equalsIgnoreCase(this.builder.host) + ? this.builder.hostnameOverride + : null) .withClientCache(flightClientCache) .withConnectTimeout(builder.connectTimeout); @@ -677,6 +684,8 @@ public static final class Builder { @VisibleForTesting boolean disableCertificateVerification; + @VisibleForTesting @Nullable String hostnameOverride; + @VisibleForTesting boolean useSystemTrustStore = true; @VisibleForTesting String tlsRootCertificatesPath; @@ -732,6 +741,7 @@ public Builder() {} this.token = original.token; this.useEncryption = original.useEncryption; this.disableCertificateVerification = original.disableCertificateVerification; + this.hostnameOverride = original.hostnameOverride; this.useSystemTrustStore = original.useSystemTrustStore; this.tlsRootCertificatesPath = original.tlsRootCertificatesPath; this.clientCertificatePath = original.clientCertificatePath; @@ -840,6 +850,17 @@ public Builder withDisableCertificateVerification( return this; } + /** + * Sets the hostname used for TLS certificate verification and SNI. + * + * @param hostnameOverride the hostname override, or null to use the connection host. + * @return this instance. + */ + public Builder withHostnameOverride(@Nullable final String hostnameOverride) { + this.hostnameOverride = hostnameOverride; + return this; + } + /** * Sets whether to use the certificates from the operating system. * @@ -1065,6 +1086,9 @@ public ArrowFlightSqlClientHandler build() throws SQLException { buildTimeMiddlewareFactories.forEach(clientBuilder::intercept); if (useEncryption) { clientBuilder.useTls(); + if (hostnameOverride != null) { + clientBuilder.overrideHostname(hostnameOverride); + } } Location location = getLocation(); clientBuilder.location(location); diff --git a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImpl.java b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImpl.java index d0ba74dbcc..a79e4e838e 100644 --- a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImpl.java +++ b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImpl.java @@ -112,6 +112,15 @@ public String getTlsRootCertificatesPath() { return ArrowFlightConnectionProperty.TLS_ROOT_CERTS.getString(properties); } + /** + * Gets the hostname used for TLS certificate verification and SNI, if configured. + * + * @return the hostname override, or null when not configured. + */ + public String getHostnameOverride() { + return ArrowFlightConnectionProperty.HOSTNAME_OVERRIDE.getString(properties); + } + public String getClientCertificatePath() { return ArrowFlightConnectionProperty.CLIENT_CERTIFICATE.getString(properties); } @@ -254,6 +263,7 @@ public enum ArrowFlightConnectionProperty implements ConnectionProperty { PASSWORD("password", null, Type.STRING, false), USE_ENCRYPTION("useEncryption", true, Type.BOOLEAN, false), CERTIFICATE_VERIFICATION("disableCertificateVerification", false, Type.BOOLEAN, false), + HOSTNAME_OVERRIDE("hostnameOverride", null, Type.STRING, false), TRUST_STORE("trustStore", null, Type.STRING, false), TRUST_STORE_PASSWORD("trustStorePassword", null, Type.STRING, false), USE_SYSTEM_TRUST_STORE("useSystemTrustStore", true, Type.BOOLEAN, false), diff --git a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandlerBuilderTest.java b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandlerBuilderTest.java index a60a71f23d..37c2229c0d 100644 --- a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandlerBuilderTest.java +++ b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandlerBuilderTest.java @@ -137,6 +137,7 @@ public void testDefaults() { assertNull(builder.password); assertTrue(builder.useEncryption); assertFalse(builder.disableCertificateVerification); + assertNull(builder.hostnameOverride); assertNull(builder.trustStorePath); assertNull(builder.trustStorePassword); assertTrue(builder.useSystemTrustStore); @@ -152,6 +153,18 @@ public void testDefaults() { assertNull(builder.driverVersion); } + @Test + public void testHostnameOverrideIsCopied() { + final String hostnameOverride = "flight.example.com"; + final ArrowFlightSqlClientHandler.Builder rootBuilder = + new ArrowFlightSqlClientHandler.Builder().withHostnameOverride(hostnameOverride); + + final ArrowFlightSqlClientHandler.Builder copiedBuilder = + new ArrowFlightSqlClientHandler.Builder(rootBuilder); + + assertEquals(hostnameOverride, copiedBuilder.hostnameOverride); + } + @Test public void testCatalog() { ArrowFlightSqlClientHandler.Builder rootBuilder = new ArrowFlightSqlClientHandler.Builder(); diff --git a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImplTest.java b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImplTest.java index ecce7708c0..909e7cd39a 100644 --- a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImplTest.java +++ b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImplTest.java @@ -20,6 +20,7 @@ import static org.apache.arrow.driver.jdbc.utils.ArrowFlightConnectionConfigImpl.ArrowFlightConnectionProperty.CATALOG; import static org.apache.arrow.driver.jdbc.utils.ArrowFlightConnectionConfigImpl.ArrowFlightConnectionProperty.CONNECT_TIMEOUT_MILLIS; import static org.apache.arrow.driver.jdbc.utils.ArrowFlightConnectionConfigImpl.ArrowFlightConnectionProperty.HOST; +import static org.apache.arrow.driver.jdbc.utils.ArrowFlightConnectionConfigImpl.ArrowFlightConnectionProperty.HOSTNAME_OVERRIDE; import static org.apache.arrow.driver.jdbc.utils.ArrowFlightConnectionConfigImpl.ArrowFlightConnectionProperty.PASSWORD; import static org.apache.arrow.driver.jdbc.utils.ArrowFlightConnectionConfigImpl.ArrowFlightConnectionProperty.PORT; import static org.apache.arrow.driver.jdbc.utils.ArrowFlightConnectionConfigImpl.ArrowFlightConnectionProperty.THREAD_POOL_SIZE; @@ -106,6 +107,12 @@ public static Stream provideParameters() { useEncryption, (Function) ArrowFlightConnectionConfigImpl::useEncryption), + Arguments.of( + HOSTNAME_OVERRIDE, + "flight.example.com", + "flight.example.com", + (Function) + ArrowFlightConnectionConfigImpl::getHostnameOverride), Arguments.of( THREAD_POOL_SIZE, threadPoolSize,