Skip to content

Commit 4911ed5

Browse files
author
Fred Park
committed
GH-1319: [JDBC] Match hostnameOverride endpoints on host and port
Only apply hostnameOverride to endpoint locations on the same host and port as the connection, so endpoints on other ports are verified against their own hostname. Ignore IPv6 brackets when comparing hosts, since a host set through properties may not have them.
1 parent 7ee704d commit 4911ed5

4 files changed

Lines changed: 159 additions & 22 deletions

File tree

‎docs/source/flight_sql_jdbc_driver.rst‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ case-sensitive. The supported parameters are:
126126
connecting through a tunnel, a private endpoint, or an IP address
127127
that does not match the server certificate. Endpoints returned by
128128
the server are only verified against this hostname if they are on
129-
the same host as the connection. Ignored when ``useEncryption`` is
130-
false.
129+
the same host and port as the connection. Ignored when
130+
``useEncryption`` is false.
131131

132132
* - clientCertificate
133133
- null

‎flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -861,25 +861,37 @@ public Builder withHostnameOverride(@Nullable final String hostnameOverride) {
861861

862862
/**
863863
* Gets the hostname override to use when connecting to an endpoint location. The override is
864-
* only valid for the host it was configured for, so endpoints on any other host are verified
865-
* against their own hostname. Like the override itself, it is ignored when the original
866-
* connection does not use encryption.
864+
* only valid for the host and port it was configured for, so endpoints at any other location
865+
* are verified against their own hostname. Like the override itself, it is ignored when the
866+
* original connection does not use encryption.
867867
*
868868
* @param endpointUri the endpoint location.
869-
* @return the hostname override if the endpoint is on the original host, otherwise null.
869+
* @return the hostname override if the endpoint is on the original host and port, otherwise
870+
* null.
870871
*/
871872
@VisibleForTesting
872873
@Nullable String getHostnameOverrideForEndpoint(final URI endpointUri) {
873-
final String endpointHost = endpointUri.getHost();
874+
final String endpointHost = stripBrackets(endpointUri.getHost());
874875
if (!useEncryption
875876
|| hostnameOverride == null
876877
|| endpointHost == null
877-
|| !endpointHost.equalsIgnoreCase(host)) {
878+
|| endpointUri.getPort() != port
879+
|| !endpointHost.equalsIgnoreCase(stripBrackets(host))) {
878880
return null;
879881
}
880882
return hostnameOverride;
881883
}
882884

885+
/**
886+
* Strips the brackets from an IPv6 literal. {@link URI#getHost()} keeps them, but a host set
887+
* through properties may not have them.
888+
*/
889+
private static @Nullable String stripBrackets(@Nullable final String host) {
890+
return host != null && host.startsWith("[") && host.endsWith("]")
891+
? host.substring(1, host.length() - 1)
892+
: host;
893+
}
894+
883895
/**
884896
* Sets whether to use the certificates from the operating system.
885897
*

‎flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ConnectionTlsHostnameOverrideTest.java‎

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,21 @@
3232
import java.sql.Statement;
3333
import java.util.ArrayDeque;
3434
import java.util.Arrays;
35+
import java.util.Collections;
3536
import java.util.Deque;
3637
import java.util.Properties;
3738
import org.apache.arrow.driver.jdbc.authentication.UserPasswordAuthentication;
3839
import org.apache.arrow.driver.jdbc.utils.ArrowFlightConnectionConfigImpl.ArrowFlightConnectionProperty;
3940
import org.apache.arrow.driver.jdbc.utils.FlightSqlTestCertificates;
4041
import org.apache.arrow.driver.jdbc.utils.MockFlightSqlProducer;
4142
import org.apache.arrow.driver.jdbc.utils.PartitionedFlightSqlProducer;
43+
import org.apache.arrow.flight.FlightDescriptor;
4244
import org.apache.arrow.flight.FlightEndpoint;
45+
import org.apache.arrow.flight.FlightInfo;
4346
import org.apache.arrow.flight.FlightServer;
4447
import org.apache.arrow.flight.Location;
4548
import org.apache.arrow.flight.Ticket;
49+
import org.apache.arrow.flight.sql.impl.FlightSql;
4650
import org.apache.arrow.memory.BufferAllocator;
4751
import org.apache.arrow.memory.RootAllocator;
4852
import org.apache.arrow.vector.IntVector;
@@ -122,6 +126,11 @@ private static void assertCertificateRejected(final String url) {
122126
connection.isValid(0);
123127
}
124128
});
129+
assertCausedByCertificateException(exception);
130+
}
131+
132+
/** Asserts that the server certificate being rejected caused {@code exception}. */
133+
private static void assertCausedByCertificateException(final Throwable exception) {
125134
final Deque<Throwable> pending = new ArrayDeque<>();
126135
pending.add(exception);
127136
while (!pending.isEmpty()) {
@@ -192,7 +201,44 @@ public void testHostnameOverrideIsIgnoredWithoutEncryption() throws Exception {
192201
}
193202

194203
@Test
195-
public void testHostnameOverrideIsAppliedToEndpointOnConnectionHost() throws Exception {
204+
public void testHostnameOverrideIsAppliedToEndpointOnConnectionLocation() throws Exception {
205+
final Schema schema =
206+
new Schema(
207+
Arrays.asList(Field.nullablePrimitive("int_column", new ArrowType.Int(32, true))));
208+
final Ticket ticket = new Ticket("partition".getBytes(StandardCharsets.UTF_8));
209+
210+
try (VectorSchemaRoot partition = VectorSchemaRoot.create(schema, allocator)) {
211+
partition.setRowCount(1);
212+
((IntVector) partition.getVector(0)).set(0, 42);
213+
214+
try (SelfServingFlightSqlProducer producer =
215+
new SelfServingFlightSqlProducer(schema, ticket, partition);
216+
FlightServer server =
217+
FlightServer.builder(allocator, Location.forGrpcTls(IP_ADDRESS, 0), producer)
218+
.useTls(certKey.cert, certKey.key)
219+
.build()
220+
.start()) {
221+
// The endpoint is at the same location as the connection, so it must be verified against
222+
// the hostname override as well.
223+
producer.location = Location.forGrpcTls(IP_ADDRESS, server.getPort());
224+
225+
try (Connection connection =
226+
DriverManager.getConnection(
227+
tlsUrl(IP_ADDRESS, server.getPort())
228+
+ "&hostnameOverride="
229+
+ CERTIFICATE_HOSTNAME);
230+
Statement statement = connection.createStatement();
231+
ResultSet resultSet = statement.executeQuery("SELECT partitioned_data")) {
232+
assertTrue(resultSet.next());
233+
assertEquals(42, resultSet.getInt(1));
234+
assertFalse(resultSet.next());
235+
}
236+
}
237+
}
238+
}
239+
240+
@Test
241+
public void testHostnameOverrideIsNotAppliedToEndpointOnDifferentPort() throws Exception {
196242
final Schema schema =
197243
new Schema(
198244
Arrays.asList(Field.nullablePrimitive("int_column", new ArrowType.Int(32, true))));
@@ -210,8 +256,8 @@ public void testHostnameOverrideIsAppliedToEndpointOnConnectionHost() throws Exc
210256
partition.setRowCount(1);
211257
((IntVector) partition.getVector(0)).set(0, 42);
212258

213-
// The endpoint is on the same IP address as the connection, so it must be verified against
214-
// the hostname override as well.
259+
// The endpoint is on the connection's IP address but another port, so it is verified
260+
// against the IP address, which the certificate does not cover.
215261
final FlightEndpoint endpoint =
216262
new FlightEndpoint(ticket, Location.forGrpcTls(IP_ADDRESS, dataServer.getPort()));
217263

@@ -227,12 +273,64 @@ public void testHostnameOverrideIsAppliedToEndpointOnConnectionHost() throws Exc
227273
tlsUrl(IP_ADDRESS, rootServer.getPort())
228274
+ "&hostnameOverride="
229275
+ CERTIFICATE_HOSTNAME);
230-
Statement statement = connection.createStatement();
231-
ResultSet resultSet = statement.executeQuery("SELECT partitioned_data")) {
232-
assertTrue(resultSet.next());
233-
assertEquals(42, resultSet.getInt(1));
234-
assertFalse(resultSet.next());
276+
Statement statement = connection.createStatement()) {
277+
final SQLException exception =
278+
assertThrows(
279+
SQLException.class,
280+
() -> {
281+
try (ResultSet resultSet = statement.executeQuery("SELECT partitioned_data")) {
282+
resultSet.next();
283+
}
284+
});
285+
assertCausedByCertificateException(exception);
235286
}
236287
}
237288
}
289+
290+
/**
291+
* Returns a single endpoint at {@link #location}, which is set once the server has started, and
292+
* serves its data, so the endpoint is at the same location as the connection.
293+
*/
294+
private static final class SelfServingFlightSqlProducer extends PartitionedFlightSqlProducer {
295+
private final Schema schema;
296+
private final Ticket ticket;
297+
private final PartitionedFlightSqlProducer.DataOnlyFlightSqlProducer dataProducer;
298+
private volatile Location location;
299+
300+
SelfServingFlightSqlProducer(
301+
final Schema schema, final Ticket ticket, final VectorSchemaRoot data) {
302+
super(schema);
303+
this.schema = schema;
304+
this.ticket = ticket;
305+
this.dataProducer = new PartitionedFlightSqlProducer.DataOnlyFlightSqlProducer(ticket, data);
306+
}
307+
308+
private FlightInfo flightInfo(final FlightDescriptor descriptor) {
309+
return FlightInfo.builder(
310+
schema, descriptor, Collections.singletonList(new FlightEndpoint(ticket, location)))
311+
.build();
312+
}
313+
314+
@Override
315+
public FlightInfo getFlightInfoStatement(
316+
final FlightSql.CommandStatementQuery command,
317+
final CallContext context,
318+
final FlightDescriptor descriptor) {
319+
return flightInfo(descriptor);
320+
}
321+
322+
@Override
323+
public FlightInfo getFlightInfoPreparedStatement(
324+
final FlightSql.CommandPreparedStatementQuery command,
325+
final CallContext context,
326+
final FlightDescriptor descriptor) {
327+
return flightInfo(descriptor);
328+
}
329+
330+
@Override
331+
public void getStream(
332+
final CallContext context, final Ticket ticket, final ServerStreamListener listener) {
333+
dataProducer.getStream(context, ticket, listener);
334+
}
335+
}
238336
}

