/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* 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
*/
package org.eclipse.smarthome.io.rest.internal.filter;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.core.HttpHeaders;
import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.io.rest.internal.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
/**
* A PostMatching filter used to add CORS HTTP headers on responses for requests with CORS
* headers.
*
* Based on http://www.w3.org/TR/cors
*
* This implementation does not allow specific request/response headers nor cookies (allowCredentials).
*
* @author Antoine Besnard - Initial contribution
*
*/
@Provider
public class CorsFilter implements ContainerResponseFilter {
private static final String HTTP_HEAD_METHOD = "HEAD";
private static final String HTTP_DELETE_METHOD = "DELETE";
private static final String HTTP_PUT_METHOD = "PUT";
private static final String HTTP_POST_METHOD = "POST";
private static final String HTTP_GET_METHOD = "GET";
private static final String HTTP_OPTIONS_METHOD = "OPTIONS";
private static final String CONTENT_TYPE_HEADER = HttpHeaders.CONTENT_TYPE;
private static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
private static final String ACCESS_CONTROL_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods";
private static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
private static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
private static final String ORIGIN_HEADER = "Origin";
private static final String VARY_HEADER = "Vary";
private static final String VARY_HEADER_WILDCARD = "*";
private static final String HEADERS_SEPARATOR = ",";
private static final List<String> ACCEPTED_HTTP_METHODS_LIST = Lists.newArrayList(HTTP_GET_METHOD, HTTP_POST_METHOD,
HTTP_PUT_METHOD, HTTP_DELETE_METHOD, HTTP_HEAD_METHOD, HTTP_OPTIONS_METHOD);
private static final String ACCEPTED_HTTP_METHODS = Joiner.on(HEADERS_SEPARATOR).join(ACCEPTED_HTTP_METHODS_LIST);
private final transient Logger logger = LoggerFactory.getLogger(CorsFilter.class);
private boolean isEnabled;
public CorsFilter() {
// Disable the filter by default
this.isEnabled = false;
}
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
if (isEnabled && !processPreflight(requestContext, responseContext)) {
processRequest(requestContext, responseContext);
}
}
/**
* Process the CORS request and response.
*
* @param requestContext
* @param responseContext
*/
private void processRequest(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
// Process the request only if if is an acceptable request method and if it is different from an OPTIONS request
// (OPTIONS requests are not processed here)
if (ACCEPTED_HTTP_METHODS_LIST.contains(requestContext.getMethod())
&& !HTTP_OPTIONS_METHOD.equals(requestContext.getMethod())) {
String origin = getValue(requestContext.getHeaders(), ORIGIN_HEADER);
if (StringUtils.isNotBlank(origin)) {
responseContext.getHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
}
}
}
/**
* Process a preflight CORS request.
*
* @param requestContext
* @param responseContext
* @return true if it is a preflight request that has been processed.
*/
private boolean processPreflight(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
boolean isCorsPreflight = false;
if (HTTP_OPTIONS_METHOD.equals(requestContext.getMethod())) {
// Look for the mandatory CORS preflight request headers
String origin = getValue(requestContext.getHeaders(), ORIGIN_HEADER);
String realRequestMethod = getValue(requestContext.getHeaders(), ACCESS_CONTROL_REQUEST_METHOD);
isCorsPreflight = StringUtils.isNotBlank(origin) && StringUtils.isNotBlank(realRequestMethod);
if (isCorsPreflight) {
responseContext.getHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
responseContext.getHeaders().add(ACCESS_CONTROL_ALLOW_METHODS_HEADER, ACCEPTED_HTTP_METHODS);
responseContext.getHeaders().add(ACCESS_CONTROL_ALLOW_HEADERS, CONTENT_TYPE_HEADER);
// Add the accepted request headers
appendVaryHeader(responseContext);
}
}
return isCorsPreflight;
}
/**
* Get the first value of a header which may contains several values.
*
* @param headers
* @param header
* @return The first value from the given header or null if the header is
* not found.
*
*/
private String getValue(MultivaluedMap<String, String> headers, String header) {
List<String> values = headers.get(header);
if (values == null || values.isEmpty()) {
return null;
}
return values.get(0).toString();
}
/**
* Append the Vary header if necessary to the response.
*
* @param responseContext
*/
private void appendVaryHeader(ContainerResponseContext responseContext) {
String varyHeader = getValue(responseContext.getStringHeaders(), VARY_HEADER);
if (StringUtils.isBlank(varyHeader)) {
// If the Vary header is not present, just add it.
responseContext.getHeaders().add(VARY_HEADER, ORIGIN_HEADER);
} else if (!VARY_HEADER_WILDCARD.equals(varyHeader)) {
// If it is already present and its value is not the Wildcard, append the Origin header.
responseContext.getHeaders().putSingle(VARY_HEADER, varyHeader + HEADERS_SEPARATOR + ORIGIN_HEADER);
}
}
protected void activate(Map<String, Object> properties) {
if (properties != null) {
String corsPropertyValue = (String) properties.get(Constants.CORS_PROPERTY);
this.isEnabled = "true".equalsIgnoreCase(corsPropertyValue);
}
if(this.isEnabled) {
logger.info("enabled CORS for REST API.");
}
}
}