com.gooddata.http.client.GoodDataHttpClient.java Source code

Java tutorial

Introduction

Here is the source code for com.gooddata.http.client.GoodDataHttpClient.java

Source

/*
 * Copyright (C) 2007-2015, GoodData(R) Corporation. All rights reserved.
 * This program is made available under the terms of the BSD License.
 */
package com.gooddata.http.client;

import static com.gooddata.http.client.LoginSSTRetrievalStrategy.LOGIN_URL;
import static org.apache.commons.lang.Validate.notNull;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AUTH;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.net.URI;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * <p>Http client with ability to handle GoodData authentication.</p>
 *
 * <h3>Usage</h3>
 *
 * <h4>Authentication using login</h4>
 * <pre>
 * // create HTTP client with your settings
 * HttpClient httpClient = HttpClientBuilder.create().build();
 *
 * // create login strategy, which wil obtain SST via login
 * SSTRetrievalStrategy sstStrategy = new LoginSSTRetrievalStrategy("user@domain.com", "my secret");
 *
 * // wrap your HTTP client into GoodData HTTP client
 * HttpClient client = new GoodDataHttpClient(httpClient, new HttpHost("server.com", 123), sstStrategy);
 *
 * // use GoodData HTTP client
 * HttpGet getProject = new HttpGet("/gdc/projects");
 * getProject.addHeader("Accept", ContentType.APPLICATION_JSON.getMimeType());
 * HttpResponse getProjectResponse = client.execute(httpHost, getProject);
 * </pre>
 *
 * <h4>Authentication using super-secure token (SST)</h4>
 *
 * <pre>
 * // create HTTP client
 * HttpClient httpClient = ...
 *
 * // create login strategy (you must somehow obtain SST)
 * SSTRetrievalStrategy sstStrategy = new SimpleSSTRetrievalStrategy("my super-secure token");
 *
 * // wrap your HTTP client into GoodData HTTP client
 * HttpClient client = new GoodDataHttpClient(httpClient, new HttpHost("server.com", 123), sstStrategy);
 *
 * // use GoodData HTTP client
 * HttpGet getProject = new HttpGet("/gdc/projects");
 * getProject.addHeader("Accept", ContentType.APPLICATION_JSON.getMimeType());
 * HttpResponse getProjectResponse = client.execute(httpHost, getProject);
 * </pre>
 */
public class GoodDataHttpClient implements HttpClient {

    private static final String TOKEN_URL = "/gdc/account/token";
    public static final String COOKIE_GDC_AUTH_TT = "cookie=GDCAuthTT";
    public static final String COOKIE_GDC_AUTH_SST = "cookie=GDCAuthSST";

    static final String TT_HEADER = "X-GDC-AuthTT";
    static final String SST_HEADER = "X-GDC-AuthSST";

    static final String YAML_CONTENT_TYPE = "application/yaml";

    private enum GoodDataChallengeType {
        SST, TT, UNKNOWN
    }

    private final Log log = LogFactory.getLog(getClass());

    private final HttpClient httpClient;

    private final SSTRetrievalStrategy sstStrategy;

    /** Host performing authentication - eg. issuing TT tokens */
    private final HttpHost authHost;

    /** this lock is used to ensure that no threads will try to send requests while authentication is performed */
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    /**
     * This lock guards that only one thread enters the authentication (obtaining TT/SST) section.
     * We need second lock we cannot call tryLock() on ReadWriteLock.writeLock as it returns false not only when another thread
     * holds the write lock already (what we want here) but also when another thread holds a read lock (what we do NOT want)
     */
    private final Lock authLock = new ReentrantLock();

    /** current SST (or null if not yet obtained) */
    private String sst;

    /** TT to be set into the header (or null if not yet obtained) */
    private String tt;

