From 870af9e40ec89b7bf03eaf14f7740281e97b7a77 Mon Sep 17 00:00:00 2001 From: Mateus Aubin Date: Wed, 18 Mar 2026 19:49:15 +0100 Subject: [PATCH 1/4] feat(flight-jdbc): Add gRPC proxy detector for HTTP CONNECT tunneling (#1) - Introduces ArrowFlightProxyDetector: implements gRPC ProxyDetector with multi-level resolution priority * proxyDisable=force override * proxyBypassPattern matching (glob format, case-insensitive) * Explicit proxyHost+proxyPort configuration * ProxySelector.getDefault() fallback - Updates NettyClientBuilder to install proxy detector during channel creation - Extends ArrowFlightConnection/ArrowFlightSqlClientHandler to pass proxy config - Adds comprehensive unit tests for bypass pattern matching and proxy resolution issue: https://linear.app/iomete/issue/CE-208/ --- .../arrow/flight/grpc/NettyClientBuilder.java | 30 +- .../driver/jdbc/ArrowFlightConnection.java | 3 + .../client/ArrowFlightSqlClientHandler.java | 71 +++- .../ArrowFlightConnectionConfigImpl.java | 20 + .../jdbc/utils/ArrowFlightProxyDetector.java | 155 ++++++++ ...rrowFlightSqlClientHandlerBuilderTest.java | 36 ++ .../utils/ArrowFlightProxyDetectorTest.java | 349 ++++++++++++++++++ 7 files changed, 657 insertions(+), 7 deletions(-) create mode 100644 flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetector.java create mode 100644 flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetectorTest.java diff --git a/flight/flight-core/src/main/java/org/apache/arrow/flight/grpc/NettyClientBuilder.java b/flight/flight-core/src/main/java/org/apache/arrow/flight/grpc/NettyClientBuilder.java index 42cdaac016..d688b26689 100644 --- a/flight/flight-core/src/main/java/org/apache/arrow/flight/grpc/NettyClientBuilder.java +++ b/flight/flight-core/src/main/java/org/apache/arrow/flight/grpc/NettyClientBuilder.java @@ -185,6 +185,28 @@ public NettyChannelBuilder build() { "Scheme is not supported: " + location.getUri().getScheme()); } + try { + configureChannel(builder); + } catch (SSLException e) { + throw new RuntimeException(e); + } + return builder; + } + + /** + * Build a {@link NettyChannelBuilder} using {@code forTarget()} instead of {@code forAddress()}. + * + *

This is required for proxy support: only the DNS resolver path ({@code dns:///host:port}) + * invokes the {@link io.grpc.ProxyDetector}. {@code forAddress()} bypasses it entirely. + */ + public NettyChannelBuilder buildForTarget(String target) throws SSLException { + final NettyChannelBuilder builder = NettyChannelBuilder.forTarget(target); + configureChannel(builder); + return builder; + } + + /** Apply TLS and message-size configuration to an already-created {@link NettyChannelBuilder}. */ + protected void configureChannel(NettyChannelBuilder builder) throws SSLException { if (this.forceTls || LocationSchemes.GRPC_TLS.equals(location.getUri().getScheme())) { builder.useTransportSecurity(); @@ -210,11 +232,8 @@ public NettyChannelBuilder build() { sslContextBuilder.keyManager(this.clientCertificate, this.clientKey); } } - try { - builder.sslContext(sslContextBuilder.build()); - } catch (SSLException e) { - throw new RuntimeException(e); - } + + builder.sslContext(sslContextBuilder.build()); if (this.overrideHostname != null) { builder.overrideAuthority(this.overrideHostname); @@ -227,6 +246,5 @@ public NettyChannelBuilder build() { .maxTraceEvents(MAX_CHANNEL_TRACE_EVENTS) .maxInboundMessageSize(maxInboundMessageSize) .maxInboundMetadataSize(maxInboundMessageSize); - return builder; } } 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..c7c6e65d3c 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 @@ -128,6 +128,9 @@ private static ArrowFlightSqlClientHandler createNewClientHandler( .withConnectTimeout(config.getConnectTimeout()) .withDriverVersion(driverVersion) .withOAuthConfiguration(config.getOauthConfiguration()) + .withProxySettings(config.getProxyHost(), config.getProxyPort()) + .withProxyBypassPattern(config.getProxyBypassPattern()) + .withProxyDisable(config.getProxyDisable()) .build(); } catch (final SQLException e) { try { 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 f0ea284239..60dc7e57e0 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 @@ -38,6 +38,7 @@ import org.apache.arrow.driver.jdbc.client.utils.ClientAuthenticationUtils; import org.apache.arrow.driver.jdbc.client.utils.FlightClientCache; import org.apache.arrow.driver.jdbc.client.utils.FlightLocationQueue; +import org.apache.arrow.driver.jdbc.utils.ArrowFlightProxyDetector; import org.apache.arrow.flight.CallOption; import org.apache.arrow.flight.CallStatus; import org.apache.arrow.flight.CloseSessionRequest; @@ -693,6 +694,14 @@ public static final class Builder { DriverVersion driverVersion; + @VisibleForTesting String proxyHost; + + @VisibleForTesting Integer proxyPort; + + @VisibleForTesting String proxyBypassPattern; + + @VisibleForTesting String proxyDisable; + public Builder() {} /** @@ -1000,6 +1009,63 @@ public Builder withOAuthConfiguration(final OAuthConfiguration oauthConfig) { return this; } + /** + * Sets the explicit proxy host and port for this connection. + * + * @param proxyHost the proxy hostname or IP, or {@code null} to clear + * @param proxyPort the proxy port, or {@code null} to clear + * @return this builder instance + */ + public Builder withProxySettings( + @Nullable final String proxyHost, @Nullable final Integer proxyPort) { + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + return this; + } + + /** + * Sets the proxy bypass pattern in {@code http.nonProxyHosts} format ({@code |}-separated glob + * patterns). + * + * @param proxyBypassPattern bypass pattern, or {@code null} to clear + * @return this builder instance + */ + public Builder withProxyBypassPattern(@Nullable final String proxyBypassPattern) { + this.proxyBypassPattern = proxyBypassPattern; + return this; + } + + /** + * Sets the proxy disable flag. Pass {@code "force"} to disable proxy even when system + * properties or environment variables configure one. + * + * @param proxyDisable disable flag value, or {@code null} to clear + * @return this builder instance + */ + public Builder withProxyDisable(@Nullable final String proxyDisable) { + this.proxyDisable = proxyDisable; + return this; + } + + private String getDnsTarget() { + // Validate port is in valid range (0-65535) before passing to gRPC. + // gRPC's DNS resolver validates the port on a background thread, but background thread + // exceptions do not propagate to the caller and prevent proper error handling. + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("port out of range: " + port); + } + // IPv6 addresses need brackets in the URI authority component + String normalizedHost = host.contains(":") && !host.startsWith("[") ? "[" + host + "]" : host; + return "dns:///" + normalizedHost + ":" + port; + } + + private NettyChannelBuilder getChannelBuilder(final NettyClientBuilder clientBuilder) + throws IOException { + // forTarget() ensures the DNS resolver is used, which invokes ProxyDetector. + // forAddress() bypasses it entirely. + return clientBuilder.buildForTarget(getDnsTarget()); + } + public String getCacheKey() { return getLocation().toString(); } @@ -1075,9 +1141,12 @@ public ArrowFlightSqlClientHandler build() throws SQLException { } } - NettyChannelBuilder channelBuilder = clientBuilder.build(); + NettyChannelBuilder channelBuilder = getChannelBuilder(clientBuilder); channelBuilder.userAgent(userAgent); + // Always install — returns null when no proxy is applicable, safe for all connections + channelBuilder.proxyDetector( + new ArrowFlightProxyDetector(proxyHost, proxyPort, proxyBypassPattern, proxyDisable)); if (connectTimeout != null) { channelBuilder.withOption( 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..cf35bb1dd0 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 @@ -182,6 +182,22 @@ public boolean useClientCache() { return ArrowFlightConnectionProperty.USE_CLIENT_CACHE.getBoolean(properties); } + public String getProxyHost() { + return ArrowFlightConnectionProperty.PROXY_HOST.getString(properties); + } + + public Integer getProxyPort() { + return ArrowFlightConnectionProperty.PROXY_PORT.getInteger(properties); + } + + public String getProxyBypassPattern() { + return ArrowFlightConnectionProperty.PROXY_BYPASS_PATTERN.getString(properties); + } + + public String getProxyDisable() { + return ArrowFlightConnectionProperty.PROXY_DISABLE.getString(properties); + } + /** * Gets the {@link CallOption}s from this {@link ConnectionConfig}. * @@ -284,6 +300,10 @@ public enum ArrowFlightConnectionProperty implements ConnectionProperty { OAUTH_EXCHANGE_AUDIENCE("oauth.exchange.aud", null, Type.STRING, false), OAUTH_EXCHANGE_REQUESTED_TOKEN_TYPE( "oauth.exchange.requestedTokenType", null, Type.STRING, false), + PROXY_HOST("proxyHost", null, Type.STRING, false), + PROXY_PORT("proxyPort", null, Type.NUMBER, false), + PROXY_BYPASS_PATTERN("proxyBypassPattern", null, Type.STRING, false), + PROXY_DISABLE("proxyDisable", null, Type.STRING, false), ; private final String camelName; diff --git a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetector.java b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetector.java new file mode 100644 index 0000000000..baadde45ac --- /dev/null +++ b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetector.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.arrow.driver.jdbc.utils; + +import io.grpc.HttpConnectProxiedSocketAddress; +import io.grpc.ProxiedSocketAddress; +import io.grpc.ProxyDetector; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * gRPC {@link ProxyDetector} for Arrow Flight JDBC connections. + * + *

Resolution priority: + * + *

    + *
  1. {@code proxyDisable=force} → no proxy (always) + *
  2. Target host matches {@code proxyBypassPattern} → no proxy + *
  3. Explicit {@code proxyHost} + {@code proxyPort} → use that proxy + *
  4. {@link ProxySelector} default → use first non-DIRECT proxy found + *
  5. No proxy + *
+ * + *

{@code proxyBypassPattern} follows the {@code http.nonProxyHosts} format: {@code |}-separated + * glob patterns where {@code *} matches any sequence of characters. Matching is case-insensitive. + */ +public final class ArrowFlightProxyDetector implements ProxyDetector { + + @Nullable private final String proxyHost; + @Nullable private final Integer proxyPort; + @Nullable private final String bypassPattern; + private final boolean forceDisabled; + @Nullable private final ProxySelector proxySelector; + + /** Production constructor — falls back to {@link ProxySelector#getDefault()}. */ + public ArrowFlightProxyDetector( + @Nullable String proxyHost, + @Nullable Integer proxyPort, + @Nullable String bypassPattern, + @Nullable String proxyDisable) { + this(proxyHost, proxyPort, bypassPattern, proxyDisable, ProxySelector.getDefault()); + } + + /** Package-private constructor for testing — accepts an explicit {@link ProxySelector}. */ + ArrowFlightProxyDetector( + @Nullable String proxyHost, + @Nullable Integer proxyPort, + @Nullable String bypassPattern, + @Nullable String proxyDisable, + @Nullable ProxySelector proxySelector) { + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.bypassPattern = bypassPattern; + this.forceDisabled = "force".equalsIgnoreCase(proxyDisable); + this.proxySelector = proxySelector; + } + + @Override + @Nullable + public ProxiedSocketAddress proxyFor(SocketAddress targetAddress) throws IOException { + if (forceDisabled) { + return null; + } + InetSocketAddress inetTarget = (InetSocketAddress) targetAddress; + if (matchesBypass(inetTarget.getHostString())) { + return null; + } + InetSocketAddress proxyAddr = resolveProxy(inetTarget); + if (proxyAddr == null) { + return null; + } + return HttpConnectProxiedSocketAddress.newBuilder() + .setTargetAddress(inetTarget) + .setProxyAddress(proxyAddr) + .build(); + } + + /** + * Returns true if {@code host} matches any pattern in the bypass list. + * + *

Patterns use {@code http.nonProxyHosts} format: {@code |}-separated globs, where {@code *} + * is a wildcard for any sequence of characters. + */ + private boolean matchesBypass(String host) { + if (bypassPattern == null || bypassPattern.isEmpty()) { + return false; + } + for (String pattern : bypassPattern.split("\\|")) { + String trimmed = pattern.trim(); + if (trimmed.isEmpty()) { + continue; + } + // Convert glob to regex: escape dots, replace * with .* + String regex = trimmed.replace(".", "\\.").replace("*", ".*"); + if (host.matches("(?i)" + regex)) { + return true; + } + } + return false; + } + + /** + * Resolves which proxy address to use for the given target. + * + *

Tries explicit proxy first, then {@link ProxySelector}. + * + * @return resolved proxy address, or {@code null} if no proxy should be used + */ + @Nullable + private InetSocketAddress resolveProxy(InetSocketAddress target) { + if (proxyHost != null && proxyPort != null) { + // new InetSocketAddress(String, int) resolves IP literals without DNS; + // for hostnames it attempts DNS — required since HttpConnectProxiedSocketAddress + // validates the proxy address is resolved. + return new InetSocketAddress(proxyHost, proxyPort); + } + if (proxySelector == null) { + return null; + } + URI uri; + try { + uri = new URI("https", null, target.getHostString(), target.getPort(), null, null, null); + } catch (URISyntaxException e) { + return null; + } + List proxies = proxySelector.select(uri); + for (Proxy proxy : proxies) { + if (proxy.type() != Proxy.Type.DIRECT && proxy.address() instanceof InetSocketAddress) { + return (InetSocketAddress) proxy.address(); + } + } + return null; + } +} 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..d857429f1b 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 @@ -150,6 +150,12 @@ public void testDefaults() { assertNull(builder.flightClientCache); assertNull(builder.connectTimeout); assertNull(builder.driverVersion); + + // Proxy fields default to null + assertNull(builder.proxyHost); + assertNull(builder.proxyPort); + assertNull(builder.proxyBypassPattern); + assertNull(builder.proxyDisable); } @Test @@ -175,4 +181,34 @@ public void testCatalog() { assertTrue(rootBuilder.catalog.isPresent()); assertEquals(nameWithSpaces, rootBuilder.catalog.get()); } + + @Test + public void testProxyFieldsSetViaWithMethods() { + final ArrowFlightSqlClientHandler.Builder builder = new ArrowFlightSqlClientHandler.Builder(); + + builder.withProxySettings("proxy.corp.net", 8080); + assertEquals("proxy.corp.net", builder.proxyHost); + assertEquals(Integer.valueOf(8080), builder.proxyPort); + + builder.withProxyBypassPattern("*.internal|localhost"); + assertEquals("*.internal|localhost", builder.proxyBypassPattern); + + builder.withProxyDisable("force"); + assertEquals("force", builder.proxyDisable); + } + + @Test + public void testProxySettingsAcceptNull() { + final ArrowFlightSqlClientHandler.Builder builder = new ArrowFlightSqlClientHandler.Builder(); + + builder.withProxySettings(null, null); + assertNull(builder.proxyHost); + assertNull(builder.proxyPort); + + builder.withProxyBypassPattern(null); + assertNull(builder.proxyBypassPattern); + + builder.withProxyDisable(null); + assertNull(builder.proxyDisable); + } } diff --git a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetectorTest.java b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetectorTest.java new file mode 100644 index 0000000000..088300d093 --- /dev/null +++ b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightProxyDetectorTest.java @@ -0,0 +1,349 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.arrow.driver.jdbc.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.grpc.HttpConnectProxiedSocketAddress; +import io.grpc.ProxiedSocketAddress; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.URI; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** Tests for {@link ArrowFlightProxyDetector}. */ +class ArrowFlightProxyDetectorTest { + + private static final InetSocketAddress TARGET = + InetSocketAddress.createUnresolved("dev.iomete.cloud", 443); + + // Use IP literals — InetSocketAddress resolves them without DNS, avoiding test-env failures. + // HttpConnectProxiedSocketAddress requires a resolved proxy address. + private static final String PROXY_HOST = "192.168.1.1"; + private static final int PROXY_PORT = 8080; + + @Nested + // force disabled nullifies explicit proxy; force disabled overrides ProxySelector + class ForceDisabled { + + @Test + void givenForceDisabled_whenProxyFor_thenReturnsNull() throws IOException { + // GIVEN: force disabled with explicit proxy configured + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector(PROXY_HOST, PROXY_PORT, null, "force", noProxySelector()); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertNull(result); + } + + @Test + void givenForceDisabledAndProxySelectorReturnsProxy_whenProxyFor_thenReturnsNull() + throws IOException { + // GIVEN: force disabled overrides even ProxySelector + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + null, null, null, "force", proxySelectorReturning("192.168.1.2", 3128)); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertNull(result); + } + } + + @Nested + // returns configured proxy address; preserves target address + class ExplicitProxy { + + @Test + void givenExplicitProxy_whenProxyFor_thenReturnsConfiguredProxy() throws IOException { + // GIVEN: explicit proxyHost + proxyPort + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector(PROXY_HOST, PROXY_PORT, null, null, noProxySelector()); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertProxyAddress(result, PROXY_HOST, PROXY_PORT); + } + + @Test + void givenExplicitProxy_whenProxyFor_thenTargetAddressPreserved() throws IOException { + // GIVEN: explicit proxy + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector(PROXY_HOST, PROXY_PORT, null, null, noProxySelector()); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN: target address is forwarded through the proxy + HttpConnectProxiedSocketAddress httpProxy = + assertInstanceOf(HttpConnectProxiedSocketAddress.class, result); + assertEquals(TARGET, httpProxy.getTargetAddress()); + } + } + + @Nested + // matching pattern bypasses proxy; non-matching pattern allows proxy; multiple pipe-separated + // patterns; case-insensitive matching + class BypassPattern { + + @Test + void givenBypassMatchesTarget_whenProxyFor_thenReturnsNull() throws IOException { + // GIVEN: bypass pattern matches target, explicit proxy configured + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, PROXY_PORT, "*.iomete.cloud", null, noProxySelector()); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN: bypass wins over explicit proxy + assertNull(result); + } + + @Test + void givenBypassDoesNotMatchTarget_whenProxyFor_thenReturnsProxy() throws IOException { + // GIVEN: bypass pattern does NOT match target + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, PROXY_PORT, "*.internal.net", null, noProxySelector()); + + // WHEN + ProxiedSocketAddress result = + sut.proxyFor(InetSocketAddress.createUnresolved("api.example.com", 443)); + + // THEN + assertProxyAddress(result, PROXY_HOST, PROXY_PORT); + } + + @Test + void givenMultipleBypassPatterns_whenProxyFor_thenAnyMatchBypasses() throws IOException { + // GIVEN: pipe-separated patterns (http.nonProxyHosts format) + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, + PROXY_PORT, + "localhost|*.internal|10.*|exact.host.com", + null, + noProxySelector()); + + // THEN: each pattern type is tested + assertNull(sut.proxyFor(InetSocketAddress.createUnresolved("localhost", 443)), "exact match"); + assertNull( + sut.proxyFor(InetSocketAddress.createUnresolved("db.internal", 5432)), "suffix wildcard"); + assertNull( + sut.proxyFor(InetSocketAddress.createUnresolved("10.0.0.1", 443)), "prefix wildcard"); + assertNull( + sut.proxyFor(InetSocketAddress.createUnresolved("exact.host.com", 443)), + "exact FQDN match"); + + // non-matching host goes through proxy + assertNotNull( + sut.proxyFor(InetSocketAddress.createUnresolved("external.com", 443)), + "non-matching host should use proxy"); + } + + @Test + void givenBypassPatternIsCaseInsensitive_whenProxyFor_thenMatchesRegardlessOfCase() + throws IOException { + // GIVEN: mixed-case bypass vs mixed-case target + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, PROXY_PORT, "*.Example.COM", null, noProxySelector()); + + // THEN + assertNull(sut.proxyFor(InetSocketAddress.createUnresolved("API.EXAMPLE.COM", 443))); + assertNull(sut.proxyFor(InetSocketAddress.createUnresolved("api.example.com", 443))); + } + } + + @Nested + // uses selector when no explicit proxy; null when selector returns DIRECT; picks first non-DIRECT + // from multi-proxy list; handles null ProxySelector + class ProxySelectorFallback { + + @Test + void givenNoExplicitProxyAndSelectorReturnsProxy_whenProxyFor_thenUsesSelector() + throws IOException { + // GIVEN: no explicit proxy, ProxySelector returns an HTTP proxy + String selectorHost = "192.168.1.2"; + int selectorPort = 3128; + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + null, null, null, null, proxySelectorReturning(selectorHost, selectorPort)); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertProxyAddress(result, selectorHost, selectorPort); + } + + @Test + void givenNoExplicitProxyAndSelectorReturnsDirect_whenProxyFor_thenReturnsNull() + throws IOException { + // GIVEN: ProxySelector returns DIRECT (no proxy) + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector(null, null, null, null, noProxySelector()); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertNull(result); + } + + @Test + void givenNoExplicitProxyAndSelectorReturnsMultiple_whenProxyFor_thenUsesFirstNonDirect() + throws IOException { + // GIVEN: ProxySelector returns [DIRECT, HTTP proxy] + ProxySelector selector = mock(ProxySelector.class); + InetSocketAddress proxyAddr = new InetSocketAddress("192.168.1.3", 9090); + List proxies = List.of(Proxy.NO_PROXY, new Proxy(Proxy.Type.HTTP, proxyAddr)); + when(selector.select(any(URI.class))).thenReturn(proxies); + + ArrowFlightProxyDetector sut = new ArrowFlightProxyDetector(null, null, null, null, selector); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN: skips DIRECT, uses the HTTP proxy + assertProxyAddress(result, "192.168.1.3", 9090); + } + + @Test + void givenNullProxySelector_whenProxyFor_thenReturnsNull() throws IOException { + // GIVEN: null ProxySelector (can happen if JVM has no default) + ArrowFlightProxyDetector sut = new ArrowFlightProxyDetector(null, null, null, null, null); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertNull(result); + } + } + + @Nested + // returns null when nothing is configured + class NoProxyConfigured { + + @Test + void givenNothingConfigured_whenProxyFor_thenReturnsNull() throws IOException { + // GIVEN: no proxy, no bypass, not disabled, selector returns DIRECT + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector(null, null, null, null, noProxySelector()); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN + assertNull(result); + } + } + + @Nested + // forceDisabled > bypass > explicit > selector + class PriorityOrder { + + @Test + void givenForceDisabledWithExplicitProxyAndBypass_whenProxyFor_thenForceDisabledWins() + throws IOException { + // GIVEN: all settings configured, force disabled takes highest priority + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, + PROXY_PORT, + "*.other.com", + "force", + proxySelectorReturning("192.168.1.2", 3128)); + + // THEN + assertNull(sut.proxyFor(TARGET)); + } + + @Test + void givenBypassMatchesAndExplicitProxy_whenProxyFor_thenBypassWins() throws IOException { + // GIVEN: target matches bypass, explicit proxy also configured + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, + PROXY_PORT, + "*.iomete.cloud", + null, + proxySelectorReturning("192.168.1.2", 3128)); + + // THEN: bypass takes priority over explicit proxy + assertNull(sut.proxyFor(TARGET)); + } + + @Test + void givenExplicitProxyAndSelector_whenProxyFor_thenExplicitWins() throws IOException { + // GIVEN: both explicit proxy and ProxySelector configured + ArrowFlightProxyDetector sut = + new ArrowFlightProxyDetector( + PROXY_HOST, PROXY_PORT, null, null, proxySelectorReturning("192.168.1.2", 3128)); + + // WHEN + ProxiedSocketAddress result = sut.proxyFor(TARGET); + + // THEN: explicit proxy takes priority over selector + assertProxyAddress(result, PROXY_HOST, PROXY_PORT); + } + } + + private static ProxySelector noProxySelector() { + ProxySelector selector = mock(ProxySelector.class); + when(selector.select(any(URI.class))).thenReturn(Collections.singletonList(Proxy.NO_PROXY)); + return selector; + } + + private static ProxySelector proxySelectorReturning(String host, int port) { + ProxySelector selector = mock(ProxySelector.class); + InetSocketAddress proxyAddr = new InetSocketAddress(host, port); + Proxy proxy = new Proxy(Proxy.Type.HTTP, proxyAddr); + when(selector.select(any(URI.class))).thenReturn(Collections.singletonList(proxy)); + return selector; + } + + private static void assertProxyAddress( + ProxiedSocketAddress result, String expectedHost, int expectedPort) { + assertNotNull(result); + HttpConnectProxiedSocketAddress httpProxy = + assertInstanceOf(HttpConnectProxiedSocketAddress.class, result); + InetSocketAddress proxyAddr = (InetSocketAddress) httpProxy.getProxyAddress(); + assertEquals(expectedHost, proxyAddr.getHostString()); + assertEquals(expectedPort, proxyAddr.getPort()); + } +} From 69ba4db048d7ca9e7d12ef07a5f225fb7d333b16 Mon Sep 17 00:00:00 2001 From: Mateus Aubin Date: Thu, 26 Mar 2026 14:41:40 +0100 Subject: [PATCH 2/4] release: Package IOMETE fork as 19.0.0-iomete.1 (#2) * release: bump version to 19.0.0-iomete.1 and add IOMETE manifest - Set all module versions from 19.0.0-SNAPSHOT to 19.0.0-iomete.1 - Add ManifestResourceTransformer to flight-sql-jdbc-driver shade plugin with Built-By, Implementation-Vendor, Implementation-Version, and X-Fork-Source entries identifying this as an IOMETE fork of Arrow 19.0.0 * chore: add build config for JDK 17 * fix: remove machine-specific dotGitDirectory from git-commit-id-plugin config --- adapter/avro/pom.xml | 2 +- adapter/jdbc/pom.xml | 2 +- adapter/orc/pom.xml | 2 +- algorithm/pom.xml | 2 +- arrow-variant/pom.xml | 2 +- bom/pom.xml | 2 +- c/pom.xml | 2 +- compression/pom.xml | 2 +- dataset/pom.xml | 2 +- flight/flight-core/pom.xml | 2 +- flight/flight-integration-tests/pom.xml | 2 +- flight/flight-sql-jdbc-core/pom.xml | 2 +- flight/flight-sql-jdbc-driver/pom.xml | 10 +++++++++- flight/flight-sql/pom.xml | 2 +- flight/pom.xml | 2 +- format/pom.xml | 2 +- gandiva/pom.xml | 2 +- memory/memory-core/pom.xml | 2 +- memory/memory-netty-buffer-patch/pom.xml | 2 +- memory/memory-netty/pom.xml | 2 +- memory/memory-unsafe/pom.xml | 2 +- memory/pom.xml | 2 +- mise.toml | 2 ++ performance/pom.xml | 2 +- pom.xml | 4 ++-- tools/pom.xml | 2 +- vector/pom.xml | 2 +- 27 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 mise.toml diff --git a/adapter/avro/pom.xml b/adapter/avro/pom.xml index 827d19f2a2..4cb437703b 100644 --- a/adapter/avro/pom.xml +++ b/adapter/avro/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 ../../pom.xml diff --git a/adapter/jdbc/pom.xml b/adapter/jdbc/pom.xml index 2f621d7a05..a5baf315d4 100644 --- a/adapter/jdbc/pom.xml +++ b/adapter/jdbc/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 ../../pom.xml diff --git a/adapter/orc/pom.xml b/adapter/orc/pom.xml index c96ab36119..0821b03523 100644 --- a/adapter/orc/pom.xml +++ b/adapter/orc/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 ../../pom.xml diff --git a/algorithm/pom.xml b/algorithm/pom.xml index 898c2605b6..9b6d95c37a 100644 --- a/algorithm/pom.xml +++ b/algorithm/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-algorithm Arrow Algorithms diff --git a/arrow-variant/pom.xml b/arrow-variant/pom.xml index 3a842178a4..b4d5608b54 100644 --- a/arrow-variant/pom.xml +++ b/arrow-variant/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-variant Arrow Variant diff --git a/bom/pom.xml b/bom/pom.xml index 0de43a1217..5af49b1ef4 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -29,7 +29,7 @@ under the License. org.apache.arrow arrow-bom - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 pom Arrow Bill of Materials diff --git a/c/pom.xml b/c/pom.xml index c90b6dc0ef..5d7c50bc1b 100644 --- a/c/pom.xml +++ b/c/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-c-data diff --git a/compression/pom.xml b/compression/pom.xml index 29f8b41788..7d932706dd 100644 --- a/compression/pom.xml +++ b/compression/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-compression Arrow Compression diff --git a/dataset/pom.xml b/dataset/pom.xml index 1852c6eddc..688229fddf 100644 --- a/dataset/pom.xml +++ b/dataset/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-dataset diff --git a/flight/flight-core/pom.xml b/flight/flight-core/pom.xml index f1d58a0cad..597ac3aaa2 100644 --- a/flight/flight-core/pom.xml +++ b/flight/flight-core/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-flight - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 flight-core diff --git a/flight/flight-integration-tests/pom.xml b/flight/flight-integration-tests/pom.xml index f0f10ada43..f525160e8a 100644 --- a/flight/flight-integration-tests/pom.xml +++ b/flight/flight-integration-tests/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-flight - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 flight-integration-tests diff --git a/flight/flight-sql-jdbc-core/pom.xml b/flight/flight-sql-jdbc-core/pom.xml index da00baf32a..4e910eae9a 100644 --- a/flight/flight-sql-jdbc-core/pom.xml +++ b/flight/flight-sql-jdbc-core/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-flight - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 flight-sql-jdbc-core diff --git a/flight/flight-sql-jdbc-driver/pom.xml b/flight/flight-sql-jdbc-driver/pom.xml index 559c42597d..b36019202c 100644 --- a/flight/flight-sql-jdbc-driver/pom.xml +++ b/flight/flight-sql-jdbc-driver/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-flight - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 flight-sql-jdbc-driver @@ -138,6 +138,14 @@ under the License. + + + IOMETE + IOMETE + 19.0.0-iomete.1 + Apache Arrow Java 19.0.0 + + META-INF/LICENSE.txt diff --git a/flight/flight-sql/pom.xml b/flight/flight-sql/pom.xml index a5954819c3..cc26976302 100644 --- a/flight/flight-sql/pom.xml +++ b/flight/flight-sql/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-flight - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 flight-sql diff --git a/flight/pom.xml b/flight/pom.xml index 2fc3e89ef8..5272f0baeb 100644 --- a/flight/pom.xml +++ b/flight/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-flight diff --git a/format/pom.xml b/format/pom.xml index d3578b63d2..7a8fdb6b43 100644 --- a/format/pom.xml +++ b/format/pom.xml @@ -23,7 +23,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-format diff --git a/gandiva/pom.xml b/gandiva/pom.xml index 5367bfdedf..a0c4069d2a 100644 --- a/gandiva/pom.xml +++ b/gandiva/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 org.apache.arrow.gandiva diff --git a/memory/memory-core/pom.xml b/memory/memory-core/pom.xml index 72ee69d60a..e65de94da1 100644 --- a/memory/memory-core/pom.xml +++ b/memory/memory-core/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-memory - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-memory-core diff --git a/memory/memory-netty-buffer-patch/pom.xml b/memory/memory-netty-buffer-patch/pom.xml index 07dc7d2403..c8ccc77f0f 100644 --- a/memory/memory-netty-buffer-patch/pom.xml +++ b/memory/memory-netty-buffer-patch/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-memory - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-memory-netty-buffer-patch diff --git a/memory/memory-netty/pom.xml b/memory/memory-netty/pom.xml index 6d660da117..9647482887 100644 --- a/memory/memory-netty/pom.xml +++ b/memory/memory-netty/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-memory - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-memory-netty diff --git a/memory/memory-unsafe/pom.xml b/memory/memory-unsafe/pom.xml index 92dc0c9fe5..f8c7f68122 100644 --- a/memory/memory-unsafe/pom.xml +++ b/memory/memory-unsafe/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-memory - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-memory-unsafe diff --git a/memory/pom.xml b/memory/pom.xml index bc34c26050..8ad4e850b2 100644 --- a/memory/pom.xml +++ b/memory/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-memory pom diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000000..262fd088c7 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +java = "temurin-17" diff --git a/performance/pom.xml b/performance/pom.xml index 3f18188e3a..5389a792fd 100644 --- a/performance/pom.xml +++ b/performance/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-performance jar diff --git a/pom.xml b/pom.xml index 19625617b1..26a49a69dd 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 pom Apache Arrow Java Root POM @@ -92,7 +92,7 @@ under the License. - 1695310533 + 1773860567 ${project.build.directory}/generated-sources 1.9.0 5.12.2 diff --git a/tools/pom.xml b/tools/pom.xml index d43adb1fdf..cdc261a9a2 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-tools Arrow Tools diff --git a/vector/pom.xml b/vector/pom.xml index f46bd0e7b4..cdc0b6e0d0 100644 --- a/vector/pom.xml +++ b/vector/pom.xml @@ -22,7 +22,7 @@ under the License. org.apache.arrow arrow-java-root - 19.0.0-SNAPSHOT + 19.0.0-iomete.1 arrow-vector Arrow Vectors From b8b597a070caa1f1256d6a267ff3150220aeb32b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:25:57 +0000 Subject: [PATCH 3/4] Bump org.apache.derby:derby in /flight/flight-sql (#3) Bumps org.apache.derby:derby from 10.15.2.0 to 10.17.1.0. --- updated-dependencies: - dependency-name: org.apache.derby:derby dependency-version: 10.17.1.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- flight/flight-sql/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flight/flight-sql/pom.xml b/flight/flight-sql/pom.xml index cc26976302..b6305cafb1 100644 --- a/flight/flight-sql/pom.xml +++ b/flight/flight-sql/pom.xml @@ -89,7 +89,7 @@ under the License. org.apache.derby derby - 10.15.2.0 + 10.17.1.0 test From 782e4731dc7c999433047cdc9b3ced109d59335c Mon Sep 17 00:00:00 2001 From: Mateus Aubin Date: Tue, 31 Mar 2026 19:18:07 +0200 Subject: [PATCH 4/4] =?UTF-8?q?revert:=20downgrade=20Derby=2010.17.1.0=20?= =?UTF-8?q?=E2=86=92=2010.15.2.0=20(Java=2019+=20incompatibility)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Derby 10.17.1.0 requires Java 19+ due to internal use of restricted APIs (sun.misc.Unsafe). No backport has been released by the Derby maintainers, despite the fix existing in the repo. The project currently targets Java versions below 19, making this upgrade a build-breaking change. Derby is effectively unmaintained (dead project). Longer-term options discussed during daily sync: - Accept risk: dependency is test-scoped, not shipped to users - Remove Derby from our fork and disable affected tests - Replace with H2 or another embedded DB - Manage our own Derby 10.16.1.2 release - Upgrade arrow-java/JDBC to JDK-21 (would allow keeping 10.17.1.0) Decision: revert for now; track in Vanta noting this is test-scope only and originates from the upstream arrow-java fork. Reverts b8b597a070caa1f1256d6a267ff3150220aeb32b --- flight/flight-sql/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flight/flight-sql/pom.xml b/flight/flight-sql/pom.xml index b6305cafb1..cc26976302 100644 --- a/flight/flight-sql/pom.xml +++ b/flight/flight-sql/pom.xml @@ -89,7 +89,7 @@ under the License. org.apache.derby derby - 10.17.1.0 + 10.15.2.0 test