Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### 7.8-SNAPSHOT

#### Bugs
* Fix #7950: (kubernetes-client-api) `NO_PROXY` hostname entries now match only exact hosts or dot-delimited subdomains, preventing lookalike suffixes from bypassing the configured proxy
* Fix #7953: (httpclient-jdk) bodyless requests now preserve the requested HTTP method instead of silently defaulting to `GET`. `JdkHttpClientImpl.requestBuilder` only called `HttpRequest.Builder.method(...)` inside the `body != null` branch, so a bodyless `DELETE`/`POST`/`PUT`/`PATCH` (such as `client.raw(uri, "DELETE", null)`) was sent as `GET` on the JDK backend; the method is now set with `BodyPublishers.noBody()` when there is no body, matching the OkHttp, Jetty and Vert.x backends
* Fix #7435: (kubernetes-client) A `SharedIndexInformer`'s periodic resync no longer stops permanently and silently when a single resync cycle throws. `DefaultSharedIndexInformer.scheduleResync` runs the resync through `Utils.scheduleAtFixedRate`, whose self-rescheduling chain re-arms the next cycle only when the previous one completes normally; an uncaught exception completed the (unobserved) `resyncFuture` exceptionally and the resync was never scheduled again, with no log, while the independent watch kept `isWatching()` reporting `true` (a restart was required to recover). The resync command now catches and `WARN`-logs the failure so the schedule fires again at the next interval
* Fix #7933: (kubernetes-client-api) Deterministic TLS trust failures (untrusted cert, expired cert, hostname mismatch) are now classified as terminal and fail fast instead of being retried by the shared `StandardHttpClient.shouldRetry` backoff loop (~19 s drain). The classifier walks both `getCause()` and `getSuppressed()` trees for `CertificateException`, `CertPathValidatorException`, `CertPathBuilderException`, and `SSLPeerUnverifiedException`. Affects all five HTTP client modules (jdk, jetty, okhttp, vertx-4, vertx-5) on both the HTTP request and WebSocket connect paths
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
Expand Down Expand Up @@ -291,15 +292,37 @@ static boolean isHostMatchedByNoProxy(String host, String[] noProxies) throws Ma
if (new IpAddressMatcher(noProxyIpOrSubnet.get()).matches(host)) {
return true;
}
} else {
if (host.endsWith(noProxy)) {
return true;
}
} else if (isHostnameMatchedByNoProxy(host, noProxy)) {
return true;
}
}
return false;
}

private static boolean isHostnameMatchedByNoProxy(String host, String noProxy) {
if (host == null || noProxy == null || noProxy.isEmpty()) {
return false;
}
String normalizedHost = normalizeHostnameForNoProxy(host);
String normalizedNoProxy = normalizeHostnameForNoProxy(noProxy);
if (normalizedNoProxy.startsWith(".")) {
normalizedNoProxy = normalizedNoProxy.substring(1);
}
if (normalizedNoProxy.isEmpty()) {
return false;
}
return normalizedHost.equals(normalizedNoProxy)
|| normalizedHost.endsWith("." + normalizedNoProxy);
}

private static String normalizeHostnameForNoProxy(String hostname) {
String normalizedHostname = hostname.toLowerCase(Locale.ROOT);
if (normalizedHostname.endsWith(".")) {
return normalizedHostname.substring(0, normalizedHostname.length() - 1);
}
return normalizedHostname;
}

private static Optional<String> extractIpAddressOrSubnet(String ipAddressOrSubnet) {
final Matcher ipMatcher = IP_PATTERN.matcher(ipAddressOrSubnet);
if (ipMatcher.matches()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,22 @@ void testConfigureProxyAuth() throws Exception {
Mockito.verify(builder).proxyAuthorization("Basic dXNlcjpwYXNzd29yZA==");
}

@Test
void testConfigureProxyDoesNotBypassLookalikeNoProxySuffix() throws Exception {
Config config = new ConfigBuilder()
.withMasterUrl("https://evilcorp.example.")
.withHttpsProxy("http://proxy.internal:8080")
.withNoProxy("corp.example.")
.build();
Builder builder = Mockito.mock(HttpClient.Builder.class, Mockito.RETURNS_SELF);

HttpClientUtils.configureProxy(config, builder);

Mockito.verify(builder).proxyAddress(new InetSocketAddress("proxy.internal", 8080));
Mockito.verify(builder).proxyType(HttpClient.ProxyType.HTTP);
Mockito.verify(builder, Mockito.never()).proxyType(HttpClient.ProxyType.DIRECT);
}

@Test
void testApplyCommonConfigurationWithTlsServerName() {
// Given
Expand Down Expand Up @@ -211,7 +227,10 @@ Stream<Arguments> masterHostnameDoesMatchNoProxyInput() {
return Stream.of(
arguments("192.168.1.100", new String[] { "192.168.1.0/24" }),
arguments("master.example.com", new String[] { "master.example.com" }),
arguments("master.example.com.", new String[] { "master.example.com" }),
arguments("master.example.com", new String[] { "master.example.com." }),
arguments("master.example.com", new String[] { ".example.com" }),
arguments("master.example.com.", new String[] { ".example.com." }),
arguments("master.example.com",
new String[] { "circleci-internal-outer-build-agent", "one.com", "other.com", ".com" }),
arguments("192.168.1.110", new String[] { "192.168.1.110" }),
Expand Down Expand Up @@ -239,6 +258,11 @@ Stream<Arguments> masterHostnameDoesNotMatchNoProxyInput() {
arguments("master.example.com", new String[0]),
arguments("master.example.com", new String[] { "master1.example.com" }),
arguments("master.example.com", new String[] { ".example1.com" }),
arguments("evilcorp.example", new String[] { "corp.example" }),
arguments("evilcorp.example.", new String[] { "corp.example" }),
arguments("evilcorp.example", new String[] { "corp.example." }),
arguments("evilcorp.example.", new String[] { ".corp.example." }),
arguments("master.example.com", new String[] { "." }),
arguments("master.example.com", new String[] { "circleci-internal-outer-build-agent", }),
arguments("master.example.com", new String[] { "one.com", "other.com", "master.example.", ".co" }),
arguments("192.168.1.110", new String[] { "192.168.1.111" }),
Expand Down
Loading