    /**
     * Construct object.
     * @deprecated use {@link #GoodDataHttpClient(HttpClient, HttpHost, SSTRetrievalStrategy)}
     * @param httpClient Http client
     * @param sstStrategy super-secure token (SST) obtaining strategy
     * @throws IllegalArgumentException if {@code sstStrategy} argument is not an instance of {@link LoginSSTRetrievalStrategy}
     */
    @Deprecated
    public GoodDataHttpClient(final HttpClient httpClient, final SSTRetrievalStrategy sstStrategy) {
        notNull(httpClient);
        this.httpClient = httpClient;
        if (sstStrategy instanceof LoginSSTRetrievalStrategy) {
            this.sstStrategy = sstStrategy;
            this.authHost = ((LoginSSTRetrievalStrategy) sstStrategy).getHttpHost();
            notNull(authHost, "HTTP host cannot be null");
        } else {
            throw new IllegalArgumentException(
                    "This constructor is deprecated and works with LoginSSTRetrievalStrategy argument only!");
        }
    }

    /**
     * Construct object.
     * @deprecated use {@link #GoodDataHttpClient(HttpHost, SSTRetrievalStrategy)}
     * @param sstStrategy super-secure token (SST) obtaining strategy
     */
    @Deprecated
    public GoodDataHttpClient(final SSTRetrievalStrategy sstStrategy) {
        this(HttpClientBuilder.create().build(), sstStrategy);
    }

    /**
     * Construct object.
     * @param httpClient Http client
     * @param authHost http host
     * @param sstStrategy super-secure token (SST) obtaining strategy
     */
    public GoodDataHttpClient(final HttpClient httpClient, final HttpHost authHost,
            final SSTRetrievalStrategy sstStrategy) {
        notNull(httpClient);
        notNull(authHost, "HTTP host cannot be null");
        notNull(sstStrategy);
        this.httpClient = httpClient;
        this.authHost = authHost;
        this.sstStrategy = sstStrategy;
    }

    /**
     * Construct object.
     * @param authHost http host
     * @param sstStrategy super-secure token (SST) obtaining strategy
     */
    public GoodDataHttpClient(final HttpHost authHost, final SSTRetrievalStrategy sstStrategy) {
        this(HttpClientBuilder.create().build(), authHost, sstStrategy);
    }

