/** * Copyright 2005-2014 Restlet * * The contents of this file are subject to the terms of one of the following * open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can * select the license that you prefer but you may not use this file except in * compliance with one of these Licenses. * * You can obtain a copy of the Apache 2.0 license at * http://www.opensource.org/licenses/apache-2.0 * * You can obtain a copy of the EPL 1.0 license at * http://www.opensource.org/licenses/eclipse-1.0 * * See the Licenses for the specific language governing permissions and * limitations under the Licenses. * * Alternatively, you can obtain a royalty free commercial license with less * limitations, transferable or non-transferable, directly at * http://restlet.com/products/restlet-framework * * Restlet is a registered trademark of Restlet S.A.S. */ package org.restlet.engine.application; import java.util.HashSet; import java.util.Set; import java.util.logging.Logger; import org.restlet.Context; import org.restlet.Request; import org.restlet.Response; import org.restlet.data.Method; import org.restlet.data.Status; import org.restlet.engine.util.SetUtils; /** * Helps to generate response CORS headers.<br> * The CORS specification defines a subset of methods qualified as simple HEAD, * GET and POST. Any other methods should send a preflight request with the * method OPTIONS. * * @see <a href="http://www.w3.org/TR/cors">W3C CORS Specification</a> * @see <a href="http://www.w3.org/TR/cors/#simple-method">Simple methods</a> * * @author Manuel Boillod */ public class CorsResponseHelper { private static Logger LOGGER = Context.getCurrentLogger(); /** * If true, copies the value of 'Access-Control-Request-Headers' request * header into the 'Access-Control-Allow-Headers' response header. If false, * use {@link #allowedHeaders}. Default is true. */ public boolean allowAllRequestedHeaders = true; /** * If true, add 'Access-Control-Allow-Credentials' header. Default is false. */ public boolean allowedCredentials = false; /** * The value of 'Access-Control-Allow-Headers' response header. Used only if * {@link #allowAllRequestedHeaders} is false. */ public Set<String> allowedHeaders = null; /** The value of 'Access-Control-Allow-Origin' header. Default is '*'. */ public Set<String> allowedOrigins = SetUtils.newHashSet("*"); /** The value of 'Access-Control-Expose-Headers' response header. */ public Set<String> exposedHeaders = null; /** * Adds CORS headers to the given response. * * @param request * The current request. * @param response * The response. */ public void addCorsResponseHeaders(Request request, Response response) { String origin = request.getHeaders().getFirstValue("Origin", true); if (origin == null) { // Not a CORS request return; } Set<Method> allowedMethods = new HashSet<>(response.getAllowedMethods()); // Header 'Allow' is not relevant in CORS request. response.getAllowedMethods().clear(); if (!allowedOrigins.contains("*") && !allowedOrigins.contains(origin)) { // Origin not allowed LOGGER.fine("Origin " + origin + " not allowed for CORS request"); return; } boolean isPreflightRequest = Method.OPTIONS.equals(request.getMethod()); if (isPreflightRequest) { // Default OPTIONS method in a server resource returns a // {@link Status#CLIENT_ERROR_METHOD_NOT_ALLOWED} if the method is // not implemented // or a {@link Status#SUCCESS_NO_CONTENT} or a {@link // Status#SUCCESS_NO_CONTENT} if // the method is implemented and the call succeed. // Other status are considered as error. // Preflight request returns a 200 status except if server resource // method . // If the preflight request is not allowed, CORS response headers // will not be added. if (Status.SUCCESS_OK.equals(response.getStatus()) || Status.SUCCESS_NO_CONTENT.equals(response.getStatus()) || Status.CLIENT_ERROR_METHOD_NOT_ALLOWED.equals(response .getStatus())) { response.setStatus(Status.SUCCESS_OK); } else { LOGGER.fine("The CORS preflight request failed in server resource."); return; } Method requestedMethod = request.getAccessControlRequestMethod(); if (requestedMethod == null) { // Requested Method is required LOGGER.fine("A CORS preflight request should specified header 'Access-Control-Request-Method'"); return; } if (!allowedMethods.contains(requestedMethod)) { // Method not allowed LOGGER.fine("The CORS preflight request ask for methods not allowed in header 'Access-Control-Request-Method'"); return; } Set<String> requestedHeaders = request .getAccessControlRequestHeaders(); if (requestedHeaders == null) { requestedHeaders = SetUtils.newHashSet(); } if (!allowAllRequestedHeaders && (allowedHeaders == null || !isAllHeadersAllowed( allowedHeaders, requestedHeaders))) { // Headers not allowed LOGGER.fine("The CORS preflight request ask for headers not allowed in header 'Access-Control-Request-Headers'"); return; } // Header 'Access-Control-Allow-Methods' response.setAccessControlAllowMethods(allowedMethods); // Header 'Access-Control-Allow-Headers' response.setAccessControlAllowHeaders(requestedHeaders); } else { // simple request // Header 'Access-Control-Expose-Headers' if (exposedHeaders != null && !exposedHeaders.isEmpty()) { response.setAccessControlExposeHeaders(exposedHeaders); } } // Header 'Access-Control-Allow-Credentials' if (allowedCredentials) { response.setAccessControlAllowCredentials(true); } // Header 'Access-Control-Allow-Origin' if (!allowedCredentials && allowedOrigins.contains("*")) { response.setAccessControlAllowOrigin("*"); } else { response.setAccessControlAllowOrigin(origin); } } /** * Returns the modifiable set of headers allowed by the actual request on * the current resource.<br> * Note that when used with HTTP connectors, this property maps to the * "Access-Control-Allow-Headers" header. * * @return The set of headers allowed by the actual request on the current * resource. */ public Set<String> getAllowedHeaders() { return allowedHeaders; } /** * Returns the URI an origin server allows for the requested resource. Use * "*" as a wildcard character.<br> * Note that when used with HTTP connectors, this property maps to the * "Access-Control-Allow-Origin" header. * * @return The origin allowed by the requested resource. */ public Set<String> getAllowedOrigins() { return allowedOrigins; } /** * Returns a modifiable whitelist of headers an origin server allows for the * requested resource.<br> * Note that when used with HTTP connectors, this property maps to the * "Access-Control-Expose-Headers" header. * * @return The set of headers an origin server allows for the requested * resource. */ public Set<String> getExposedHeaders() { return exposedHeaders; } /** * Returns true if all requested headers are allowed (case-insensitive). * * @param allowHeaders * The allowed headers. * @param requestedHeaders * The requested headers. * @return True if all requested headers are allowed (case-insensitive). */ private boolean isAllHeadersAllowed(Set<String> allowHeaders, Set<String> requestedHeaders) { for (String requestedHeader : requestedHeaders) { boolean headerAllowed = false; for (String allowHeader : allowHeaders) { if (allowHeader.equalsIgnoreCase(requestedHeader)) { headerAllowed = true; break; } } if (!headerAllowed) { return false; } } return true; } /** * If true, indicates that the value of 'Access-Control-Request-Headers' * request header will be copied into the 'Access-Control-Allow-Headers' * response header. If false, use {@link #allowedHeaders}. */ public boolean isAllowAllRequestedHeaders() { return allowAllRequestedHeaders; } /** * If true, adds 'Access-Control-Allow-Credentials' header. * * @return True, if the 'Access-Control-Allow-Credentials' header will be * added. */ public boolean isAllowedCredentials() { return allowedCredentials; } /** * Returns true if the request is a CORS request. * * @param request * The current request. * @return true if the request is a CORS request. */ public boolean isCorsRequest(Request request) { return request.getHeaders().getFirstValue("Origin", true) != null; } /** * If true, copies the value of 'Access-Control-Request-Headers' request * header into the 'Access-Control-Allow-Headers' response header. If false, * use {@link #allowedHeaders}. * * @param allowAllRequestedHeaders * True to copy the value of 'Access-Control-Request-Headers' * request header into the 'Access-Control-Allow-Headers' * response header. If false, use {@link #allowedHeaders}. */ public void setAllowAllRequestedHeaders( boolean allowAllRequestedHeaders) { this.allowAllRequestedHeaders = allowAllRequestedHeaders; } /** * If true, adds 'Access-Control-Allow-Credentials' header. * * @param allowedCredentials * True to add the 'Access-Control-Allow-Credentials' header. */ public void setAllowedCredentials(boolean allowedCredentials) { this.allowedCredentials = allowedCredentials; } /** * Sets the value of the 'Access-Control-Allow-Headers' response header. * Used only if {@link #allowAllRequestedHeaders} is false. * * @param allowedHeaders * The value of 'Access-Control-Allow-Headers' response header. */ public void setAllowedHeaders(Set<String> allowedHeaders) { this.allowedHeaders = allowedHeaders; } /** * Sets the value of 'Access-Control-Allow-Origin' header. * * @param allowedOrigins * The value of 'Access-Control-Allow-Origin' header. */ public void setAllowedOrigins(Set<String> allowedOrigins) { this.allowedOrigins = allowedOrigins; } /** * Sets the value of 'Access-Control-Expose-Headers' response header. * * @param exposedHeaders * The value of 'Access-Control-Expose-Headers' response header. */ public void setExposedHeaders(Set<String> exposedHeaders) { this.exposedHeaders = exposedHeaders; } }