package restx.security; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import restx.*; import restx.factory.Component; import java.io.IOException; import java.util.Collection; import static java.util.Arrays.asList; /** * User: xavierhanin * Date: 2/7/13 * Time: 9:33 AM */ @Component public class CORSFilter extends CORSHandler implements RestxFilter, RestxHandler { private static final Logger logger = LoggerFactory.getLogger(CORSFilter.class); private static final Collection<String> SIMPLE_METHODS = asList("GET", "HEAD", "POST"); private final Iterable<CORSAuthorizer> authorizers; public CORSFilter(Iterable<CORSAuthorizer> authorizers) { this.authorizers = authorizers; } @Override public Optional<RestxHandlerMatch> match(RestxRequest req) { Optional<String> origin = req.getHeader("Origin"); if (origin.isPresent() && !isSameOrigin(req, origin.get()) && !isPreflightRequest(req)) { CORS cors = CORS.check(authorizers, req, origin.get(), req.getHttpMethod(), req.getRestxPath()); if (cors.isAccepted()) { return Optional.of(new RestxHandlerMatch(new StdRestxRequestMatch("*", req.getRestxPath(), ImmutableMap.<String, String>of(), ImmutableMap.of("cors", cors)), this)); } else { if (isSimpleCORSRequest(req)) { logger.info("Unauthorized simple CORS request; Origin={}; Method={}", origin.get(), req.getHttpMethod()); } else { // the check should already have been done by the preflight request, so we shouldn't get to that point // but we never know how the client is actually implemented logger.info("Unauthorized CORS request (not captured by pre flight); Origin={}; Method={}", origin.get(), req.getHttpMethod()); } return unauthorized(req); } } return Optional.absent(); } private boolean isPreflightRequest(RestxRequest req) { return req.getHeader("Origin").isPresent() && req.getHeader("Access-Control-Request-Method").isPresent() && "OPTIONS".equals(req.getHttpMethod()); } protected boolean isSimpleCORSRequest(RestxRequest req) { // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS if (!SIMPLE_METHODS.contains(req.getHttpMethod())) { return false; } Optional<String> origin = req.getHeader("Origin"); if (!origin.isPresent()) { return false; } if ("POST".equals(req.getHttpMethod())) { if (!asList("application/x-www-form-urlencoded", "multipart/form-data", "text/plain") .contains(req.getContentType())) { return false; } } return true; } private boolean isSameOrigin(RestxRequest req, String origin) { // same origin check. // see http://stackoverflow.com/questions/15512331/chrome-adding-origin-header-to-same-origin-request Optional<String> host = req.getHeader("Host"); if (!host.isPresent()) { // no host header, can't check same origin return false; } if (origin.endsWith(host.get())) { logger.debug("Same Origin request not considered as CORS Request: {}", req); return true; } else { return false; } } @Override public void handle(RestxRequestMatch match, RestxRequest req, RestxResponse resp, RestxContext ctx) throws IOException { AcceptedCORS cors = (AcceptedCORS) match.getOtherParams().get("cors"); resp.setHeader("Access-Control-Allow-Origin", cors.getOrigin()); if (!cors.getHeaders().isEmpty()) { resp.setHeader("Access-Control-Allow-Headers", Joiner.on(", ").join(cors.getHeaders())); } if (!cors.getMethods().isEmpty()) { resp.setHeader("Access-Control-Allow-Methods", Joiner.on(", ").join(cors.getMethods())); } if (cors.getAllowCredentials().or(false)) { resp.setHeader("Access-Control-Allow-Credentials", "true"); } ctx.nextHandlerMatch().handle(req, resp, ctx); } public String toString() { return "CORSFilter"; } }