com.microsoft.tfs.core.config.httpclient.DefaultHTTPClientFactory.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.core.config.httpclient.DefaultHTTPClientFactory.java

Source

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the repository root.

package com.microsoft.tfs.core.config.httpclient;

import java.net.URI;
import java.text.MessageFormat;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.core.runtime.IBundleGroup;
import org.eclipse.core.runtime.IBundleGroupProvider;
import org.eclipse.core.runtime.Platform;

import com.microsoft.tfs.core.config.ConnectionInstanceData;
import com.microsoft.tfs.core.config.EnvironmentVariables;
import com.microsoft.tfs.core.config.httpclient.internal.DefaultSSLProtocolSocketFactory;
import com.microsoft.tfs.core.httpclient.CookieCredentials;
import com.microsoft.tfs.core.httpclient.Credentials;
import com.microsoft.tfs.core.httpclient.DefaultNTCredentials;
import com.microsoft.tfs.core.httpclient.HostConfiguration;
import com.microsoft.tfs.core.httpclient.HttpClient;
import com.microsoft.tfs.core.httpclient.HttpConnectionManager;
import com.microsoft.tfs.core.httpclient.HttpMethod;
import com.microsoft.tfs.core.httpclient.HttpState;
import com.microsoft.tfs.core.httpclient.JwtCredentials;
import com.microsoft.tfs.core.httpclient.MultiThreadedHttpConnectionManager;
import com.microsoft.tfs.core.httpclient.PreemptiveUsernamePasswordCredentials;
import com.microsoft.tfs.core.httpclient.UsernamePasswordCredentials;
import com.microsoft.tfs.core.httpclient.auth.AuthScope;
import com.microsoft.tfs.core.httpclient.params.HttpClientParams;
import com.microsoft.tfs.core.httpclient.params.HttpConnectionManagerParams;
import com.microsoft.tfs.core.httpclient.params.HttpMethodParams;
import com.microsoft.tfs.core.httpclient.protocol.Protocol;
import com.microsoft.tfs.core.httpclient.protocol.SecureProtocolSocketFactory;
import com.microsoft.tfs.core.product.CoreVersionInfo;
import com.microsoft.tfs.core.product.ProductInformation;
import com.microsoft.tfs.core.product.ProductName;
import com.microsoft.tfs.core.ws.runtime.transport.HTTPConnectionCanceller;
import com.microsoft.tfs.core.ws.runtime.transport.IdleHTTPConnectionCloser;
import com.microsoft.tfs.jni.PlatformMiscUtils;
import com.microsoft.tfs.util.Check;

/**
 * <p>
 * A default implementation of the {@link HTTPClientFactory} interface that uses
 * a {@link ConnectionInstanceData} to configure an {@link HttpClient}. This
 * implementation is intended to be subclassed and provides a number of hooks
 * that subclasses may override.
 * </p>
 *
 * @see HTTPClientFactory
 * @see HttpClient
 *
 * @since TEE-SDK-10.1
 * @threadsafety thread-safe
 */
public class DefaultHTTPClientFactory implements ConfigurableHTTPClientFactory {
    public static final String CONNECT_TIMEOUT_SECONDS_PROPERTY = "com.microsoft.tfs.core.connectTimeoutSeconds"; //$NON-NLS-1$
    public static final int CONNECT_TIMEOUT_SECONDS_DEFAULT = 30;

    public static final String SOCKET_TIMEOUT_SECONDS_PROPERTY = "com.microsoft.tfs.core.socketTimeoutSeconds"; //$NON-NLS-1$
    public static final int SOCKET_TIMEOUT_SECONDS_DEFAULT = 60 * 30;

    public static final String MAX_TOTAL_CONNECTIONS_PROPERTY = "com.microsoft.tfs.core.maxTotalConnections"; //$NON-NLS-1$
    public static final int MAX_TOTAL_CONNECTIONS_DEFAULT = 40;

    public static final String MAX_CONNECTIONS_PER_HOST_PROPERTY = "com.microsoft.tfs.core.maxConnectionsPerHost"; //$NON-NLS-1$
    public static final int MAX_CONNECTIONS_PER_HOST_DEFAULT = 10;

    public static final String DISABLE_HTTP_CANCEL_THREAD_PROPERTY = "com.microsoft.tfs.core.disableCancelThread"; //$NON-NLS-1$

