OssInternalHttpClientProvider.java

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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.apache.doris.httpv2.client;

import org.apache.doris.common.Config;
import org.apache.doris.common.util.InternalHttpsUtils;
import org.apache.doris.common.util.NetUtils;
import org.apache.doris.tls.server.TlsProtocolSet;

import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HttpsURLConnection;

public class OssInternalHttpClientProvider implements InternalHttpClientProvider {
    private static final int MAX_CONN_TOTAL = 256;
    private static final int MAX_CONN_PER_ROUTE = 64;

    private final CloseableHttpClient httpClient;
    private final RestTemplate restTemplate;
    private volatile HttpsClients httpsClients;

    public OssInternalHttpClientProvider() {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(MAX_CONN_TOTAL);
        connectionManager.setDefaultMaxPerRoute(MAX_CONN_PER_ROUTE);
        httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .evictExpiredConnections()
                .evictIdleConnections(30, TimeUnit.SECONDS)
                .build();
        restTemplate = new RestTemplate();
    }

    @Override
    public String normalizeInternalUrl(String url, Target target) {
        if (TlsProtocolSet.isHttpTlsActive()) {
            throw new UnsupportedOperationException("FE HTTP TLS requires TLS module");
        }
        if (!Config.enable_https || target != Target.FE || isHttps(url)) {
            return url;
        }
        return rewriteSchemeAndPort(url, "https", Config.https_port);
    }

    @Override
    public HttpURLConnection openConnection(String normalizedUrl, Target target) throws IOException {
        HttpURLConnection connection = (HttpURLConnection) new URL(normalizedUrl).openConnection();
        if (connection instanceof HttpsURLConnection && Config.enable_https && target == Target.FE) {
            HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
            httpsConnection.setSSLSocketFactory(InternalHttpsUtils.getSslContext().getSocketFactory());
            httpsConnection.setHostnameVerifier(NoopHostnameVerifier.INSTANCE);
        }
        return connection;
    }

    @Override
    public CloseableHttpClient getHttpClient(Target target) {
        if (Config.enable_https && target == Target.FE) {
            return getHttpsClients().httpClient;
        }
        return httpClient;
    }

    @Override
    public RestTemplate getRestTemplate(Target target) {
        if (!Config.enable_https || target != Target.FE) {
            return restTemplate;
        }
        return getHttpsClients().restTemplate;
    }

    private HttpsClients getHttpsClients() {
        HttpsClients clients = httpsClients;
        if (clients != null) {
            return clients;
        }
        synchronized (this) {
            clients = httpsClients;
            if (clients == null) {
                clients = new HttpsClients(
                        InternalHttpsUtils.createValidatedHttpClient(),
                        new RestTemplate(new InternalHttpsClientHttpRequestFactory()));
                httpsClients = clients;
            }
        }
        return clients;
    }

    private static boolean isHttps(String url) {
        return url != null && url.regionMatches(true, 0, "https://", 0, "https://".length());
    }

    private static String rewriteSchemeAndPort(String url, String scheme, int port) {
        try {
            URI uri = new URI(url);
            if (uri.getHost() == null) {
                throw new IllegalArgumentException("Internal HTTP URL has no host: " + url);
            }

            StringBuilder rewritten = new StringBuilder(scheme).append("://");
            if (uri.getRawUserInfo() != null) {
                rewritten.append(uri.getRawUserInfo()).append('@');
            }
            rewritten.append(NetUtils.getHostPortInAccessibleFormat(uri.getHost(), port));
            if (uri.getRawPath() != null) {
                rewritten.append(uri.getRawPath());
            }
            if (uri.getRawQuery() != null) {
                rewritten.append('?').append(uri.getRawQuery());
            }
            if (uri.getRawFragment() != null) {
                rewritten.append('#').append(uri.getRawFragment());
            }
            return rewritten.toString();
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException("Invalid internal HTTP URL: " + url, e);
        }
    }

    private static class InternalHttpsClientHttpRequestFactory extends SimpleClientHttpRequestFactory {
        @Override
        protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
            super.prepareConnection(connection, httpMethod);
            if (connection instanceof HttpsURLConnection) {
                HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
                httpsConnection.setSSLSocketFactory(InternalHttpsUtils.getSslContext().getSocketFactory());
                httpsConnection.setHostnameVerifier(NoopHostnameVerifier.INSTANCE);
            }
        }
    }

    private static class HttpsClients {
        private final CloseableHttpClient httpClient;
        private final RestTemplate restTemplate;

        private HttpsClients(CloseableHttpClient httpClient, RestTemplate restTemplate) {
            this.httpClient = httpClient;
            this.restTemplate = restTemplate;
        }
    }
}