Skip to content

Commit

Permalink
fix: allowing the usage of authenticated http proxies for https (6371)
Browse files Browse the repository at this point in the history
fix: allowing the usage of authenticated http proxies for https
---
review: remove duplicate proxy init in Jetty

Co-authored-by: Steven Hawkins <shawkins@redhat.com>
  • Loading branch information
manusa and shawkins committed Sep 25, 2024
1 parent 71354a8 commit b3b22cb
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,26 @@
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;

import static io.fabric8.kubernetes.client.utils.HttpClientUtils.decodeBasicCredentials;

public class JettyHttpClientBuilder
extends StandardHttpClientBuilder<JettyHttpClient, JettyHttpClientFactory, JettyHttpClientBuilder> {

Expand Down Expand Up @@ -91,11 +98,25 @@ 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();
final String[] userPassword = decodeBasicCredentials(this.proxyAuthorization);
if (userPassword != null) {
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, userPassword[0], userPassword[1]));
} else {
addProxyAuthInterceptor();
}
}
clientFactory.additionalConfig(sharedHttpClient, sharedWebSocketClient);
return new JettyHttpClient(this, sharedHttpClient, sharedWebSocketClient);
Expand Down
Original file line number Diff line number Diff line change
@@ -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.jetty;

import io.fabric8.kubernetes.client.http.AbstractHttpClientProxyHttpsTest;
import io.fabric8.kubernetes.client.http.HttpClient;

@SuppressWarnings("java:S2187")
public class JettyHttpClientProxyHttpsTest extends AbstractHttpClientProxyHttpsTest {
@Override
protected HttpClient.Factory getHttpClientFactory() {
return new JettyHttpClientFactory();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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;

@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((hostname, session) -> true);
return builder;
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ protected HttpClient.Factory getHttpClientFactory() {
}

@Override
protected void proxyConfigurationAddsRequiredHeaders() {
// NO-OP
protected void proxyConfigurationOtherAuthAddsRequiredHeaders() throws Exception {
// OkHttp uses a response intercept to add the auth proxy headers in case the original response failed
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;

import static io.fabric8.kubernetes.client.utils.HttpClientUtils.decodeBasicCredentials;

public class VertxHttpClientBuilder<F extends HttpClient.Factory>
extends StandardHttpClientBuilder<VertxHttpClient<F>, F, VertxHttpClientBuilder<F>> {

Expand Down Expand Up @@ -74,12 +76,18 @@ public VertxHttpClient<F> build() {
}

if (this.proxyType != HttpClient.ProxyType.DIRECT && this.proxyAddress != null) {
ProxyOptions proxyOptions = new ProxyOptions()
final ProxyOptions proxyOptions = new ProxyOptions()
.setHost(this.proxyAddress.getHostName())
.setPort(this.proxyAddress.getPort())
.setType(convertProxyType());
final String[] userPassword = decodeBasicCredentials(this.proxyAuthorization);
if (userPassword != null) {
proxyOptions.setUsername(userPassword[0]);
proxyOptions.setPassword(userPassword[1]);
} else {
addProxyAuthInterceptor();
}
options.setProxyOptions(proxyOptions);
addProxyAuthInterceptor();
}

final String[] protocols;
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,24 @@ public static String basicCredentials(String username, String password) {
return basicCredentials(username + ":" + password);
}

public static String[] decodeBasicCredentials(String basicCredentials) {
if (basicCredentials == null) {
return null;
}
try {
final String encodedCredentials = basicCredentials.replaceFirst("Basic ", "");
final String decodedProxyAuthorization = new String(Base64.getDecoder().decode(encodedCredentials),
StandardCharsets.UTF_8);
final String[] userPassword = decodedProxyAuthorization.split(":");
if (userPassword.length == 2) {
return userPassword;
}
} catch (Exception ignored) {
// Ignored
}
return null;
}

/**
* @deprecated you should not need to call this method directly. Please create your own HttpClient.Factory
* should you need to customize your clients.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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.Context;
import io.fabric8.mockwebserver.DefaultMockServer;
import io.fabric8.mockwebserver.ServerRequest;
import io.fabric8.mockwebserver.ServerResponse;
import io.fabric8.mockwebserver.dsl.HttpMethod;
import io.fabric8.mockwebserver.internal.MockDispatcher;
import io.fabric8.mockwebserver.internal.MockSSLContextFactory;
import io.fabric8.mockwebserver.internal.SimpleRequest;
import io.fabric8.mockwebserver.internal.SimpleResponse;
import io.fabric8.mockwebserver.utils.ResponseProvider;
import okhttp3.Headers;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
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.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import static io.fabric8.kubernetes.client.utils.HttpClientUtils.basicCredentials;
import static org.assertj.core.api.Assertions.assertThat;

public abstract class AbstractHttpClientProxyHttpsTest {

private static SocketPolicy defaultResponseSocketPolicy;
private static Map<ServerRequest, Queue<ServerResponse>> responses;
private static DefaultMockServer server;

@BeforeAll
static void beforeAll() {
defaultResponseSocketPolicy = SocketPolicy.KEEP_OPEN;
responses = new HashMap<>();
final MockWebServer okHttpMockWebServer = new MockWebServer();
final MockDispatcher dispatcher = new MockDispatcher(responses) {
@Override
public MockResponse peek() {
return new MockResponse().setSocketPolicy(defaultResponseSocketPolicy);
}
};
server = new DefaultMockServer(new Context(), okHttpMockWebServer, responses, dispatcher, true);
server.start();
okHttpMockWebServer.useHttps(MockSSLContextFactory.create().getSocketFactory(), true);
}

@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 {
final AtomicReference<RecordedRequest> initialConnectRequest = new AtomicReference<>();
final ResponseProvider<String> bodyProvider = new ResponseProvider<String>() {

@Override
public String getBody(RecordedRequest request) {
return "";
}

@Override
public void setHeaders(Headers headers) {
}

@Override
public int getStatusCode(RecordedRequest request) {
defaultResponseSocketPolicy = SocketPolicy.UPGRADE_TO_SSL_AT_END; // for jetty to upgrade after the challenge
if (request.getHeader(StandardHttpHeaders.PROXY_AUTHORIZATION) != null) {
initialConnectRequest.compareAndSet(null, request);
return 200;
}
return 407;
}

@Override
public Headers getHeaders() {
return new Headers.Builder().add("Proxy-Authenticate", "Basic").build();
}

};
responses.computeIfAbsent(new SimpleRequest(HttpMethod.CONNECT, "/"), k -> new ArrayDeque<>())
.add(new SimpleResponse(true, bodyProvider, null, 0, TimeUnit.SECONDS));
// Given
final HttpClient.Builder builder = getHttpClientFactory().newBuilder()
.sslContext(null, SSLUtils.trustManagers(null, null, true, null, null))
.proxyAddress(new InetSocketAddress("localhost", server.getPort()))
.proxyAuthorization(basicCredentials("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
assertThat(initialConnectRequest)
.doesNotHaveNullValue()
.hasValueMatching(r -> r.getHeader("Proxy-Authorization").equals("Basic YXV0aDpjcmVk"));
}
}
}
Loading

0 comments on commit b3b22cb

Please sign in to comment.