All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.gravitee.gateway.http.connector.AbstractConnector Maven / Gradle / Ivy

There is a newer version: 3.13.1
Show newest version
/**
 * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
 *
 * 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 io.gravitee.gateway.http.connector;

import io.gravitee.common.component.AbstractLifecycleComponent;
import io.gravitee.common.http.HttpHeaders;
import io.gravitee.common.util.MultiValueMap;
import io.gravitee.definition.model.HttpClientSslOptions;
import io.gravitee.definition.model.HttpProxy;
import io.gravitee.definition.model.ProtocolVersion;
import io.gravitee.definition.model.endpoint.HttpEndpoint;
import io.gravitee.definition.model.ssl.jks.JKSKeyStore;
import io.gravitee.definition.model.ssl.jks.JKSTrustStore;
import io.gravitee.definition.model.ssl.pem.PEMKeyStore;
import io.gravitee.definition.model.ssl.pem.PEMTrustStore;
import io.gravitee.definition.model.ssl.pkcs12.PKCS12KeyStore;
import io.gravitee.definition.model.ssl.pkcs12.PKCS12TrustStore;
import io.gravitee.gateway.api.Connector;
import io.gravitee.gateway.api.proxy.ProxyConnection;
import io.gravitee.gateway.api.proxy.ProxyRequest;
import io.gravitee.gateway.core.endpoint.EndpointException;
import io.vertx.core.Context;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpVersion;
import io.vertx.core.net.*;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;

/**
 * @author David BRASSELY (david.brassely at graviteesource.com)
 * @author GraviteeSource Team
 */
public abstract class AbstractConnector extends AbstractLifecycleComponent implements Connector {

    private static final String URI_PARAM_SEPARATOR = "&";
    private static final char URI_PARAM_SEPARATOR_CHAR = '&';
    private static final char URI_PARAM_VALUE_SEPARATOR_CHAR = '=';
    private static final char URI_QUERY_DELIMITER_CHAR = '?';
    private static final CharSequence URI_QUERY_DELIMITER_CHAR_SEQUENCE = "?";

    private final Logger LOGGER = LoggerFactory.getLogger(AbstractConnector.class);

    protected static final int UNSECURE_PORT = 80;
    protected static final int SECURE_PORT = 443;

    @Autowired
    private Vertx vertx;

    @Autowired
    private Environment environment;

    protected final T endpoint;

    private HttpClientOptions options;

    /**
     * Dummy {@link URLStreamHandler} implementation to avoid unknown protocol issue with default implementation
     * (which knows how to handle only http and https protocol).
     */
    private final URLStreamHandler URL_HANDLER = new URLStreamHandler() {
        @Override
        protected URLConnection openConnection(URL u) {
            return null;
        }
    };

    @Autowired
    public AbstractConnector(T endpoint) {
        this.endpoint = endpoint;
    }

    private final Map httpClients = new ConcurrentHashMap<>();

    private final AtomicInteger requestTracker = new AtomicInteger(0);

    @Override
    public ProxyConnection request(ProxyRequest proxyRequest) {
        // For Vertx HTTP client query parameters have to be passed along the URI
        final String uri = appendQueryParameters(proxyRequest.uri(), proxyRequest.parameters());

        // Add the endpoint reference in metrics to know which endpoint has been invoked while serving the request
        proxyRequest.metrics().setEndpoint(uri);

        try {
            final URL url = new URL(null, uri, URL_HANDLER);

            final String protocol = url.getProtocol();

            final int port = url.getPort() != -1
                ? url.getPort()
                : protocol.charAt(protocol.length() - 1) == 's' ? SECURE_PORT : UNSECURE_PORT;

            final String host = (port == UNSECURE_PORT || port == SECURE_PORT) ? url.getHost() : url.getHost() + ':' + port;

            proxyRequest.headers().set(HttpHeaders.HOST, host);

            // Enhance proxy request with endpoint configuration
            if (endpoint.getHeaders() != null && !endpoint.getHeaders().isEmpty()) {
                endpoint.getHeaders().forEach(proxyRequest.headers()::set);
            }

            // Create the connector to the upstream
            final AbstractHttpProxyConnection connection = create(proxyRequest);

            // Grab an instance of the HTTP client
            final HttpClient client = httpClients.computeIfAbsent(Vertx.currentContext(), createHttpClient());

            requestTracker.incrementAndGet();

            // Connect to the upstream
            return connection.connect(
                client,
                port,
                url.getHost(),
                (url.getQuery() == null) ? url.getPath() : url.getPath() + URI_QUERY_DELIMITER_CHAR + url.getQuery(),
                result -> requestTracker.decrementAndGet()
            );
        } catch (MalformedURLException ex) {
            throw new IllegalArgumentException();
        }
    }

    protected abstract AbstractHttpProxyConnection create(ProxyRequest proxyRequest);

    @Override
    protected void doStart() throws Exception {
        this.options = this.getOptions();
        printHttpClientConfiguration();
    }

    private String appendQueryParameters(String uri, MultiValueMap parameters) {
        if (parameters != null && !parameters.isEmpty()) {
            StringJoiner parametersAsString = new StringJoiner(URI_PARAM_SEPARATOR);
            parameters.forEach(
                (paramName, paramValues) -> {
                    if (paramValues != null) {
                        for (String paramValue : paramValues) {
                            if (paramValue == null) {
                                parametersAsString.add(paramName);
                            } else {
                                parametersAsString.add(paramName + URI_PARAM_VALUE_SEPARATOR_CHAR + paramValue);
                            }
                        }
                    }
                }
            );

            if (uri.contains(URI_QUERY_DELIMITER_CHAR_SEQUENCE)) {
                return uri + URI_PARAM_SEPARATOR_CHAR + parametersAsString.toString();
            } else {
                return uri + URI_QUERY_DELIMITER_CHAR + parametersAsString.toString();
            }
        } else {
            return uri;
        }
    }

    protected HttpClientOptions getOptions() throws EndpointException {
        HttpClientOptions options = new HttpClientOptions();

        options.setPipelining(endpoint.getHttpClientOptions().isPipelining());
        options.setKeepAlive(endpoint.getHttpClientOptions().isKeepAlive());
        options.setIdleTimeout((int) (endpoint.getHttpClientOptions().getIdleTimeout() / 1000));
        options.setConnectTimeout((int) endpoint.getHttpClientOptions().getConnectTimeout());
        options.setUsePooledBuffers(true);
        options.setMaxPoolSize(endpoint.getHttpClientOptions().getMaxConcurrentConnections());
        options.setTryUseCompression(endpoint.getHttpClientOptions().isUseCompression());

        if (endpoint.getHttpClientOptions().getVersion() == ProtocolVersion.HTTP_2) {
            options.setProtocolVersion(HttpVersion.HTTP_2);
            options.setHttp2ClearTextUpgrade(endpoint.getHttpClientOptions().isClearTextUpgrade());
            options.setHttp2MaxPoolSize(endpoint.getHttpClientOptions().getMaxConcurrentConnections());
        }

        URL target;
        try {
            target = new URL(null, endpoint.getTarget(), URL_HANDLER);
        } catch (MalformedURLException e) {
            throw new EndpointException("Endpoint target is not valid " + endpoint.getTarget());
        }

        // Configure proxy
        HttpProxy proxy = endpoint.getHttpProxy();
        if (proxy != null && proxy.isEnabled()) {
            ProxyOptions proxyOptions;

            if (proxy.isUseSystemProxy()) {
                proxyOptions = getSystemProxyOptions();
            } else {
                proxyOptions = new ProxyOptions();
                proxyOptions.setHost(proxy.getHost());
                proxyOptions.setPort(proxy.getPort());
                proxyOptions.setUsername(proxy.getUsername());
                proxyOptions.setPassword(proxy.getPassword());
                proxyOptions.setType(ProxyType.valueOf(proxy.getType().name()));
            }
            options.setProxyOptions(proxyOptions);
        }

        HttpClientSslOptions sslOptions = endpoint.getHttpClientSslOptions();

        final String protocol = target.getProtocol();

        if (protocol.charAt(protocol.length() - 1) == 's') {
            // Configure SSL
            options.setSsl(true).setUseAlpn(true);

            if (sslOptions != null) {
                options.setVerifyHost(sslOptions.isHostnameVerifier()).setTrustAll(sslOptions.isTrustAll());

                // Client trust configuration
                if (!sslOptions.isTrustAll() && sslOptions.getTrustStore() != null) {
                    switch (sslOptions.getTrustStore().getType()) {
                        case PEM:
                            PEMTrustStore pemTrustStore = (PEMTrustStore) sslOptions.getTrustStore();
                            PemTrustOptions pemTrustOptions = new PemTrustOptions();
                            if (pemTrustStore.getPath() != null && !pemTrustStore.getPath().isEmpty()) {
                                pemTrustOptions.addCertPath(pemTrustStore.getPath());
                            } else if (pemTrustStore.getContent() != null && !pemTrustStore.getContent().isEmpty()) {
                                pemTrustOptions.addCertValue(io.vertx.core.buffer.Buffer.buffer(pemTrustStore.getContent()));
                            } else {
                                throw new EndpointException("Missing PEM certificate value for endpoint " + endpoint.getName());
                            }
                            options.setPemTrustOptions(pemTrustOptions);
                            break;
                        case PKCS12:
                            PKCS12TrustStore pkcs12TrustStore = (PKCS12TrustStore) sslOptions.getTrustStore();
                            PfxOptions pfxOptions = new PfxOptions();
                            pfxOptions.setPassword(pkcs12TrustStore.getPassword());
                            if (pkcs12TrustStore.getPath() != null && !pkcs12TrustStore.getPath().isEmpty()) {
                                pfxOptions.setPath(pkcs12TrustStore.getPath());
                            } else if (pkcs12TrustStore.getContent() != null && !pkcs12TrustStore.getContent().isEmpty()) {
                                pfxOptions.setValue(io.vertx.core.buffer.Buffer.buffer(pkcs12TrustStore.getContent()));
                            } else {
                                throw new EndpointException("Missing PKCS12 value for endpoint " + endpoint.getName());
                            }
                            options.setPfxTrustOptions(pfxOptions);
                            break;
                        case JKS:
                            JKSTrustStore jksTrustStore = (JKSTrustStore) sslOptions.getTrustStore();
                            JksOptions jksOptions = new JksOptions();
                            jksOptions.setPassword(jksTrustStore.getPassword());
                            if (jksTrustStore.getPath() != null && !jksTrustStore.getPath().isEmpty()) {
                                jksOptions.setPath(jksTrustStore.getPath());
                            } else if (jksTrustStore.getContent() != null && !jksTrustStore.getContent().isEmpty()) {
                                jksOptions.setValue(io.vertx.core.buffer.Buffer.buffer(jksTrustStore.getContent()));
                            } else {
                                throw new EndpointException("Missing JKS value for endpoint " + endpoint.getName());
                            }
                            options.setTrustStoreOptions(jksOptions);
                            break;
                    }
                }

                // Client authentication configuration
                if (sslOptions.getKeyStore() != null) {
                    switch (sslOptions.getKeyStore().getType()) {
                        case PEM:
                            PEMKeyStore pemKeyStore = (PEMKeyStore) sslOptions.getKeyStore();
                            PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions();
                            if (pemKeyStore.getCertPath() != null && !pemKeyStore.getCertPath().isEmpty()) {
                                pemKeyCertOptions.setCertPath(pemKeyStore.getCertPath());
                            } else if (pemKeyStore.getCertContent() != null && !pemKeyStore.getCertContent().isEmpty()) {
                                pemKeyCertOptions.setCertValue(io.vertx.core.buffer.Buffer.buffer(pemKeyStore.getCertContent()));
                            }
                            if (pemKeyStore.getKeyPath() != null && !pemKeyStore.getKeyPath().isEmpty()) {
                                pemKeyCertOptions.setKeyPath(pemKeyStore.getKeyPath());
                            } else if (pemKeyStore.getKeyContent() != null && !pemKeyStore.getKeyContent().isEmpty()) {
                                pemKeyCertOptions.setKeyValue(io.vertx.core.buffer.Buffer.buffer(pemKeyStore.getKeyContent()));
                            }
                            options.setPemKeyCertOptions(pemKeyCertOptions);
                            break;
                        case PKCS12:
                            PKCS12KeyStore pkcs12KeyStore = (PKCS12KeyStore) sslOptions.getKeyStore();
                            PfxOptions pfxOptions = new PfxOptions();
                            pfxOptions.setPassword(pkcs12KeyStore.getPassword());
                            if (pkcs12KeyStore.getPath() != null && !pkcs12KeyStore.getPath().isEmpty()) {
                                pfxOptions.setPath(pkcs12KeyStore.getPath());
                            } else if (pkcs12KeyStore.getContent() != null && !pkcs12KeyStore.getContent().isEmpty()) {
                                pfxOptions.setValue(io.vertx.core.buffer.Buffer.buffer(pkcs12KeyStore.getContent()));
                            }
                            options.setPfxKeyCertOptions(pfxOptions);
                            break;
                        case JKS:
                            JKSKeyStore jksKeyStore = (JKSKeyStore) sslOptions.getKeyStore();
                            JksOptions jksOptions = new JksOptions();
                            jksOptions.setPassword(jksKeyStore.getPassword());
                            if (jksKeyStore.getPath() != null && !jksKeyStore.getPath().isEmpty()) {
                                jksOptions.setPath(jksKeyStore.getPath());
                            } else if (jksKeyStore.getContent() != null && !jksKeyStore.getContent().isEmpty()) {
                                jksOptions.setValue(io.vertx.core.buffer.Buffer.buffer(jksKeyStore.getContent()));
                            }
                            options.setKeyStoreOptions(jksOptions);
                            break;
                    }
                }
            }
        }

        return options;
    }