‎flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandlerBuilderTest.java‎

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,22 +170,32 @@ public void testHostnameOverrideForEndpointOnSameHost() {
170170
final ArrowFlightSqlClientHandler.Builder builder =
171171
new ArrowFlightSqlClientHandler.Builder()
172172
.withHost("127.0.0.1")
173+
.withPort(1234)
173174
.withHostnameOverride("flight.example.com");
174175

175176
assertEquals(
176177
"flight.example.com",
177178
builder.getHostnameOverrideForEndpoint(URI.create("grpc+tls://127.0.0.1:1234")));
178-
// The port is not part of the match.
179-
assertEquals(
180-
"flight.example.com",
181-
builder.getHostnameOverrideForEndpoint(URI.create("grpc+tls://127.0.0.1:5678")));
179+
}
180+
181+
@Test
182+
public void testHostnameOverrideForEndpointOnDifferentPortIsCleared() {
183+
final ArrowFlightSqlClientHandler.Builder builder =
184+
new ArrowFlightSqlClientHandler.Builder()
185+
.withHost("127.0.0.1")
186+
.withPort(1234)
187+
.withHostnameOverride("flight.example.com");
188+
189+
assertNull(builder.getHostnameOverrideForEndpoint(URI.create("grpc+tls://127.0.0.1:5678")));
190+
assertNull(builder.getHostnameOverrideForEndpoint(URI.create("grpc+tls://127.0.0.1")));
182191
}
183192

