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

aQute.openapi.provider.cors.CORSImplementation Maven / Gradle / Ivy

package aQute.openapi.provider.cors;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;

import aQute.lib.strings.Strings;
import aQute.libg.glob.Glob;
import aQute.openapi.provider.CORS;

/**
 * To test:
 * https://www.test-cors.org/#?client_method=PUT&client_credentials=false&client_postdata=%7B%7D&server_url=http%3A%2F%2Flocalhost%3A8080%2Fv2%2Fuser%2Ffoo&server_enable=true&server_status=200&server_credentials=false&server_tabs=remote
 */
public class CORSImplementation implements CORS {
	Set	listOfOrigins			= new HashSet<>();
	Set	listOfExposedHeaders	= new HashSet<>();
	Set	listOfHeaders			= new HashSet<>();
	boolean		supportCredentials		= false;
	int			maxAge;
	Logger		logger;

	public CORSImplementation(Logger logger, String[] listOfOrigins, String[] listOfExposedHeaders,
			String[] listOfHeaders, boolean supportCredentials, int maxAge) {
		this.logger = logger;
		this.listOfOrigins = toGlobs(listOfOrigins);
		this.listOfExposedHeaders = listOfExposedHeaders == null ? Collections.emptySet()
				: Stream.of(listOfExposedHeaders).collect(Collectors.toSet());
		this.listOfHeaders = toGlobs(listOfHeaders);
		this.supportCredentials = supportCredentials;
		this.maxAge = maxAge;
	}

	/**
	 * Simple Cross-Origin Request
	 */

	public boolean fixup(HttpServletRequest request, HttpServletResponse response) {

		// If the Origin header is not present terminate this set of steps.
		// The request is outside the scope of this specification.

		String origin = request.getHeader("Origin");
		if (origin == null) {
			logger.debug("{} no expected Origin header set", this);
			return false;
		}

		// 2. If the value of the Origin header is not a case-sensitive
		// match for any of the values in list of origins do not set
		// any additional headers and terminate this set of steps.

		boolean wildcard = listOfOrigins.isEmpty();

		if (!wildcard && !in(listOfOrigins, origin)) {
			logger.warn("{} Invalid origin {}, allowed {}", this, origin, listOfOrigins);
			return false;
		}

		// If the resource supports credentials add a single
		// Access-Control-Allow-Origin header,
		// with the value of the Origin header as value, and add a single
		// Access-Control-Allow-Credentials header with the case-sensitive
		// string "true" as value.

		String allowOrigin = response.getHeader("Access-Control-Allow-Origin");

		if (supportCredentials) {
			if (allowOrigin == null)
				response.addHeader("Access-Control-Allow-Origin", origin);
			String allowCredentials = response.getHeader("Access-Control-Allow-Credentials");
			if (allowCredentials == null)
				response.addHeader("Access-Control-Allow-Credentials", "true");
		} else {

			// Otherwise, add a single Access-Control-Allow-Origin header, with
			// either the value of the Origin header or the string "*" as value.

			if (allowOrigin == null)
				response.addHeader("Access-Control-Allow-Origin", origin);
		}

		// If the list of exposed headers is not empty add one or more
		// Access-Control-Expose-Headers headers, with as values the header
		// field names given in the list of exposed headers.

		if (!listOfExposedHeaders.isEmpty()) {
			for (String h : listOfExposedHeaders) {
				response.addHeader("Access-Control-Expose-Headers", h);
			}
		}
		return true;
	}

	/**
	 * This method is called for any resource that has a method defined. The
	 * methods are passed as a parameter. This is primarily used as a preflight
	 * check for the CORS protocol
	 * 
	 * @param methods an array of methods for this URL
	 * @return true if found
	 * @throws IOException
	 */
	public boolean doOptions(HttpServletRequest request, HttpServletResponse response, String... methods)
			throws IOException {

		if (!"OPTIONS".equals(request.getMethod()))
			return false;

		return preflight(request, response, methods);
	}