    @Override
    protected void doStop() throws Exception {
        LOGGER.info(
            "Graceful shutdown of HTTP Client for endpoint[{}] target[{}] requests[{}]",
            endpoint.getName(),
            endpoint.getTarget(),
            requestTracker.get()
        );
        long shouldEndAt = System.currentTimeMillis() + endpoint.getHttpClientOptions().getReadTimeout();

        while (requestTracker.get() != 0 && System.currentTimeMillis() <= shouldEndAt) {
            TimeUnit.MILLISECONDS.sleep(100);
        }

        if (requestTracker.get() > 0) {
            LOGGER.warn("Cancel requests[{}] for endpoint[{}] target[{}]", requestTracker.get(), endpoint.getName(), endpoint.getTarget());
        }

        httpClients
            .values()
            .forEach(
                httpClient -> {
                    try {
                        httpClient.close();
                    } catch (IllegalStateException ise) {
                        LOGGER.warn(ise.getMessage());
                    }
                }
            );
    }

    private Function createHttpClient() {
        return context -> vertx.createHttpClient(options);
    }

    private void printHttpClientConfiguration() {
        LOGGER.info("Create HTTP connector with configuration: ");
        LOGGER.info(
            "\t" +
            options.getProtocolVersion() +
            " {" +
            "ConnectTimeout='" +
            options.getConnectTimeout() +
            '\'' +
            ", KeepAlive='" +
            options.isKeepAlive() +
            '\'' +
            ", IdleTimeout='" +
            options.getIdleTimeout() +
            '\'' +
            ", MaxChunkSize='" +
            options.getMaxChunkSize() +
            '\'' +
            ", MaxPoolSize='" +
            options.getMaxPoolSize() +
            '\'' +
            ", MaxWaitQueueSize='" +
            options.getMaxWaitQueueSize() +
            '\'' +
            ", Pipelining='" +
            options.isPipelining() +
            '\'' +
            ", PipeliningLimit='" +
            options.getPipeliningLimit() +
            '\'' +
            ", TryUseCompression='" +
            options.isTryUseCompression() +
            '\'' +
            '}'
        );

        if (options.isSsl()) {
            LOGGER.info("\tSSL {" + "TrustAll='" + options.isTrustAll() + '\'' + ", VerifyHost='" + options.isVerifyHost() + '\'' + '}');
        }

        if (options.getProxyOptions() != null) {
            LOGGER.info(
                "\tProxy {" +
                "Type='" +
                options.getProxyOptions().getType() +
                ", Host='" +
                options.getProxyOptions().getHost() +
                '\'' +
                ", Port='" +
                options.getProxyOptions().getPort() +
                '\'' +
                '}'
            );
        }
    }

