JiraWebSession.java Source code

Java tutorial

Introduction

Here is the source code for JiraWebSession.java

Source

/*******************************************************************************
 * Copyright (c) 2004, 2009 Brock Janiczak and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Brock Janiczak - initial API and implementation
 *     Tasktop Technologies - improvements
 *******************************************************************************/

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.axis.transport.http.HTTPConstants;
import org.apache.commons.httpclient.Cookie;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpConnectionManager;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.commons.net.AbstractWebLocation;
import org.eclipse.mylyn.commons.net.AuthenticationCredentials;
import org.eclipse.mylyn.commons.net.AuthenticationType;
import org.eclipse.mylyn.commons.net.Policy;
import org.eclipse.mylyn.commons.net.UnsupportedRequestException;
import org.eclipse.mylyn.commons.net.WebUtil;

import com.atlassian.connector.eclipse.internal.jira.core.JiraCorePlugin;
import com.atlassian.connector.eclipse.internal.jira.core.service.JiraAuthenticationException;
import com.atlassian.connector.eclipse.internal.jira.core.service.JiraCaptchaRequiredException;
import com.atlassian.connector.eclipse.internal.jira.core.service.JiraClient;
import com.atlassian.connector.eclipse.internal.jira.core.service.JiraException;
import com.atlassian.connector.eclipse.internal.jira.core.service.JiraInvalidResponseTypeException;
import com.atlassian.connector.eclipse.internal.jira.core.service.JiraRemoteMessageException;
import com.atlassian.connector.eclipse.internal.jira.core.service.JiraServiceUnavailableException;

/**
 * @author Brock Janiczak
 * @author Steffen Pingel
 */
public class JiraWebSession {

    private static final int MAX_REDIRECTS = 3;

    private final JiraClient client;

    private String baseUrl;

    private String characterEncoding;

    private final boolean secure;

    private boolean insecureRedirect;

    private boolean logEnabled;

    private final AbstractWebLocation location;

    protected static final String USER_AGENT = "JiraConnector"; //$NON-NLS-1$

    private static final Object SESSION_ID_COOKIE = "JSESSIONID"; //$NON-NLS-1$x

    private HttpClient httpClient;

    private HostConfiguration hostConfiguration;

    private final Lock authenticationLock;

    private volatile boolean reauthenticate;

    public JiraWebSession(JiraClient client, String baseUrl) {
        this.client = client;
        this.baseUrl = baseUrl;
        this.secure = baseUrl.startsWith("https"); //$NON-NLS-1$
        this.location = client.getLocation();
        this.authenticationLock = new ReentrantLock();
    }

    public JiraWebSession(JiraClient client) {
        this(client, client.getBaseUrl());
    }

    // sss Session  eclipsejira
    public void doInSession(JiraWebSessionCallback callback, IProgressMonitor monitor) throws JiraException {
        monitor = Policy.monitorFor(monitor);
        boolean doLogin = hostConfiguration == null || reauthenticate;
        int MAX_RETRIES = doLogin ? 1 : 2; //if login gets skipped, a second try might be needed
        for (int i = 1; i <= MAX_RETRIES; i++) {
            try {
                lock(monitor);
                if (httpClient == null) {
                    HttpConnectionManager connectionManager = WebUtil.getConnectionManager();
                    httpClient = new HttpClient(connectionManager);
                    WebUtil.configureHttpClient(httpClient, "JiraConnector"); //$NON-NLS-1$
                    httpClient.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
                    httpClient.getParams().setParameter(HttpMethodParams.SINGLE_COOKIE_HEADER, Boolean.TRUE);
                }
                if (doLogin) {
                    reauthenticate = false;
                    hostConfiguration = login(httpClient, monitor);
                }
            } finally {
                unlock(monitor);
            }
            try {
                // check if session is expired
                if (doLogin || isAuthenticated(httpClient, hostConfiguration, monitor)) {
                    callback.configure(httpClient, hostConfiguration, baseUrl,
                            client.getLocalConfiguration().getFollowRedirects());
                    callback.run(client, baseUrl, monitor);
                    return;
                } else {
                    doLogin = true;
                }
            } catch (IOException e) {
                throw new JiraException(e);
            } catch (JiraException e) {
                //if an exception occurs because of a missing login
                if (isAuthenticationFailure(e)) {
                    //rinse and repeat with login
                    doLogin = true;
                } else {
                    throw e;
                }
            }
        }
    }