    public static final String ECLIPSE_GROUP_NAME = "Eclipse Platform"; //$NON-NLS-1$

    /**
     * The maximum length in characters of the extra user agent text part.
     */
    public static final int USER_AGENT_EXTRA_TEXT_MAX_CHARS = 30;

    /**
     * The maximum length in characters of the operating system info part.
     */
    public static final int USER_AGENT_OS_INFO_MAX_CHARS = 30;

    private static final Log log = LogFactory.getLog(DefaultHTTPClientFactory.class);

    /**
     * A service thread that closes connections that have been idle a long time
     * to improve quality of service on broken networks (where TCP resets happen
     * often).
     */
    private static final IdleHTTPConnectionCloser closerThread = new IdleHTTPConnectionCloser();

    /**
     * A service thread that aborts {@link HttpMethod}s when a monitor object
     * signals that the method should be canceled.
     */
    private static final HTTPConnectionCanceller cancelThread = new HTTPConnectionCanceller();

    static {
        closerThread.start();

        if (System.getProperty(DISABLE_HTTP_CANCEL_THREAD_PROPERTY) == null) {
            cancelThread.start();
        }
    }

    private final ConnectionInstanceData connectionInstanceData;

    /**
     * Creates a new {@link DefaultHTTPClientFactory} that will data contained
     * in the specified instance data to configure {@link HttpClient}s.
     *
     * @param serverURI
     *        the {@link URI} that will be connected to (must not be
     *        <code>null</code>)
     */
    public DefaultHTTPClientFactory(final ConnectionInstanceData connectionInstanceData) {
        Check.notNull(connectionInstanceData, "connectionInstanceData"); //$NON-NLS-1$
        this.connectionInstanceData = connectionInstanceData;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.microsoft.tfs.core.config.HttpClientFactory#newHttpClient()
     */
    @Override
    public HttpClient newHTTPClient() {
        final HttpConnectionManager connectionManager = createConnectionManager(connectionInstanceData);

        final HttpClient httpClient = createHTTPClient(connectionManager, connectionInstanceData);

        configureClientParams(httpClient, httpClient.getParams(), connectionInstanceData);

        configureClientCredentials(httpClient, httpClient.getState(), connectionInstanceData);

        configureClientProxy(httpClient, httpClient.getHostConfiguration(), httpClient.getState(),
                connectionInstanceData);

        configureClient(httpClient, connectionInstanceData);

        logHTTPClientConfiguration(httpClient);

        return httpClient;
    }

    private void logHTTPClientConfiguration(final HttpClient httpClient) {
        final StringBuffer configurationMessage = new StringBuffer();

        configurationMessage.append(MessageFormat.format("HttpClient configured for {0}", //$NON-NLS-1$
                connectionInstanceData.getServerURI()));

        final Credentials credentials = httpClient.getState().getCredentials(AuthScope.ANY);
        if (credentials != null) {
            if (credentials instanceof DefaultNTCredentials) {
                configurationMessage.append(", authenticating as logged in user"); //$NON-NLS-1$
            } else if (credentials instanceof UsernamePasswordCredentials) {
                configurationMessage.append(MessageFormat.format(", authenticating as {0}", //$NON-NLS-1$
                        ((UsernamePasswordCredentials) credentials).getUsername()));
            } else if (credentials instanceof CookieCredentials) {
                configurationMessage.append(", authenticating with ACS token"); //$NON-NLS-1$
            } else if (credentials instanceof JwtCredentials) {
                configurationMessage.append(", authenticating with JWT token"); //$NON-NLS-1$
            }
        }

        if (httpClient.getHostConfiguration().getProxyHost() != null) {
            configurationMessage.append(MessageFormat.format(", proxy={0}", //$NON-NLS-1$
                    httpClient.getHostConfiguration().getProxyHost()));

            if (httpClient.getHostConfiguration().getProxyPort() != -1) {
                configurationMessage.append(MessageFormat.format(":{0}", //$NON-NLS-1$
                        Integer.toString(httpClient.getHostConfiguration().getProxyPort())));
            }
        }

        log.info(configurationMessage.toString());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void dispose(final HttpClient httpClient) {
        log.debug("Disposing"); //$NON-NLS-1$
        final HttpConnectionManager connectionManager = httpClient.getHttpConnectionManager();
        if (!(connectionManager instanceof MultiThreadedHttpConnectionManager)) {
            log.debug(
                    "Nothing to dispose: connectionManager is not an instance of MultiThreadedHttpConnectionManager"); //$NON-NLS-1$
            return;
        }

        final MultiThreadedHttpConnectionManager multiThreadedConnectionManager = (MultiThreadedHttpConnectionManager) connectionManager;

        log.debug("Shutting down the Multi Threaded Http Connection Manager"); //$NON-NLS-1$
        multiThreadedConnectionManager.shutdown();

        log.debug("Disposed"); //$NON-NLS-1$
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public HttpConnectionManager createConnectionManager(final ConnectionInstanceData connectionInstanceData) {
        final MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();

        final HttpConnectionManagerParams params = connectionManager.getParams();

        /*
         * The max total connection limit is higher than the per-host limit to
         * account for multiple connections (to different TFS servers) that
         * share an HttpClient.
         */
        params.setMaxTotalConnections(
                Integer.getInteger(MAX_TOTAL_CONNECTIONS_PROPERTY, MAX_TOTAL_CONNECTIONS_DEFAULT));
        params.setMaxConnectionsPerHost(HostConfiguration.ANY_HOST_CONFIGURATION,
                Integer.getInteger(MAX_CONNECTIONS_PER_HOST_PROPERTY, MAX_CONNECTIONS_PER_HOST_DEFAULT));

        /*
         * Set the connection timeout.
         */
        params.setConnectionTimeout(
                Integer.getInteger(CONNECT_TIMEOUT_SECONDS_PROPERTY, CONNECT_TIMEOUT_SECONDS_DEFAULT) * 1000);

        return connectionManager;
    }

    /**
     * {@inheritDoc}
     *
     * <p>
     * This method will modify the HTTPClient {@link Protocol} registration for
     * "https" protocols to use a custom {@link SecureProtocolSocketFactory}
     * that will allow users to accept untrusted certificates.
     * </p>
     */
    @Override
    public HttpClient createHTTPClient(final HttpConnectionManager connectionManager,
            final ConnectionInstanceData connectionInstanceData) {
        /*
         * Register our SSL socket factory with HTTPClient, allowing us to
         * accept untrusted certificates.
         */
        Protocol.registerProtocol("https", new Protocol("https", new DefaultSSLProtocolSocketFactory(), 443)); //$NON-NLS-1$ //$NON-NLS-2$

        return new HttpClient(connectionManager);
    }

    /**
     * Called from
     * {@link #createHTTPClient(HttpConnectionManager, ConnectionInstanceData)}
     * to test whether an {@link HttpClient} created for the specified
     * connection should be configured to accept untrusted SSL certificates.
     * Subclasses may override. The default behavior is to check an environment
     * variable. If the value of the environment variable
     * {@link EnvironmentVariables#ACCEPT_UNTRUSTED_CERTIFICATES} is set,
     * <code>true</code> is returned. Otherwise, <code>false</code> is returned.
     *
     * @param connectionInstanceData
     *        the {@link ConnectionInstanceData} being used to supply
     *        configuration data (never <code>null</code>)
     * @return <code>true</code> to configure the new {@link HttpClient}
     *         instance to accept untrusted SSL certificates
     */
    protected boolean shouldAcceptUntrustedCertificates(final ConnectionInstanceData connectionInstanceData) {
        /*
         * If the environment variable is set, we accept untrusted certificates.
         */

        if (PlatformMiscUtils.getInstance()
                .getEnvironmentVariable(EnvironmentVariables.ACCEPT_UNTRUSTED_CERTIFICATES) != null) {
            return true;
        }

        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void configureClientParams(final HttpClient httpClient, final HttpClientParams params,
            final ConnectionInstanceData connectionInstanceData) {
        params.setBooleanParameter("http.protocol.expect-continue", false); //$NON-NLS-1$

        params.setParameter(HttpMethodParams.SO_TIMEOUT,
                Integer.getInteger(SOCKET_TIMEOUT_SECONDS_PROPERTY, SOCKET_TIMEOUT_SECONDS_DEFAULT) * 1000);

        /*
         * Setup the user agent
         */
        final String userAgent = getUserAgent(httpClient, connectionInstanceData);

        if (userAgent != null) {
            params.setParameter(HttpMethodParams.USER_AGENT, userAgent);
        }

        /*
         * Set the SSL socket factory to accept untrusted certificates, if
         * requested.
         */
        if (shouldAcceptUntrustedCertificates(connectionInstanceData)) {
            params.setBooleanParameter(DefaultSSLProtocolSocketFactory.ACCEPT_UNTRUSTED_CERTIFICATES_PARAMETER,
                    Boolean.TRUE);
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Use {@link #getUserAgentExtraString(HttpClient, ConnectionInstanceData)}
     * to customize thet user agent string.
     */
    @Override
    public final String getUserAgent(final HttpClient httpClient,
            final ConnectionInstanceData connectionInstanceData) {
        /*
         * This method is final to ensure the format can be parsed for SQM data
         * on the server. Some fields are truncated to help keep the string
         * under 129 chars for SQM, though truncation should be rare (those
         * fields usually aren't so long).
         */

        final ProductName productName = ProductInformation.getCurrent();

        // Like "Team Explorer Everywhere"
        /*
         * Changed for testing purposes to avoid legacy=1 parameter in the URI
         * generated by TFS as workaround for old clients
         */
        final String applicationName;
        if (EnvironmentVariables.getBoolean(EnvironmentVariables.USE_LEGACY_MSA, false)) {
            /*
             * Like "Team Explorer Everywhere"
             *
             * The legacy=1 parameter in the URI will be generated by TFS as
             * workaround for old clients
             */
            applicationName = productName.getFamilyShortNameNOLOC();
        } else {
            /*
             * The javascriptNotify will be used by TFS to stop redirection
             * chain in the Federated Authentication sequence and provide
             * FedAuth cookies to the new client.
             */
            applicationName = "Team  Explorer  Everywhere"; //$NON-NLS-1$
        }

        // Like "11"
        final String sqmID = Integer.toString(productName.getSQMID());

        // Like "Plugin" or "CLC" or "SDK"
        final String shortName = productName.getProductShortNameNOLOC();

        // Like "11.0.0.201108162203" but could be a bit longer like
        // "12.34.45.201108162203"
        final String version = MessageFormat.format("{0}.{1}.{2}.{3}", //$NON-NLS-1$
                CoreVersionInfo.getMajorVersion(), CoreVersionInfo.getMinorVersion(),
                CoreVersionInfo.getServiceVersion(), CoreVersionInfo.getBuildVersion());

        // Like "Eclipse_sdk 4.2"
        final String productInfo = getProductInformation(shortName);

        // Like "1.5.2"
        final String javaVersion = System.getProperty("java.version"); //$NON-NLS-1$

        // Like "Linux amd64 2.6.38-8-generic" or "Windows 7 amd64 6.1"
        final String osInfo = truncate(MessageFormat.format("{0} {1} {2}", //$NON-NLS-1$
                System.getProperty("os.name"), //$NON-NLS-1$
                System.getProperty("os.arch"), //$NON-NLS-1$
                System.getProperty("os.version")), USER_AGENT_OS_INFO_MAX_CHARS); //$NON-NLS-1$

        // Format the first part, append more
        final StringBuffer ua = new StringBuffer(MessageFormat.format("{0}, SKU:{1} ({2} {3} {4}{5}; {6}", //$NON-NLS-1$
                applicationName, sqmID, shortName, version, productInfo, javaVersion, osInfo));

        // Extra goes last (if present)
        final String extra = getUserAgentExtraString(httpClient, connectionInstanceData);
        if (extra != null) {
            ua.append("; "); //$NON-NLS-1$
            ua.append(truncate(extra, USER_AGENT_EXTRA_TEXT_MAX_CHARS));
        }

        ua.append(")"); //$NON-NLS-1$

        return ua.toString();
    }

    private String truncate(final String s, final int maxLength) {
        if (s == null || s.length() == 0 || maxLength < 1 || s.length() <= maxLength) {
            return s;
        }

        return s.substring(0, maxLength);
    }

    /**
     * <p>
     * Subclasses can override to provide extra text that gets appended to the
     * parenthetical part of the user agent HTTP header. If <code>null</code> or
     * empty string is returned no extra text is appended. The returned string
     * should be {@value #USER_AGENT_EXTRA_TEXT_MAX_CHARS} characters or less
     * (it will be truncated if it exceeds this limit).
     * </p>
     * <p>
     * The user agent header is formatted like:
     * </p>
     *
     * <pre>
     * ProductFamily, SKU:XX (ProductName 1.2.3.4567890; OS Arch Version<b>; extra text goes here if present</b>)
     * </pre>
     *
     * @param httpClient
     *        the {@link HttpClient} being configured (must not be
     *        <code>null</code>)
     * @param connectionInstanceData
     *        the connection instance data (must not be <code>null</code>)
     * @return a string to put at the end of the parenthetical part of the user
     *         agent header ({@value #USER_AGENT_EXTRA_TEXT_MAX_CHARS}
     *         characaters or less), or <code>null</code> or the emptry string
     *         to omit the extra part
     */
    protected String getUserAgentExtraString(final HttpClient httpClient,
            final ConnectionInstanceData connectionInstanceData) {
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void configureClientCredentials(final HttpClient httpClient, final HttpState state,
            final ConnectionInstanceData connectionInstanceData) {
        final Credentials credentials = createCredentials(connectionInstanceData);

        if (credentials != null) {
            state.setCredentials(AuthScope.ANY, credentials);
        }

        /*
         * Only do preemptive authentication for Cookie and JWT Credentials and
         * special PreemptiveUsernamePasswordCredentials. These credentials may
         * be set on the HttpClient in response to federated authentication
         * challenges later.
         */
        httpClient.getParams().setPreemptiveAuthenticationTypes(new Class[] { CookieCredentials.class,
                JwtCredentials.class, PreemptiveUsernamePasswordCredentials.class });
    }

    /**
     * Called from
     * {@link #configureClientCredentials(HttpClient, HttpState, ConnectionInstanceData)}
     * to create a new {@link Credentials} instance for the specified
     * {@link ConnectionInstanceData}. Subclasses may override. The default
     * behavior is to simply return default credentials.
     *
     * @param connectionInstanceData
     *        the {@link ConnectionInstanceData} to get configuration data from
     *        (never <code>null</code>)
     * @return a {@link Credentials} object or <code>null</code> to not use
     *         {@link Credentials}
     */
    protected Credentials createCredentials(final ConnectionInstanceData connectionInstanceData) {
        return connectionInstanceData.getCredentials() != null ? connectionInstanceData.getCredentials()
                : new DefaultNTCredentials();
    }

    protected Credentials createProxyCredentials(final ConnectionInstanceData connectionInstanceData) {
        return new DefaultNTCredentials();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void configureClientProxy(final HttpClient httpClient, final HostConfiguration hostConfiguration,
            final HttpState httpState, final ConnectionInstanceData connectionInstanceData) {
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void configureClient(final HttpClient httpClient, final ConnectionInstanceData connectionInstanceData) {
        addClientToCloserThread(httpClient);
    }

    /**
     * Called to add the specified {@link HttpClient} instance to a static
     * thread that will handle idle connection closing.
     *
     * @param httpClient
     *        the {@link HttpClient} instance to add (must not be
     *        <code>null</code>)
     */
    protected final void addClientToCloserThread(final HttpClient httpClient) {
        closerThread.addClient(httpClient);
    }

    /**
     * It returns two strings, the first is the eclipse provider and the second
     * is the eclipse version.
     */
    private final String getProductInformation(final String shortName) {

        final String[] productInfo = new String[2];
        String productVendorVersion = ""; //$NON-NLS-1$

        // collect this information only in case of running as eclipse plugin
        if (shortName.equals(ProductName.PLUGIN)) {
            productInfo[0] = Platform.getProduct().getName().replace(' ', '_');

            final IBundleGroupProvider[] providers = Platform.getBundleGroupProviders();

            if (providers != null) {
                for (final IBundleGroupProvider provider : providers) {
                    final IBundleGroup[] groups = provider.getBundleGroups();
                    for (final IBundleGroup group : groups) {
                        final String groupName = group.getName();
                        final String groupVersion = group.getVersion();

                        if (groupName.equalsIgnoreCase(ECLIPSE_GROUP_NAME)) {
                            final int index = groupVersion.indexOf(".v"); //$NON-NLS-1$
                            if (index > 0) {
                                productInfo[1] = groupVersion.substring(0, index);
                            } else {
                                productInfo[1] = groupVersion;
                            }
                            break;
                        }
                    }
                }
            }
        }

        if (productInfo[0] != null && productInfo[1] != null) {
            productVendorVersion = MessageFormat.format("{0} {1} ", productInfo[0], productInfo[1]); //$NON-NLS-1$
        }
        return productVendorVersion;
    }
}