Skip to content

Commit

Permalink
Proxy Support
Browse files Browse the repository at this point in the history
This change adds support for proxies in the Reactor Netty implementation.
Proxies are configured along with the rest of the connection information and
added to the root HttpClient that all implementations use.  In addition, the
SslCertificateTruster was updated to use the proxy when connecting to download
SSL certificates for trusting.

[#479]
  • Loading branch information
nebhale committed May 10, 2016
1 parent fa75b96 commit 85af172
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 52 deletions.
Expand Up @@ -18,17 +18,16 @@

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.tuple.Tuple2;
import reactor.io.netty.config.ClientOptions;
import reactor.io.netty.http.HttpClient;
import reactor.io.netty.http.HttpException;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
Expand All @@ -40,15 +39,20 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;

import static reactor.io.netty.config.NettyHandlerNames.SslHandler;

final class DefaultSslCertificateTruster implements SslCertificateTruster {

private final Logger logger = LoggerFactory.getLogger("cloudfoundry-client.trust");

private final AtomicReference<X509TrustManager> delegate;

private final ProxyContext proxyContext;

private final Set<Tuple2<String, Integer>> trustedHostsAndPorts;

DefaultSslCertificateTruster() {
DefaultSslCertificateTruster(ProxyContext proxyContext) {
this.proxyContext = proxyContext;
this.delegate = new AtomicReference<>(getTrustManager(getTrustManagerFactory(null)));
this.trustedHostsAndPorts = Collections.newSetFromMap(new ConcurrentHashMap<>());
}
Expand Down Expand Up @@ -78,7 +82,7 @@ public void trust(String host, int port, Duration duration) {
this.logger.warn("Trusting SSL Certificate for {}:{}", host, port);

X509TrustManager trustManager = this.delegate.get();
X509Certificate[] untrustedCertificates = getUntrustedCertificates(host, port, duration, trustManager);
X509Certificate[] untrustedCertificates = getUntrustedCertificates(host, port, duration, this.proxyContext, trustManager);

if (untrustedCertificates != null) {
KeyStore trustStore = addToTrustStore(untrustedCertificates, trustManager);
Expand Down Expand Up @@ -107,6 +111,13 @@ private static KeyStore addToTrustStore(X509Certificate[] untrustedCertificates,
}
}

private static HttpClient getHttpClient(ProxyContext proxyContext, CertificateCollectingTrustManager collector) {
return HttpClient.create(ClientOptions.create()
.sslSupport()
.pipelineConfigurer(pipeline -> proxyContext.getHttpProxyHandler().ifPresent(handler -> pipeline.addBefore(SslHandler, null, handler)))
.sslConfigurer(ssl -> ssl.trustManager(new StaticTrustManagerFactory(collector))));
}

private static X509TrustManager getTrustManager(TrustManagerFactory trustManagerFactory) {
for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
if (trustManager instanceof X509TrustManager) {
Expand All @@ -128,37 +139,31 @@ private static TrustManagerFactory getTrustManagerFactory(KeyStore trustStore) {
}
}

private static X509Certificate[] getUntrustedCertificates(String host, int port, Duration duration, X509TrustManager delegate) {
try {
CertificateCollectingTrustManager collector = new CertificateCollectingTrustManager(delegate);

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{collector}, null);
private static X509Certificate[] getUntrustedCertificates(String host, int port, Duration duration, ProxyContext proxyContext, X509TrustManager delegate) {
CertificateCollectingTrustManager collector = new CertificateCollectingTrustManager(delegate);

SSLSocketFactory factory = sslContext.getSocketFactory();
SSLSocket socket = (SSLSocket) factory.createSocket(host, port);
socket.setSoTimeout((int) duration.toMillis());

try {
socket.startHandshake();
socket.close();
} catch (SSLException e) {
// Swallow exception
}
try {
getHttpClient(proxyContext, collector)
.get(getUri(host, port))
.get(duration);
} catch (HttpException e) {
// swallow expected exception
}

X509Certificate[] chain = collector.getCollectedCertificateChain();
if (chain == null) {
throw new IllegalStateException("Could not obtain server certificate chain");
}
X509Certificate[] chain = collector.getCollectedCertificateChain();
if (chain == null) {
throw new IllegalStateException("Could not obtain server certificate chain");
}

if (collector.isTrusted()) {
return null;
} else {
return chain;
}
} catch (IOException | NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
if (collector.isTrusted()) {
return null;
} else {
return chain;
}
}

private static String getUri(String host, int port) {
return UriComponentsBuilder.newInstance().scheme("https").host(host).port(port).toUriString();
}

}
Expand Up @@ -29,6 +29,8 @@
import java.util.Map;
import java.util.Optional;

import static reactor.io.netty.config.NettyHandlerNames.SslHandler;

public final class DefaultConnectionContext implements ConnectionContext {

private static final int DEFAULT_PORT = 443;
Expand All @@ -48,10 +50,20 @@ public final class DefaultConnectionContext implements ConnectionContext {
private final Optional<SslCertificateTruster> sslCertificateTruster;

@Builder
DefaultConnectionContext(@NonNull AuthorizationProvider authorizationProvider, @NonNull String host, ObjectMapper objectMapper, Integer port, Boolean trustCertificates) {
DefaultConnectionContext(@NonNull AuthorizationProvider authorizationProvider, @NonNull String host, ObjectMapper objectMapper, Integer port, String proxyHost, String proxyPassword,
Integer proxyPort, String proxyUsername, Boolean trustCertificates) {

ProxyContext proxyContext = ProxyContext.builder()
.host(proxyHost)
.password(proxyPassword)
.port(proxyPort)
.username(proxyUsername)
.build();

this.sslCertificateTruster = createSslCertificateTruster(proxyContext, trustCertificates);
this.httpClient = createHttpClient(proxyContext, this.sslCertificateTruster);

this.authorizationProvider = authorizationProvider;
this.sslCertificateTruster = createSslCertificateTruster(trustCertificates);
this.httpClient = createHttpClient(this.sslCertificateTruster);
this.root = getRoot(host, port, this.sslCertificateTruster);
this.objectMapper = getObjectMapper(objectMapper);
this.info = getInfo(this.httpClient, this.objectMapper, this.root);
Expand Down Expand Up @@ -86,15 +98,16 @@ public Mono<String> getRoot(String key) {
.cache();
}

private static HttpClient createHttpClient(Optional<SslCertificateTruster> sslCertificateTruster) {
ClientOptions clientOptions = ClientOptions.create().sslSupport();
sslCertificateTruster.ifPresent(trustManager -> clientOptions.ssl().trustManager(new StaticTrustManagerFactory(trustManager)));
return HttpClient.create(clientOptions);
private static HttpClient createHttpClient(ProxyContext proxyContext, Optional<SslCertificateTruster> sslCertificateTruster) {
return HttpClient.create(ClientOptions.create()
.sslSupport()
.pipelineConfigurer(pipeline -> proxyContext.getHttpProxyHandler().ifPresent(handler -> pipeline.addBefore(SslHandler, null, handler)))
.sslConfigurer(ssl -> sslCertificateTruster.ifPresent(trustManager -> ssl.trustManager(new StaticTrustManagerFactory(trustManager)))));
}

private static Optional<SslCertificateTruster> createSslCertificateTruster(Boolean trustCertificates) {
private static Optional<SslCertificateTruster> createSslCertificateTruster(ProxyContext proxyContext, Boolean trustCertificates) {
if (Optional.ofNullable(trustCertificates).orElse(false)) {
return Optional.of(new DefaultSslCertificateTruster());
return Optional.of(new DefaultSslCertificateTruster(proxyContext));
} else {
return Optional.empty();
}
Expand Down
@@ -0,0 +1,54 @@
/*
* Copyright 2013-2016 the original author or authors.
*
* 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 org.cloudfoundry.reactor.util;

import io.netty.channel.ChannelHandler;
import io.netty.handler.proxy.HttpProxyHandler;
import lombok.Builder;
import org.springframework.util.StringUtils;

import java.net.InetSocketAddress;
import java.util.Optional;

@Builder
final class ProxyContext {

private final String host;

private final String password;

private final Integer port;

private final String username;

Optional<ChannelHandler> getHttpProxyHandler() {
if (StringUtils.hasText(this.host)) {
InetSocketAddress proxyAddress = new InetSocketAddress(this.host, Optional.ofNullable(this.port).orElse(8080));

HttpProxyHandler httpProxyHandler;
if (this.username != null) {
httpProxyHandler = new HttpProxyHandler(proxyAddress, this.username, this.password);
} else {
httpProxyHandler = new HttpProxyHandler(proxyAddress);
}

return Optional.of(httpProxyHandler);
}

return Optional.empty();
}
}
Expand Up @@ -203,15 +203,21 @@ public final class SpringCloudFoundryClient implements CloudFoundryClient, Conne
Boolean skipSslValidation,
String clientId,
String clientSecret,
String proxyHost,
String proxyPassword,
Integer proxyPort,
String proxyUsername,
@NonNull String username,
@NonNull String password,
@Singular List<DeserializationProblemHandler> problemHandlers) {

this(getConnectionContext(host, port, skipSslValidation, clientId, clientSecret, username, password), host, port, skipSslValidation, getSchedulerGroup(), problemHandlers);
this(getConnectionContext(host, port, skipSslValidation, clientId, clientSecret, username, password), host, port, proxyHost, proxyPassword, proxyPort, proxyUsername, skipSslValidation,
getSchedulerGroup(), problemHandlers);
new CloudFoundryClientCompatibilityChecker(this.info).check();
}

SpringCloudFoundryClient(String host, Integer port, Boolean skipSslValidation, RestOperations restOperations, URI root, Scheduler schedulerGroup, OAuth2TokenProvider tokenProvider) {
SpringCloudFoundryClient(String host, Integer port, String proxyHost, String proxyPassword, Integer proxyPort, String proxyUsername, Boolean skipSslValidation, RestOperations restOperations,
URI root, Scheduler schedulerGroup, OAuth2TokenProvider tokenProvider) {
this.applicationsV2 = new SpringApplicationsV2(restOperations, root, schedulerGroup);
this.buildpacks = new SpringBuildpacks(restOperations, root, schedulerGroup);
this.packages = new SpringPackages(restOperations, root, schedulerGroup);
Expand All @@ -226,6 +232,10 @@ public final class SpringCloudFoundryClient implements CloudFoundryClient, Conne
.objectMapper(new ObjectMapper()
.setSerializationInclusion(NON_NULL))
.port(port)
.proxyHost(proxyHost)
.proxyPassword(proxyPassword)
.proxyPort(proxyPort)
.proxyUsername(proxyUsername)
.trustCertificates(skipSslValidation)
.build();

Expand Down Expand Up @@ -269,14 +279,17 @@ public final class SpringCloudFoundryClient implements CloudFoundryClient, Conne
}

// Let's take a moment to reflect on the fact that this bridge constructor is needed to counter a useless compiler constraint
private SpringCloudFoundryClient(ConnectionContext connectionContext, String host, Integer port, Boolean skipSslValidation, Scheduler schedulerGroup, List<DeserializationProblemHandler>
private SpringCloudFoundryClient(ConnectionContext connectionContext, String host, Integer port, String proxyHost, String proxyPassword, Integer proxyPort, String proxyUsername,
Boolean skipSslValidation, Scheduler schedulerGroup, List<DeserializationProblemHandler>
problemHandlers) {
this(host, port, skipSslValidation, getRestOperations(connectionContext, problemHandlers), getRoot(host, port, connectionContext.getSslCertificateTruster()), schedulerGroup);
this(host, port, proxyHost, proxyPassword, proxyPort, proxyUsername, skipSslValidation, getRestOperations(connectionContext, problemHandlers), getRoot(host, port,
connectionContext.getSslCertificateTruster()), schedulerGroup);
}

// Let's take a moment to reflect on the fact that this bridge constructor is needed to counter a useless compiler constraint
private SpringCloudFoundryClient(String host, Integer port, Boolean skipSslValidation, OAuth2RestOperations restOperations, URI root, Scheduler schedulerGroup) {
this(host, port, skipSslValidation, restOperations, root, schedulerGroup, new OAuth2RestOperationsOAuth2TokenProvider(restOperations));
private SpringCloudFoundryClient(String host, Integer port, String proxyHost, String proxyPassword, Integer proxyPort, String proxyUsername, Boolean skipSslValidation,
OAuth2RestOperations restOperations, URI root, Scheduler schedulerGroup) {
this(host, port, proxyHost, proxyPassword, proxyPort, proxyUsername, skipSslValidation, restOperations, root, schedulerGroup, new OAuth2RestOperationsOAuth2TokenProvider(restOperations));
}

@Override
Expand Down
Expand Up @@ -24,7 +24,7 @@

public final class SpringCloudFoundryClientTest extends AbstractRestTest {

private final SpringCloudFoundryClient client = new SpringCloudFoundryClient("test-host", null, null, this.restTemplate, this.root, PROCESSOR_GROUP, this.tokenProvider);
private final SpringCloudFoundryClient client = new SpringCloudFoundryClient("test-host", null, null, null, null, null, null, this.restTemplate, this.root, PROCESSOR_GROUP, this.tokenProvider);

@Test
public void applicationUsageEvents() {
Expand Down
Expand Up @@ -29,8 +29,8 @@
import org.cloudfoundry.operations.CloudFoundryOperations;
import org.cloudfoundry.operations.CloudFoundryOperationsBuilder;
import org.cloudfoundry.reactor.doppler.ReactorDopplerClient;
import org.cloudfoundry.spring.client.SpringCloudFoundryClient;
import org.cloudfoundry.reactor.uaa.ReactorUaaClient;
import org.cloudfoundry.spring.client.SpringCloudFoundryClient;
import org.cloudfoundry.uaa.UaaClient;
import org.cloudfoundry.util.PaginationUtils;
import org.cloudfoundry.util.ResourceUtils;
Expand Down Expand Up @@ -68,13 +68,21 @@ CloudFoundryCleaner cloudFoundryCleaner(CloudFoundryClient cloudFoundryClient, M
SpringCloudFoundryClient cloudFoundryClient(@Value("${test.host}") String host,
@Value("${test.username}") String username,
@Value("${test.password}") String password,
@Value("${test.skipSslValidation:false}") Boolean skipSslValidation) {
@Value("${test.skipSslValidation:false}") Boolean skipSslValidation,
@Value("${test.proxyHost:}") String proxyHost,
@Value("${test.proxyPassword:}") String proxyPassword,
@Value("${test.proxyPort:}") Integer proxyPort,
@Value("${test.proxyUsername:}") String proxyUsername) {

return SpringCloudFoundryClient.builder()
.host(host)
.username(username)
.password(password)
.skipSslValidation(skipSslValidation)
.proxyHost(proxyHost)
.proxyPassword(proxyPassword)
.proxyPort(proxyPort)
.proxyUsername(proxyUsername)
.problemHandler(new FailingDeserializationProblemHandler()) // Test-only problem handler
.build();
}
Expand Down

0 comments on commit 85af172

Please sign in to comment.