    private boolean isAuthenticated(HttpClient httpClient, HostConfiguration hostConfiguration,
            IProgressMonitor monitor) throws JiraException {
        String url = baseUrl + "/secure/UpdateUserPreferences!default.jspa"; //$NON-NLS-1$
        GetMethod method = new GetMethod(url);
        method.setFollowRedirects(false);

        try {
            int statusCode = WebUtil.execute(httpClient, hostConfiguration, method, monitor);

            if (statusCode == HttpStatus.SC_OK) {
                return !method.getResponseBodyAsString().contains("/login.jsp?os_destination"); //$NON-NLS-1$
            }
        } catch (IOException e) {
            throw new JiraException(e);
        }
        return false;
    }

    private boolean isAuthenticationFailure(JiraException e) {
        if (e instanceof JiraRemoteMessageException) {
            String message = ((JiraRemoteMessageException) e).getHtmlMessage();
            return message != null && message.contains("login.jsp"); //$NON-NLS-1$
        }
        return (e instanceof JiraInvalidResponseTypeException);
    }

    public void doLogout(IProgressMonitor monitor) throws JiraException {
        monitor = Policy.monitorFor(monitor);
        try {
            lock(monitor);
            if (hostConfiguration == null) {
                // never logged in
                return;
            }
            logout(httpClient, hostConfiguration, monitor);
            hostConfiguration = null;
        } finally {
            unlock(monitor);
        }
    }

    private void lock(IProgressMonitor monitor) {
        while (!monitor.isCanceled()) {
            try {
                if (authenticationLock.tryLock(2000, TimeUnit.MILLISECONDS)) {
                    return;
                }
            } catch (InterruptedException e) {
                throw new OperationCanceledException();
            }
        }
        throw new OperationCanceledException();
    }

    private void unlock(IProgressMonitor monitor) {
        authenticationLock.unlock();
    }

    protected String getCharacterEncoding() {
        return characterEncoding;
    }

    protected String getBaseURL() {
        return baseUrl;
    }

    protected boolean isInsecureRedirect() {
        return insecureRedirect;
    }

    protected boolean isSecure() {
        return secure;
    }

    protected boolean isLogEnabled() {
        return logEnabled;
    }

    protected void setLogEnabled(boolean logEnabled) {
        this.logEnabled = logEnabled;
    }

    private String getContentType() {
        String characterEncoding = getCharacterEncoding();
        if (characterEncoding == null) {
            characterEncoding = client.getLocalConfiguration().getCharacterEncoding();
        }
        if (characterEncoding == null) {
            characterEncoding = client.getLocalConfiguration().getDefaultCharacterEncoding();
        }
        return "application/x-www-form-urlencoded; charset=" + characterEncoding; //$NON-NLS-1$
    }

