All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cybermkd.route.handler.cors.CORSHandler Maven / Gradle / Ivy

package com.cybermkd.route.handler.cors;

import com.cybermkd.common.http.HttpMethod;
import com.cybermkd.common.http.HttpRequest;
import com.cybermkd.common.http.HttpResponse;
import com.cybermkd.common.http.exception.WebException;
import com.cybermkd.common.http.result.HttpStatus;
import com.cybermkd.common.util.Joiner;
import com.cybermkd.common.util.Lister;
import com.cybermkd.log.Logger;
import com.cybermkd.route.handler.Handler;

import java.util.Enumeration;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by ice on 14-12-22.
 */
public class CORSHandler extends Handler {
    public static final String ACCESS_CONTROL_REQUEST_METHOD_HEADER = "Access-Control-Request-Method";
    public static final String ACCESS_CONTROL_REQUEST_HEADERS_HEADER = "Access-Control-Request-Headers";
    // Response headers
    public static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
    public static final String ACCESS_CONTROL_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods";
    public static final String ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers";
    public static final String ACCESS_CONTROL_MAX_AGE_HEADER = "Access-Control-Max-Age";
    public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER = "Access-Control-Allow-Credentials";
    public static final String ACCESS_CONTROL_EXPOSE_HEADERS_HEADER = "Access-Control-Expose-Headers";
    private static final Logger logger = Logger.getLogger(CORSHandler.class);
    // Request headers
    private static final String ORIGIN_HEADER = "Origin";
    // Implementation constants
    private static final List SIMPLE_HTTP_METHODS = Lister.of(HttpMethod.GET, HttpMethod.POST, HttpMethod.HEAD);

    private boolean anyOriginAllowed = true;
    private boolean anyHeadersAllowed = false;
    private List allowedOrigins = Lister.of("*");
    private List allowedMethods = Lister.of(HttpMethod.GET, HttpMethod.POST, HttpMethod.HEAD);
    private List allowedHeaders = Lister.of("X-Requested-With", "Content-Type", "Accept", "Origin");
    private List exposedHeaders = null;
    private int preflightMaxAge = 1800;
    private boolean allowCredentials = true;
    private boolean chainPreflight = true;

    public CORSHandler() {
    }

    public CORSHandler(String allowedMethods) {
        this(null, allowedMethods, null);
    }

    public CORSHandler(String allowedMethods, String allowedHeaders) {
        this(null, allowedMethods, allowedHeaders);
    }

    public CORSHandler(String allowedOrigins, String allowedMethods, String allowedHeaders) {
        this(allowedOrigins, allowedMethods, allowedHeaders, null);
    }

    /**
     * @param allowedOrigins Multiple origins allowed, separated default *
     * @param allowedMethods Multiple httpMethods allowed, separated default GET,POST,HEAD
     * @param allowedHeaders Multiple headers allowed, separated default X-Requested-With,Content-Type,Accept,Origin
     * @param exposedHeaders Multiple origins expose, separated default null
     */
    public CORSHandler(String allowedOrigins, String allowedMethods, String allowedHeaders, String exposedHeaders) {
        if (allowedOrigins != null)
            this.allowedOrigins = Lister.of(allowedOrigins.split(","));
        if (allowedMethods != null)
            this.allowedMethods = Lister.of(allowedMethods.split(","));
        if (allowedHeaders != null)
            this.allowedHeaders = Lister.of(allowedHeaders.split(","));
        if (exposedHeaders != null)
            this.exposedHeaders = Lister.of(exposedHeaders.split(","));
    }