    private ProxyOptions getSystemProxyOptions() {
        StringBuilder errors = new StringBuilder();
        ProxyOptions proxyOptions = new ProxyOptions();

        // System proxy must be well configured. Check that this is the case.
        if (environment.containsProperty("system.proxy.host")) {
            proxyOptions.setHost(environment.getProperty("system.proxy.host"));
        } else {
            errors.append("'system.proxy.host' ");
        }

        try {
            proxyOptions.setPort(Integer.parseInt(Objects.requireNonNull(environment.getProperty("system.proxy.port"))));
        } catch (Exception e) {
            errors.append("'system.proxy.port' [").append(environment.getProperty("system.proxy.port")).append("] ");
        }

        try {
            proxyOptions.setType(ProxyType.valueOf(environment.getProperty("system.proxy.type")));
        } catch (Exception e) {
            errors.append("'system.proxy.type' [").append(environment.getProperty("system.proxy.type")).append("] ");
        }

        proxyOptions.setUsername(environment.getProperty("system.proxy.username"));
        proxyOptions.setPassword(environment.getProperty("system.proxy.password"));

        if (errors.length() == 0) {
            return proxyOptions;
        } else {
            LOGGER.warn(
                "An api endpoint (name[{}] type[{}] target[{}]) requires a system proxy to be defined but some configurations are missing or not well defined: {}",
                endpoint.getName(),
                endpoint.getType(),
                endpoint.getTarget(),
                errors
            );
            LOGGER.warn("Ignoring system proxy");
            return null;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy