/*
* Copyright 2013-2017 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;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import org.cloudfoundry.Nullable;
import org.cloudfoundry.reactor.util.DefaultSslCertificateTruster;
import org.cloudfoundry.reactor.util.JsonCodec;
import org.cloudfoundry.reactor.util.NetworkLogging;
import org.cloudfoundry.reactor.util.SslCertificateTruster;
import org.cloudfoundry.reactor.util.StaticTrustManagerFactory;
import org.cloudfoundry.reactor.util.UserAgent;
import org.immutables.value.Value;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import reactor.ipc.netty.http.client.HttpClient;
import reactor.ipc.netty.http.client.HttpClientRequest;
import reactor.ipc.netty.options.ClientOptions;
import reactor.ipc.netty.resources.LoopResources;
import reactor.ipc.netty.resources.PoolResources;
import javax.annotation.PreDestroy;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
import static io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS;
import static io.netty.channel.ChannelOption.SO_KEEPALIVE;
import static io.netty.channel.ChannelOption.SO_RCVBUF;
import static io.netty.channel.ChannelOption.SO_SNDBUF;
/**
* The default implementation of the {@link ConnectionContext} interface. This is the implementation that should be used for most non-testing cases.
*/
@Value.Immutable
abstract class _DefaultConnectionContext implements ConnectionContext {
private static final int DEFAULT_PORT = 443;
private static final Pattern HOSTNAME_PATTERN = Pattern.compile("^[a-zA-Z0-9-.]+$");
private static final int RECEIVE_BUFFER_SIZE = 10 * 1024 * 1024;
private static final int SEND_BUFFER_SIZE = 10 * 1024 * 1024;
private static final int UNDEFINED_PORT = -1;
/**
* Disposes resources created to service this connection context
*/
@PreDestroy
public final void dispose() {
getConnectionPool().ifPresent(PoolResources::dispose);
getThreadPool().dispose();
}
/**
* The number of connections to use when processing requests and responses. Setting this to `null` disables connection pooling.
*/
@Nullable
@Value.Default
public Integer getConnectionPoolSize() {
return PoolResources.DEFAULT_POOL_MAX_CONNECTION;
}
@Override
@Value.Default
public HttpClient getHttpClient() {
return HttpClient.create(options -> {
options
.loopResources(getThreadPool())
.option(SO_SNDBUF, SEND_BUFFER_SIZE)
.option(SO_RCVBUF, RECEIVE_BUFFER_SIZE)
.disablePool();
getConnectionPool().ifPresent(options::poolResources);
getKeepAlive().ifPresent(keepAlive -> options.option(SO_KEEPALIVE, keepAlive));
getProxyConfiguration().ifPresent(c -> options.proxy(ClientOptions.Proxy.HTTP, c.getHost(), c.getPort().orElse(null), c.getUsername().orElse(null), u -> c.getPassword().orElse(null)));
getConnectTimeout().ifPresent(socketTimeout -> options.option(CONNECT_TIMEOUT_MILLIS, (int) socketTimeout.toMillis()));
options.sslSupport(ssl -> getSslCertificateTruster().ifPresent(trustManager -> ssl.trustManager(new StaticTrustManagerFactory(trustManager))));
getSslHandshakeTimeout().ifPresent(options::sslHandshakeTimeout);
});
}
@Override
@Value.Default
public ObjectMapper getObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper()
.disable(FAIL_ON_UNKNOWN_PROPERTIES)
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.registerModule(new Jdk8Module())
.setSerializationInclusion(NON_NULL);
getProblemHandlers().forEach(objectMapper::addHandler);
return objectMapper;
}
@Value.Default
public Integer getPort() {
return DEFAULT_PORT;
}
@Value.Derived
public Mono<String> getRoot() {
Integer port = getPort();
UriComponentsBuilder builder = UriComponentsBuilder.newInstance().scheme("https").host(getApiHost());
if (port != null) {
builder.port(port);
}
UriComponents components = normalize(builder, getScheme());
trust(components, getSslCertificateTruster());
return Mono.just(components.toUriString());
}
@Override
public Mono<String> getRoot(String key) {
return getInfo()
.map(info -> normalize(UriComponentsBuilder.fromUriString(info.get(key)), getScheme()))
.doOnNext(components -> trust(components, getSslCertificateTruster()))
.map(UriComponents::toUriString)
.cache();
}
/**
* The number of worker threads to use when processing requests and responses
*/
@Value.Default
public Integer getThreadPoolSize() {
return LoopResources.DEFAULT_IO_WORKER_COUNT;
}
@Value.Check
void checkForValidApiHost() {
Matcher matcher = HOSTNAME_PATTERN.matcher(getApiHost());
if (!matcher.matches()) {
throw new IllegalArgumentException(String.format("API hostname %s is not correctly formatted (e.g. 'api.local.pcfdev.io')", getApiHost()));
}
}
/**
* The hostname of the API root. Typically something like {@code api.run.pivotal.io}.
*/
abstract String getApiHost();
/**
* The {@code CONNECT_TIMEOUT_MILLIS} value
*/
abstract Optional<Duration> getConnectTimeout();
@Value.Derived
Optional<PoolResources> getConnectionPool() {
return Optional.ofNullable(getConnectionPoolSize())
.map(connectionPoolSize -> PoolResources.fixed("cloudfoundry-client", connectionPoolSize));
}
@SuppressWarnings("unchecked")
@Value.Derived
Mono<Map<String, String>> getInfo() {
return getRoot()
.map(uri -> UriComponentsBuilder.fromUriString(uri).pathSegment("v2", "info").build().encode().toUriString())
.then(uri -> getHttpClient()
.get(uri, request -> Mono.just(request)
.map(UserAgent::addUserAgent)
.flatMapMany(HttpClientRequest::send))
.doOnSubscribe(NetworkLogging.get(uri))
.transform(NetworkLogging.response(uri)))
.transform(JsonCodec.decode(getObjectMapper(), Map.class))
.map(m -> (Map<String, String>) m)
.cache();
}
/**
* The {@code SO_KEEPALIVE} value
*/
abstract Optional<Boolean> getKeepAlive();
/**
* Jackson deserialization problem handlers. Typically only used for testing.
*/
abstract List<DeserializationProblemHandler> getProblemHandlers();
/**
* The (optional) proxy configuration
*/
abstract Optional<ProxyConfiguration> getProxyConfiguration();
@Value.Derived
String getScheme() {
if (getSecure().orElse(true)) {
return "https";
} else {
return "http";
}
}
/**
* Whether the connection to the root API should be secure (i.e. using HTTPS).
*/
abstract Optional<Boolean> getSecure();
/**
* Whether to skip SSL certificate validation for all hosts reachable from the API host. Defaults to {@code false}.
*/
abstract Optional<Boolean> getSkipSslValidation();
@Value.Derived
Optional<SslCertificateTruster> getSslCertificateTruster() {
if (getSkipSslValidation().orElse(false)) {
return Optional.of(new DefaultSslCertificateTruster(getProxyConfiguration()));
} else {
return Optional.empty();
}
}
/**
* The timeout for the SSL handshake negotiation
*/
abstract Optional<Duration> getSslHandshakeTimeout();
@Value.Derived
LoopResources getThreadPool() {
return LoopResources.create("cloudfoundry-client", getThreadPoolSize(), true);
}
private static void trust(UriComponents components, Optional<SslCertificateTruster> sslCertificateTruster) {
sslCertificateTruster.ifPresent(t -> t.trust(components.getHost(), components.getPort(), Duration.ofSeconds(30)));
}
private UriComponents normalize(UriComponentsBuilder builder, String scheme) {
UriComponents components = builder.build();
builder.scheme(scheme);
if (UNDEFINED_PORT == components.getPort()) {
builder.port(getPort());
}
return builder.build().encode();
}
}