	private boolean preflight(HttpServletRequest request, HttpServletResponse response, String... methods)
			throws IOException {

		// 1. If the Origin header is not present terminate this set of steps.

		String origin = request.getHeader("Origin");
		if (origin == null) {
			logger.debug("{} no expected Origin header set", request);
			return false;
		}

		// 2. If the value of the Origin header is not a case-sensitive
		// match for any of the values in list of origins do not set
		// any additional headers and terminate this set of steps.

		boolean wildcard = listOfOrigins.isEmpty();

		if (!wildcard && !in(listOfOrigins, origin)) {
			logger.warn("{} Invalid origin {}, allowed {}", request, origin, listOfOrigins);
			return false;
		}

		// Let method be the value as result of parsing the
		// Access-Control-Request-Method header.
		// If there is no Access-Control-Request-Method header or if parsing
		// failed, do not set
		// any additional headers and terminate this set of steps. The request
		// is outside the scope of this specification

		String method = request.getHeader("Access-Control-Request-Method");
		if (method == null) {
			logger.warn("{} Missing expected CORS header  Access-Control-Request-Method", request);
			response.setStatus(400);
			return true;
		}

		// If method is not a case-sensitive match for any of the values in
		// list of methods do not set any additional headers and terminate this
		// set of steps.

		if (!Strings.in(methods, method.trim().toUpperCase())) {
			logger.warn("{} Not an allowed method {}", request, method);
			response.setStatus(400);
			return true;
		}

		// Let header field-names be the values as result of parsing the
		// Access-Control-Request-Headers headers.
		// If there are no Access-Control-Request-Headers headers let
		// header field-names be the empty list.
		// If parsing failed do not set any additional headers and terminate
		// this set of steps. The request is outside the scope of this
		// specification.

		List headers;
		String rawRequestHeaders = request.getHeader("Access-Control-Request-Headers");
		if (rawRequestHeaders == null)
			headers = Collections.emptyList();
		else {
			rawRequestHeaders = rawRequestHeaders.toLowerCase();
			headers = Strings.split(rawRequestHeaders);
		}

		// If any of the header field-names is not a ASCII case-insensitive
		// match for any of the values in list of headers do not set any
		// additional headers and terminate this set of steps.

		header: for (String h : headers) {

			if (in(listOfHeaders, h))
				continue header;

			logger.warn("{} Not an allowed header {}, allowed {}", request, h, listOfHeaders);
			response.setStatus(400);
			return true;
		}

		// If the resource supports credentials add a single
		// Access-Control-Allow-Origin header,
		// with the value of the Origin header as value, and add a single
		// Access-Control-Allow-Credentials
		// header with the case-sensitive string "true" as value.

		if (supportCredentials) {
			response.setHeader("Access-Control-Allow-Credentials", "true");
		}

		// Optionally add a single Access-Control-Max-Age header with as value
		// the amount of seconds the user agent is allowed to cache the result
		// of the request.

		if (maxAge > 0) {
			response.setIntHeader("Access-Control-Max-Age", maxAge);
		}

		// Add one or more Access-Control-Allow-Methods headers consisting of (a
		// subset of) the list of methods.

		Arrays.sort(methods);
		response.setHeader("Access-Control-Allow-Methods", Strings.join(methods));

		// If each of the header field-names is a simple header and none is
		// Content-Type, this step may be skipped.

		boolean eachSimpleHeader = headers.stream().allMatch(this::isSimpleHeader);
		boolean noneIsContentType = !headers.contains("content-type");
		boolean skipStep = eachSimpleHeader && noneIsContentType;

		if (!skipStep) {

			// Add one or more Access-Control-Allow-Headers headers consisting
			// of (a subset of) the list of headers.

			response.setHeader("Access-Control-Allow-Headers", Strings.join(headers));
		}
		response.setStatus(204);
		return true;
	}

	private boolean in(Set globs, String term) {
		for (Glob g : globs) {
			if (g.matcher(term).matches())
				return true;
		}
		return false;
	}

	/*
	 * A header is said to be a simple header if the header field name is an
	 * ASCII case-insensitive match for Accept, Accept-Language, or
	 * Content-Language or if it is an ASCII case-insensitive match for
	 * Content-Type and the header field value media type (excluding parameters)
	 * is an ASCII case-insensitive match for application/x-www-form-urlencoded,
	 * multipart/form-data, or text/plain.
	 * @param lowerCaseHeader
	 * @return
	 */
	private boolean isSimpleHeader(String lowerCaseHeader) {
		switch (lowerCaseHeader) {
			case "accept" :
			case "accept-language" :
			case "content-language" :
			case "content-type" :
				return true;

			default :
				return false;
		}
	}

	private Set toGlobs(String[] listOfOrigins) {
		return Stream.of(listOfOrigins).map(Glob::new).collect(Collectors.toSet());
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy