Skip to content

Commit b3b22cb

Browse files
manusashawkins
andcommitted
fix: allowing the usage of authenticated http proxies for https (6371)
fix: allowing the usage of authenticated http proxies for https --- review: remove duplicate proxy init in Jetty Co-authored-by: Steven Hawkins <[email protected]>
1 parent 71354a8 commit b3b22cb

File tree

9 files changed

+319
-10
lines changed

9 files changed

+319
-10
lines changed

httpclient-jetty/src/main/java/io/fabric8/kubernetes/client/jetty/JettyHttpClientBuilder.java

+23-2
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,26 @@
2424
import org.eclipse.jetty.client.HttpProxy;
2525
import org.eclipse.jetty.client.Origin;
2626
import org.eclipse.jetty.client.Socks4Proxy;
27+
import org.eclipse.jetty.client.Socks5Proxy;
28+
import org.eclipse.jetty.client.api.Authentication;
2729
import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic;
2830
import org.eclipse.jetty.client.http.HttpClientConnectionFactory;
2931
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
32+
import org.eclipse.jetty.client.util.BasicAuthentication;
3033
import org.eclipse.jetty.http2.client.HTTP2Client;
3134
import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2;
3235
import org.eclipse.jetty.io.ClientConnector;
3336
import org.eclipse.jetty.util.ssl.SslContextFactory;
3437
import org.eclipse.jetty.websocket.client.WebSocketClient;
3538

39+
import java.net.URI;
40+
import java.net.URISyntaxException;
3641
import java.time.Duration;
3742
import java.util.Optional;
3843
import java.util.stream.Stream;
3944