184193
@Test
185194
public void testHostnameOverrideForEndpointMatchesHostCaseInsensitively() {
186195
final ArrowFlightSqlClientHandler.Builder builder =
187196
new ArrowFlightSqlClientHandler.Builder()
188197
.withHost("Tunnel.Local")
198+
.withPort(1234)
189199
.withHostnameOverride("flight.example.com");
190200

191201
assertEquals(
@@ -195,10 +205,25 @@ public void testHostnameOverrideForEndpointMatchesHostCaseInsensitively() {
195205

196206
@Test
197207
public void testHostnameOverrideForEndpointOnIpv6Host() {
198-
// Both the JDBC URL and endpoint locations are parsed with URI#getHost, which keeps brackets.
208+
// The JDBC URL is parsed with URI#getHost, which keeps the brackets, like endpoint locations.
199209
final ArrowFlightSqlClientHandler.Builder builder =
200210
new ArrowFlightSqlClientHandler.Builder()
201211
.withHost(URI.create("jdbc://[::1]:1234").getHost())
212+
.withPort(1234)
213+
.withHostnameOverride("flight.example.com");
214+
215+
assertEquals(
216+
"flight.example.com",
217+
builder.getHostnameOverrideForEndpoint(URI.create("grpc+tls://[::1]:1234")));
218+
}
219+
220+
@Test
221+
public void testHostnameOverrideForEndpointOnIpv6HostWithoutBrackets() {
222+
// A host set through properties or a DataSource may not have brackets.
223+
final ArrowFlightSqlClientHandler.Builder builder =
224+
new ArrowFlightSqlClientHandler.Builder()
225+
.withHost("::1")
226+
.withPort(1234)
202227
.withHostnameOverride("flight.example.com");
203228

204229
assertEquals(
@@ -211,6 +236,7 @@ public void testHostnameOverrideForEndpointOnDifferentHostIsCleared() {
211236
final ArrowFlightSqlClientHandler.Builder builder =
212237
new ArrowFlightSqlClientHandler.Builder()
213238
.withHost("127.0.0.1")
239+
.withPort(1234)
214240
.withHostnameOverride("flight.example.com");
215241

216242
assertNull(builder.getHostnameOverrideForEndpoint(URI.create("grpc+tls://10.0.0.1:1234")));
@@ -223,6 +249,7 @@ public void testHostnameOverrideForEndpointWithoutEncryption() {
223249
final ArrowFlightSqlClientHandler.Builder builder =
224250
new ArrowFlightSqlClientHandler.Builder()
225251
.withHost("127.0.0.1")
252+
.withPort(1234)
226253
.withEncryption(false)
227254
.withHostnameOverride("flight.example.com");
228255

@@ -248,7 +275,7 @@ public void testHostnameOverrideIsTrimmed() {
248275
@Test
249276
public void testHostnameOverrideForEndpointWithoutOverride() {
250277
final ArrowFlightSqlClientHandler.Builder builder =
251-
new ArrowFlightSqlClientHandler.Builder().withHost("127.0.0.1");
278+
new ArrowFlightSqlClientHandler.Builder().withHost("127.0.0.1").withPort(1234);
252279

253280
assertNull(builder.getHostnameOverrideForEndpoint(URI.create("grpc+tls://127.0.0.1:1234")));
254281
}

0 commit comments

Comments
 (0)