/*
*
* Copyright (C) 2007-2015 Licensed to the Comunes Association (CA) under
* one or more contributor license agreements (see COPYRIGHT for details).
* The CA licenses this file to you under the GNU Affero General Public
* License version 3, (the "License"); you may not use this file except in
* compliance with the License. This file is part of kune.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package cc.kune.core.server.rack.filters.rest;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.thetransactioncompany.cors.CORSConfiguration;
import com.thetransactioncompany.cors.CORSConfigurationException;
import com.thetransactioncompany.cors.CORSConfigurationLoader;
import com.thetransactioncompany.cors.CORSFilter;
import com.thetransactioncompany.cors.CORSOriginDeniedException;
import com.thetransactioncompany.cors.CORSRequestHandler;
import com.thetransactioncompany.cors.CORSRequestType;
import com.thetransactioncompany.cors.HTTPMethod;
import com.thetransactioncompany.cors.HeaderFieldName;
import com.thetransactioncompany.cors.InvalidCORSRequestException;
import com.thetransactioncompany.cors.UnsupportedHTTPHeaderException;
import com.thetransactioncompany.cors.UnsupportedHTTPMethodException;
/**
* Cross-Origin Resource Sharing (CORS) servlet filter.
*
* <p>
* The filter intercepts incoming HTTP requests and applies the CORS policy as
* specified by the filter init parameters. The actual CORS request is processed
* by the {@link CORSRequestHandler} class.
*
* <p>
* Supported configuration parameters:
*
* <ul>
* <li>cors.allowGenericHttpRequests {true|false} defaults to {@code true}.
* <li>cors.allowOrigin {"*"|origin-list} defaults to {@code *}.
* <li>cors.allowSubdomains {true|false} defaults to {@code false}.
* <li>cors.supportedMethods {method-list} defaults to {@code "GET, POST,
* HEAD, OPTIONS"}.
* <li>cors.supportedHeaders {"*"|header-list} defaults to {@code *}.
* <li>cors.exposedHeaders {header-list} defaults to empty list.
* <li>cors.supportsCredentials {true|false} defaults to {@code true}.
* <li>cors.maxAge {int} defaults to {@code -1} (unspecified).
* </ul>
*
* @author Vladimir Dzhuvinov
* @author David Bellem
*
* Modified {@link CORSFilter} for kune by vjrj@ourproject.org
*/
public abstract class AbstractCustomCORSFilter implements Filter {
/**
* The CORS filer configuration.
*/
private CORSConfiguration config;
/**
* Encapsulates the CORS request handling logic.
*/
private CORSRequestHandler handler;
protected abstract void customDoFilter(HttpServletRequest request, HttpServletResponse response,
final FilterChain chain) throws IOException, ServletException;
/**
* Called by the web container to indicate to a filter that it is being taken
* out of service.
*/
@Override
public void destroy() {
// do nothing
}
/**
* Filters an HTTP request / response pair according to the configured CORS
* policy. Also tags the request with CORS information to downstream handlers.
*
* @param request
* The servlet request.
* @param response
* The servlet response.
* @param chain
* The servlet filter chain.
*
* @throws IOException
* On a I/O exception.
* @throws ServletException
* On a general request processing exception.
*/
private void doFilter(final HttpServletRequest request, final HttpServletResponse response,
final FilterChain chain) throws IOException, ServletException {
// Tag
handler.tagRequest(request);
final CORSRequestType type = CORSRequestType.detect(request);
try {
if (type.equals(CORSRequestType.ACTUAL)) {
// Simple / actual CORS request
handler.handleActualRequest(request, response);
customDoFilter(request, response, chain);
} else if (type.equals(CORSRequestType.PREFLIGHT)) {
// Preflight CORS request, handle but don't
// pass further down the chain
handler.handlePreflightRequest(request, response);
} else if (config.allowGenericHttpRequests) {
// Not a CORS request, but allow it through
request.setAttribute("cors.isCorsRequest", false); // tag
customDoFilter(request, response, chain);
} else {
// Generic HTTP requests denied
request.setAttribute("cors.isCorsRequest", false); // tag
printMessage(response, HttpServletResponse.SC_FORBIDDEN, "Generic HTTP requests not allowed");
}
} catch (final InvalidCORSRequestException e) {
request.setAttribute("cors.isCorsRequest", false); // tag
printMessage(response, HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
} catch (final CORSOriginDeniedException e) {
final String msg = e.getMessage() + ": " + e.getRequestOrigin();
printMessage(response, HttpServletResponse.SC_FORBIDDEN, msg);
} catch (final UnsupportedHTTPMethodException e) {
String msg = e.getMessage();
final HTTPMethod method = e.getRequestedMethod();
if (method != null) {
msg = msg + ": " + method.toString();
}
printMessage(response, HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} catch (final UnsupportedHTTPHeaderException e) {
String msg = e.getMessage();
final HeaderFieldName header = e.getRequestHeader();
if (header != null) {
msg = msg + ": " + header.toString();
}
printMessage(response, HttpServletResponse.SC_FORBIDDEN, msg);
}
}
/**
* Called by the servlet container each time a request / response pair is
* passed through the chain due to a client request for a resource at the end
* of the chain.
*
* @param request
* The servlet request.
* @param response
* The servlet response.
* @param chain
* The servlet filter chain.
*
* @throws IOException
* On a I/O exception.
* @throws ServletException
* On a general request processing exception.
*/
@Override
public void doFilter(final ServletRequest request, final ServletResponse response,
final FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
// Cast to HTTP
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
} else {
throw new ServletException("Cannot filter non-HTTP requests/responses");
}
}
/**
* Gets the configuration of this CORS filter.
*
* @return The configuration, {@code null} if the filter is not initialised.
*/
public CORSConfiguration getConfiguration() {
return config;
}
/**
* This method is invoked by the web container to initialise the filter at
* startup.
*
* @param filterConfig
* The servlet filter configuration. Must not be {@code null}.
*
* @throws ServletException
* On a filter initialisation exception.
*/
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
final CORSConfigurationLoader configLoader = new CORSConfigurationLoader(filterConfig);
try {
config = configLoader.load();
} catch (final CORSConfigurationException e) {
throw new ServletException(e.getMessage(), e);
}
handler = new CORSRequestHandler(config);
}
/**
* Produces a simple HTTP text/plain response with the specified status code
* and message.
*
* <p>
* Note: The CORS filter avoids falling back to the default web container
* error page (typically a richly-formatted HTML page) to make it easier for
* XHR debugger tools to identify the cause of failed requests.
*
* @param sc
* The HTTP status code.
* @param msg
* The message.
*
* @throws IOException
* On a I/O exception.
* @throws ServletException
* On a general request processing exception.
*/
protected void printMessage(final HttpServletResponse response, final int sc, final String msg)
throws IOException, ServletException {
// Set the status code
response.setStatus(sc);
// Write the error message
response.resetBuffer();
response.setContentType("text/plain");
final PrintWriter out = response.getWriter();
out.println("Cross-Origin Resource Sharing (CORS) Filter: " + msg);
}
}