io.gravitee.gateway.http.connector.VertxHttpClient.java Source code

Java tutorial

Introduction

Here is the source code for io.gravitee.gateway.http.connector.VertxHttpClient.java

Source

/**
 * 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.http.HttpHeadersValues;
import io.gravitee.common.http.HttpStatusCode;
import io.gravitee.definition.model.HttpClientSslOptions;
import io.gravitee.definition.model.HttpProxy;
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.buffer.Buffer;
import io.gravitee.gateway.api.proxy.ProxyConnection;
import io.gravitee.gateway.api.proxy.ProxyRequest;
import io.gravitee.gateway.api.proxy.ProxyResponse;
import io.gravitee.gateway.api.proxy.ws.WebSocketProxyRequest;
import io.gravitee.gateway.core.endpoint.EndpointException;
import io.gravitee.gateway.core.proxy.EmptyProxyResponse;
import io.gravitee.gateway.core.proxy.ws.SwitchProtocolProxyResponse;
import io.gravitee.gateway.http.connector.ws.VertxWebSocketFrame;
import io.gravitee.gateway.http.connector.ws.VertxWebSocketProxyConnection;
import io.netty.channel.ConnectTimeoutException;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.vertx.core.Context;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.*;
import io.vertx.core.net.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;

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

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

    private static final String HTTPS_SCHEME = "https";
    private static final int DEFAULT_HTTP_PORT = 80;
    private static final int DEFAULT_HTTPS_PORT = 443;
    private static final Set<CharSequence> HOP_HEADERS;

    private static final Set<CharSequence> WS_HOP_HEADERS;

    static {
        Set<CharSequence> hopHeaders = new HashSet<>();
        Set<CharSequence> wsHopHeaders = new HashSet<>();

        // Hop-by-hop headers
        hopHeaders.add(HttpHeaderNames.CONNECTION);
        hopHeaders.add(HttpHeaderNames.KEEP_ALIVE);
        hopHeaders.add(HttpHeaderNames.PROXY_AUTHORIZATION);
        hopHeaders.add(HttpHeaderNames.PROXY_AUTHENTICATE);
        hopHeaders.add(HttpHeaderNames.PROXY_CONNECTION);
        hopHeaders.add(HttpHeaderNames.TE);
        hopHeaders.add(HttpHeaderNames.TRAILER);
        hopHeaders.add(HttpHeaderNames.UPGRADE);

        // Hop-by-hop headers Websocket
        wsHopHeaders.add(HttpHeaderNames.KEEP_ALIVE);
        wsHopHeaders.add(HttpHeaderNames.PROXY_AUTHORIZATION);
        wsHopHeaders.add(HttpHeaderNames.PROXY_AUTHENTICATE);
        wsHopHeaders.add(HttpHeaderNames.PROXY_CONNECTION);
        wsHopHeaders.add(HttpHeaderNames.TE);
        wsHopHeaders.add(HttpHeaderNames.TRAILER);

        HOP_HEADERS = Collections.unmodifiableSet(hopHeaders);
        WS_HOP_HEADERS = Collections.unmodifiableSet(wsHopHeaders);
    }

    @Autowired
    private Vertx vertx;

    private final HttpEndpoint endpoint;

    private HttpClientOptions httpClientOptions;

    @Autowired
    public VertxHttpClient(HttpEndpoint endpoint) {
        this.endpoint = endpoint;
    }

    private final Map<Context, HttpClient> httpClients = new HashMap<>();

    @Override
    public ProxyConnection request(ProxyRequest proxyRequest) {
        HttpClient httpClient = httpClients.computeIfAbsent(Vertx.currentContext(), createHttpClient());

        // Remove hop-by-hop headers.
        if (!proxyRequest.isWebSocket()) {
            for (CharSequence header : HOP_HEADERS) {
                proxyRequest.headers().remove(header);
            }
        } else {
            for (CharSequence header : WS_HOP_HEADERS) {
                proxyRequest.headers().remove(header);
            }
        }

        final URI uri = proxyRequest.uri();
        final int port = uri.getPort() != -1 ? uri.getPort() : (HTTPS_SCHEME.equals(uri.getScheme()) ? 443 : 80);

        final String host = (port == DEFAULT_HTTP_PORT || port == DEFAULT_HTTPS_PORT) ? uri.getHost()
                : uri.getHost() + ':' + port;

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

        // Apply headers from endpoint
        if (endpoint.getHeaders() != null && !endpoint.getHeaders().isEmpty()) {
            endpoint.getHeaders().forEach(proxyRequest.headers()::set);
        }

        String relativeUri = (uri.getRawQuery() == null) ? uri.getRawPath()
                : uri.getRawPath() + '?' + uri.getRawQuery();

        if (proxyRequest.isWebSocket()) {
            VertxWebSocketProxyConnection webSocketProxyConnection = new VertxWebSocketProxyConnection();
            WebSocketProxyRequest wsProxyRequest = (WebSocketProxyRequest) proxyRequest;

            httpClient.websocket(port, uri.getHost(), relativeUri, new Handler<WebSocket>() {
                @Override
                public void handle(WebSocket event) {
                    // The client -> gateway connection must be upgraded now that the one between gateway -> upstream
                    // has been accepted
                    wsProxyRequest.upgrade();

                    // From server to client
                    wsProxyRequest.frameHandler(frame -> {
                        if (frame.type() == io.gravitee.gateway.api.ws.WebSocketFrame.Type.BINARY) {
                            event.writeBinaryMessage(io.vertx.core.buffer.Buffer.buffer(frame.data().getBytes()));
                        } else if (frame.type() == io.gravitee.gateway.api.ws.WebSocketFrame.Type.TEXT) {
                            event.writeTextMessage(frame.data().toString());
                        }
                    });

                    wsProxyRequest.closeHandler(result -> event.close());

                    // From client to server
                    event.frameHandler(frame -> wsProxyRequest.write(new VertxWebSocketFrame(frame)));

                    event.closeHandler(event1 -> wsProxyRequest.close());

                    event.exceptionHandler(new Handler<Throwable>() {
                        @Override
                        public void handle(Throwable throwable) {
                            wsProxyRequest.reject(HttpStatusCode.BAD_REQUEST_400);
                            ProxyResponse clientResponse = new EmptyProxyResponse(HttpStatusCode.BAD_REQUEST_400);

                            clientResponse.headers().set(HttpHeaders.CONNECTION,
                                    HttpHeadersValues.CONNECTION_CLOSE);
                            webSocketProxyConnection.handleResponse(clientResponse);
                        }
                    });

                    // Tell the reactor that the request has been handled by the HTTP client
                    webSocketProxyConnection.handleResponse(new SwitchProtocolProxyResponse());
                }
            }, throwable -> {
                if (throwable instanceof WebsocketRejectedException) {
                    wsProxyRequest.reject(((WebsocketRejectedException) throwable).getStatus());
                    ProxyResponse clientResponse = new EmptyProxyResponse(
                            ((WebsocketRejectedException) throwable).getStatus());

                    clientResponse.headers().set(HttpHeaders.CONNECTION, HttpHeadersValues.CONNECTION_CLOSE);
                    webSocketProxyConnection.handleResponse(clientResponse);
                } else {
                    wsProxyRequest.reject(HttpStatusCode.BAD_GATEWAY_502);
                    ProxyResponse clientResponse = new EmptyProxyResponse(HttpStatusCode.BAD_GATEWAY_502);

                    clientResponse.headers().set(HttpHeaders.CONNECTION, HttpHeadersValues.CONNECTION_CLOSE);
                    webSocketProxyConnection.handleResponse(clientResponse);
                }
            });

            return webSocketProxyConnection;
        } else {
            // Prepare HTTP request
            HttpClientRequest clientRequest = httpClient.request(HttpMethod.valueOf(proxyRequest.method().name()),
                    port, uri.getHost(), relativeUri);
            clientRequest.setTimeout(endpoint.getHttpClientOptions().getReadTimeout());
            clientRequest.setFollowRedirects(endpoint.getHttpClientOptions().isFollowRedirects());

            if (proxyRequest.method() == io.gravitee.common.http.HttpMethod.OTHER) {
                clientRequest.setRawMethod(proxyRequest.rawMethod());
            }

            VertxProxyConnection proxyConnection = new VertxProxyConnection(proxyRequest, clientRequest);
            clientRequest.handler(
                    clientResponse -> handleClientResponse(proxyConnection, clientResponse, clientRequest));

            clientRequest.connectionHandler(connection -> {
                connection.exceptionHandler(ex -> {
                    // I don't want to fill my logs with error
                });
            });

            clientRequest.exceptionHandler(event -> {
                if (!proxyConnection.isCanceled() && !proxyConnection.isTransmitted()) {
                    proxyRequest.metrics().setMessage(event.getMessage());

                    if (proxyConnection.timeoutHandler() != null && (event instanceof ConnectException
                            || event instanceof TimeoutException || event instanceof NoRouteToHostException
                            || event instanceof UnknownHostException)) {
                        proxyConnection.handleConnectTimeout(event);
                    } else {
                        ProxyResponse clientResponse = new EmptyProxyResponse(
                                ((event instanceof ConnectTimeoutException) || (event instanceof TimeoutException))
                                        ? HttpStatusCode.GATEWAY_TIMEOUT_504
                                        : HttpStatusCode.BAD_GATEWAY_502);

                        clientResponse.headers().set(HttpHeaders.CONNECTION, HttpHeadersValues.CONNECTION_CLOSE);
                        proxyConnection.handleResponse(clientResponse);
                    }
                }
            });

            return proxyConnection;
        }
    }

    private void handleClientResponse(final VertxProxyConnection proxyConnection,
            final HttpClientResponse clientResponse, final HttpClientRequest clientRequest) {
        VertxProxyResponse proxyClientResponse = new VertxProxyResponse(clientResponse);
        proxyConnection.setProxyResponse(proxyClientResponse);

        // Copy HTTP headers
        clientResponse.headers().names().forEach(headerName -> proxyClientResponse.headers().put(headerName,
                clientResponse.headers().getAll(headerName)));

        proxyClientResponse.pause();

        // Copy body content
        clientResponse.handler(event -> proxyClientResponse.bodyHandler().handle(Buffer.buffer(event.getBytes())));

        // Signal end of the response
        clientResponse.endHandler(v -> proxyClientResponse.endHandler().handle(null));

        clientResponse.exceptionHandler(throwable -> {
            LOGGER.error("Unexpected error while handling backend response for request {} {} - {}",
                    clientRequest.method(), clientRequest.absoluteURI(), throwable.getMessage());
            ProxyResponse clientResponse1 = new EmptyProxyResponse(HttpStatusCode.BAD_GATEWAY_502);

            proxyConnection.handleResponse(clientResponse1);
        });

        proxyConnection.handleResponse(proxyClientResponse);
    }

    @Override
    protected void doStart() throws Exception {
        httpClientOptions = new HttpClientOptions();

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

        // Configure proxy
        HttpProxy proxy = endpoint.getHttpProxy();
        if (proxy != null && proxy.isEnabled()) {
            ProxyOptions 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()));

            httpClientOptions.setProxyOptions(proxyOptions);
        }

        URI target = URI.create(endpoint.getTarget());
        HttpClientSslOptions sslOptions = endpoint.getHttpClientSslOptions();

        if (HTTPS_SCHEME.equalsIgnoreCase(target.getScheme())) {
            // Configure SSL
            httpClientOptions.setSsl(true);

            if (sslOptions != null) {
                httpClientOptions.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());
                        }
                        this.httpClientOptions.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());
                        }
                        this.httpClientOptions.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());
                        }
                        this.httpClientOptions.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()));
                        }
                        this.httpClientOptions.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()));
                        }
                        this.httpClientOptions.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()));
                        }
                        this.httpClientOptions.setKeyStoreOptions(jksOptions);
                        break;
                    }
                }
            }
        }

        printHttpClientConfiguration(httpClientOptions);
    }

    @Override
    protected void doStop() throws Exception {
        LOGGER.info("Closing HTTP Client for '{}' endpoint [{}]", endpoint.getName(), endpoint.getTarget());

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

    private Function<Context, HttpClient> createHttpClient() {
        return context -> vertx.createHttpClient(httpClientOptions);
    }

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

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

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