    //sss JiraWebSession  eclipsejira
    private HostConfiguration login(HttpClient httpClient, IProgressMonitor monitor) throws JiraException {
        RedirectTracker tracker = new RedirectTracker();

        final String restLogin = "/rest/gadget/1.0/login"; //$NON-NLS-1$ // JIRA 4.x has additional endpoint for login that tells if CAPTCHA limit was hit
        final String loginAction = "/secure/Dashboard.jspa"; //$NON-NLS-1$;
        boolean jira4x = true;

        for (int i = 0; i <= MAX_REDIRECTS; i++) {
            AuthenticationCredentials credentials = location.getCredentials(AuthenticationType.REPOSITORY);
            if (credentials == null) {
                // TODO prompt user?
                credentials = new AuthenticationCredentials("", ""); //$NON-NLS-1$ //$NON-NLS-2$
            }

            String url = baseUrl + (jira4x ? restLogin : loginAction);

            PostMethod login = new PostMethod(url);
            login.setFollowRedirects(false);
            login.setRequestHeader("Content-Type", getContentType()); //$NON-NLS-1$
            login.addParameter("os_username", credentials.getUserName()); //$NON-NLS-1$
            login.addParameter("os_password", credentials.getPassword()); //$NON-NLS-1$
            login.addParameter("os_destination", "/success"); //$NON-NLS-1$ //$NON-NLS-2$

            tracker.addUrl(url);

            try {
                HostConfiguration hostConfiguration = WebUtil.createHostConfiguration(httpClient, location,
                        monitor);
                int statusCode = WebUtil.execute(httpClient, hostConfiguration, login, monitor);

                if (statusCode == HttpStatus.SC_NOT_FOUND) {
                    jira4x = false;
                    continue;
                }

                if (needsReauthentication(httpClient, login, monitor)) {
                    continue;
                } else if (statusCode != HttpStatus.SC_MOVED_TEMPORARILY
                        && statusCode != HttpStatus.SC_MOVED_PERMANENTLY) {
                    throw new JiraServiceUnavailableException("Unexpected status code during login: " + statusCode); //$NON-NLS-1$
                }

                tracker.addRedirect(url, login, statusCode);

                this.characterEncoding = login.getResponseCharSet();

                Header locationHeader = login.getResponseHeader("location"); //$NON-NLS-1$
                if (locationHeader == null) {
                    throw new JiraServiceUnavailableException("Invalid redirect, missing location"); //$NON-NLS-1$
                }
                url = locationHeader.getValue();
                tracker.checkForCircle(url);
                if (!insecureRedirect && isSecure() && url.startsWith("http://")) { //$NON-NLS-1$
                    tracker.log("Redirect to insecure location during login to repository: " + client.getBaseUrl()); //$NON-NLS-1$
                    insecureRedirect = true;
                }
                if (url.endsWith("/success")) { //$NON-NLS-1$
                    String newBaseUrl = url.substring(0, url.lastIndexOf("/success")); //$NON-NLS-1$
                    if (baseUrl.equals(newBaseUrl) || !client.getLocalConfiguration().getFollowRedirects()) {
                        // success
                        addAuthenticationCookie(httpClient, login);
                        return hostConfiguration;
                    } else {
                        // need to login to make sure HttpClient picks up the session cookie
                        baseUrl = newBaseUrl;
                    }
                }
            } catch (IOException e) {
                throw new JiraServiceUnavailableException(e);
            } finally {
                login.releaseConnection();
            }
        }

        tracker.log(
                "Exceeded maximum number of allowed redirects during login to repository: " + client.getBaseUrl()); //$NON-NLS-1$

        throw new JiraServiceUnavailableException("Exceeded maximum number of allowed redirects during login"); //$NON-NLS-1$
    }

    private void addAuthenticationCookie(HttpClient httpClient, PostMethod method) {
        Cookie[] cookies = httpClient.getState().getCookies();
        for (Cookie cookie : cookies) {
            if (SESSION_ID_COOKIE.equals(cookie.getName())) {
                // already have cookie
                return;
            }
        }

        for (Header header : method.getResponseHeaders()) {
            if (header.getName().equalsIgnoreCase(HTTPConstants.HEADER_SET_COOKIE)) {
                String cookie = header.getValue();
                // chop of path
                int index = cookie.indexOf(';');
                if (index != -1) {
                    cookie = cookie.substring(0, index);
                }
                // get session id 
                int i = cookie.indexOf("="); //$NON-NLS-1$
                String key = (i != -1) ? cookie.substring(0, i) : cookie;
                cookie = (i != -1) ? cookie.substring(i + 1) : ""; //$NON-NLS-1$
                httpClient.getState().addCookie(new Cookie(WebUtil.getHost(baseUrl), key, cookie,
                        WebUtil.getRequestPath(baseUrl), null, isSecure(baseUrl)));
            }
        }
    }

    private static boolean isSecure(String repositoryUrl) {
        return repositoryUrl.matches("https.*"); //$NON-NLS-1$
    }

