com.spotify.helios.client.DefaultHttpConnector.java Source code

Java tutorial

Introduction

Here is the source code for com.spotify.helios.client.DefaultHttpConnector.java

Source

/*
 * Copyright (c) 2014 Spotify AB.
 *
 * 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 com.spotify.helios.client;

import com.google.common.base.Joiner;
import com.google.common.base.Throwables;

import com.spotify.helios.common.HeliosException;
import com.spotify.helios.common.Json;

import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.lang.reflect.Field;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;

import javax.net.ssl.HttpsURLConnection;

import static java.net.HttpURLConnection.HTTP_BAD_GATEWAY;

// TODO (mbrown): rename
public class DefaultHttpConnector implements HttpConnector {

    private static final Logger log = LoggerFactory.getLogger(DefaultHttpConnector.class);

    private final EndpointIterator endpointIterator;
    private final HostnameVerifierProvider hostnameVerifierProvider;

    private final int httpTimeoutMillis;
    private HttpsHandler extraHttpsHandler;

    public DefaultHttpConnector(final EndpointIterator endpointIterator, final int httpTimeoutMillis,
            final boolean sslHostnameVerificationEnabled) {
        this.endpointIterator = endpointIterator;
        this.httpTimeoutMillis = httpTimeoutMillis;
        this.hostnameVerifierProvider = new HostnameVerifierProvider(sslHostnameVerificationEnabled,
                new DefaultHostnameVerifier());
        this.extraHttpsHandler = null;
    }

    @Override
    public HttpURLConnection connect(final URI uri, final String method, final byte[] entity,
            final Map<String, List<String>> headers) throws HeliosException {
        final Endpoint endpoint = endpointIterator.next();
        final String endpointHost = endpoint.getUri().getHost();

        try {
            HttpURLConnection connection = connect0(uri, method, entity, headers, endpointHost);

            if (connection.getResponseCode() == HTTP_BAD_GATEWAY) {
                throw new HeliosException(String.format("Request to %s returned %s, master is down", uri,
                        connection.getResponseCode()));
            }
            return connection;

        } catch (ConnectException | SocketTimeoutException | UnknownHostException e) {
            // UnknownHostException happens if we can't resolve hostname into IP address.
            // UnknownHostException's getMessage method returns just the hostname which is a
            // useless message, so log the exception class name to provide more info.
            log.debug(e.toString());
            throw new HeliosException("Unable to connect to master", e);
        } catch (IOException e) {
            throw new HeliosException(e);
        }
    }

    private HttpURLConnection connect0(final URI ipUri, final String method, final byte[] entity,
            final Map<String, List<String>> headers, final String endpointHost) throws IOException {
        if (log.isTraceEnabled()) {
            log.trace("req: {} {} {} {} {} {}", method, ipUri, headers.size(),
                    Joiner.on(',').withKeyValueSeparator("=").join(headers), entity.length,
                    Json.asPrettyStringUnchecked(entity));
        } else {
            log.debug("req: {} {} {} {}", method, ipUri, headers.size(), entity.length);
        }

        final HttpURLConnection connection = (HttpURLConnection) ipUri.toURL().openConnection();
        handleHttps(connection, endpointHost, hostnameVerifierProvider, extraHttpsHandler);

        connection.setRequestProperty("Accept-Encoding", "gzip");
        connection.setInstanceFollowRedirects(false);
        connection.setConnectTimeout(httpTimeoutMillis);
        connection.setReadTimeout(httpTimeoutMillis);
        for (Map.Entry<String, List<String>> header : headers.entrySet()) {
            for (final String value : header.getValue()) {
                connection.addRequestProperty(header.getKey(), value);
            }
        }
        if (entity.length > 0) {
            connection.setDoOutput(true);
            connection.getOutputStream().write(entity);
        }

        setRequestMethod(connection, method, connection instanceof HttpsURLConnection);

        return connection;
    }

    private static void handleHttps(final HttpURLConnection connection, final String hostname,
            final HostnameVerifierProvider hostnameVerifierProvider, final HttpsHandler extraHttpsHandler) {

        if (!(connection instanceof HttpsURLConnection)) {
            return;
        }

        // We verify the TLS certificate against the original hostname since verifying against the
        // IP address will fail
        System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
        connection.setRequestProperty("Host", hostname);

        final HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
        httpsConnection.setHostnameVerifier(hostnameVerifierProvider.verifierFor(hostname));

        if (extraHttpsHandler != null) {
            extraHttpsHandler.handle(httpsConnection);
        }
    }

    private static void setRequestMethod(final HttpURLConnection connection, final String method,
            final boolean isHttps) {
        // Nasty workaround for ancient HttpURLConnection only supporting few methods
        final Class<?> httpURLConnectionClass = connection.getClass();
        try {
            Field methodField;
            HttpURLConnection delegate;
            if (isHttps) {
                final Field delegateField = httpURLConnectionClass.getDeclaredField("delegate");
                delegateField.setAccessible(true);
                delegate = (HttpURLConnection) delegateField.get(connection);
                methodField = delegate.getClass().getSuperclass().getSuperclass().getSuperclass()
                        .getDeclaredField("method");
            } else {
                delegate = connection;
                methodField = httpURLConnectionClass.getSuperclass().getDeclaredField("method");
            }

            methodField.setAccessible(true);
            methodField.set(delegate, method);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw Throwables.propagate(e);
        }
    }

    @Override
    public void close() throws Exception {
    }

    public void setExtraHttpsHandler(final HttpsHandler extraHttpsHandler) {
        this.extraHttpsHandler = extraHttpsHandler;
    }
}