    private GoodDataChallengeType identifyGoodDataChallenge(final HttpResponse response) {
        if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
            final Header[] headers = response.getHeaders(AUTH.WWW_AUTH);
            if (headers != null) {
                for (final Header header : headers) {
                    final String challenge = header.getValue();
                    if (challenge.contains(COOKIE_GDC_AUTH_SST)) {
                        // this is actually not used as in refreshTT() we rely on status code only
                        return GoodDataChallengeType.SST;
                    } else if (challenge.contains(COOKIE_GDC_AUTH_TT)) {
                        return GoodDataChallengeType.TT;
                    }
                }
            }
        }
        return GoodDataChallengeType.UNKNOWN;
    }

    private HttpResponse handleResponse(final HttpHost httpHost, final HttpRequest request,
            final HttpResponse originalResponse, final HttpContext context) throws IOException {
        final GoodDataChallengeType challenge = identifyGoodDataChallenge(originalResponse);
        if (challenge == GoodDataChallengeType.UNKNOWN) {
            return originalResponse;
        }
        EntityUtils.consume(originalResponse.getEntity());

        try {
            if (authLock.tryLock()) {
                //only one thread requiring authentication will get here.
                final Lock writeLock = rwLock.writeLock();
                writeLock.lock();
                boolean doSST = true;
                try {
                    if (challenge == GoodDataChallengeType.TT && sst != null) {
                        if (refreshTt()) {
                            doSST = false;
                        }
                    }
                    if (doSST) {
                        sst = sstStrategy.obtainSst(httpClient, authHost);
                        if (!refreshTt()) {
                            throw new GoodDataAuthException("Unable to obtain TT after successfully obtained SST");
                        }
                    }
                } catch (GoodDataAuthException e) {
                    return new BasicHttpResponse(new BasicStatusLine(originalResponse.getProtocolVersion(),
                            HttpStatus.SC_UNAUTHORIZED, e.getMessage()));
                } finally {
                    writeLock.unlock();
                }
            } else {
                // the other thread is performing auth and thus is holding the write lock
                // lets wait until it is finished (the write lock is granted) and then continue
                authLock.lock();
            }
        } finally {
            authLock.unlock();
        }
        return this.execute(httpHost, request, context);
    }

    /**
     * Refresh temporary token.
     * @return
     * <ul>
     *     <li><code>true</code> TT refresh successful</li>
     *     <li><code>false</code> TT refresh unsuccessful (SST expired)</li>
     * </ul>
     * @throws GoodDataAuthException error
     */
    private boolean refreshTt() throws IOException {
        log.debug("Obtaining TT");

        final HttpGet request = new HttpGet(TOKEN_URL);
        try {
            request.setHeader(HttpHeaders.ACCEPT, YAML_CONTENT_TYPE);
            request.setHeader(SST_HEADER, sst);
            final HttpResponse response = httpClient.execute(authHost, request, (HttpContext) null);
            final int status = response.getStatusLine().getStatusCode();
            switch (status) {
            case HttpStatus.SC_OK:
                tt = TokenUtils.extractToken(response);
                return true;
            case HttpStatus.SC_UNAUTHORIZED:
                // we probably may check if SST challenge is present to be sure the problem is the expired SST
                return false;
            default:
                throw new GoodDataAuthException("Unable to obtain TT, HTTP status: " + status);
            }
        } finally {
            request.reset();
        }
    }

    @SuppressWarnings("deprecation")
    @Override
    public HttpParams getParams() {
        return httpClient.getParams();
    }

    @SuppressWarnings("deprecation")
    @Override
    public ClientConnectionManager getConnectionManager() {
        return httpClient.getConnectionManager();
    }

    @Override
    public HttpResponse execute(HttpHost target, HttpRequest request) throws IOException {
        return execute(target, request, (HttpContext) null);
    }

    @Override
    public <T> T execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler)
            throws IOException {
        return execute(target, request, responseHandler, null);
    }

    @Override
    public <T> T execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler,
            HttpContext context) throws IOException {
        HttpResponse resp = execute(target, request, context);
        return responseHandler.handleResponse(resp);
    }

    @Override
    public HttpResponse execute(HttpUriRequest request) throws IOException {
        return execute(request, (HttpContext) null);
    }

    @Override
    public HttpResponse execute(HttpUriRequest request, HttpContext context) throws IOException {
        final URI uri = request.getURI();
        final HttpHost httpHost = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
        return execute(httpHost, request, context);
    }

    @Override
    public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler) throws IOException {
        return execute(request, responseHandler, null);
    }

    @Override
    public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context)
            throws IOException {
        final HttpResponse resp = execute(request, context);
        return responseHandler.handleResponse(resp);
    }

    @Override
    public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context) throws IOException {
        notNull(request, "Request can't be null");

        final boolean logoutRequest = isLogoutRequest(target, request);
        final Lock lock = logoutRequest ? rwLock.writeLock() : rwLock.readLock();

        lock.lock();

        final HttpResponse resp;
        try {
            if (tt != null) {
                // this adds TT header to EVERY request to ALL hosts made by this HTTP client
                // however the server performs additional checks to ensure client is not using forged TT
                request.setHeader(TT_HEADER, tt);

                if (logoutRequest) {
                    try {
                        sstStrategy.logout(httpClient, target, request.getRequestLine().getUri(), sst, tt);
                        tt = null;
                        sst = null;

                        return new BasicHttpResponse(new BasicStatusLine(request.getProtocolVersion(),
                                HttpStatus.SC_NO_CONTENT, "Logout successful"));
                    } catch (GoodDataLogoutException e) {
                        return new BasicHttpResponse(new BasicStatusLine(request.getProtocolVersion(),
                                e.getStatusCode(), e.getStatusText()));
                    }
                }
            }
            resp = this.httpClient.execute(target, request, context);
        } finally {
            lock.unlock();
        }
        return handleResponse(target, request, resp, context);
    }

    private boolean isLogoutRequest(HttpHost target, HttpRequest request) {
        return authHost.equals(target) && "DELETE".equals(request.getRequestLine().getMethod())
                && URI.create(request.getRequestLine().getUri()).getPath().startsWith(LOGIN_URL);
    }
}