    public final void handle(HttpRequest request, HttpResponse response, boolean[] isHandled) {
        String origin = request.getHeader(ORIGIN_HEADER);
        // Is it a cross origin request ?
        if (origin != null && isEnabled(request)) {
            if (originMatches(origin)) {
                if (isSimpleRequest(request)) {
                    logger.debug("Cross-origin request to %s is a simple cross-origin request", request.getRestPath());
                    handleSimpleResponse(request, response, origin);
                } else if (isPreflightRequest(request)) {
                    logger.debug("Cross-origin request to %s is a preflight cross-origin request", request.getRestPath());
                    handlePreflightResponse(request, response, origin);
                    if (chainPreflight)
                        logger.debug("Preflight cross-origin request to %s forwarded to application", request.getRestPath());
                    else
                        throw new WebException(HttpStatus.FORBIDDEN, "Unauthorized CORS request");
                } else {
                    logger.debug("Cross-origin request to %s is a non-simple cross-origin request", request.getRestPath());
                    handleSimpleResponse(request, response, origin);
                }
            } else {
                logger.debug("Cross-origin request to " + request.getRestPath() + " with origin " + origin + " does not match allowed origins " + allowedOrigins);
            }
        }
        nextHandler.handle(request, response, isHandled);
    }

    protected boolean isEnabled(HttpRequest request) {
        // WebSocket clients such as Chrome 5 implement a version of the WebSocket
        // protocol that does not accept extra response headers on the upgrade response
        for (Enumeration connections = request.getHeaders("Connection"); connections.hasMoreElements(); ) {
            String connection = (String) connections.nextElement();
            if ("Upgrade".equalsIgnoreCase(connection)) {
                for (Enumeration upgrades = request.getHeaders("Upgrade"); upgrades.hasMoreElements(); ) {
                    String upgrade = (String) upgrades.nextElement();
                    if ("WebSocket".equalsIgnoreCase(upgrade))
                        return false;
                }
            }
        }
        return true;
    }

    private boolean originMatches(String originList) {
        if (anyOriginAllowed)
            return true;

        if (originList.trim().length() == 0)
            return false;

        String[] origins = originList.split(" ");
        for (String origin : origins) {
            if (origin.trim().length() == 0)
                continue;

            for (String allowedOrigin : allowedOrigins) {
                if (allowedOrigin.contains("*")) {
                    Matcher matcher = createMatcher(origin, allowedOrigin);
                    if (matcher.matches())
                        return true;
                } else if (allowedOrigin.equals(origin)) {
                    return true;
                }
            }
        }
        return false;
    }

    private Matcher createMatcher(String origin, String allowedOrigin) {
        String regex = parseAllowedWildcardOriginToRegex(allowedOrigin);
        Pattern pattern = Pattern.compile(regex);
        return pattern.matcher(origin);
    }

    private String parseAllowedWildcardOriginToRegex(String allowedOrigin) {
        String regex = allowedOrigin.replace(".", "\\.");
        return regex.replace("*", ".*"); // we want to be greedy here to match multiple subdomains, thus we use .*
    }

    private boolean isSimpleRequest(HttpRequest request) {

        if (SIMPLE_HTTP_METHODS.contains(request.getHttpMethod())) {
            // TODO: implement better detection of simple headers
            // The specification says that for a request to be simple, custom request headers must be simple.
            // Here for simplicity I just check if there is a Access-Control-Request-Method header,
            // which is required for preflight requests
            return request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null;
        }
        return false;
    }

    private boolean isPreflightRequest(HttpRequest request) {
        if (HttpMethod.OPTIONS.equalsIgnoreCase(request.getHttpMethod())) {
            return true;
        }
        if (request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null) {
            return false;
        }
        return true;
    }

    private void handleSimpleResponse(HttpRequest request, HttpResponse response, String origin) {
        response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
        //W3C CORS spec http://www.w3.org/TR/cors/#resource-implementation
        if (!anyOriginAllowed)
            response.addHeader("Vary", ORIGIN_HEADER);
        if (allowCredentials)
            response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
        if (exposedHeaders != null && !exposedHeaders.isEmpty())
            response.setHeader(ACCESS_CONTROL_EXPOSE_HEADERS_HEADER, Joiner.on(",").join(exposedHeaders));
    }

