/* * #%L * Wisdom-Framework * %% * Copyright (C) 2013 - 2014 Wisdom Framework * %% * 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. * #L% */ package org.wisdom.framework.filters; import com.google.common.base.Joiner; import org.wisdom.api.http.HttpMethod; import org.wisdom.api.http.Result; import org.wisdom.api.http.Results; import org.wisdom.api.interception.Filter; import org.wisdom.api.interception.RequestContext; import org.wisdom.api.router.Route; import org.wisdom.api.router.Router; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.regex.Pattern; import static org.wisdom.api.http.HeaderNames.*; /** * A filter to support CORS (Cross-origin resource sharing). * Wisdom provides a configuration based implementation, but you can extend this class directly to cusotmize the CORS * support. * CORS is defined by the W3C as a recommendation : http://www.w3.org/TR/cors/ */ public abstract class AbstractCorsFilter implements Filter { private final Router router; /** * Creates an {@link org.wisdom.framework.filters.AbstractCorsFilter} instance. * * @param router the router */ public AbstractCorsFilter(Router router) { this.router = router; } /** * Interception method. * It checks whether or not the request requires CORS support or not. It also checks whether the requests is allowed * or not. * * @param route the router * @param context the filter context * @return the result, containing the CORS headers as defined in the recommendation * @throws Exception if the result cannot be handled correctly. */ public Result call(Route route, RequestContext context) throws Exception { // Is CORS required? String originHeader = context.request().getHeader(ORIGIN); if (originHeader != null) { originHeader = originHeader.toLowerCase(); } // If not Preflight if (route.getHttpMethod() != HttpMethod.OPTIONS) { return retrieveAndReturnResult(context, originHeader); } // OPTIONS route exists, don't use filter! (might manually implement // CORS?) if (!route.isUnbound()) { return context.proceed(); } // Try "Preflight" // Find existing methods for other routes Collection<Route> routes = router.getRoutes(); List<String> methods = new ArrayList<>(4); // expect POST PUT GET DELETE for (Route r : routes) { if (r.matches(r.getHttpMethod(), route.getUrl())) { methods.add(r.getHttpMethod().name()); } } // If there's none, proceed to 404 if (methods.isEmpty()) { return context.proceed(); } String requestMethod = context.request().getHeader(ACCESS_CONTROL_REQUEST_METHOD); // If it's not a CORS request, just proceed! if (originHeader == null || requestMethod == null) { return context.proceed(); } Result res = Results.ok(); // setup result if (!methods.contains(requestMethod.toUpperCase())) { res = Results.unauthorized("No such method for this route"); } Integer maxAge = getMaxAge(); if (maxAge != null) { res = res.with(ACCESS_CONTROL_MAX_AGE, String.valueOf(maxAge)); } // Otherwise we should be return OK with the appropriate headers. String exposedHeaders = getExposedHeadersHeader(); String allowedHosts = getAllowedHostsHeader(originHeader); String allowedMethods = Joiner.on(", ").join(methods); Result result = res.with(ACCESS_CONTROL_ALLOW_ORIGIN, allowedHosts) .with(ACCESS_CONTROL_ALLOW_METHODS, allowedMethods).with(ACCESS_CONTROL_ALLOW_HEADERS, exposedHeaders); if (getAllowCredentials()) { result = result.with(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); } return result; } protected Result retrieveAndReturnResult(RequestContext context, String originHeader) throws Exception { Result result = context.proceed(); // Is it actually a CORS request? if (originHeader != null) { String allowedHosts = getAllowedHostsHeader(originHeader); result = result.with(ACCESS_CONTROL_ALLOW_ORIGIN, allowedHosts); if (getAllowCredentials()) { result = result.with(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); } if (!getExposedHeaders().isEmpty()) { result = result.with(ACCESS_CONTROL_EXPOSE_HEADERS, getExposedHeadersHeader()); } } return result; } private String getExposedHeadersHeader() { return Joiner.on(", ").join(getExposedHeaders()); } private String getAllowedHostsHeader(final String origin) { final List<String> allowedHosts = getAllowedHosts(); //If wildcard is used, only return the request supplied origin if(getAllowCredentials() && allowedHosts.contains("*")){ return origin; }else { return Joiner.on(", ").join(getAllowedHosts()); } } /** * By default intercepts all requests. It is highly recommended to override this method. * * @return {@code .*} */ public Pattern uri() { return Pattern.compile(".*"); } /** * The filter priority, 0 by default (closest to the action method, but before the interceptors) * * @return the filter priority */ public int priority() { return 0; } /** * Gets the list of exposed headers. * * @return the list of exposed headers */ public abstract List<String> getExposedHeaders(); /** * Gets the list of allowed hosts * * @return the list of host */ public abstract List<String> getAllowedHosts(); /** * Checks whether the server allow credentials. * * @return {@code true} if the server allows credentials */ public abstract boolean getAllowCredentials(); /** * Gets the max-age of the result (cache configuration). * * @return the max age. */ public abstract Integer getMaxAge(); }