OAuthTokenInterceptors.java

package org.syncany.plugins.transfer.oauth;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.io.IOUtils;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.Headers;
import io.undertow.util.StatusCodes;

/**
 * Factory class to generate some common {@link OAuthTokenInterceptor}s.
 *
 * @author Christian Roth (christian.roth@port17.de)
 */
public abstract class OAuthTokenInterceptors {

	private static final Logger logger = Logger.getLogger(OAuthTokenInterceptors.class.getName());

	/**
	 * Has to be {@value} because it's the first step of the OAuth process.
	 */
	static final String PATH_PREFIX = "/";

	/**
	 * Get a common {@link OAuthTokenInterceptor} depending on the chosen {@link OAuthMode}.
	 * If {@link OAuthMode#BROWSER} is used a {@link HashTokenInterceptor}
	 * is returned and a {@link OAuthTokenInterceptors.RedirectTokenInterceptor} in {@link OAuthMode#SERVER}.
	 *
	 * @param mode {@link OAuthMode} supported by the {@link org.syncany.plugins.transfer.TransferPlugin}.
	 * @return Either a {@link HashTokenInterceptor} or a {@link OAuthTokenInterceptors.RedirectTokenInterceptor}
	 */
	public static OAuthTokenInterceptor newTokenInterceptorForMode(OAuthMode mode) {
		switch (mode) {
			case BROWSER:
				return new HashTokenInterceptor();

			case SERVER:
				return new RedirectTokenInterceptor();

			default:
				throw new RuntimeException("Unknown OAuth mode");
		}
	}

	/**
	 * {@link OAuthTokenInterceptor} implementation which bypasses some protection mechanisms to allow the token extraction.
	 * In {@link OAuthMode#BROWSER}, the service provider uses the fragment part (the part after the #) of a URL to send over
	 * a token. However, this part cannot be retrieved by a WebServer. A {@link HashTokenInterceptor}
	 * appends the fragment variables to the query parameters of the URL.
	 */
	public static class HashTokenInterceptor implements OAuthTokenInterceptor {

		public static final String PLACEHOLDER_FOR_EXTRACT_PATH = "%extractPath%";
		private static final String HTML_SITE_RESOURCE_PATH = "/org/syncany/plugins/oauth/HashTokenInterceptor.html";

		private final String html;

		public HashTokenInterceptor() {
			try(InputStream htmlSiteStream = HashTokenInterceptor.class.getResourceAsStream(HTML_SITE_RESOURCE_PATH)) {
				this.html = IOUtils.toString(htmlSiteStream).replace(PLACEHOLDER_FOR_EXTRACT_PATH, OAuthTokenWebListener.ExtractingTokenInterceptor.PATH_PREFIX);
			}
			catch (IOException e) {
				logger.log(Level.SEVERE, "Unable to read html site from " + HTML_SITE_RESOURCE_PATH, e);
				throw new RuntimeException("Unable to read html site from " + HTML_SITE_RESOURCE_PATH);
			}
		}

		@Override
		public String getPathPrefix() {
			return PATH_PREFIX;
		}

		@Override
		public void handleRequest(HttpServerExchange exchange) throws Exception {
			exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html");
			exchange.setResponseCode(StatusCodes.OK);
			exchange.getResponseSender().send(html);
			exchange.endExchange();
		}
	}

	/**
	 * A {@link RedirectTokenInterceptor} can be seen as an empty {@link OAuthTokenInterceptor} because it only redirects
	 * to the next step of the OAuth process which is the extraction of the token from the URL. It's needed in {@link OAuthMode#SERVER}
	 * since the token parameter is already provided in the URL's query part.
	 */
	public static class RedirectTokenInterceptor implements OAuthTokenInterceptor {

		@Override
		public String getPathPrefix() {
			return PATH_PREFIX;
		}

		@Override
		public void handleRequest(HttpServerExchange exchange) throws Exception {
			final String redirectToUrl = String.format("%s/%s?%s", exchange.getRequestURL(), OAuthTokenWebListener.ExtractingTokenInterceptor.PATH_PREFIX, exchange.getQueryString());
			final URI redirectToUri = URI.create(redirectToUrl).normalize();

			logger.log(Level.INFO, "Redirecting to " + redirectToUri);

			exchange.setResponseCode(StatusCodes.FOUND);
			exchange.getResponseHeaders().put(Headers.LOCATION, redirectToUri.toString());
			exchange.endExchange();
		}
	}

}