    private void handlePreflightResponse(HttpRequest request, HttpResponse response, String origin) {
        boolean methodAllowed = isMethodAllowed(request);

        if (!methodAllowed)
            return;
        List headersRequested = getAccessControlRequestHeaders(request);
        boolean headersAllowed = areHeadersAllowed(headersRequested);
        if (!headersAllowed)
            return;
        response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
        //W3C CORS spec http://www.w3.org/TR/cors/#resource-implementation
        if (!anyOriginAllowed)
            response.addHeader("Vary", ORIGIN_HEADER);
        if (allowCredentials)
            response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
        if (preflightMaxAge > 0)
            response.setHeader(ACCESS_CONTROL_MAX_AGE_HEADER, String.valueOf(preflightMaxAge));
        response.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, Joiner.on(",").join(allowedMethods));
        if (anyHeadersAllowed)
            response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, Joiner.on(",").join(headersRequested));
        else
            response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, Joiner.on(",").join(allowedHeaders));
    }

    private boolean isMethodAllowed(HttpRequest request) {
        String accessControlRequestMethod = request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER);
        logger.debug("%s is %s", ACCESS_CONTROL_REQUEST_METHOD_HEADER, accessControlRequestMethod);
        boolean result = false;
        if (accessControlRequestMethod != null)
            result = allowedMethods.contains(accessControlRequestMethod);
        logger.debug("Method %s is" + (result ? "" : " not") + " among allowed methods %s", accessControlRequestMethod, allowedMethods);
        return result;
    }

    List getAccessControlRequestHeaders(HttpRequest request) {
        String accessControlRequestHeaders = request.getHeader(ACCESS_CONTROL_REQUEST_HEADERS_HEADER);
        logger.debug("%s is %s", ACCESS_CONTROL_REQUEST_HEADERS_HEADER, accessControlRequestHeaders);
        if (accessControlRequestHeaders == null)
            return Lister.of();

        List requestedHeaders = Lister.of();
        String[] headers = accessControlRequestHeaders.split(",");
        for (String header : headers) {
            String h = header.trim();
            if (h.length() > 0)
                requestedHeaders.add(h);
        }
        return requestedHeaders;
    }


    private boolean areHeadersAllowed(List requestedHeaders) {
        if (anyHeadersAllowed) {
            logger.debug("Any header is allowed");
            return true;
        }

        boolean result = true;
        for (String requestedHeader : requestedHeaders) {
            boolean headerAllowed = false;
            for (String allowedHeader : allowedHeaders) {
                if (requestedHeader.equalsIgnoreCase(allowedHeader.trim())) {
                    headerAllowed = true;
                    break;
                }
            }
            if (!headerAllowed) {
                result = false;
                break;
            }
        }
        logger.debug("Headers [%s] are" + (result ? "" : " not") + " among allowed headers %s", requestedHeaders, allowedHeaders);
        return result;
    }


    public List getAllowedOrigins() {
        return allowedOrigins;
    }

    public void setAllowedOrigins(String... allowedOrigins) {
        if (allowedOrigins.length == 1 && allowedOrigins[0].equals("*")) {
            this.anyOriginAllowed = true;
        }
        this.allowedOrigins = Lister.of(allowedOrigins);
    }

    public List getAllowedMethods() {
        return allowedMethods;
    }

    public void setAllowedMethods(String... allowedMethods) {
        this.allowedMethods = Lister.of(allowedMethods);
    }

    public List getAllowedHeaders() {
        return allowedHeaders;
    }

    public void setAllowedHeaders(String... allowedHeaders) {
        if (allowedHeaders.length == 1 && allowedHeaders[0].equals("*")) {
            this.anyHeadersAllowed = true;
        }
        this.allowedHeaders = Lister.of(allowedHeaders);
    }

    public List getExposedHeaders() {
        return exposedHeaders;
    }

    public void setExposedHeaders(String... exposedHeaders) {
        this.exposedHeaders = Lister.of(exposedHeaders);
    }

    public int getPreflightMaxAge() {
        return preflightMaxAge;
    }

    public void setPreflightMaxAge(int preflightMaxAge) {
        this.preflightMaxAge = preflightMaxAge;
    }

    public boolean isAllowCredentials() {
        return allowCredentials;
    }

    public void setAllowCredentials(boolean allowCredentials) {
        this.allowCredentials = allowCredentials;
    }

    public boolean isChainPreflight() {
        return chainPreflight;
    }

    public void setChainPreflight(boolean chainPreflight) {
        this.chainPreflight = chainPreflight;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy