aboutsummaryrefslogtreecommitdiffstats
path: root/http-utils/src/main/java/ai/vespa/util/http/hc5/VespaHttpClientBuilder.java
blob: c5ebafb2425335eaa7a8803f1c92e95662ef2730 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package ai.vespa.util.http.hc5;

import org.apache.hc.client5.http.config.ConnectionConfig;
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.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;

import javax.net.ssl.HostnameVerifier;

import java.util.concurrent.TimeUnit;

import static com.yahoo.security.tls.MixedMode.PLAINTEXT_CLIENT_MIXED_SERVER;
import static com.yahoo.security.tls.TransportSecurityUtils.getInsecureMixedMode;
import static com.yahoo.security.tls.TransportSecurityUtils.getSystemTlsContext;
import static com.yahoo.security.tls.TransportSecurityUtils.isTransportSecurityEnabled;

/**
 * Sync HTTP client builder <em>for internal Vespa communications over http/https.</em>
 * Configures Vespa mTLS and handles TLS mixed mode automatically.
 * Custom connection managers must be configured through {@link #connectionManagerFactory(HttpClientConnectionManagerFactory)}.
 *
 * @author jonmv
 */
public class VespaHttpClientBuilder {

    private HttpClientConnectionManagerFactory connectionManagerFactory = PoolingHttpClientConnectionManager::new;
    private HostnameVerifier hostnameVerifier = new NoopHostnameVerifier();
    private boolean rewriteHttpToHttps = true;
    private final ConnectionConfig.Builder connectionConfigBuilder = ConnectionConfig.custom();

    public interface HttpClientConnectionManagerFactory {
        PoolingHttpClientConnectionManager create(Registry<ConnectionSocketFactory> socketFactories);
    }

    private VespaHttpClientBuilder() {
    }

    public static VespaHttpClientBuilder custom() {
        return new VespaHttpClientBuilder();
    }

    public VespaHttpClientBuilder connectionManagerFactory(HttpClientConnectionManagerFactory connectionManagerFactory) {
        this.connectionManagerFactory = connectionManagerFactory;
        return this;
    }

    public VespaHttpClientBuilder hostnameVerifier(HostnameVerifier hostnameVerifier) {
        this.hostnameVerifier = hostnameVerifier;
        return this;
    }
    public VespaHttpClientBuilder rewriteHttpToHttps(boolean enable) {
        this.rewriteHttpToHttps = enable;
        return this;
    }
    public VespaHttpClientBuilder connectTimeout(long connectTimeout, TimeUnit timeUnit) {
        connectionConfigBuilder.setConnectTimeout(connectTimeout, timeUnit);
        return this;
    }
    public VespaHttpClientBuilder connectTimeout(Timeout connectTimeout) {
        connectionConfigBuilder.setConnectTimeout(connectTimeout);
        return this;
    }
    public VespaHttpClientBuilder socketTimeout(long connectTimeout, TimeUnit timeUnit) {
        connectionConfigBuilder.setConnectTimeout(connectTimeout, timeUnit);
        return this;
    }
    public VespaHttpClientBuilder validateAfterInactivity(TimeValue validateAfterInactivity) {
        connectionConfigBuilder.setValidateAfterInactivity(validateAfterInactivity);
        return this;
    }
    public VespaHttpClientBuilder socketTimeout(Timeout connectTimeout) {
        connectionConfigBuilder.setConnectTimeout(connectTimeout);
        return this;
    }

    public HttpClientBuilder apacheBuilder() {
        HttpClientBuilder builder = HttpClientBuilder.create();
        addSslSocketFactory(builder, new HttpClientConnectionManagerFactoryProxy(), hostnameVerifier);
        if (rewriteHttpToHttps)
            addHttpsRewritingRoutePlanner(builder);

        builder.disableConnectionState(); // Share connections between subsequent requests.
        builder.disableCookieManagement();
        builder.disableAuthCaching();
        builder.disableRedirectHandling();

        return builder;
    }
    public CloseableHttpClient buildClient() {
        return apacheBuilder().build();
    }

    private class HttpClientConnectionManagerFactoryProxy implements HttpClientConnectionManagerFactory {
        @Override
        public PoolingHttpClientConnectionManager create(Registry<ConnectionSocketFactory> socketFactories) {
            PoolingHttpClientConnectionManager manager = connectionManagerFactory.create(socketFactories);
            manager.setDefaultConnectionConfig(connectionConfigBuilder.build());
            return manager;
        }
    }

    private static void addSslSocketFactory(HttpClientBuilder builder, HttpClientConnectionManagerFactory connectionManagerFactory,
                                            HostnameVerifier hostnameVerifier) {
        getSystemTlsContext().ifPresent(tlsContext -> {
            SSLConnectionSocketFactory socketFactory = SslConnectionSocketFactory.of(tlsContext, hostnameVerifier);
            builder.setConnectionManager(connectionManagerFactory.create(createRegistry(socketFactory)));
            // Workaround that allows re-using https connections, see https://stackoverflow.com/a/42112034/1615280 for details.
            // Proper solution would be to add a request interceptor that adds a x500 principal as user token,
            // but certificate subject CN is not accessible through the TlsContext currently.
            builder.setUserTokenHandler((route, context) -> null);
        });
    }

    private static Registry<ConnectionSocketFactory> createRegistry(SSLConnectionSocketFactory sslSocketFactory) {
        return RegistryBuilder.<ConnectionSocketFactory>create()
                .register("https", sslSocketFactory)
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .build();
    }

    private static void addHttpsRewritingRoutePlanner(HttpClientBuilder builder) {
        if (isTransportSecurityEnabled() && getInsecureMixedMode() != PLAINTEXT_CLIENT_MIXED_SERVER)
            builder.setRoutePlanner(new HttpToHttpsRoutePlanner());
    }

}