    private boolean needsReauthentication(HttpClient httpClient, PostMethod method, IProgressMonitor monitor)
            throws JiraAuthenticationException, IOException, JiraServiceUnavailableException {
        final AuthenticationType authenticationType;
        int code = method.getStatusCode();
        if (code == HttpStatus.SC_OK) {
            authenticationType = AuthenticationType.REPOSITORY;
        } else if (code == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
            authenticationType = AuthenticationType.PROXY;
        } else {
            return false;
        }

        // the server specified a different character set than what was returned by the server, try to re-authenticate 
        if (!getContentType().endsWith(method.getResponseCharSet())) {
            this.characterEncoding = method.getResponseCharSet();
            return true;
        }

        if (method.getResponseHeader("Content-Type").getValue().startsWith("application/json")) { //$NON-NLS-1$//$NON-NLS-2$
            // we're talking to JIRA 4.x
            String json = method.getResponseBodyAsString(1024);
            if (json.contains("\"captchaFailure\":true")) { //$NON-NLS-1$
                throw new JiraCaptchaRequiredException(null);
            }
            if (json.contains("\"loginFailedByPermissions\":true")) { //$NON-NLS-1$
                throw new JiraServiceUnavailableException("You don't have permission to login"); //$NON-NLS-1$
            }
        }

        try {
            // PLE-960, PLE-1259 - we don't want a background monitor here, we want to force requestCredentials to prompt the user
            location.requestCredentials(authenticationType, null,
                    Policy.isBackgroundMonitor(monitor) ? SubMonitor.convert(monitor) : monitor);
        } catch (UnsupportedRequestException ignored) {
            throw new JiraAuthenticationException("Login failed."); //$NON-NLS-1$
        }

        return true;
    }

    private void logout(HttpClient httpClient, HostConfiguration hostConfiguration, IProgressMonitor monitor)
            throws JiraException {
        GetMethod logout = new GetMethod(baseUrl + "/logout"); //$NON-NLS-1$
        logout.setFollowRedirects(false);
        try {
            WebUtil.execute(httpClient, hostConfiguration, logout, monitor);
            httpClient.getState().clear(); //clear cookies and credentials for full logout
        } catch (IOException e) {
            // It doesn't matter if the logout fails. The server will clean up
            // the session eventually
        } finally {
            logout.releaseConnection();
        }
    }

    public void purgeSession() {
        this.reauthenticate = true;
    }

    private class RedirectTracker {

        ArrayList<RedirectInfo> redirects = new ArrayList<RedirectInfo>();

        Set<String> urls = new HashSet<String>();

        private boolean loggedCircle;

        public void addUrl(String url) {
            urls.add(url);
        }

        public void checkForCircle(String url) {
            if (!loggedCircle && urls.contains(url)) {
                log("Circular redirect detected while login in to repository: " + client.getBaseUrl()); //$NON-NLS-1$
                loggedCircle = true;
            }
        }

        public void addRedirect(String url, HttpMethodBase method, int statusCode) {
            redirects.add(new RedirectInfo(url, statusCode, method.getResponseHeaders()));
        }

        public void log(String message) {
            if (!isLogEnabled()) {
                return;
            }

            MultiStatus status = new MultiStatus(JiraCorePlugin.ID_PLUGIN, 0, message, null);
            for (RedirectInfo info : redirects) {
                status.add(new Status(IStatus.WARNING, JiraCorePlugin.ID_PLUGIN, 0, info.toString(), null));
            }
            StatusHandler.log(status);
        }

    }

    private class RedirectInfo {

        final int statusCode;

        final String url;

        final Header[] responseHeaders;

        public RedirectInfo(String url, int statusCode, Header[] responseHeaders) {
            this.url = url;
            this.statusCode = statusCode;
            this.responseHeaders = responseHeaders;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("Request: "); //$NON-NLS-1$
            sb.append(url).append('\n');
            sb.append("Status: ").append(statusCode).append('\n'); //$NON-NLS-1$
            // log useful but insensitive headers
            for (Header header : responseHeaders) {
                if (header.getName().equalsIgnoreCase("Server") || header.getName().equalsIgnoreCase("Location")) { //$NON-NLS-1$ //$NON-NLS-2$
                    sb.append(header.toExternalForm());
                }
            }
            return sb.toString();
        }

    }

}