com.cloudant.http.internal.interceptors.CookieInterceptor.java Source code

Java tutorial

Introduction

Here is the source code for com.cloudant.http.internal.interceptors.CookieInterceptor.java

Source

/*
 * Copyright  2015, 2017 IBM Corp. All rights reserved.
 *
 * 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.cloudant.http.internal.interceptors;

import com.cloudant.http.Http;
import com.cloudant.http.HttpConnection;
import com.cloudant.http.HttpConnectionInterceptorContext;
import com.cloudant.http.HttpConnectionRequestInterceptor;
import com.cloudant.http.HttpConnectionResponseInterceptor;

import org.apache.commons.io.IOUtils;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.CookieManager;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Adds cookie authentication support to http requests.
 *
 * It does this by adding the cookie header for CouchDB
 * using request interceptor pipeline in {@link HttpConnection}.
 *
 * If a response has a response code of 401, it will fetch a cookie from
 * the server using provided credentials and tell {@link HttpConnection} to reply
 * the request by setting {@link HttpConnectionInterceptorContext#replayRequest} property to true.
 *
 * If the request to get the cookie for use in future request fails with a 401 status code
 * (or any status that indicates client error) cookie authentication will not be attempted again.
 */
public class CookieInterceptor implements HttpConnectionRequestInterceptor, HttpConnectionResponseInterceptor {

    private final static Logger logger = Logger.getLogger(CookieInterceptor.class.getCanonicalName());
    private final byte[] sessionRequestBody;
    private final CookieManager cookieManager = new CookieManager();
    private final AtomicBoolean shouldAttemptCookieRequest = new AtomicBoolean(true);
    private final URL sessionURL;

    /**
     * Constructs a cookie interceptor. Credentials should be supplied not URL encoded, this class
     * will perform the necessary URL encoding.
     *
     * @param username The username to use when getting the cookie (not URL encoded)
     * @param password The password to use when getting the cookie (not URL encoded)
     * @param baseURL  The base URL to use when constructing an `_session` request.
     */
    public CookieInterceptor(String username, String password, String baseURL) {

        try {
            this.sessionURL = new URL(String.format("%s/_session", baseURL));
            username = URLEncoder.encode(username, "UTF-8");
            password = URLEncoder.encode(password, "UTF-8");
            this.sessionRequestBody = String.format("name=%s&password=%s", username, password).getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            //all JVMs should support UTF-8, so this should not happen
            throw new RuntimeException(e);
        } catch (MalformedURLException e) {
            // this should be a valid URL since the builder is passing it in
            logger.log(Level.SEVERE, "Failed to create URL for _session endpoint", e);
            throw new RuntimeException(e);
        }
    }

    @Override
    public HttpConnectionInterceptorContext interceptRequest(HttpConnectionInterceptorContext context) {

        HttpURLConnection connection = context.connection.getConnection();

        // First time we will have no cookies
        if (cookieManager.getCookieStore().getCookies().isEmpty() && shouldAttemptCookieRequest.get()) {
            if (!requestCookie(context)) {
                // Requesting a cookie failed, set a flag if we failed so we won't try again
                shouldAttemptCookieRequest.set(false);
            }
        }

        if (shouldAttemptCookieRequest.get()) {

            // Debug logging
            if (logger.isLoggable(Level.FINEST)) {
                logger.finest("Attempt to add cookie to request.");
                logger.finest("Cookies are stored for URIs: " + cookieManager.getCookieStore().getURIs());
            }

            // Apply any saved cookies to the request
            try {
                Map<String, List<String>> requestCookieHeaders = cookieManager.get(connection.getURL().toURI(),
                        connection.getRequestProperties());
                for (Map.Entry<String, List<String>> requestCookieHeader : requestCookieHeaders.entrySet()) {
                    List<String> cookies = requestCookieHeader.getValue();
                    if (cookies != null && !cookies.isEmpty()) {
                        connection.setRequestProperty(requestCookieHeader.getKey(),
                                listToSemicolonSeparatedString(cookies));
                    } else {
                        logger.finest("No cookie values to set.");
                    }
                }
            } catch (IOException e) {
                logger.log(Level.SEVERE, "Failed to read request properties", e);
            } catch (URISyntaxException e) {
                logger.log(Level.SEVERE, "Failed to convert request URL to URI for cookie " + "retrieval.");
            }
        }
        return context;
    }

