com.microsoft.tfs.client.common.ui.config.UITransportRequestHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.client.common.ui.config.UITransportRequestHandler.java

Source

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

package com.microsoft.tfs.client.common.ui.config;

import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.swt.browser.Browser;

import com.microsoft.tfs.client.common.credentials.EclipseCredentialsManagerFactory;
import com.microsoft.tfs.client.common.ui.framework.helper.UIHelpers;
import com.microsoft.tfs.core.config.ConnectionInstanceData;
import com.microsoft.tfs.core.config.EnvironmentVariables;
import com.microsoft.tfs.core.config.auth.DefaultTransportRequestHandler;
import com.microsoft.tfs.core.config.httpclient.ConfigurableHTTPClientFactory;
import com.microsoft.tfs.core.config.httpclient.DefaultHTTPClientFactory;
import com.microsoft.tfs.core.config.persistence.DefaultPersistenceStoreProvider;
import com.microsoft.tfs.core.credentials.CachedCredentials;
import com.microsoft.tfs.core.credentials.CredentialsManager;
import com.microsoft.tfs.core.httpclient.Cookie;
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.Header;
import com.microsoft.tfs.core.httpclient.HttpClient;
import com.microsoft.tfs.core.httpclient.URI;
import com.microsoft.tfs.core.httpclient.URIException;
import com.microsoft.tfs.core.httpclient.UsernamePasswordCredentials;
import com.microsoft.tfs.core.httpclient.UsernamePasswordCredentials.PatCredentials;
import com.microsoft.tfs.core.httpclient.cookie.CookiePolicy;
import com.microsoft.tfs.core.httpclient.cookie.CookieSpec;
import com.microsoft.tfs.core.ws.runtime.client.SOAPRequest;
import com.microsoft.tfs.core.ws.runtime.client.SOAPService;
import com.microsoft.tfs.core.ws.runtime.exceptions.FederatedAuthException;
import com.microsoft.tfs.core.ws.runtime.exceptions.FederatedAuthFailedException;
import com.microsoft.tfs.core.ws.runtime.exceptions.UnauthorizedException;
import com.microsoft.tfs.util.StringUtil;

/**
 * A {@link TransportAuthHandler} for the UI client, capable of handling
 * federated authentication exceptions by using service credentials or prompting
 * the user to reauthenticate to obtain new cookies. When new auth data is
 * computed, it updates the {@link ConnectionInstanceData} it is constructed
 * with in addition to configuring the {@link HttpClient}.
 *
 * @threadsafety unknown
 */
public class UITransportRequestHandler extends DefaultTransportRequestHandler {
    private static final Log log = LogFactory.getLog(UITransportRequestHandler.class);

    /*
     * Only one authentication runnable (per mechanism) should run at a time.
     * Serialize them.
     */

    private final Object runnableLock = new Object();
    private UITransportAuthRunnable dialogRunnable = null;

