diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce87cba..1443754 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,6 +85,7 @@ jobs: bash publish.sh lib-lang bash publish.sh lib-lock bash publish.sh lib-lock-redis + bash publish.sh lib-util-net bash publish.sh lib-httpx bash publish.sh lib-mail bash publish.sh lib-node-id-redis @@ -94,7 +95,6 @@ jobs: bash publish.sh lib-util-string bash publish.sh lib-util-time bash publish.sh lib-util-tsid - bash publish.sh lib-util-net bash publish.sh lib-pool bash publish.sh lib-random bash publish.sh lib-retry diff --git a/lib-httpx/README.md b/lib-httpx/README.md index 48244c1..52f52a7 100644 --- a/lib-httpx/README.md +++ b/lib-httpx/README.md @@ -10,7 +10,7 @@ Add the dependency to your `build.gradle`: ```gradle dependencies { - implementation 'io.seqera:lib-httpx:2.6.0' + implementation 'io.seqera:lib-httpx:2.7.0' } ``` @@ -25,7 +25,7 @@ dependencies { - **Custom Token Storage**: Pluggable token store interface for distributed deployments (Redis, database, etc.) - **WWW-Authenticate Support**: Automatic handling of HTTP authentication challenges (Basic and Bearer schemes) - **Anonymous Authentication**: Fallback to anonymous authentication when credentials aren't provided -- **Proxy Support**: Authenticated forward-proxy support via `.proxy(...)`/`.authenticator(...)`, or an `HxProxyConfig` value applied with `.withProxyConfig(...)` +- **Proxy Support**: Authenticated forward-proxy support via `.proxy(...)`/`.authenticator(...)`, or an `io.seqera.util.net.ProxyConfig` (from `io.seqera:lib-util-net`) applied with `.withProxyConfig(...)` - **Configurable**: Customizable retry policies, timeouts, token refresh, authentication settings, and cookie policies - **Generic Integration**: Compatible with any `Retryable.Config` for flexible retry configuration - **Thread-safe**: Safe for concurrent use with atomic token refresh coordination @@ -403,16 +403,17 @@ HxClient client = HxClient.newBuilder() .build(); ``` -For callers that resolve proxy settings themselves (host, port, optional credentials per protocol and -`NO_PROXY` entries), `HxProxyConfig` bundles them into a single value and produces the matching selector -and a proxy-only authenticator (credentials are supplied only for proxy authentication challenges, never -to origin servers). Apply it in one call with `.withProxyConfig(...)`: +For callers that resolve proxy settings themselves, `io.seqera.util.net.ProxyConfig` (from +`io.seqera:lib-util-net`, an `api` dependency of this library) bundles a proxy URI or the +`HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` environment variables into a single value and produces the matching +selector and a proxy-only authenticator (credentials are supplied only for proxy authentication +challenges, never to origin servers). Apply it in one call with `.withProxyConfig(...)`: ```java -HxProxyConfig proxy = HxProxyConfig.newBuilder() - .httpsProxy("proxy.example.com", 8080, "user", "pass") - .noProxy(List.of("internal.example.com")) - .build(); +// prefer the explicit-credentials overload - it avoids having to percent-encode a password +// containing '@', ':' or '%' that would otherwise be embedded in the URI +ProxyConfig proxy = ProxyConfig.fromUri("http://proxy.example.com:8080", "user", "pass", + List.of("internal.example.com")); HxClient client = HxClient.newBuilder() .withProxyConfig(proxy) // no-op if proxy is null .build(); @@ -469,7 +470,7 @@ HxClient client = HxClient.newBuilder() - **`HxClient`**: Main HTTP client with retry, JWT, and WWW-Authenticate functionality - **`HxConfig`**: Configuration builder with all available options -- **`HxProxyConfig`**: Forward-proxy settings (selector + proxy-only authenticator) assembled from explicit values via its builder +- **`io.seqera.util.net.ProxyConfig`** (from `io.seqera:lib-util-net`): Forward-proxy settings (selector + proxy-only authenticator) resolved from a URI or the environment, applied via `HxClient.Builder.withProxyConfig(...)` - **`HxAuth`**: Interface for authentication credentials with stable identity across refreshes - **`HxTokenStore`**: Interface for pluggable token storage (default: in-memory ConcurrentHashMap) - **`HxTokenManager`**: Thread-safe JWT token lifecycle management with multi-session support diff --git a/lib-httpx/VERSION b/lib-httpx/VERSION index e70b452..9aa3464 100644 --- a/lib-httpx/VERSION +++ b/lib-httpx/VERSION @@ -1 +1 @@ -2.6.0 +2.7.0 \ No newline at end of file diff --git a/lib-httpx/build.gradle b/lib-httpx/build.gradle index 76c51b4..deb95b0 100644 --- a/lib-httpx/build.gradle +++ b/lib-httpx/build.gradle @@ -24,6 +24,7 @@ version = "${project.file('VERSION').text.trim()}" dependencies { api project(':lib-retry') + api project(':lib-util-net') implementation 'com.google.code.gson:gson:2.10.1' testImplementation 'com.github.tomakehurst:wiremock:3.0.1' diff --git a/lib-httpx/changelog.txt b/lib-httpx/changelog.txt index d8a86b0..739452a 100644 --- a/lib-httpx/changelog.txt +++ b/lib-httpx/changelog.txt @@ -1,5 +1,22 @@ # lib-httpx changelog +2.7.0 +- BREAKING (see note below): remove HxProxyConfig in favour of io.seqera.util.net.ProxyConfig + (io.seqera:lib-util-net), so the proxy selector / authenticator / no-proxy semantics live in a + single place shared with Wave and Nextflow +- HxClient.Builder.withProxyConfig now accepts an io.seqera.util.net.ProxyConfig; callers that built an + HxProxyConfig should resolve a ProxyConfig instead - its toProxySelector()/toAuthenticator() are + unchanged. For raw credentials prefer the explicit overload + ProxyConfig.fromUri(uri, user, pass, noProxy) over embedding them in the URI, so a password with + '@'/':'/'%' does not need percent-encoding; fromEnvironment(env) reads HTTP_PROXY/HTTPS_PROXY/NO_PROXY +- Note: HxProxyConfig could hold an http proxy without an https one (or vice versa); fromUri applies one + endpoint to both protocols, so asymmetric per-protocol config from explicit values is now only + expressible via fromEnvironment +- lib-httpx now depends on io.seqera:lib-util-net +- NOTE: this removes a public type, so it is source-incompatible for any caller of HxProxyConfig. + It is released as a minor (2.7.0) rather than a major because the only affected consumer is + Nextflow, which is migrated in lock-step; callers pinning <= 2.6.0 are unaffected. + 2.6.0 - 12 Aug 2026 - Add HxClient.shouldRetryOnException(HttpRequest, Throwable) - the hook the retry policy now consults - so a subclass can decide on the exception per request diff --git a/lib-httpx/src/main/java/io/seqera/http/HxClient.java b/lib-httpx/src/main/java/io/seqera/http/HxClient.java index 9b66919..9ae5622 100644 --- a/lib-httpx/src/main/java/io/seqera/http/HxClient.java +++ b/lib-httpx/src/main/java/io/seqera/http/HxClient.java @@ -40,6 +40,7 @@ import io.seqera.http.auth.AuthenticationChallenge; import io.seqera.http.auth.AuthenticationScheme; import io.seqera.http.auth.WwwAuthenticateParser; +import io.seqera.util.net.ProxyConfig; import io.seqera.util.retry.Retryable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -1195,12 +1196,12 @@ public Builder version(HttpClient.Version version) { *

The selector is forwarded to the inner {@link HttpClient.Builder} and is also * inherited by the internal HTTP clients used for JWT token refresh and anonymous * Bearer token retrieval. Proxy settings are never resolved from the environment - * automatically; supply them explicitly here (or via {@link #withProxyConfig(HxProxyConfig)}). + * automatically; supply them explicitly here (or via {@link #withProxyConfig(ProxyConfig)}). * * @param proxySelector the proxy selector to use * @return this Builder instance * @see HttpClient.Builder#proxy(ProxySelector) - * @see HxProxyConfig + * @see ProxyConfig */ public Builder proxy(ProxySelector proxySelector) { this.proxySelector = proxySelector; @@ -1220,12 +1221,12 @@ public Builder proxy(ProxySelector proxySelector) { * {@link Authenticator#setDefault(Authenticator)} - proxy credentials only take effect * when supplied via this method. For Basic proxy authentication of HTTPS traffic the JDK's * {@code jdk.http.auth.tunneling.disabledSchemes} property must also be cleared - see - * {@link HxProxyConfig} for details. + * {@link ProxyConfig} for details. * * @param authenticator the authenticator providing credentials * @return this Builder instance * @see HttpClient.Builder#authenticator(Authenticator) - * @see HxProxyConfig#toAuthenticator() + * @see ProxyConfig#toAuthenticator() */ public Builder authenticator(Authenticator authenticator) { this.proxyAuthenticator = authenticator; @@ -1233,16 +1234,17 @@ public Builder authenticator(Authenticator authenticator) { } /** - * Applies the proxy selector and authenticator carried by the given {@link HxProxyConfig}, - * a convenience over calling {@link #proxy(ProxySelector)} and - * {@link #authenticator(Authenticator)} separately. A {@code null} config is a no-op, so - * callers can pass an optionally-resolved configuration directly. + * Applies the proxy selector and authenticator carried by the given + * {@link ProxyConfig} (from {@code io.seqera:lib-util-net}), a convenience over calling + * {@link #proxy(ProxySelector)} and {@link #authenticator(Authenticator)} separately. A + * {@code null} config is a no-op, so callers can pass an optionally-resolved configuration + * directly. * * @param config the proxy configuration to apply, or null for none * @return this Builder instance - * @see HxProxyConfig + * @see ProxyConfig */ - public Builder withProxyConfig(HxProxyConfig config) { + public Builder withProxyConfig(ProxyConfig config) { if( config == null ) return this; if( config.getHttpProxy() != null || config.getHttpsProxy() != null ) diff --git a/lib-httpx/src/main/java/io/seqera/http/HxConfig.java b/lib-httpx/src/main/java/io/seqera/http/HxConfig.java index 6ba1e41..cfeb87b 100644 --- a/lib-httpx/src/main/java/io/seqera/http/HxConfig.java +++ b/lib-httpx/src/main/java/io/seqera/http/HxConfig.java @@ -701,7 +701,7 @@ public Builder withRefreshCookiePolicy(CookiePolicy policy) { * * @param proxySelector the proxy selector, or null to use the JVM default behaviour * @return this builder instance for method chaining - * @see HxProxyConfig + * @see io.seqera.util.net.ProxyConfig */ public Builder proxySelector(ProxySelector proxySelector) { this.proxySelector = proxySelector; @@ -711,12 +711,12 @@ public Builder proxySelector(ProxySelector proxySelector) { /** * Sets the authenticator used to supply credentials to an authenticating forward proxy, * applied to the main HTTP client and the internal token refresh clients. See - * {@link HxProxyConfig} for the {@code Authenticator.setDefault} and Basic-over-HTTPS - * tunnelling caveats. + * {@link io.seqera.util.net.ProxyConfig} for the {@code Authenticator.setDefault} and + * Basic-over-HTTPS tunnelling caveats. * * @param proxyAuthenticator the authenticator providing proxy credentials, or null for none * @return this builder instance for method chaining - * @see HxProxyConfig#toAuthenticator() + * @see io.seqera.util.net.ProxyConfig#toAuthenticator() */ public Builder proxyAuthenticator(Authenticator proxyAuthenticator) { this.proxyAuthenticator = proxyAuthenticator; diff --git a/lib-httpx/src/main/java/io/seqera/http/HxProxyConfig.java b/lib-httpx/src/main/java/io/seqera/http/HxProxyConfig.java deleted file mode 100644 index 6e75fa7..0000000 --- a/lib-httpx/src/main/java/io/seqera/http/HxProxyConfig.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright 2026, Seqera Labs - * - * 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 io.seqera.http; - -import java.io.IOException; -import java.net.Authenticator; -import java.net.InetSocketAddress; -import java.net.PasswordAuthentication; -import java.net.Proxy; -import java.net.ProxySelector; -import java.net.SocketAddress; -import java.net.URI; -import java.util.List; -import java.util.Locale; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Holds HTTP/HTTPS forward-proxy settings supplied explicitly (host, port and optional - * credentials per protocol, plus {@code NO_PROXY} entries) and exposes them as a - * {@link ProxySelector} and a proxy-only {@link Authenticator} suitable for - * {@link java.net.http.HttpClient}. Build one with {@link #newBuilder()}. - * - *

The library never reads the environment or system properties on its own; the caller - * resolves proxy settings however it wishes and passes the values to {@link Builder}. - * - *

NO_PROXY entries are honoured as a list of host names or domain suffixes - * (optionally prefixed with . or *.); the single entry * - * disables proxying entirely. Loopback targets (localhost, 127.*, - * [::1]) always bypass the proxy, mirroring the JDK default - * http.nonProxyHosts behaviour. CIDR notation entries are not supported. - * - *

Why an explicit Authenticator?
- * {@link java.net.http.HttpClient} ignores {@link Authenticator#setDefault(Authenticator)}; - * proxy credentials only take effect when supplied via - * {@link java.net.http.HttpClient.Builder#authenticator(Authenticator)}. The authenticator - * returned by {@link #toAuthenticator()} releases credentials only for - * {@link Authenticator.RequestorType#PROXY} requests that match the configured proxy - * host and port, never for origin-server challenges. - * - *

Basic auth over HTTPS tunnels:
- * The JDK disables the Basic scheme for HTTPS CONNECT tunnelling by default via - * jdk.http.auth.tunneling.disabledSchemes=Basic (see - * $JAVA_HOME/conf/net.properties). For proxy credentials to be sent on the - * CONNECT request of HTTPS traffic, that property must be cleared, e.g. with - * -Djdk.http.auth.tunneling.disabledSchemes=. - * - * @see HxClient.Builder#proxy(ProxySelector) - * @see HxClient.Builder#authenticator(Authenticator) - */ -public class HxProxyConfig { - - private static final Logger log = LoggerFactory.getLogger(HxProxyConfig.class); - - /** - * Connection details for a single proxy server, with optional credentials. - */ - static final class ProxyEntry { - final String host; - final int port; - final String username; - final String password; - - ProxyEntry(String host, int port, String username, String password) { - this.host = host; - this.port = port; - this.username = username; - this.password = password; - } - - boolean hasCredentials() { - return username != null && !username.isEmpty(); - } - - InetSocketAddress address() { - return InetSocketAddress.createUnresolved(host, port); - } - } - - private final ProxyEntry httpProxy; - private final ProxyEntry httpsProxy; - private final List noProxyHosts; - - private HxProxyConfig(ProxyEntry httpProxy, ProxyEntry httpsProxy, List noProxyHosts) { - this.httpProxy = httpProxy; - this.httpsProxy = httpsProxy; - this.noProxyHosts = noProxyHosts; - } - - /** - * Creates a builder to assemble a proxy configuration from explicit values, for callers that - * resolve proxy settings themselves rather than from the environment. - * - * @return a new {@link Builder} - */ - public static Builder newBuilder() { - return new Builder(); - } - - /** - * Builder for {@link HxProxyConfig}: the proxy details are supplied explicitly - * (host, port and optional credentials per protocol, plus {@code NO_PROXY} entries). - */ - public static final class Builder { - private ProxyEntry httpProxy; - private ProxyEntry httpsProxy; - private List noProxyHosts = List.of(); - - public Builder httpProxy(String host, int port, String username, String password) { - this.httpProxy = host != null ? new ProxyEntry(host, port, username, password) : null; - return this; - } - - public Builder httpsProxy(String host, int port, String username, String password) { - this.httpsProxy = host != null ? new ProxyEntry(host, port, username, password) : null; - return this; - } - - public Builder noProxy(List hosts) { - this.noProxyHosts = hosts == null - ? List.of() - : hosts.stream() - .filter(h -> h != null) - .map(h -> h.trim().toLowerCase(Locale.ROOT)) - .filter(h -> !h.isEmpty()) - .toList(); - return this; - } - - public HxProxyConfig build() { - return new HxProxyConfig(httpProxy, httpsProxy, noProxyHosts); - } - } - - /** - * Creates a {@link ProxySelector} that routes requests through the configured proxies, - * bypassing hosts matched by {@code NO_PROXY} and loopback addresses. - * - * @return a new ProxySelector reflecting this configuration - */ - public ProxySelector toProxySelector() { - // precompute the proxy lists - select() runs once per outbound request - final List direct = List.of(Proxy.NO_PROXY); - final List viaHttpProxy = httpProxy != null - ? List.of(new Proxy(Proxy.Type.HTTP, httpProxy.address())) - : direct; - final List viaHttpsProxy = httpsProxy != null - ? List.of(new Proxy(Proxy.Type.HTTP, httpsProxy.address())) - : direct; - return new ProxySelector() { - @Override - public List select(URI uri) { - if (uri == null || isBypassed(uri.getHost())) - return direct; - return "https".equalsIgnoreCase(uri.getScheme()) ? viaHttpsProxy : viaHttpProxy; - } - - @Override - public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { - log.debug("Failed to connect to proxy {} for {}: {}", sa, uri, ioe.getMessage()); - } - }; - } - - /** - * Creates an {@link Authenticator} that supplies the configured proxy credentials. - * - *

Credentials are released only for {@link Authenticator.RequestorType#PROXY} - * requests whose host and port match the configured proxy, never for origin-server - * authentication challenges. - * - * @return a new Authenticator, or null when no proxy credentials are configured - */ - public Authenticator toAuthenticator() { - if (!hasCredentials()) - return null; - return new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - if (getRequestorType() != RequestorType.PROXY) - return null; - final ProxyEntry entry = credentialsFor(getRequestingHost(), getRequestingPort()); - if (entry == null) - return null; - final String password = entry.password != null ? entry.password : ""; - return new PasswordAuthentication(entry.username, password.toCharArray()); - } - }; - } - - /** - * @return true when at least one configured proxy carries credentials - */ - public boolean hasCredentials() { - return (httpProxy != null && httpProxy.hasCredentials()) - || (httpsProxy != null && httpsProxy.hasCredentials()); - } - - ProxyEntry getHttpProxy() { - return httpProxy; - } - - ProxyEntry getHttpsProxy() { - return httpsProxy; - } - - /** - * Determines whether the given target host must bypass the proxy, either because it is - * a loopback address or because it matches a {@code NO_PROXY} entry. - */ - boolean isBypassed(String host) { - if (host == null) - return true; - final String target = host.toLowerCase(Locale.ROOT); - // always bypass loopback targets, consistent with the JDK default `http.nonProxyHosts` - if (target.equals("localhost") || target.startsWith("127.") || target.equals("::1") || target.equals("[::1]")) - return true; - for (String entry : noProxyHosts) { - if (entry.equals("*")) - return true; - // "*.example.com" and ".example.com" match sub-domains only; - // "example.com" matches the host itself and any sub-domain - final String suffix = entry.startsWith("*.") ? entry.substring(1) : entry; - if (suffix.startsWith(".")) { - if (target.endsWith(suffix)) - return true; - } - else if (target.equals(suffix) || target.endsWith("." + suffix)) { - return true; - } - } - return false; - } - - private ProxyEntry credentialsFor(String host, int port) { - if (httpsProxy != null && httpsProxy.hasCredentials() && httpsProxy.host.equalsIgnoreCase(host) && httpsProxy.port == port) - return httpsProxy; - if (httpProxy != null && httpProxy.hasCredentials() && httpProxy.host.equalsIgnoreCase(host) && httpProxy.port == port) - return httpProxy; - return null; - } -} diff --git a/lib-httpx/src/test/groovy/io/seqera/http/HxClientProxyAuthIntegrationTest.groovy b/lib-httpx/src/test/groovy/io/seqera/http/HxClientProxyAuthIntegrationTest.groovy index b7dbe93..dc95ffe 100644 --- a/lib-httpx/src/test/groovy/io/seqera/http/HxClientProxyAuthIntegrationTest.groovy +++ b/lib-httpx/src/test/groovy/io/seqera/http/HxClientProxyAuthIntegrationTest.groovy @@ -24,6 +24,7 @@ import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse +import io.seqera.util.net.ProxyConfig import spock.lang.Specification /** @@ -202,11 +203,9 @@ class HxClientProxyAuthIntegrationTest extends Specification { client.getConfig().getProxyAuthenticator().is(authenticator) } - def 'should route through the proxy via withProxyConfig and the HxProxyConfig builder'() { - given: 'a proxy config assembled from explicit values' - def proxyConfig = HxProxyConfig.newBuilder() - .httpProxy('127.0.0.1', proxyPort, 'alice', 's3cret') - .build() + def 'should route through the proxy via withProxyConfig and a lib-util-net ProxyConfig'() { + given: 'a proxy config resolved from an explicit uri' + def proxyConfig = ProxyConfig.fromUri("http://alice:s3cret@127.0.0.1:${proxyPort}".toString()) def client = HxClient.newBuilder() .withProxyConfig(proxyConfig) .build() diff --git a/lib-httpx/src/test/groovy/io/seqera/http/HxProxyConfigTest.groovy b/lib-httpx/src/test/groovy/io/seqera/http/HxProxyConfigTest.groovy deleted file mode 100644 index b5d40d9..0000000 --- a/lib-httpx/src/test/groovy/io/seqera/http/HxProxyConfigTest.groovy +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2026, Seqera Labs - * - * 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 io.seqera.http - -import java.net.Authenticator -import java.net.PasswordAuthentication -import java.net.Proxy - -import spock.lang.Specification - -/** - * Test cases for {@link HxProxyConfig} proxy selection and proxy authentication behaviour. - */ -class HxProxyConfigTest extends Specification { - - private static PasswordAuthentication invokeAuth(Authenticator authenticator, Authenticator.RequestorType type, String host, int port) { - // drive the protected Authenticator API the same way the JDK HTTP client does - return authenticator.requestPasswordAuthenticationInstance( - host, null, port, 'http', 'proxy auth', 'basic', new URL('http://example.com'), type ) - } - - def 'builder should assemble a proxy config from explicit values'() { - when: - def config = HxProxyConfig.newBuilder() - .httpsProxy('proxy.example.com', 8080, 'foo', 'bar') - .noProxy(['internal.example.com']) - .build() - - then: - config.httpsProxy.host == 'proxy.example.com' - config.httpsProxy.port == 8080 - config.httpsProxy.username == 'foo' - config.httpsProxy.password == 'bar' - config.httpProxy == null - config.hasCredentials() - and: 'the selector honours the explicit NO_PROXY entry' - config.isBypassed('internal.example.com') - !config.isBypassed('api.example.com') - and: - config.toAuthenticator() != null - } - - def 'builder without credentials yields no authenticator'() { - when: - def config = HxProxyConfig.newBuilder() - .httpProxy('proxy.local', 3128, null, null) - .build() - - then: - config.httpProxy.host == 'proxy.local' - config.httpProxy.port == 3128 - !config.hasCredentials() - config.toAuthenticator() == null - } - - def 'should select the proxy matching the target scheme'() { - given: - def config = HxProxyConfig.newBuilder() - .httpProxy('http-proxy', 3128, null, null) - .httpsProxy('https-proxy', 3129, null, null) - .build() - def selector = config.toProxySelector() - - when: - def httpResult = selector.select(URI.create('http://api.example.com/foo')) - def httpsResult = selector.select(URI.create('https://api.example.com/foo')) - - then: - (httpResult[0].address() as InetSocketAddress).hostString == 'http-proxy' - (httpResult[0].address() as InetSocketAddress).port == 3128 - (httpsResult[0].address() as InetSocketAddress).hostString == 'https-proxy' - (httpsResult[0].address() as InetSocketAddress).port == 3129 - } - - def 'should connect directly when no proxy matches the target scheme'() { - given: - def config = HxProxyConfig.newBuilder().httpsProxy('proxy', 3128, null, null).build() - - expect: - config.toProxySelector().select(URI.create('http://api.example.com/foo')) == [Proxy.NO_PROXY] - } - - def 'should bypass proxy for NO_PROXY entries'() { - given: - def config = HxProxyConfig.newBuilder() - .httpProxy('proxy', 3128, null, null) - .httpsProxy('proxy', 3128, null, null) - .noProxy(['internal.example.com', '.corp.example.org', '*.svc.cluster.local']) - .build() - - expect: - config.isBypassed(host) == bypassed - - where: - host | bypassed - 'internal.example.com' | true - 'sub.internal.example.com' | true - 'other.example.com' | false - 'internal.example.com.evil.io'| false - 'foo.corp.example.org' | true - 'corp.example.org' | false - 'db.default.svc.cluster.local'| true - } - - def 'should bypass everything when NO_PROXY is a wildcard'() { - given: - def config = HxProxyConfig.newBuilder().httpsProxy('proxy', 3128, null, null).noProxy(['*']).build() - - expect: - config.toProxySelector().select(URI.create('https://api.example.com')) == [Proxy.NO_PROXY] - } - - def 'should always bypass loopback targets'() { - given: - def config = HxProxyConfig.newBuilder().httpsProxy('proxy', 3128, null, null).build() - - expect: - config.isBypassed('localhost') - config.isBypassed('127.0.0.1') - !config.isBypassed('api.example.com') - } - - def 'should provide credentials only for matching proxy requests'() { - given: - def config = HxProxyConfig.newBuilder().httpsProxy('proxy.example.com', 8080, 'foo', 'bar').build() - def authenticator = config.toAuthenticator() - - when: 'the proxy itself requests authentication' - def auth = invokeAuth(authenticator, Authenticator.RequestorType.PROXY, 'proxy.example.com', 8080) - - then: - auth.userName == 'foo' - new String(auth.password) == 'bar' - - when: 'an origin server requests authentication' - auth = invokeAuth(authenticator, Authenticator.RequestorType.SERVER, 'proxy.example.com', 8080) - - then: - auth == null - - when: 'a different host requests proxy authentication' - auth = invokeAuth(authenticator, Authenticator.RequestorType.PROXY, 'other-proxy.example.com', 8080) - - then: - auth == null - - when: 'the right host but a different port requests proxy authentication' - auth = invokeAuth(authenticator, Authenticator.RequestorType.PROXY, 'proxy.example.com', 9999) - - then: - auth == null - } -}