    @Override
    public HttpConnectionInterceptorContext interceptResponse(HttpConnectionInterceptorContext context) {

        // Check if this interceptor is valid before attempting any kind of renewal
        if (shouldAttemptCookieRequest.get()) {

            HttpURLConnection connection = context.connection.getConnection();

            // If we got a 401 or 403 we might need to renew the cookie
            try {
                boolean renewCookie = false;
                int statusCode = connection.getResponseCode();

                if (statusCode == HttpURLConnection.HTTP_FORBIDDEN
                        || statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
                    InputStream errorStream = connection.getErrorStream();
                    String errorString = null;
                    if (errorStream != null) {
                        try {
                            // Get the string value of the error stream
                            errorString = IOUtils.toString(errorStream, "UTF-8");
                        } finally {
                            IOUtils.closeQuietly(errorStream);
                        }
                    }
                    logger.log(Level.FINE, String.format(Locale.ENGLISH, "Intercepted " + "response %d %s",
                            statusCode, errorString));
                    switch (statusCode) {
                    case HttpURLConnection.HTTP_FORBIDDEN: //403
                        // Check if it was an expiry case
                        // Check using a regex to avoid dependency on a JSON library.
                        // Note (?siu) flags used for . to also match line breaks and for
                        // unicode
                        // case insensitivity.
                        if (errorString != null && errorString
                                .matches("(?siu)" + ".*\\\"error\\\"\\s*:\\s*\\\"credentials_expired\\\".*")) {
                            // Was expired - set boolean to renew cookie
                            renewCookie = true;
                        } else {
                            // Wasn't a credentials expired, throw exception
                            HttpConnectionInterceptorException toThrow = new HttpConnectionInterceptorException(
                                    errorString);
                            // Set the flag for deserialization
                            toThrow.deserialize = errorString != null;
                            throw toThrow;
                        }
                        break;
                    case HttpURLConnection.HTTP_UNAUTHORIZED: //401
                        // We need to get a new cookie
                        renewCookie = true;
                        break;
                    default:
                        break;
                    }

                    if (renewCookie) {
                        logger.finest("Cookie was invalid attempt to get new cookie.");
                        boolean success = requestCookie(context);
                        if (success) {
                            // New cookie obtained, replay the request
                            context.replayRequest = true;
                        } else {
                            // Didn't successfully renew, maybe creds are invalid
                            context.replayRequest = false; // Don't replay
                            shouldAttemptCookieRequest.set(false); // Set the flag to stop trying
                        }
                    }
                } else {
                    // Store any cookies provided on the response
                    storeCookiesFromResponse(connection);
                }
            } catch (IOException e) {
                logger.log(Level.SEVERE, "Error reading response code or body from request", e);
            }
        }
        return context;

    }

    private boolean requestCookie(HttpConnectionInterceptorContext context) {
        try {
            HttpConnection conn = Http.POST(sessionURL, "application/x-www-form-urlencoded");
            conn.setRequestBody(sessionRequestBody);

            //when we request the session we need all interceptors except this one

            conn.requestInterceptors.addAll(context.connection.requestInterceptors);
            conn.requestInterceptors.remove(this);
            conn.responseInterceptors.addAll(context.connection.responseInterceptors);
            conn.responseInterceptors.remove(this);

            HttpURLConnection connection = conn.execute().getConnection();
            int responseCode = connection.getResponseCode();

            if (responseCode / 100 == 2) {

                if (sessionHasStarted(connection.getInputStream())) {
                    return storeCookiesFromResponse(connection);
                } else {
                    return false;
                }
            } else {
                InputStream errorStream = connection.getErrorStream();
                try {
                    if (errorStream != null) {
                        // Consume the error stream to avoid leaking connections
                        String error = IOUtils.toString(errorStream, "UTF-8");
                        // Log the error stream content
                        logger.fine(error);
                    }
                } finally {
                    IOUtils.closeQuietly(errorStream);
                }
                if (responseCode == 401) {
                    logger.severe("Credentials are incorrect, cookie authentication will not be"
                            + " attempted again by this interceptor object");
                } else {
                    // catch any other response code
                    logger.log(Level.SEVERE, "Failed to get cookie from server, response code {0}, "
                            + "cookie authentication will not be attempted again", responseCode);
                }
            }
        } catch (IOException e) {
            logger.log(Level.SEVERE, "Failed to read cookie response", e);
        }
        return false;
    }

    private boolean sessionHasStarted(InputStream responseStream) throws IOException {
        try {
            // Get the response body as a string
            String response = IOUtils.toString(responseStream, "UTF-8");

            // Only check for ok:true, https://issues.apache.org/jira/browse/COUCHDB-1356
            // means we cannot check that the name returned is the one we sent.

            // Check the response body for "ok" : true using a regex because we don't want a JSON
            // library dependency for something so simple in a shared HTTP artifact used in both
            // java-cloudant and sync-android. Note (?siu) flags used for . to also match line
            // breaks and for unicode case insensitivity.

            return response.matches("(?s)(?i)(?u).*\\\"ok\\\"\\s*:\\s*true.*");
        } finally {
            IOUtils.closeQuietly(responseStream);
        }

    }

    private boolean storeCookiesFromResponse(HttpURLConnection connection) {

        // Store any cookies from the response in the CookieManager
        try {
            logger.finest("Storing cookie.");
            cookieManager.put(connection.getURL().toURI(), connection.getHeaderFields());
            return true;
        } catch (IOException e) {
            logger.log(Level.SEVERE, "Failed to read cookie response header", e);
            return false;
        } catch (URISyntaxException e) {
            logger.log(Level.SEVERE, "Failed to convert request URL to URI for cookie storage.");
            return false;
        }
    }

    private String listToSemicolonSeparatedString(List<String> cookieStrings) {
        // RFC 6265 says multiple cookie pairs should be "; " separated
        StringBuilder builder = new StringBuilder();
        int index = 0; // Count from 0 since we will increment before comparing to size
        for (String cookieString : cookieStrings) {
            builder.append(cookieString);
            if (++index != cookieStrings.size()) {
                builder.append("; ");
            }
        }
        return builder.toString();
    }

}