From ee40cf07c97958217fc656b0d998122f760e4e6a Mon Sep 17 00:00:00 2001 From: Steve Hawkins Date: Tue, 17 Sep 2024 20:53:12 -0400 Subject: [PATCH] fix: allowing for the usage of authenticated http proxies for https closes: #6350 Signed-off-by: Steve Hawkins --- CHANGELOG.md | 1 + .../client/jetty/JettyHttpClientBuilder.java | 18 +++- .../jetty/JettyHttpClientProxyHttpsTest.java | 29 +++++ .../okhttp/OkHttpClientBuilderImpl.java | 6 +- .../okhttp/OkHttpClientProxyHttpsTest.java | 43 ++++++++ .../client/okhttp/OkHttpClientProxyTest.java | 5 - .../client/vertx/VertxHttpClientBuilder.java | 3 +- .../vertx/VertxHttpClientProxyHttpsTest.java | 27 +++++ .../mockwebserver/DefaultMockServer.java | 22 +++- .../mockwebserver/dsl/HttpMethodable.java | 1 + .../fabric8/mockwebserver/dsl/Replyable.java | 8 +- .../internal/MockDispatcher.java | 7 +- .../internal/MockServerExpectationImpl.java | 84 ++++++++------- .../internal/SimpleResponse.java | 7 +- .../mockwebserver/utils/ResponseProvider.java | 5 + .../utils/ResponseProviders.java | 6 +- .../kubernetes/client/http/HttpClient.java | 2 +- .../http/StandardHttpClientBuilder.java | 17 +-- .../client/utils/HttpClientUtils.java | 18 +++- .../client/KubernetesClientBuilderTest.java | 4 +- .../AbstractHttpClientProxyHttpsTest.java | 100 ++++++++++++++++++ .../http/AbstractHttpClientProxyTest.java | 29 ++++- .../client/utils/HttpClientUtilsTest.java | 2 +- 23 files changed, 370 insertions(+), 74 deletions(-) create mode 100644 httpclient-jetty/src/test/java/io/fabric8/kubernetes/client/jetty/JettyHttpClientProxyHttpsTest.java create mode 100644 httpclient-okhttp/src/test/java/io/fabric8/kubernetes/client/okhttp/OkHttpClientProxyHttpsTest.java create mode 100644 httpclient-vertx/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientProxyHttpsTest.java create mode 100644 kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/AbstractHttpClientProxyHttpsTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 8174d2a804a..aec21323b5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * Fix #6247: Support for proxy authentication from proxy URL user info * Fix #6281: use GitHub binary repo for Kube API Tests * Fix #6282: Allow annotated types with Pattern, Min, and Max with Lists and Maps and CRD generation +* Fix #6350: Allowing authenticated http proxy usage with Jetty, vertx, and JDK for https endpoints * Fix #5480: Move `io.fabric8:zjsonpatch` to KubernetesClient project #### Dependency Upgrade diff --git a/httpclient-jetty/src/main/java/io/fabric8/kubernetes/client/jetty/JettyHttpClientBuilder.java b/httpclient-jetty/src/main/java/io/fabric8/kubernetes/client/jetty/JettyHttpClientBuilder.java index 0935ab8a234..bf97e2862c1 100644 --- a/httpclient-jetty/src/main/java/io/fabric8/kubernetes/client/jetty/JettyHttpClientBuilder.java +++ b/httpclient-jetty/src/main/java/io/fabric8/kubernetes/client/jetty/JettyHttpClientBuilder.java @@ -24,15 +24,20 @@ import org.eclipse.jetty.client.HttpProxy; import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.Socks4Proxy; +import org.eclipse.jetty.client.Socks5Proxy; +import org.eclipse.jetty.client.api.Authentication; import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; import org.eclipse.jetty.client.http.HttpClientConnectionFactory; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; +import org.eclipse.jetty.client.util.BasicAuthentication; import org.eclipse.jetty.http2.client.HTTP2Client; import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.websocket.client.WebSocketClient; +import java.net.URI; +import java.net.URISyntaxException; import java.time.Duration; import java.util.Optional; import java.util.stream.Stream; @@ -91,11 +96,20 @@ public JettyHttpClient build() { case SOCKS4: sharedHttpClient.getProxyConfiguration().addProxy(new Socks4Proxy(address, false)); break; + case SOCKS5: + sharedHttpClient.getProxyConfiguration().addProxy(new Socks5Proxy(address, false)); + break; default: throw new KubernetesClientException("Unsupported proxy type"); } - sharedHttpClient.getProxyConfiguration().addProxy(new HttpProxy(address, false)); - addProxyAuthInterceptor(); + URI proxyUri; + try { + proxyUri = new URI("http://" + proxyAddress.getHostString() + ":" + proxyAddress.getPort()); + } catch (URISyntaxException e) { + throw KubernetesClientException.launderThrowable(e); + } + sharedHttpClient.getAuthenticationStore() + .addAuthentication(new BasicAuthentication(proxyUri, Authentication.ANY_REALM, proxyUsername, proxyPassword)); } clientFactory.additionalConfig(sharedHttpClient, sharedWebSocketClient); return new JettyHttpClient(this, sharedHttpClient, sharedWebSocketClient); diff --git a/httpclient-jetty/src/test/java/io/fabric8/kubernetes/client/jetty/JettyHttpClientProxyHttpsTest.java b/httpclient-jetty/src/test/java/io/fabric8/kubernetes/client/jetty/JettyHttpClientProxyHttpsTest.java new file mode 100644 index 00000000000..af4a96f5e0e --- /dev/null +++ b/httpclient-jetty/src/test/java/io/fabric8/kubernetes/client/jetty/JettyHttpClientProxyHttpsTest.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * 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.fabric8.kubernetes.client.jetty; + +import io.fabric8.kubernetes.client.http.AbstractHttpClientProxyHttpsTest; +import io.fabric8.kubernetes.client.http.HttpClient; +import okhttp3.mockwebserver.SocketPolicy; + +@SuppressWarnings("java:S2187") +public class JettyHttpClientProxyHttpsTest extends AbstractHttpClientProxyHttpsTest { + @Override + protected HttpClient.Factory getHttpClientFactory() { + server.setDefaultSocketPolicy(SocketPolicy.KEEP_OPEN); // we need a challenge before switching to ssl + return new JettyHttpClientFactory(); + } +} diff --git a/httpclient-okhttp/src/main/java/io/fabric8/kubernetes/client/okhttp/OkHttpClientBuilderImpl.java b/httpclient-okhttp/src/main/java/io/fabric8/kubernetes/client/okhttp/OkHttpClientBuilderImpl.java index 43dbc0e22e2..547dd798176 100644 --- a/httpclient-okhttp/src/main/java/io/fabric8/kubernetes/client/okhttp/OkHttpClientBuilderImpl.java +++ b/httpclient-okhttp/src/main/java/io/fabric8/kubernetes/client/okhttp/OkHttpClientBuilderImpl.java @@ -19,6 +19,7 @@ import io.fabric8.kubernetes.client.http.HttpClient.ProxyType; import io.fabric8.kubernetes.client.http.StandardHttpClientBuilder; import io.fabric8.kubernetes.client.http.StandardHttpHeaders; +import io.fabric8.kubernetes.client.utils.HttpClientUtils; import okhttp3.Authenticator; import okhttp3.ConnectionSpec; import okhttp3.OkHttpClient; @@ -74,10 +75,11 @@ public OkHttpClientImpl initialBuild(okhttp3.OkHttpClient.Builder builder) { builder.proxy(Proxy.NO_PROXY); } else if (proxyAddress != null) { builder.proxy(new Proxy(convertProxyType(), proxyAddress)); - if (proxyAuthorization != null) { + if (proxyUsername != null && proxyPassword != null) { + String auth = HttpClientUtils.basicCredentials(proxyUsername, proxyPassword); builder.proxyAuthenticator( (route, response) -> response.request().newBuilder() - .header(StandardHttpHeaders.PROXY_AUTHORIZATION, proxyAuthorization).build()); + .header(StandardHttpHeaders.PROXY_AUTHORIZATION, auth).build()); } } if (tlsVersions != null) { diff --git a/httpclient-okhttp/src/test/java/io/fabric8/kubernetes/client/okhttp/OkHttpClientProxyHttpsTest.java b/httpclient-okhttp/src/test/java/io/fabric8/kubernetes/client/okhttp/OkHttpClientProxyHttpsTest.java new file mode 100644 index 00000000000..158c595e8e5 --- /dev/null +++ b/httpclient-okhttp/src/test/java/io/fabric8/kubernetes/client/okhttp/OkHttpClientProxyHttpsTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * 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.fabric8.kubernetes.client.okhttp; + +import io.fabric8.kubernetes.client.http.AbstractHttpClientProxyHttpsTest; +import io.fabric8.kubernetes.client.http.HttpClient; +import okhttp3.OkHttpClient.Builder; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +@SuppressWarnings("java:S2187") +public class OkHttpClientProxyHttpsTest extends AbstractHttpClientProxyHttpsTest { + @Override + protected HttpClient.Factory getHttpClientFactory() { + return new OkHttpClientFactory() { + @Override + protected Builder newOkHttpClientBuilder() { + Builder builder = super.newOkHttpClientBuilder(); + builder.hostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }); + return builder; + } + }; + } +} diff --git a/httpclient-okhttp/src/test/java/io/fabric8/kubernetes/client/okhttp/OkHttpClientProxyTest.java b/httpclient-okhttp/src/test/java/io/fabric8/kubernetes/client/okhttp/OkHttpClientProxyTest.java index 7b76a1e7e81..70e8007fabe 100644 --- a/httpclient-okhttp/src/test/java/io/fabric8/kubernetes/client/okhttp/OkHttpClientProxyTest.java +++ b/httpclient-okhttp/src/test/java/io/fabric8/kubernetes/client/okhttp/OkHttpClientProxyTest.java @@ -25,9 +25,4 @@ protected HttpClient.Factory getHttpClientFactory() { return new OkHttpClientFactory(); } - @Override - protected void proxyConfigurationAddsRequiredHeaders() { - // NO-OP - // OkHttp uses a response intercept to add the auth proxy headers in case the original response failed - } } diff --git a/httpclient-vertx/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientBuilder.java b/httpclient-vertx/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientBuilder.java index 7ff55ec8d3a..9221a1b1d19 100644 --- a/httpclient-vertx/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientBuilder.java +++ b/httpclient-vertx/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientBuilder.java @@ -77,9 +77,10 @@ public VertxHttpClient build() { ProxyOptions proxyOptions = new ProxyOptions() .setHost(this.proxyAddress.getHostName()) .setPort(this.proxyAddress.getPort()) + .setUsername(this.proxyUsername) + .setPassword(this.proxyPassword) .setType(convertProxyType()); options.setProxyOptions(proxyOptions); - addProxyAuthInterceptor(); } final String[] protocols; diff --git a/httpclient-vertx/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientProxyHttpsTest.java b/httpclient-vertx/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientProxyHttpsTest.java new file mode 100644 index 00000000000..c93b80b9789 --- /dev/null +++ b/httpclient-vertx/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientProxyHttpsTest.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * 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.fabric8.kubernetes.client.vertx; + +import io.fabric8.kubernetes.client.http.AbstractHttpClientProxyHttpsTest; +import io.fabric8.kubernetes.client.http.HttpClient; + +@SuppressWarnings("java:S2187") +public class VertxHttpClientProxyHttpsTest extends AbstractHttpClientProxyHttpsTest { + @Override + protected HttpClient.Factory getHttpClientFactory() { + return new VertxHttpClientFactory(); + } +} diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/DefaultMockServer.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/DefaultMockServer.java index cee4d8f0679..81520e41cc7 100644 --- a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/DefaultMockServer.java +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/DefaultMockServer.java @@ -22,6 +22,7 @@ import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; +import okhttp3.mockwebserver.SocketPolicy; import java.io.IOException; import java.net.InetAddress; @@ -38,6 +39,7 @@ public class DefaultMockServer implements MockServer { private final Context context; private final boolean useHttps; + private boolean tunnelProxy; private final MockWebServer server; private final Map> responses; private final AtomicInteger lastRequestCount; @@ -45,6 +47,7 @@ public class DefaultMockServer implements MockServer { private final AtomicBoolean initialized = new AtomicBoolean(); private final AtomicBoolean shutdown = new AtomicBoolean(); + private Dispatcher dispatcher; public DefaultMockServer() { this(new Context(), new MockWebServer(), new HashMap<>(), false); @@ -72,17 +75,34 @@ public DefaultMockServer(Context context, MockWebServer server, Map(); this.lastRequestCount = new AtomicInteger(0); this.server.setDispatcher(dispatcher); + this.dispatcher = dispatcher; + } + + public void setHttpsTunnelProxy(boolean tunnelProxy) { + this.tunnelProxy = tunnelProxy; } private void startInternal() { if (initialized.compareAndSet(false, true)) { if (useHttps) { - server.useHttps(MockSSLContextFactory.create().getSocketFactory(), false); + server.useHttps(MockSSLContextFactory.create().getSocketFactory(), tunnelProxy); + if (tunnelProxy) { + setDefaultSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END); + } } onStart(); } } + /** + * Only works when using the default {@link MockDispatcher} type + * + * @param socketPolicy to be returned when peeking at the dispatcher + */ + public void setDefaultSocketPolicy(SocketPolicy socketPolicy) { + ((MockDispatcher) dispatcher).setDefaultSocketPolicy(socketPolicy); + } + private void shutdownInternal() { if (shutdown.compareAndSet(false, true)) { onShutdown(); diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/dsl/HttpMethodable.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/dsl/HttpMethodable.java index 4565d647ffc..46c0f72f329 100644 --- a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/dsl/HttpMethodable.java +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/dsl/HttpMethodable.java @@ -29,4 +29,5 @@ public interface HttpMethodable { T patch(); + T connect(); } diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/dsl/Replyable.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/dsl/Replyable.java index aa20f43aafd..cbe9d827035 100644 --- a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/dsl/Replyable.java +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/dsl/Replyable.java @@ -22,12 +22,12 @@ public interface Replyable { - T andReply(int statusCode, BodyProvider contentSupplier); + T andReply(int statusCode, BodyProvider contentSupplier); - T andReply(ResponseProvider contentSupplier); + T andReply(ResponseProvider contentSupplier); - T andReplyChunked(int statusCode, BodyProvider> content); + T andReplyChunked(int statusCode, BodyProvider> content); - T andReplyChunked(ResponseProvider> content); + T andReplyChunked(ResponseProvider> content); } diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/internal/MockDispatcher.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/internal/MockDispatcher.java index 32b818dd14c..0e3067a6e66 100644 --- a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/internal/MockDispatcher.java +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/internal/MockDispatcher.java @@ -32,14 +32,19 @@ public class MockDispatcher extends Dispatcher { private final Map> responses; private final Collection webSocketSessions = new ConcurrentLinkedQueue<>(); + private SocketPolicy defaultSocketPolicy = SocketPolicy.EXPECT_CONTINUE; public MockDispatcher(Map> responses) { this.responses = responses; } + public void setDefaultSocketPolicy(SocketPolicy defaultSocketPolicy) { + this.defaultSocketPolicy = defaultSocketPolicy; + } + @Override public MockResponse peek() { - return new MockResponse().setSocketPolicy(SocketPolicy.EXPECT_CONTINUE); + return new MockResponse().setSocketPolicy(defaultSocketPolicy); } @Override diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/internal/MockServerExpectationImpl.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/internal/MockServerExpectationImpl.java index ec01cebe62e..df7e82e04b2 100644 --- a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/internal/MockServerExpectationImpl.java +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/internal/MockServerExpectationImpl.java @@ -32,6 +32,7 @@ import io.fabric8.mockwebserver.utils.ResponseProviders; import okhttp3.Headers; import okhttp3.mockwebserver.RecordedRequest; +import okhttp3.mockwebserver.SocketPolicy; import java.util.ArrayDeque; import java.util.ArrayList; @@ -118,6 +119,13 @@ public DelayPathable>> responses); } + @Override + public DelayPathable>> connect() { + return new MockServerExpectationImpl(context, HttpMethod.CONNECT, path, bodyProvider, chunksProvider, delay, delayUnit, + times, + responses); + } + @Override public ReturnOrWebsocketable> withPath(String path) { return new MockServerExpectationImpl(context, method, path, bodyProvider, chunksProvider, delay, delayUnit, times, @@ -131,12 +139,12 @@ public TimesOnceableOrHttpHeaderable andReturn(int statusCode, Object cont } @Override - public TimesOnceableOrHttpHeaderable andReply(int statusCode, BodyProvider content) { + public TimesOnceableOrHttpHeaderable andReply(int statusCode, BodyProvider content) { return andReply(ResponseProviders.of(statusCode, content)); } @Override - public TimesOnceableOrHttpHeaderable andReply(ResponseProvider content) { + public TimesOnceableOrHttpHeaderable andReply(ResponseProvider content) { return new MockServerExpectationImpl(context, method, path, toString(content), chunksProvider, delay, delayUnit, times, responses); } @@ -148,12 +156,12 @@ public TimesOnceableOrHttpHeaderable andReturnChunked(int statusCode, Obje } @Override - public TimesOnceableOrHttpHeaderable andReplyChunked(int statusCode, BodyProvider> contents) { + public TimesOnceableOrHttpHeaderable andReplyChunked(int statusCode, BodyProvider> contents) { return andReplyChunked(ResponseProviders.of(statusCode, contents)); } @Override - public TimesOnceableOrHttpHeaderable andReplyChunked(ResponseProvider> contents) { + public TimesOnceableOrHttpHeaderable andReplyChunked(ResponseProvider> contents) { return new MockServerExpectationImpl(context, method, path, bodyProvider, listToString(contents), delay, delayUnit, times, responses); } @@ -229,57 +237,27 @@ private ServerResponse createResponse(boolean repeatable, long delay, TimeUnit d } } - private ResponseProvider toString(final ResponseProvider provider) { - return new ResponseProvider() { + private ResponseProvider toString(final ResponseProvider provider) { + return new ResponseProviderWrapper(provider) { @Override public String getBody(RecordedRequest request) { Object object = provider.getBody(request); return MockServerExpectationImpl.this.toString(object); } - - @Override - public int getStatusCode(RecordedRequest request) { - return provider.getStatusCode(request); - } - - @Override - public Headers getHeaders() { - return provider.getHeaders(); - } - - @Override - public void setHeaders(Headers headers) { - provider.setHeaders(headers); - } }; } - private ResponseProvider> listToString(final ResponseProvider> provider) { - return new ResponseProvider>() { + private ResponseProvider> listToString(final ResponseProvider> provider) { + return new ResponseProviderWrapper>(provider) { @Override public List getBody(RecordedRequest request) { - List objects = provider.getBody(request); + List objects = provider.getBody(request); List strings = new ArrayList<>(objects.size()); for (Object o : objects) { strings.add(MockServerExpectationImpl.this.toString(o)); } return strings; } - - @Override - public int getStatusCode(RecordedRequest request) { - return provider.getStatusCode(request); - } - - @Override - public Headers getHeaders() { - return provider.getHeaders(); - } - - @Override - public void setHeaders(Headers headers) { - provider.setHeaders(headers); - } }; } @@ -301,6 +279,34 @@ private List toString(Object[] object) { .collect(Collectors.toList()); } + private static abstract class ResponseProviderWrapper implements ResponseProvider { + private final ResponseProvider provider; + + private ResponseProviderWrapper(ResponseProvider provider) { + this.provider = provider; + } + + @Override + public int getStatusCode(RecordedRequest request) { + return provider.getStatusCode(request); + } + + @Override + public Headers getHeaders() { + return provider.getHeaders(); + } + + @Override + public void setHeaders(Headers headers) { + provider.setHeaders(headers); + } + + @Override + public SocketPolicy getSocketPolicy(RecordedRequest request) { + return provider.getSocketPolicy(request); + } + } + private static final class WebSocketSessionConverter implements Function> { diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/internal/SimpleResponse.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/internal/SimpleResponse.java index 637f179c892..b6740a748b2 100644 --- a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/internal/SimpleResponse.java +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/internal/SimpleResponse.java @@ -66,6 +66,7 @@ public MockResponse toMockResponse(RecordedRequest request) { MockResponse mockResponse = new MockResponse(); mockResponse.setHeaders(bodyProvider.getHeaders()); mockResponse.setResponseCode(bodyProvider.getStatusCode(request)); + mockResponse.setSocketPolicy(bodyProvider.getSocketPolicy(request)); if (webSocketSession != null) { mockResponse.withWebSocketUpgrade(webSocketSession); @@ -99,10 +100,12 @@ public boolean isRepeatable() { @Override public boolean equals(Object o) { - if (this == o) + if (this == o) { return true; - if (o == null || getClass() != o.getClass()) + } + if (o == null || getClass() != o.getClass()) { return false; + } SimpleResponse that = (SimpleResponse) o; return repeatable == that.repeatable && responseDelay == that.responseDelay && Objects.equals(bodyProvider, that.bodyProvider) && Objects.equals(webSocketSession, that.webSocketSession) diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/utils/ResponseProvider.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/utils/ResponseProvider.java index 53d591c44f0..b187bdb5992 100644 --- a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/utils/ResponseProvider.java +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/utils/ResponseProvider.java @@ -17,6 +17,7 @@ import okhttp3.Headers; import okhttp3.mockwebserver.RecordedRequest; +import okhttp3.mockwebserver.SocketPolicy; /** * A class that allows returning a response given a certain request. @@ -29,4 +30,8 @@ public interface ResponseProvider extends BodyProvider { void setHeaders(Headers headers); + default SocketPolicy getSocketPolicy(RecordedRequest request) { + return SocketPolicy.KEEP_OPEN; + } + } diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/utils/ResponseProviders.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/utils/ResponseProviders.java index cf297aafffc..171ae2f2373 100644 --- a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/utils/ResponseProviders.java +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/utils/ResponseProviders.java @@ -123,10 +123,12 @@ public void setHeaders(Headers headers) { @Override public boolean equals(Object o) { - if (this == o) + if (this == o) { return true; - if (o == null || getClass() != o.getClass()) + } + if (o == null || getClass() != o.getClass()) { return false; + } FixedResponseProvider that = (FixedResponseProvider) o; diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/HttpClient.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/HttpClient.java index fd67e415020..6bb413e949d 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/HttpClient.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/HttpClient.java @@ -115,7 +115,7 @@ interface Builder extends DerivedClientBuilder { Builder proxyAddress(InetSocketAddress proxyAddress); - Builder proxyAuthorization(String credentials); + Builder proxyBasicCredentials(String username, String password); Builder tlsVersions(TlsVersion... tlsVersions); diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/StandardHttpClientBuilder.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/StandardHttpClientBuilder.java index f4d5caa5869..a005f506a40 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/StandardHttpClientBuilder.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/StandardHttpClientBuilder.java @@ -18,6 +18,7 @@ import io.fabric8.kubernetes.client.http.HttpClient.DerivedClientBuilder; import io.fabric8.kubernetes.client.http.HttpClient.ProxyType; import io.fabric8.kubernetes.client.internal.SSLUtils; +import io.fabric8.kubernetes.client.utils.HttpClientUtils; import lombok.Getter; import org.slf4j.LoggerFactory; @@ -38,7 +39,8 @@ public abstract class StandardHttpClientBuilder interceptors = new LinkedHashMap<>(); protected Duration connectTimeout; protected SSLContext sslContext; - protected String proxyAuthorization; + protected String proxyUsername; + protected String proxyPassword; protected InetSocketAddress proxyAddress; protected boolean followRedirects; protected boolean preferHttp11; @@ -102,8 +104,9 @@ public T proxyAddress(InetSocketAddress proxyAddress) { } @Override - public T proxyAuthorization(String credentials) { - this.proxyAuthorization = credentials; + public T proxyBasicCredentials(String username, String password) { + this.proxyUsername = username; + this.proxyPassword = password; return (T) this; } @@ -148,7 +151,8 @@ public T copy(C client) { copy.keyManagers = this.keyManagers; copy.interceptors = new LinkedHashMap<>(this.interceptors); copy.proxyAddress = this.proxyAddress; - copy.proxyAuthorization = this.proxyAuthorization; + copy.proxyUsername = this.proxyUsername; + copy.proxyPassword = this.proxyPassword; copy.tlsVersions = this.tlsVersions; copy.preferHttp11 = this.preferHttp11; copy.followRedirects = this.followRedirects; @@ -160,12 +164,13 @@ public T copy(C client) { } protected void addProxyAuthInterceptor() { - if (proxyAuthorization != null) { + if (proxyUsername != null && proxyPassword != null) { + String auth = HttpClientUtils.basicCredentials(proxyUsername, proxyPassword); this.interceptors.put("PROXY-AUTH", new Interceptor() { @Override public void before(BasicBuilder builder, HttpRequest httpRequest, RequestTags tags) { - builder.setHeader(StandardHttpHeaders.PROXY_AUTHORIZATION, proxyAuthorization); + builder.setHeader(StandardHttpHeaders.PROXY_AUTHORIZATION, auth); } }); diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/HttpClientUtils.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/HttpClientUtils.java index d98d71d2ef8..277467831eb 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/HttpClientUtils.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/HttpClientUtils.java @@ -25,11 +25,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; @@ -229,12 +231,22 @@ static void configureProxy(Config config, HttpClient.Builder builder) builder.proxyAddress(new InetSocketAddress(proxyUri.getHost(), proxyUri.getPort())); if (config.getProxyUsername() != null) { - builder.proxyAuthorization(basicCredentials(config.getProxyUsername(), config.getProxyPassword())); + builder.proxyBasicCredentials(config.getProxyUsername(), config.getProxyPassword()); } - String userInfo = proxyUri.getUserInfo(); + String userInfo = proxyUri.getRawUserInfo(); if (userInfo != null) { - builder.proxyAuthorization(basicCredentials(userInfo)); + String[] parts = userInfo.split(":"); + if (parts.length == 2) { + try { + builder.proxyBasicCredentials(URLDecoder.decode(parts[0], StandardCharsets.UTF_8.name()), + URLDecoder.decode(parts[1], StandardCharsets.UTF_8.name())); + } catch (UnsupportedEncodingException e) { + throw KubernetesClientException.launderThrowable(e); + } + } else { + throw new KubernetesClientException("Proxy user info not in the form of username:password"); + } } builder.proxyType(toProxyType(proxyUri.getScheme())); diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/KubernetesClientBuilderTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/KubernetesClientBuilderTest.java index c64de23e5b0..728009c2102 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/KubernetesClientBuilderTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/KubernetesClientBuilderTest.java @@ -32,9 +32,9 @@ void testHttpClientConfiguration() { Factory mockFactory = Mockito.mock(HttpClient.Factory.class); HttpClient.Builder mockBuilder = Mockito.mock(HttpClient.Builder.class); Mockito.when(mockFactory.newBuilder(Mockito.any())).thenReturn(mockBuilder); - builder.withHttpClientFactory(mockFactory).withHttpClientBuilderConsumer(b -> b.proxyAuthorization("something")); + builder.withHttpClientFactory(mockFactory).withHttpClientBuilderConsumer(b -> b.proxyBasicCredentials("some", "thing")); builder.getHttpClient(); - Mockito.verify(mockBuilder).proxyAuthorization("something"); + Mockito.verify(mockBuilder).proxyBasicCredentials("some", "thing"); } @Test diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/AbstractHttpClientProxyHttpsTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/AbstractHttpClientProxyHttpsTest.java new file mode 100644 index 00000000000..1e8f818543f --- /dev/null +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/AbstractHttpClientProxyHttpsTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * 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.fabric8.kubernetes.client.http; + +import io.fabric8.kubernetes.client.internal.SSLUtils; +import io.fabric8.mockwebserver.DefaultMockServer; +import io.fabric8.mockwebserver.utils.ResponseProvider; +import okhttp3.Headers; +import okhttp3.mockwebserver.RecordedRequest; +import okhttp3.mockwebserver.SocketPolicy; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertTrue; + +public abstract class AbstractHttpClientProxyHttpsTest { + + protected static DefaultMockServer server; + + @BeforeAll + static void beforeAll() { + server = new DefaultMockServer(true); + server.setHttpsTunnelProxy(true); + server.start(); + } + + @AfterAll + static void afterAll() { + server.shutdown(); + } + + protected abstract HttpClient.Factory getHttpClientFactory(); + + @Test + @DisplayName("Proxied HttpClient adds required headers to the request") + protected void proxyConfigurationAddsRequiredHeadersForHttps() throws Exception { + AtomicBoolean authenticated = new AtomicBoolean(); + server.expect().connect().withPath("/").andReply(new ResponseProvider() { + + @Override + public String getBody(RecordedRequest request) { + return ""; + } + + @Override + public void setHeaders(Headers headers) { + + } + + @Override + public int getStatusCode(RecordedRequest request) { + server.setDefaultSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END); // for jetty to upgrade after the challenge + boolean auth = request.getHeader(StandardHttpHeaders.PROXY_AUTHORIZATION) != null; + if (auth) { + authenticated.set(true); + } + return auth ? 200 : 407; + } + + @Override + public Headers getHeaders() { + return new Headers.Builder().add("Proxy-Authenticate", "Basic").build(); + } + + }).always(); + // Given + final HttpClient.Builder builder = getHttpClientFactory().newBuilder() + .sslContext(null, SSLUtils.trustManagers(null, null, true, null, null)) + .proxyAddress(new InetSocketAddress("localhost", server.getPort())) + .proxyBasicCredentials("auth", "cred"); + try (HttpClient client = builder.build()) { + // When + client.sendAsync(client.newHttpRequestBuilder() + .uri(String.format("https://0.0.0.0:%s/not-found", server.getPort() + 1)).build(), String.class) + .get(30, TimeUnit.SECONDS); + + // if it fails, then authorization was not set + assertTrue(authenticated.get()); + } + } +} diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/AbstractHttpClientProxyTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/AbstractHttpClientProxyTest.java index d977a097e4b..3fe1c82f782 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/AbstractHttpClientProxyTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/AbstractHttpClientProxyTest.java @@ -16,6 +16,8 @@ package io.fabric8.kubernetes.client.http; import io.fabric8.mockwebserver.DefaultMockServer; +import io.fabric8.mockwebserver.utils.ResponseProvider; +import okhttp3.Headers; import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -47,10 +49,33 @@ static void afterAll() { @Test @DisplayName("Proxied HttpClient adds required headers to the request") protected void proxyConfigurationAddsRequiredHeaders() throws Exception { + server.expect().get().withPath("/").andReply(new ResponseProvider() { + + @Override + public String getBody(RecordedRequest request) { + return ""; + } + + @Override + public void setHeaders(Headers headers) { + + } + + @Override + public int getStatusCode(RecordedRequest request) { + return request.getHeader(StandardHttpHeaders.PROXY_AUTHORIZATION) != null ? 200 : 407; + } + + @Override + public Headers getHeaders() { + return new Headers.Builder().add("Proxy-Authenticate", "Basic").build(); + } + + }).always(); // Given final HttpClient.Builder builder = getHttpClientFactory().newBuilder() .proxyAddress(new InetSocketAddress("localhost", server.getPort())) - .proxyAuthorization("auth:cred"); + .proxyBasicCredentials("auth", "cred"); try (HttpClient client = builder.build()) { // When client.sendAsync(client.newHttpRequestBuilder() @@ -60,7 +85,7 @@ protected void proxyConfigurationAddsRequiredHeaders() throws Exception { assertThat(server.getLastRequest()) .extracting(RecordedRequest::getHeaders) .returns("0.0.0.0:" + server.getPort(), h -> h.get("Host")) - .returns("auth:cred", h -> h.get("Proxy-Authorization")); + .returns("Basic YXV0aDpjcmVk", h -> h.get("Proxy-Authorization")); } } } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/HttpClientUtilsTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/HttpClientUtilsTest.java index 208c8c39267..104031320d9 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/HttpClientUtilsTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/HttpClientUtilsTest.java @@ -99,7 +99,7 @@ void testConfigureProxyAuth() throws Exception { HttpClientUtils.configureProxy(config, builder); Mockito.verify(builder).proxyType(HttpClient.ProxyType.HTTP); - Mockito.verify(builder).proxyAuthorization("Basic dXNlcjpwYXNzd29yZA=="); + Mockito.verify(builder).proxyBasicCredentials("user", "password"); } @Test