org.elasticsearch.xpack.watcher.common.http.HttpClient.java Source code

Java tutorial

Introduction

Here is the source code for org.elasticsearch.xpack.watcher.common.http.HttpClient.java

Source

/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License;
 * you may not use this file except in compliance with the Elastic License.
 */
package org.elasticsearch.xpack.watcher.common.http;

import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.core.internal.io.Streams;
import org.elasticsearch.xpack.core.common.socket.SocketAccess;
import org.elasticsearch.xpack.core.ssl.SSLService;
import org.elasticsearch.xpack.watcher.common.http.auth.ApplicableHttpAuth;
import org.elasticsearch.xpack.watcher.common.http.auth.HttpAuthRegistry;

import javax.net.ssl.HostnameVerifier;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HttpClient extends AbstractComponent implements Closeable {

    private static final String SETTINGS_SSL_PREFIX = "xpack.http.ssl.";
    // picking a reasonable high value here to allow for setups with lots of watch executions or many http inputs/actions
    // this is also used as the value per route, if you are connecting to the same endpoint a lot, which is likely, when
    // you are querying a remote Elasticsearch cluster
    private static final int MAX_CONNECTIONS = 500;

    private final HttpAuthRegistry httpAuthRegistry;
    private final CloseableHttpClient client;
    private final HttpProxy settingsProxy;
    private final TimeValue defaultConnectionTimeout;
    private final TimeValue defaultReadTimeout;
    private final ByteSizeValue maxResponseSize;

    public HttpClient(Settings settings, HttpAuthRegistry httpAuthRegistry, SSLService sslService) {
        super(settings);
        this.httpAuthRegistry = httpAuthRegistry;
        this.defaultConnectionTimeout = HttpSettings.CONNECTION_TIMEOUT.get(settings);
        this.defaultReadTimeout = HttpSettings.READ_TIMEOUT.get(settings);
        this.maxResponseSize = HttpSettings.MAX_HTTP_RESPONSE_SIZE.get(settings);
        this.settingsProxy = getProxyFromSettings();

        HttpClientBuilder clientBuilder = HttpClientBuilder.create();

        // ssl setup
        Settings sslSettings = settings.getByPrefix(SETTINGS_SSL_PREFIX);
        boolean isHostnameVerificationEnabled = sslService.getVerificationMode(sslSettings, Settings.EMPTY)
                .isHostnameVerificationEnabled();
        HostnameVerifier verifier = isHostnameVerificationEnabled ? new DefaultHostnameVerifier()
                : NoopHostnameVerifier.INSTANCE;
        SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(
                sslService.sslSocketFactory(sslSettings), verifier);
        clientBuilder.setSSLSocketFactory(factory);

        clientBuilder.evictExpiredConnections();
        clientBuilder.setMaxConnPerRoute(MAX_CONNECTIONS);
        clientBuilder.setMaxConnTotal(MAX_CONNECTIONS);

        client = clientBuilder.build();
    }

    public HttpResponse execute(HttpRequest request) throws IOException {
        URI uri = createURI(request);

        HttpRequestBase internalRequest;
        if (request.method == HttpMethod.HEAD) {
            internalRequest = new HttpHead(uri);
        } else {
            HttpMethodWithEntity methodWithEntity = new HttpMethodWithEntity(uri, request.method.name());
            if (request.hasBody()) {
                ByteArrayEntity entity = new ByteArrayEntity(request.body.getBytes(StandardCharsets.UTF_8));
                String contentType = request.headers().get(HttpHeaders.CONTENT_TYPE);
                if (Strings.hasLength(contentType)) {
                    entity.setContentType(contentType);
                } else {
                    entity.setContentType(ContentType.TEXT_PLAIN.toString());
                }
                methodWithEntity.setEntity(entity);
            }
            internalRequest = methodWithEntity;
        }
        internalRequest.setHeader(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());

        // headers
        if (request.headers().isEmpty() == false) {
            for (Map.Entry<String, String> entry : request.headers.entrySet()) {
                internalRequest.setHeader(entry.getKey(), entry.getValue());
            }
        }

        // BWC - hack for input requests made to elasticsearch that do not provide the right content-type header!
        if (request.hasBody() && internalRequest.containsHeader("Content-Type") == false) {
            XContentType xContentType = XContentFactory.xContentType(request.body());
            if (xContentType != null) {
                internalRequest.setHeader("Content-Type", xContentType.mediaType());
            }
        }

        RequestConfig.Builder config = RequestConfig.custom();
        setProxy(config, request, settingsProxy);
        HttpClientContext localContext = HttpClientContext.create();
        // auth
        if (request.auth() != null) {
            ApplicableHttpAuth applicableAuth = httpAuthRegistry.createApplicable(request.auth);
            CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
            applicableAuth.apply(credentialsProvider, new AuthScope(request.host, request.port));
            localContext.setCredentialsProvider(credentialsProvider);

            // preemptive auth, no need to wait for a 401 first
            AuthCache authCache = new BasicAuthCache();
            BasicScheme basicAuth = new BasicScheme();
            authCache.put(new HttpHost(request.host, request.port, request.scheme.scheme()), basicAuth);
            localContext.setAuthCache(authCache);
        }

        // timeouts
        if (request.connectionTimeout() != null) {
            config.setConnectTimeout(Math.toIntExact(request.connectionTimeout.millis()));
        } else {
            config.setConnectTimeout(Math.toIntExact(defaultConnectionTimeout.millis()));
        }

        if (request.readTimeout() != null) {
            config.setSocketTimeout(Math.toIntExact(request.readTimeout.millis()));
            config.setConnectionRequestTimeout(Math.toIntExact(request.readTimeout.millis()));
        } else {
            config.setSocketTimeout(Math.toIntExact(defaultReadTimeout.millis()));
            config.setConnectionRequestTimeout(Math.toIntExact(defaultReadTimeout.millis()));
        }

        internalRequest.setConfig(config.build());

        try (CloseableHttpResponse response = SocketAccess
                .doPrivileged(() -> client.execute(internalRequest, localContext))) {
            // headers
            Header[] headers = response.getAllHeaders();
            Map<String, String[]> responseHeaders = new HashMap<>(headers.length);
            for (Header header : headers) {
                if (responseHeaders.containsKey(header.getName())) {
                    String[] old = responseHeaders.get(header.getName());
                    String[] values = new String[old.length + 1];

                    System.arraycopy(old, 0, values, 0, old.length);
                    values[values.length - 1] = header.getValue();

                    responseHeaders.put(header.getName(), values);
                } else {
                    responseHeaders.put(header.getName(), new String[] { header.getValue() });
                }
            }

            final byte[] body;
            // not every response has a content, i.e. 204
            if (response.getEntity() == null) {
                body = new byte[0];
            } else {
                try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
                    try (InputStream is = new SizeLimitInputStream(maxResponseSize,
                            response.getEntity().getContent())) {
                        Streams.copy(is, outputStream);
                    }
                    body = outputStream.toByteArray();
                }
            }
            return new HttpResponse(response.getStatusLine().getStatusCode(), body, responseHeaders);
        }
    }

    /**
     * Enriches the config object optionally with proxy information
     *
     * @param config    The request builder config object
     * @param request   The request parsed into the HTTP client
     */
    static void setProxy(RequestConfig.Builder config, HttpRequest request, HttpProxy configuredProxy) {
        if (request.proxy != null && request.proxy.equals(HttpProxy.NO_PROXY) == false) {
            // if a proxy scheme is configured use this, but fall back to the same than the request in case there was no special
            // configuration given
            String scheme = request.proxy.getScheme() != null ? request.proxy.getScheme().scheme()
                    : Scheme.HTTP.scheme();
            HttpHost proxy = new HttpHost(request.proxy.getHost(), request.proxy.getPort(), scheme);
            config.setProxy(proxy);
        } else if (HttpProxy.NO_PROXY.equals(configuredProxy) == false) {
            HttpHost proxy = new HttpHost(configuredProxy.getHost(), configuredProxy.getPort(),
                    configuredProxy.getScheme().scheme());
            config.setProxy(proxy);
        }
    }

    /**
     * Creates a HTTP proxy from the system wide settings
     *
     * @return A http proxy instance, if no settings are configured this will be a HttpProxy.NO_PROXY instance
     */
    private HttpProxy getProxyFromSettings() {
        String proxyHost = HttpSettings.PROXY_HOST.get(settings);
        Scheme proxyScheme = HttpSettings.PROXY_SCHEME.exists(settings)
                ? Scheme.parse(HttpSettings.PROXY_SCHEME.get(settings))
                : Scheme.HTTP;
        int proxyPort = HttpSettings.PROXY_PORT.get(settings);
        if (proxyPort != 0 && Strings.hasText(proxyHost)) {
            logger.info("Using default proxy for http input and slack/hipchat/pagerduty/webhook actions [{}:{}]",
                    proxyHost, proxyPort);
        } else if (proxyPort != 0 ^ Strings.hasText(proxyHost)) {
            throw new IllegalArgumentException("HTTP proxy requires both settings: ["
                    + HttpSettings.PROXY_HOST.getKey() + "] and [" + HttpSettings.PROXY_PORT.getKey() + "]");
        }

        if (proxyPort > 0 && Strings.hasText(proxyHost)) {
            return new HttpProxy(proxyHost, proxyPort, proxyScheme);
        }

        return HttpProxy.NO_PROXY;
    }

    private URI createURI(HttpRequest request) {
        // this could be really simple, as the apache http client has a UriBuilder class, however this class is always doing
        // url path escaping, and we have done this already, so this would result in double escaping
        try {
            List<NameValuePair> qparams = new ArrayList<>(request.params.size());
            request.params.forEach((k, v) -> qparams.add(new BasicNameValuePair(k, v)));
            String format = URLEncodedUtils.format(qparams, "UTF-8");
            URI uri = URIUtils.createURI(request.scheme.scheme(), request.host, request.port, request.path,
                    Strings.isNullOrEmpty(format) ? null : format, null);

            return uri;
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(e);
        }
    }

    @Override
    public void close() throws IOException {
        client.close();
    }

    /**
     * Helper class to have all HTTP methods except HEAD allow for an body, including GET
     */
    final class HttpMethodWithEntity extends HttpEntityEnclosingRequestBase {

        private final String methodName;

        HttpMethodWithEntity(final URI uri, String methodName) {
            this.methodName = methodName;
            setURI(uri);
        }

        @Override
        public String getMethod() {
            return methodName;
        }
    }

}