Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test latest httpclient release #2348

Draft
wants to merge 8 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.Socket;
import java.net.URI;
import java.net.URL;
import java.time.Duration;
Expand All @@ -58,7 +57,7 @@
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.function.Supplier;
import java.util.concurrent.TimeUnit;
import java.util.stream.LongStream;
import javax.annotation.Nullable;
import javax.net.ssl.SSLSocketFactory;
Expand All @@ -79,10 +78,14 @@
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.DefaultHttpClientConnectionOperator;
import org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.io.DetachedSocketFactory;
import org.apache.hc.client5.http.io.HttpClientConnectionOperator;
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.URIScheme;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.io.SocketConfig;
Expand Down Expand Up @@ -394,31 +397,6 @@ public static ClientBuilder clientBuilder() {

public static final class ClientBuilder {

// Most of our servers use a keep-alive timeout of one minute, by using a slightly lower value on the
// client side we can avoid unnecessary retries due to race conditions when servers close idle connections
// as clients attempt to use them.
// Note that pooled idle connections use an infinite socket timeout so there is no reason to scale
// this value with configured timeouts.
private static final Timeout IDLE_CONNECTION_TIMEOUT = Timeout.ofSeconds(50);

// Increased from two seconds to four seconds because we have strong support for retries
// and can optimistically avoid expensive connection checks. Failures caused by NoHttpResponseExceptions
// are possible when the target closes connections prior to this timeout, and can be safely retried.
// Ideally this value would be larger for RPC, however some servers use relatively low defaults:
// apache httpd versions 1.3 and 2.0: 15 seconds:
// https://httpd.apache.org/docs/2.0/mod/core.html#keepalivetimeout
// apache httpd version 2.2 and above: 5 seconds
// https://httpd.apache.org/docs/2.2/mod/core.html#keepalivetimeout
// nodejs http server: 5 seconds
// https://nodejs.org/api/http.html#http_server_keepalivetimeout
// nginx: 75 seconds (good)
// https://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_timeout
// dropwizard: 30 seconds (see idleTimeout in the linked docs)
// https://www.dropwizard.io/en/latest/manual/configuration.html#Connectors
// wc: 60 seconds (internal)
private static final TimeValue CONNECTION_INACTIVITY_CHECK = TimeValue.ofMilliseconds(
Integer.getInteger("dialogue.experimental.inactivity.check.threshold.millis", 4_000));

@Nullable
private ClientConfiguration clientConfiguration;

Expand Down Expand Up @@ -485,40 +463,56 @@ public CloseableClient build() {

InetSocketAddress socksProxyAddress = getSocksProxyAddress(conf);
SSLSocketFactory rawSocketFactory = conf.sslSocketFactory();
Supplier<Socket> simpleSocketCreator = socksProxyAddress == null
? () -> new Socket(Proxy.NO_PROXY)
: () -> new Socket(new Proxy(Proxy.Type.SOCKS, socksProxyAddress));
SSLSocketFactory instrumentedSocketFactory =
MetricRegistries.instrument(conf.taggedMetricRegistry(), rawSocketFactory, name);
DetachedSocketFactory plainSocketFactory = new SocksSupportingDetachedSocketFactory(socksProxyAddress);

TlsSocketStrategy tlsStrategy = new DialogueTlsSocketStrategy(
instrumentedSocketFactory,
TlsProtocols.get(),
supportedCipherSuites(CipherSuites.allCipherSuites(), rawSocketFactory, name),
new InstrumentedHostnameVerifier(new DefaultHostnameVerifier(), name, conf.taggedMetricRegistry()));

ConnectInstrumentation connectInstrumentation =
new ConnectInstrumentation(conf.taggedMetricRegistry(), name);

InstrumentedDnsResolver instrumentedDnsResolver = new InstrumentedDnsResolver(
SystemDefaultDnsResolver.INSTANCE, dnsResolver, name, conf.taggedMetricRegistry());

HttpClientConnectionOperator operator =
new DefaultHttpClientConnectionOperator(
plainSocketFactory,
null,
instrumentedDnsResolver,
RegistryBuilder.<TlsSocketStrategy>create()
.register(URIScheme.HTTPS.id, tlsStrategy)
.build()) {
private static final String CONNECT_BEGAN_ATTRIBUTE = "onBeforeSocketConnectNanoTime";

@Override
protected void onBeforeSocketConnect(HttpContext httpContext, HttpHost endpointHost) {
super.onBeforeSocketConnect(httpContext, endpointHost);
httpContext.setAttribute(CONNECT_BEGAN_ATTRIBUTE, System.nanoTime());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love boxing this long->Long.
The onBefore/onAfter methods don't allow us to collect timing info when requests fail either, since onAfterSocketConnect is never called in that case

}

@Override
protected void onAfterSocketConnect(HttpContext httpContext, HttpHost endpointHost) {
super.onAfterSocketConnect(httpContext, endpointHost);
Object value = httpContext.getAttribute(CONNECT_BEGAN_ATTRIBUTE);
if (value instanceof Long) {
long duration = System.nanoTime() - (long) value;
connectInstrumentation.timer(true, httpContext).update(duration, TimeUnit.NANOSECONDS);
}
}
};

PoolingHttpClientConnectionManager internalConnectionManager = new PoolingHttpClientConnectionManager(
RegistryBuilder.<ConnectionSocketFactory>create()
.register(
URIScheme.HTTP.id,
new InstrumentedPlainConnectionSocketFactory(
simpleSocketCreator, connectInstrumentation))
.register(
URIScheme.HTTPS.id,
new InstrumentedSslConnectionSocketFactory(
connectInstrumentation,
MetricRegistries.instrument(
conf.taggedMetricRegistry(), rawSocketFactory, name),
TlsProtocols.get(),
supportedCipherSuites(
CipherSuites.allCipherSuites(), rawSocketFactory, name),
new InstrumentedHostnameVerifier(
new DefaultHostnameVerifier(), name, conf.taggedMetricRegistry()),
simpleSocketCreator))
.build(),
operator,
PoolConcurrencyPolicy.LAX,
// Allow unnecessary connections to time out reducing system load.
PoolReusePolicy.LIFO,
// No maximum time to live
TimeValue.NEG_ONE_MILLISECOND,
null,
new InstrumentedDnsResolver(
SystemDefaultDnsResolver.INSTANCE, dnsResolver, name, conf.taggedMetricRegistry()),
new InstrumentedManagedHttpConnectionFactory(
ManagedHttpClientConnectionFactory.INSTANCE, conf.taggedMetricRegistry(), name));
internalConnectionManager.setDefaultSocketConfig(SocketConfig.custom()
Expand All @@ -530,7 +524,9 @@ public CloseableClient build() {
// Doesn't appear to do anything in this release
.setSocksProxyAddress(socksProxyAddress)
.build());
internalConnectionManager.setValidateAfterInactivity(CONNECTION_INACTIVITY_CHECK);
DialogueConnectionConfigResolver connectionConfigResolver =
new DialogueConnectionConfigResolver(connectTimeout, socketTimeout);
internalConnectionManager.setConnectionConfigResolver(connectionConfigResolver);
internalConnectionManager.setMaxTotal(Integer.MAX_VALUE);
internalConnectionManager.setDefaultMaxPerRoute(Integer.MAX_VALUE);

Expand All @@ -542,7 +538,6 @@ public CloseableClient build() {

HttpClientBuilder builder = HttpClients.custom()
.setDefaultRequestConfig(RequestConfig.custom()
.setConnectTimeout(connectTimeout)
// Don't allow clients to block forever waiting on a connection to become available
.setConnectionRequestTimeout(connectTimeout)
// The response timeout is used as the socket timeout for the duration of
Expand All @@ -552,13 +547,14 @@ public CloseableClient build() {
.setRedirectsEnabled(false)
.setAuthenticationEnabled(conf.proxyCredentials().isPresent())
.setExpectContinueEnabled(false)
.setConnectionKeepAlive(IDLE_CONNECTION_TIMEOUT)
.setConnectionKeepAlive(
InactivityValidationAwareConnectionKeepAliveStrategy.IDLE_CONNECTION_TIMEOUT)
.build())
// Connection pool lifecycle must be managed separately. This allows us to configure a more
// precise IdleConnectionEvictor.
.setConnectionManagerShared(true)
.setKeepAliveStrategy(
new InactivityValidationAwareConnectionKeepAliveStrategy(internalConnectionManager, name))
new InactivityValidationAwareConnectionKeepAliveStrategy(connectionConfigResolver, name))
.setConnectionManager(connectionManager)
.setRoutePlanner(new DialogueRoutePlanner(conf.proxy()))
.disableAutomaticRetries()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.dialogue.hc5;

import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.core5.function.Resolver;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;

final class DialogueConnectionConfigResolver implements Resolver<HttpRoute, ConnectionConfig> {

// Increased from two seconds to four seconds because we have strong support for retries
// and can optimistically avoid expensive connection checks. Failures caused by NoHttpResponseExceptions
// are possible when the target closes connections prior to this timeout, and can be safely retried.
// Ideally this value would be larger for RPC, however some servers use relatively low defaults:
// apache httpd versions 1.3 and 2.0: 15 seconds:
// https://httpd.apache.org/docs/2.0/mod/core.html#keepalivetimeout
// apache httpd version 2.2 and above: 5 seconds
// https://httpd.apache.org/docs/2.2/mod/core.html#keepalivetimeout
// nodejs http server: 5 seconds
// https://nodejs.org/api/http.html#http_server_keepalivetimeout
// nginx: 75 seconds (good)
// https://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_timeout
// dropwizard: 30 seconds (see idleTimeout in the linked docs)
// https://www.dropwizard.io/en/latest/manual/configuration.html#Connectors
// wc: 60 seconds (internal)
private static final TimeValue CONNECTION_INACTIVITY_CHECK = TimeValue.ofMilliseconds(
Integer.getInteger("dialogue.experimental.inactivity.check.threshold.millis", 4_000));

private final Timeout connectTimeout;
private final Timeout socketTimeout;

// We create a new connectionConfig when the connectionInactivityCheck interval changes
// to avoid allocating a new ConnectionConfig each time the value is queried.
private volatile ConnectionConfig connectionConfig;

DialogueConnectionConfigResolver(Timeout connectTimeout, Timeout socketTimeout) {
this.connectTimeout = connectTimeout;
this.socketTimeout = socketTimeout;
setValidateAfterInactivity(CONNECTION_INACTIVITY_CHECK);
}

void setValidateAfterInactivity(TimeValue connectionInactivityCheck) {
connectionConfig = ConnectionConfig.custom()
.setValidateAfterInactivity(connectionInactivityCheck)
.setConnectTimeout(connectTimeout)
.setSocketTimeout(socketTimeout)
.build();
}

TimeValue getValidateAfterInactivity() {
return connectionConfig.getValidateAfterInactivity();
}

@Override
public ConnectionConfig resolve(HttpRoute _ignored) {
return connectionConfig;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.dialogue.hc5;

import com.palantir.logsafe.Preconditions;
import java.io.IOException;
import java.net.Socket;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import org.apache.hc.client5.http.config.TlsConfig;
import org.apache.hc.client5.http.ssl.HttpClientHostnameVerifier;
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.io.Closer;
import org.apache.hc.core5.util.Timeout;

/**
* {@link DialogueTlsSocketStrategy} is based closely on
* {@code org.apache.hc.client5.http.ssl.AbstractClientTlsStrategy}, except that it only requires a
* {@link SSLSocketFactory} rather than an {@link javax.net.ssl.SSLContext}.
* We only implement the minimal required {@link TlsSocketStrategy} interface rather than
* {@link org.apache.hc.core5.http.nio.ssl.TlsStrategy}, which isn't required by socket-based clients.
*/
final class DialogueTlsSocketStrategy implements TlsSocketStrategy {

private final SSLSocketFactory sslSocketFactory;
private final String[] supportedProtocols;
private final String[] supportedCipherSuites;
private final HostnameVerifier hostnameVerifier;

DialogueTlsSocketStrategy(
SSLSocketFactory sslSocketFactory,
String[] supportedProtocols,
String[] supportedCipherSuites,
HostnameVerifier hostnameVerifier) {
this.sslSocketFactory = Preconditions.checkNotNull(sslSocketFactory, "SSLSocketFactory is required");
this.supportedProtocols = Preconditions.checkNotNull(supportedProtocols, "supportedProtocols is required");
this.supportedCipherSuites =
Preconditions.checkNotNull(supportedCipherSuites, "supportedCipherSuites is required");
this.hostnameVerifier = Preconditions.checkNotNull(hostnameVerifier, "hostnameVerifier is required");
}

@Override
public SSLSocket upgrade(Socket socket, String target, int port, Object attachment, HttpContext _context)
throws IOException {
SSLSocket upgradedSocket = (SSLSocket) sslSocketFactory.createSocket(socket, target, port, false);
try {
executeHandshake(upgradedSocket, target, attachment);
return upgradedSocket;
} catch (IOException | RuntimeException ex) {
Closer.closeQuietly(upgradedSocket);
throw ex;
}
}

private void executeHandshake(SSLSocket upgradedSocket, String target, Object attachment) throws IOException {
SSLParameters sslParameters = upgradedSocket.getSSLParameters();
sslParameters.setProtocols(supportedProtocols);
sslParameters.setCipherSuites(supportedCipherSuites);

// If we want to enable the builtin hostname verification support:
// sslParameters.setEndpointIdentificationAlgorithm(URIScheme.HTTPS.id);

upgradedSocket.setSSLParameters(sslParameters);

if (attachment instanceof TlsConfig) {
TlsConfig tlsConfig = (TlsConfig) attachment;
Timeout handshakeTimeout = tlsConfig.getHandshakeTimeout();
if (handshakeTimeout != null) {
upgradedSocket.setSoTimeout(handshakeTimeout.toMillisecondsIntBound());
}
}

upgradedSocket.startHandshake();
verifySession(target, upgradedSocket.getSession(), hostnameVerifier);
}

private static void verifySession(String hostname, SSLSession sslsession, HostnameVerifier verifier)
throws SSLException {
if (verifier instanceof HttpClientHostnameVerifier) {
X509Certificate x509Certificate = getX509Certificate(sslsession);
((HttpClientHostnameVerifier) verifier).verify(hostname, x509Certificate);
} else if (!verifier.verify(hostname, sslsession)) {
throw new SSLPeerUnverifiedException("Certificate doesn't match any of the subject alternative names");
}
}

private static X509Certificate getX509Certificate(SSLSession sslsession) throws SSLPeerUnverifiedException {
Certificate[] certs = sslsession.getPeerCertificates();
if (certs.length < 1) {
throw new SSLPeerUnverifiedException("Peer certificate chain is empty");
}
Certificate peerCertificate = certs[0];
if (peerCertificate instanceof X509Certificate) {
return (X509Certificate) peerCertificate;
}
throw new SSLPeerUnverifiedException("Unexpected certificate type: " + peerCertificate.getType());
}
}
Loading
Loading