    /**
     * Creates a {@link UITransportAuthHandler} that updates the given profile
     * with new auth data using the given {@link DefaultHTTPClientFactory}'s
     * credential update methods.
     *
     * @param profile
     *        the profile to update with new authentication data (must not be
     *        <code>null</code>)
     * @param clientFactory
     *        the {@link DefaultHTTPClientFactory} to use to apply credentials
     *        from the profile to the {@link HttpClient} (must not be
     *        <code>null</code>)
     * @param webServiceFactory
     */
    public UITransportRequestHandler(final ConnectionInstanceData connectionInstanceData,
            final ConfigurableHTTPClientFactory clientFactory) {
        super(connectionInstanceData, clientFactory);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Status prepareRequest(final SOAPService service, final SOAPRequest request, final AtomicBoolean cancel) {
        final ConnectionInstanceData connectionInstanceData = getConnectionInstanceData();
        final Credentials credentials = connectionInstanceData.getCredentials();

        log.debug(" Preparing request with the cedentials: " + credentials == null ? "null" //$NON-NLS-1$ //$NON-NLS-2$
                : credentials.getClass().getName());

        if (credentials == null || !(credentials instanceof UsernamePasswordCredentials)) {
            return Status.CONTINUE;
        }

        final UsernamePasswordCredentials oldCredentials = (UsernamePasswordCredentials) credentials;

        /*
         * If the user has provided UsernamePasswordCredentials with an empty
         * password, this cannot be correct, so prompt them.
         */
        if (StringUtil.isNullOrEmpty(oldCredentials.getPassword())) {
            log.debug(" UsernamePasswordCredentials with an empty password detected"); //$NON-NLS-1$
            final Credentials newCredentials = getCredentials(new UITransportUsernamePasswordAuthRunnable(
                    connectionInstanceData.getServerURI(), connectionInstanceData.getCredentials()));

            if (newCredentials == null) {
                log.debug(" New UsernamePasswordCredentials not provided. Cancelling the request"); //$NON-NLS-1$
                cancel.set(true);
                return Status.CONTINUE;
            }

            log.debug(" New UsernamePasswordCredentials provided"); //$NON-NLS-1$

            // Apply the credentials data to the existing client.
            connectionInstanceData.setCredentials(newCredentials);

            getClientFactory().configureClientCredentials(service.getClient(), service.getClient().getState(),
                    connectionInstanceData);
        }

        return Status.CONTINUE;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Status handleSuccess(final SOAPService service, final SOAPRequest request) {
        final Header[] cookieHeaders = request.getPostMethod().getResponseHeaders("Set-Cookie"); //$NON-NLS-1$
        final ArrayList<Cookie> fedAuthCookies = new ArrayList<Cookie>();

        if (cookieHeaders.length > 0) {
            log.debug(" Request succeeded - Set-Cookie headers found in the response"); //$NON-NLS-1$
        }

        final URI uri;
        final String domain;
        try {
            uri = request.getPostMethod().getURI();
            domain = uri.getHost();
        } catch (final URIException e) {
            log.error("Incorrect URI", e); //$NON-NLS-1$
            return Status.CONTINUE;
        }

        int port = uri.getPort();
        if (port < 0) {
            if ("https".equalsIgnoreCase(uri.getScheme())) //$NON-NLS-1$
            {
                port = 443;
            } else {
                port = 80;
            }
        }

        /* Parse cookies according to RFC2109 */
        final CookieSpec cookieParser = CookiePolicy.getCookieSpec(CookiePolicy.RFC_2109);

        for (final Header cookieHeader : cookieHeaders) {
            log.debug(" " + cookieHeader.getName() + ": " + cookieHeader.getValue()); //$NON-NLS-1$ //$NON-NLS-2$

            /*
             * Parse the cookie headers, store the serialized cookies in the
             * profile.
             */

            try {
                /*
                 * Current FedAuth* cookies do not include any parameters, in
                 * particular the domain parameter. Let's use the original
                 * hosted server URI as the cookie's domain.
                 */
                final Cookie[] cookies = cookieParser.parse(domain, port, "/", true, cookieHeader); //$NON-NLS-1$

                for (final Cookie cookie : cookies) {
                    if (cookie.getName().startsWith("FedAuth")) //$NON-NLS-1$
                    {
                        fedAuthCookies.add(cookie);
                    }
                }
            } catch (final Exception e) {
                log.warn(MessageFormat.format("Could not parse authentication cookie {0}", cookieHeader.getValue()), //$NON-NLS-1$
                        e);
            }
        }

        if (fedAuthCookies.size() > 0) {
            final ConnectionInstanceData connectionInstanceData = getConnectionInstanceData();

            final CookieCredentials newCredentials = new CookieCredentials(
                    fedAuthCookies.toArray(new Cookie[fedAuthCookies.size()]));

            if (!(connectionInstanceData.getCredentials() instanceof CookieCredentials)
                    || !newCredentials.equals(connectionInstanceData.getCredentials())) {
                log.debug(" New Cookie Credentials created"); //$NON-NLS-1$

                // Apply the credentials data to the existing client.
                log.debug("Apply the new Cookie Credentials to the existing client."); //$NON-NLS-1$
                connectionInstanceData.setCredentials(newCredentials);

                log.debug(
                        " Save the new Cookie Credentials to the existing Client Factory for future clients in this session."); //$NON-NLS-1$
                getClientFactory().configureClientCredentials(service.getClient(), service.getClient().getState(),
                        connectionInstanceData);

                final CredentialsManager credentialsManager = EclipseCredentialsManagerFactory
                        .getCredentialsManager(DefaultPersistenceStoreProvider.INSTANCE);
                try {
                    log.debug(
                            " Save the new Cookie Credentials in the Eclipse secure storage for future sessions."); //$NON-NLS-1$
                    credentialsManager.setCredentials(new CachedCredentials(uri.toJavaNetUri(), newCredentials));
                } catch (final URISyntaxException e) {
                    log.error("Incorrect URI", e); //$NON-NLS-1$
                }
            }
        }

        return Status.CONTINUE;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Status handleException(final SOAPService service, final SOAPRequest request, final Exception exception,
            final AtomicBoolean cancel) {
        final ConnectionInstanceData connectionInstanceData = getConnectionInstanceData();

        log.info("Authentication requested: ", exception); //$NON-NLS-1$

        /*
         * Super method handles FederatedAuthException with service credentials
         * if credentials were specified. Try that first.
         */
        if (super.handleException(service, request, exception, cancel) == Status.COMPLETE) {
            log.debug("DefaultTransportAuthHandler handled auth exception for us"); //$NON-NLS-1$
            return Status.COMPLETE;
        }

        log.debug("DefaultTransportAuthHandler did not handle auth exception"); //$NON-NLS-1$

        final UITransportAuthRunnable dialogRunnable;

        /*
         * For a federated authentication exception, always raise the login to
         * ACS or OAuth credentials dialog.
         */
        if (exception instanceof FederatedAuthException) {
            log.debug(" FederatedAuthException has been raised."); //$NON-NLS-1$

            cleanupSavedCredentials(service.getClient());

            if (EnvironmentVariables.getBoolean(EnvironmentVariables.USE_OAUTH_LIBRARY, true)) {
                dialogRunnable = new UITransportOAuthRunnable(connectionInstanceData.getServerURI());
            } else {
                dialogRunnable = new UITransportFederatedFallbackAuthRunnable(connectionInstanceData.getServerURI(),
                        connectionInstanceData.getCredentials(), (FederatedAuthException) exception);
            }
        }
        /*
         * For failed username/password or PAT credentials, raise the UI dialog
         * if the service recommends prompting. The SharePoint and Reports
         * services seems to be the only ones that do not recommend.
         */
        else if (exception instanceof UnauthorizedException && service.isPromptForCredentials()) {
            log.debug(" UnauthorizedException has been raised."); //$NON-NLS-1$

            if (EnvironmentVariables.getBoolean(EnvironmentVariables.USE_OAUTH_LIBRARY, true)
                    && isPatCredentials(connectionInstanceData.getCredentials())) {
                // PAT token is probably expired. Remove it from the Eclipse
                // secure storage and retry.
                final CredentialsManager credentialsManager = EclipseCredentialsManagerFactory
                        .getGitCredentialsManager();
                credentialsManager.removeCredentials(connectionInstanceData.getServerURI());
                dialogRunnable = new UITransportOAuthRunnable(connectionInstanceData.getServerURI());
            } else {
                dialogRunnable = new UITransportUsernamePasswordAuthRunnable(connectionInstanceData.getServerURI(),
                        connectionInstanceData.getCredentials(), (UnauthorizedException) exception);
            }
        }
        /*
         * The Cookie Credentials used are incorrect. They are either corrupted
         * in Eclipse secure storage or expired. Cleanup the storage and retry
         * from scratch.
         */
        else if (exception instanceof FederatedAuthFailedException) {
            cleanupSavedCredentials(service.getClient());
            return Status.CONTINUE;
        } else {
            log.debug(" Unknown authentication type or shouldn't prompt for this service."); //$NON-NLS-1$
            return Status.CONTINUE;
        }

        log.debug(" Prompt for credentials"); //$NON-NLS-1$
        final Credentials credentials = getCredentials(dialogRunnable);

        log.debug(" The dialog returned credentials: " //$NON-NLS-1$
                + (credentials == null ? "null" : credentials.getClass().getName())); //$NON-NLS-1$

        if (credentials == null) {
            log.info(" Credentials dialog has been cancelled by the user."); //$NON-NLS-1$
            cancel.set(true);
            return Status.CONTINUE;
        }

        // Apply the credentials data to the existing client.
        log.debug("Apply the new credentials to the existing client."); //$NON-NLS-1$
        connectionInstanceData.setCredentials(credentials);

        log.debug(" Save the new credentials to the existing Client Factory for future clients in this session."); //$NON-NLS-1$
        getClientFactory().configureClientCredentials(service.getClient(), service.getClient().getState(),
                connectionInstanceData);

        return Status.COMPLETE;
    }

    private boolean isPatCredentials(Credentials credentials) {
        if (credentials == null) {
            return false;
        } else if (!(credentials instanceof UsernamePasswordCredentials)) {
            return false;
        } else {
            final String userName = ((UsernamePasswordCredentials) credentials).getUsername();
            return PatCredentials.USERNAME_FOR_CODE_ACCESS_PAT.equals(userName);
        }
    }

    private void cleanupSavedCredentials(final HttpClient client) {
        log.debug(" If any credentials were used they failed. Clean up saved credentials for the host"); //$NON-NLS-1$

        final ConnectionInstanceData connectionInstanceData = getConnectionInstanceData();

        client.getState().clearCookies();
        client.getState().clearCredentials();

        final CredentialsManager credentialsManager = EclipseCredentialsManagerFactory
                .getCredentialsManager(DefaultPersistenceStoreProvider.INSTANCE);
        credentialsManager.removeCredentials(connectionInstanceData.getServerURI());

        connectionInstanceData.setCredentials(new DefaultNTCredentials());

        getClientFactory().configureClientCredentials(client, client.getState(), connectionInstanceData);

        Browser.clearSessions();
    }

    /**
     * If there is no {@link Runnable} executing to get the users credentials,
     * then the current runnable is executed.
     *
     * @param dialogRunnable
     *        the runnable to execute to obtain credentials
     * @return the credentials, or <code>null</code> if the user cancelled
     */
    private Credentials getCredentials(final UITransportAuthRunnable dialogRunnable) {
        boolean ownsRunnable = false;
        UITransportAuthRunnable runnable;

        try {
            /*
             * If there is a runnable currently executing, we'll simply use it.
             * If there is not, we'll set up the runnable we were given.
             */
            synchronized (runnableLock) {
                /*
                 * There is no runnable currently executing. We can execute the
                 * one provided.
                 */
                if (this.dialogRunnable == null) {
                    this.dialogRunnable = dialogRunnable;
                    ownsRunnable = true;
                }

                runnable = this.dialogRunnable;
            }

            /*
             * If we need to start our runnable, do so on the UI thread.
             */
            if (ownsRunnable) {
                UIHelpers.runOnUIThread(false, runnable);
            } else {
                /*
                 * Otherwise, wait for the currently executing runnable to
                 * complete.
                 */
                while (!runnable.isComplete()) {
                    /*
                     * If we're the UI thread, service it (or we'll deadlock
                     * waiting for our runnable to complete.)
                     */
                    if (UIHelpers.getDisplay().getThread() == Thread.currentThread()) {
                        if (!UIHelpers.getDisplay().readAndDispatch()) {
                            UIHelpers.getDisplay().sleep();
                        }
                    }
                    /* Otherwise, simply sleep and wait for this to finish. */
                    else {
                        try {
                            Thread.sleep(100);
                        } catch (final InterruptedException e) {
                            log.warn("Interrupted waiting for credentials dialog", e); //$NON-NLS-1$
                        }
                    }
                }
            }

            return runnable.getCredentials();
        } finally {
            /*
             * Clear the auth runnable so that subsequent authentication
             * attempts can succeed.
             */
            if (ownsRunnable) {
                synchronized (runnableLock) {
                    this.dialogRunnable = null;
                }
            }
        }
    }
}