45+
import static io.fabric8.kubernetes.client.utils.HttpClientUtils.decodeBasicCredentials;
46+
4047
public class JettyHttpClientBuilder
4148
extends StandardHttpClientBuilder<JettyHttpClient, JettyHttpClientFactory, JettyHttpClientBuilder> {
4249

@@ -91,11 +98,25 @@ public JettyHttpClient build() {
9198
case SOCKS4:
9299
sharedHttpClient.getProxyConfiguration().addProxy(new Socks4Proxy(address, false));
93100
break;
101+
case SOCKS5:
102+
sharedHttpClient.getProxyConfiguration().addProxy(new Socks5Proxy(address, false));
103+
break;
94104
default:
95105
throw new KubernetesClientException("Unsupported proxy type");
96106
}
97-
sharedHttpClient.getProxyConfiguration().addProxy(new HttpProxy(address, false));
98-
addProxyAuthInterceptor();
107+
final String[] userPassword = decodeBasicCredentials(this.proxyAuthorization);
108+
if (userPassword != null) {
109+
URI proxyUri;
110+
try {
111+
proxyUri = new URI("http://" + proxyAddress.getHostString() + ":" + proxyAddress.getPort());
112+
} catch (URISyntaxException e) {
113+
throw KubernetesClientException.launderThrowable(e);
114+
}
115+
sharedHttpClient.getAuthenticationStore()
116+
.addAuthentication(new BasicAuthentication(proxyUri, Authentication.ANY_REALM, userPassword[0], userPassword[1]));
117+
} else {
118+
addProxyAuthInterceptor();
119+
}
99120
}
100121
clientFactory.additionalConfig(sharedHttpClient, sharedWebSocketClient);
101122
return new JettyHttpClient(this, sharedHttpClient, sharedWebSocketClient);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (C) 2015 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.fabric8.kubernetes.client.jetty;
17+
18+
import io.fabric8.kubernetes.client.http.AbstractHttpClientProxyHttpsTest;
19+
import io.fabric8.kubernetes.client.http.HttpClient;
20+
21+
@SuppressWarnings("java:S2187")
22+
public class JettyHttpClientProxyHttpsTest extends AbstractHttpClientProxyHttpsTest {
23+
@Override
24+
protected HttpClient.Factory getHttpClientFactory() {
25+
return new JettyHttpClientFactory();
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (C) 2015 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.fabric8.kubernetes.client.okhttp;
17+
18+
import io.fabric8.kubernetes.client.http.AbstractHttpClientProxyHttpsTest;
19+
import io.fabric8.kubernetes.client.http.HttpClient;
20+
import okhttp3.OkHttpClient.Builder;
21+
22+
@SuppressWarnings("java:S2187")
23+
public class OkHttpClientProxyHttpsTest extends AbstractHttpClientProxyHttpsTest {
24+
@Override
25+
protected HttpClient.Factory getHttpClientFactory() {
26+
return new OkHttpClientFactory() {
27+
@Override
28+
protected Builder newOkHttpClientBuilder() {
29+
Builder builder = super.newOkHttpClientBuilder();
30+
builder.hostnameVerifier((hostname, session) -> true);
31+
return builder;
32+
}
33+
};
34+
}
35+
}

httpclient-okhttp/src/test/java/io/fabric8/kubernetes/client/okhttp/OkHttpClientProxyTest.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ protected HttpClient.Factory getHttpClientFactory() {
2626
}
2727

2828
@Override
29-
protected void proxyConfigurationAddsRequiredHeaders() {
30-
// NO-OP
29+
protected void proxyConfigurationOtherAuthAddsRequiredHeaders() throws Exception {
3130
// OkHttp uses a response intercept to add the auth proxy headers in case the original response failed
3231
}
3332
}

httpclient-vertx/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientBuilder.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
import java.util.concurrent.atomic.AtomicBoolean;
3737
import java.util.stream.Stream;
3838

39+
import static io.fabric8.kubernetes.client.utils.HttpClientUtils.decodeBasicCredentials;
40+
3941
public class VertxHttpClientBuilder<F extends HttpClient.Factory>
4042
extends StandardHttpClientBuilder<VertxHttpClient<F>, F, VertxHttpClientBuilder<F>> {
4143

@@ -74,12 +76,18 @@ public VertxHttpClient<F> build() {
7476
}
7577

7678
if (this.proxyType != HttpClient.ProxyType.DIRECT && this.proxyAddress != null) {
77-
ProxyOptions proxyOptions = new ProxyOptions()
79+
final ProxyOptions proxyOptions = new ProxyOptions()
7880
.setHost(this.proxyAddress.getHostName())
7981
.setPort(this.proxyAddress.getPort())
8082
.setType(convertProxyType());
83+
final String[] userPassword = decodeBasicCredentials(this.proxyAuthorization);
84+
if (userPassword != null) {
85+
proxyOptions.setUsername(userPassword[0]);
86+
proxyOptions.setPassword(userPassword[1]);
87+
} else {
88+
addProxyAuthInterceptor();
89+
}
8190
options.setProxyOptions(proxyOptions);
82-
addProxyAuthInterceptor();
8391
}
8492

8593
final String[] protocols;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (C) 2015 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.fabric8.kubernetes.client.vertx;
17+
18+
import io.fabric8.kubernetes.client.http.AbstractHttpClientProxyHttpsTest;
19+
import io.fabric8.kubernetes.client.http.HttpClient;
20+
21+
@SuppressWarnings("java:S2187")
22+
public class VertxHttpClientProxyHttpsTest extends AbstractHttpClientProxyHttpsTest {
23+
@Override
24+
protected HttpClient.Factory getHttpClientFactory() {
25+
return new VertxHttpClientFactory();
26+
}
27+
}

kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/HttpClientUtils.java

+18
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,24 @@ public static String basicCredentials(String username, String password) {
132132
return basicCredentials(username + ":" + password);
133133
}
134134

135+
public static String[] decodeBasicCredentials(String basicCredentials) {
136+
if (basicCredentials == null) {
137+
return null;
138+
}
139+
try {
140+
final String encodedCredentials = basicCredentials.replaceFirst("Basic ", "");
141+
final String decodedProxyAuthorization = new String(Base64.getDecoder().decode(encodedCredentials),
142+
StandardCharsets.UTF_8);
143+
final String[] userPassword = decodedProxyAuthorization.split(":");
144+
if (userPassword.length == 2) {
145+
return userPassword;
146+
}
147+
} catch (Exception ignored) {
148+
// Ignored
149+
}
150+
return null;
151+
}
152+
135153
/**
136154
* @deprecated you should not need to call this method directly. Please create your own HttpClient.Factory
137155
* should you need to customize your clients.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright (C) 2015 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.fabric8.kubernetes.client.http;
17+
18+
import io.fabric8.kubernetes.client.internal.SSLUtils;
19+
import io.fabric8.mockwebserver.Context;
20+
import io.fabric8.mockwebserver.DefaultMockServer;
21+
import io.fabric8.mockwebserver.ServerRequest;
22+
import io.fabric8.mockwebserver.ServerResponse;
23+
import io.fabric8.mockwebserver.dsl.HttpMethod;
24+
import io.fabric8.mockwebserver.internal.MockDispatcher;
25+
import io.fabric8.mockwebserver.internal.MockSSLContextFactory;
26+
import io.fabric8.mockwebserver.internal.SimpleRequest;
27+
import io.fabric8.mockwebserver.internal.SimpleResponse;
28+
import io.fabric8.mockwebserver.utils.ResponseProvider;
29+
import okhttp3.Headers;
30+
import okhttp3.mockwebserver.MockResponse;
31+
import okhttp3.mockwebserver.MockWebServer;
32+
import okhttp3.mockwebserver.RecordedRequest;
33+
import okhttp3.mockwebserver.SocketPolicy;
34+
import org.junit.jupiter.api.AfterAll;
35+
import org.junit.jupiter.api.BeforeAll;
36+
import org.junit.jupiter.api.DisplayName;
37+
import org.junit.jupiter.api.Test;
38+
39+
import java.net.InetSocketAddress;
40+
import java.util.ArrayDeque;
41+
import java.util.HashMap;
42+
import java.util.Map;
43+
import java.util.Queue;
44+
import java.util.concurrent.TimeUnit;
45+
import java.util.concurrent.atomic.AtomicReference;
46+
47+
import static io.fabric8.kubernetes.client.utils.HttpClientUtils.basicCredentials;
48+
import static org.assertj.core.api.Assertions.assertThat;
49+
50+
public abstract class AbstractHttpClientProxyHttpsTest {
51+
52+
private static SocketPolicy defaultResponseSocketPolicy;
53+
private static Map<ServerRequest, Queue<ServerResponse>> responses;
54+
private static DefaultMockServer server;
55+
56+
@BeforeAll
57+
static void beforeAll() {
58+
defaultResponseSocketPolicy = SocketPolicy.KEEP_OPEN;
59+
responses = new HashMap<>();
60+
final MockWebServer okHttpMockWebServer = new MockWebServer();
61+
final MockDispatcher dispatcher = new MockDispatcher(responses) {
62+
@Override
63+
public MockResponse peek() {
64+
return new MockResponse().setSocketPolicy(defaultResponseSocketPolicy);
65+
}
66+
};
67+
server = new DefaultMockServer(new Context(), okHttpMockWebServer, responses, dispatcher, true);
68+
server.start();
69+
okHttpMockWebServer.useHttps(MockSSLContextFactory.create().getSocketFactory(), true);
70+
}
71+
72+
@AfterAll
73+
static void afterAll() {
74+
server.shutdown();
75+
}
76+
77+
protected abstract HttpClient.Factory getHttpClientFactory();
78+
79+
@Test
80+
@DisplayName("Proxied HttpClient adds required headers to the request")
81+
protected void proxyConfigurationAddsRequiredHeadersForHttps() throws Exception {
82+
final AtomicReference<RecordedRequest> initialConnectRequest = new AtomicReference<>();
83+
final ResponseProvider<String> bodyProvider = new ResponseProvider<String>() {
84+
85+
@Override
86+
public String getBody(RecordedRequest request) {
87+
return "";
88+
}
89+
90+
@Override
91+
public void setHeaders(Headers headers) {
92+
}
93+
94+
@Override
95+
public int getStatusCode(RecordedRequest request) {
96+
defaultResponseSocketPolicy = SocketPolicy.UPGRADE_TO_SSL_AT_END; // for jetty to upgrade after the challenge
97+
if (request.getHeader(StandardHttpHeaders.PROXY_AUTHORIZATION) != null) {
98+
initialConnectRequest.compareAndSet(null, request);
99+
return 200;
100+
}
101+
return 407;
102+
}
103+
104+
@Override
105+
public Headers getHeaders() {
106+
return new Headers.Builder().add("Proxy-Authenticate", "Basic").build();
107+
}
108+
109+
};
110+
responses.computeIfAbsent(new SimpleRequest(HttpMethod.CONNECT, "/"), k -> new ArrayDeque<>())
111+
.add(new SimpleResponse(true, bodyProvider, null, 0, TimeUnit.SECONDS));
112+
// Given
113+
final HttpClient.Builder builder = getHttpClientFactory().newBuilder()
114+
.sslContext(null, SSLUtils.trustManagers(null, null, true, null, null))
115+
.proxyAddress(new InetSocketAddress("localhost", server.getPort()))
116+
.proxyAuthorization(basicCredentials("auth", "cred"));
117+
try (HttpClient client = builder.build()) {
118+
// When
119+
client.sendAsync(client.newHttpRequestBuilder()
120+
.uri(String.format("https://0.0.0.0:%s/not-found", server.getPort() + 1)).build(), String.class)
121+
.get(30, TimeUnit.SECONDS);
122+
123+
// if it fails, then authorization was not set
124+
assertThat(initialConnectRequest)
125+
.doesNotHaveNullValue()
126+
.hasValueMatching(r -> r.getHeader("Proxy-Authorization").equals("Basic YXV0aDpjcmVk"));
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)