/* * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you 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. */ package com.linecorp.armeria.server.http.cors; import static java.util.Objects.requireNonNull; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; import com.google.common.base.Ascii; import com.linecorp.armeria.common.http.HttpHeaderNames; import com.linecorp.armeria.common.http.HttpMethod; import com.linecorp.armeria.common.http.HttpRequest; import com.linecorp.armeria.common.http.HttpResponse; import com.linecorp.armeria.server.Service; import com.linecorp.armeria.server.http.cors.CorsConfig.ConstantValueSupplier; import io.netty.util.AsciiString; /** * Builds a new {@link CorsService} or its decorator function. */ public final class CorsServiceBuilder { /** * Creates a new builder with its origin set to '*'. */ public static CorsServiceBuilder forAnyOrigin() { return new CorsServiceBuilder(); } /** * Creates a new builder with the specified origin. */ public static CorsServiceBuilder forOrigin(final String origin) { requireNonNull(origin, "origin"); if ("*".equals(origin)) { return new CorsServiceBuilder(); } return new CorsServiceBuilder(origin); } /** * Creates a new builder with the specified origins. */ public static CorsServiceBuilder forOrigins(final String... origins) { requireNonNull(origins, "origins"); for (int i = 0; i < origins.length; i++) { if (origins[i] == null) { throw new NullPointerException("origins[" + i + ']'); } } return new CorsServiceBuilder(origins); } final Set<String> origins; final boolean anyOriginSupported; boolean nullOriginAllowed; boolean credentialsAllowed; boolean shortCircuit; long maxAge; final Set<AsciiString> exposedHeaders = new HashSet<>(); @SuppressWarnings("SetReplaceableByEnumSet") final Set<HttpMethod> allowedRequestMethods = new HashSet<>(); final Set<AsciiString> allowedRequestHeaders = new HashSet<>(); final Map<AsciiString, Supplier<?>> preflightResponseHeaders = new HashMap<>(); boolean preflightResponseHeadersDisabled; /** * Creates a new instance. * * @param origins the origin to be used for this builder. */ CorsServiceBuilder(final String... origins) { final Set<String> originsCopy = new LinkedHashSet<>(); for (String o : origins) { originsCopy.add(Ascii.toLowerCase(o)); } this.origins = Collections.unmodifiableSet(originsCopy); anyOriginSupported = false; } /** * Creates a new Builder instance allowing any origin, "*" which is the * wildcard origin. */ CorsServiceBuilder() { anyOriginSupported = true; origins = Collections.emptySet(); } /** * Web browsers may set the 'Origin' request header to 'null' if a resource is loaded * from the local file system. Calling this method will enable a successful CORS response * with a {@code "null"} value for the the CORS response header 'Access-Control-Allow-Origin'. * * @return {@link CorsServiceBuilder} to support method chaining. */ public CorsServiceBuilder allowNullOrigin() { nullOriginAllowed = true; return this; } /** * By default cookies are not included in CORS requests, but this method will enable cookies to * be added to CORS requests. Calling this method will set the CORS 'Access-Control-Allow-Credentials' * response header to true. * * <p>Please note, that cookie support needs to be enabled on the client side as well. * The client needs to opt-in to send cookies by calling: * <pre> * xhr.withCredentials = true; * </pre> * * <p>The default value for 'withCredentials' is false in which case no cookies are sent. * Setting this to true will included cookies in cross origin requests. * * @return {@link CorsServiceBuilder} to support method chaining. */ public CorsServiceBuilder allowCredentials() { credentialsAllowed = true; return this; } /** * Specifies that a CORS request should be rejected if it's invalid before being * further processing. * * <p>CORS headers are set after a request is processed. This may not always be desired * and this setting will check that the Origin is valid and if it is not valid no * further processing will take place, and a error will be returned to the calling client. * * @return {@link CorsServiceBuilder} to support method chaining. */ public CorsServiceBuilder shortCircuit() { shortCircuit = true; return this; } /** * When making a preflight request the client has to perform two request with can be inefficient. * This setting will set the CORS 'Access-Control-Max-Age' response header and enables the * caching of the preflight response for the specified time. During this time no preflight * request will be made. * * @param maxAge the maximum time, in seconds, that the preflight response may be cached. * @return {@link CorsServiceBuilder} to support method chaining. */ public CorsServiceBuilder maxAge(final long maxAge) { if (maxAge <= 0) { throw new IllegalArgumentException("maxAge: " + maxAge + " (expected: > 0)"); } this.maxAge = maxAge; return this; } /** * Specifies the headers to be exposed to calling clients. * * <p>During a simple CORS request, only certain response headers are made available by the * browser, for example using: * <pre> * xhr.getResponseHeader("Content-Type"); * </pre> * * <p>The headers that are available by default are: * <ul> * <li>Cache-Control</li> * <li>Content-Language</li> * <li>Content-Type</li> * <li>Expires</li> * <li>Last-Modified</li> * <li>Pragma</li> * </ul> * * <p>To expose other headers they need to be specified which is what this method enables by * adding the headers to the CORS 'Access-Control-Expose-Headers' response header. * * @param headers the values to be added to the 'Access-Control-Expose-Headers' response header * @return {@link CorsServiceBuilder} to support method chaining. */ public CorsServiceBuilder exposeHeaders(final String... headers) { requireNonNull(headers, "headers"); for (int i = 0; i < headers.length; i++) { if (headers[i] == null) { throw new NullPointerException("headers[" + i + ']'); } } Arrays.stream(headers).map(HttpHeaderNames::of).forEach(exposedHeaders::add); return this; } /** * Specifies the headers to be exposed to calling clients. * * <p>During a simple CORS request, only certain response headers are made available by the * browser, for example using: * <pre> * xhr.getResponseHeader("Content-Type"); * </pre> * * <p>The headers that are available by default are: * <ul> * <li>Cache-Control</li> * <li>Content-Language</li> * <li>Content-Type</li> * <li>Expires</li> * <li>Last-Modified</li> * <li>Pragma</li> * </ul> * * <p>To expose other headers they need to be specified which is what this method enables by * adding the headers to the CORS 'Access-Control-Expose-Headers' response header. * * @param headers the values to be added to the 'Access-Control-Expose-Headers' response header * @return {@link CorsServiceBuilder} to support method chaining. */ public CorsServiceBuilder exposeHeaders(final AsciiString... headers) { requireNonNull(headers, "headers"); for (int i = 0; i < headers.length; i++) { if (headers[i] == null) { throw new NullPointerException("headers[" + i + ']'); } } Collections.addAll(exposedHeaders, headers); return this; } /** * Specifies the allowed set of HTTP Request Methods that should be returned in the * CORS 'Access-Control-Request-Method' response header. * * @param methods the {@link HttpMethod}s that should be allowed. * @return {@link CorsServiceBuilder} to support method chaining. */ public CorsServiceBuilder allowRequestMethods(final HttpMethod... methods) { requireNonNull(methods, "methods"); for (int i = 0; i < methods.length; i++) { if (methods[i] == null) { throw new NullPointerException("methods[" + i + ']'); } } Collections.addAll(allowedRequestMethods, methods); return this; } /** * Specifies the if headers that should be returned in the CORS 'Access-Control-Allow-Headers' * response header. * * <p>If a client specifies headers on the request, for example by calling: * <pre> * xhr.setRequestHeader('My-Custom-Header', "SomeValue"); * </pre> * the server will receive the above header name in the 'Access-Control-Request-Headers' of the * preflight request. The server will then decide if it allows this header to be sent for the * real request (remember that a preflight is not the real request but a request asking the server * if it allow a request). * * @param headers the headers to be added to the preflight 'Access-Control-Allow-Headers' response header. * @return {@link CorsServiceBuilder} to support method chaining. */ public CorsServiceBuilder allowRequestHeaders(final String... headers) { requireNonNull(headers, "headers"); for (int i = 0; i < headers.length; i++) { if (headers[i] == null) { throw new NullPointerException("headers[" + i + ']'); } } Arrays.stream(headers).map(HttpHeaderNames::of).forEach(allowedRequestHeaders::add); return this; } /** * Specifies the if headers that should be returned in the CORS 'Access-Control-Allow-Headers' * response header. * * <p>If a client specifies headers on the request, for example by calling: * <pre> * xhr.setRequestHeader('My-Custom-Header', "SomeValue"); * </pre> * the server will receive the above header name in the 'Access-Control-Request-Headers' of the * preflight request. The server will then decide if it allows this header to be sent for the * real request (remember that a preflight is not the real request but a request asking the server * if it allow a request). * * @param headers the headers to be added to the preflight 'Access-Control-Allow-Headers' response header. * @return {@link CorsServiceBuilder} to support method chaining. */ public CorsServiceBuilder allowRequestHeaders(final AsciiString... headers) { requireNonNull(headers, "headers"); for (int i = 0; i < headers.length; i++) { if (headers[i] == null) { throw new NullPointerException("headers[" + i + ']'); } } allowedRequestHeaders.addAll(Arrays.asList(headers)); return this; } /** * Returns HTTP response headers that should be added to a CORS preflight response. * * <p>An intermediary like a load balancer might require that a CORS preflight request * have certain headers set. This enables such headers to be added. * * @param name the name of the HTTP header. * @param values the values for the HTTP header. * @return {@link CorsServiceBuilder} to support method chaining. */ public CorsServiceBuilder preflightResponseHeader(final String name, final Object... values) { requireNonNull(name, "name"); return preflightResponseHeader(HttpHeaderNames.of(name), values); } /** * Returns HTTP response headers that should be added to a CORS preflight response. * * <p>An intermediary like a load balancer might require that a CORS preflight request * have certain headers set. This enables such headers to be added. * * @param name the name of the HTTP header. * @param values the values for the HTTP header. * @return {@link CorsServiceBuilder} to support method chaining. */ public CorsServiceBuilder preflightResponseHeader(final AsciiString name, final Object... values) { requireNonNull(name, "name"); requireNonNull(values, "values"); for (int i = 0;i < values.length; i++) { if (values[i] == null) { throw new NullPointerException("values[" + i + ']'); } } if (values.length == 1) { preflightResponseHeaders.put(name, new ConstantValueSupplier(values[0])); } else { preflightResponseHeader(name, Arrays.asList(values)); } return this; } /** * Returns HTTP response headers that should be added to a CORS preflight response. * * <p>An intermediary like a load balancer might require that a CORS preflight request * have certain headers set. This enables such headers to be added. * * @param name the name of the HTTP header. * @param values the values for the HTTP header. * @param <T> the type of values that the Iterable contains. * @return {@link CorsServiceBuilder} to support method chaining. */ public <T> CorsServiceBuilder preflightResponseHeader(final AsciiString name, final Iterable<T> values) { requireNonNull(name, "name"); requireNonNull(values, "values"); preflightResponseHeaders.put(name, new ConstantValueSupplier(values)); return this; } /** * Returns HTTP response headers that should be added to a CORS preflight response. * * <p>An intermediary like a load balancer might require that a CORS preflight request * have certain headers set. This enables such headers to be added. * * <p>Some values must be dynamically created when the HTTP response is created, for * example the 'Date' response header. This can be accomplished by using a {@link Supplier} * which will have its 'call' method invoked when the HTTP response is created. * * @param name the name of the HTTP header. * @param valueSupplier a {@link Supplier} which will be invoked at HTTP response creation. * @param <T> the type of the value that the {@link Supplier} can return. * @return {@link CorsServiceBuilder} to support method chaining. */ public <T> CorsServiceBuilder preflightResponseHeader(AsciiString name, Supplier<T> valueSupplier) { requireNonNull(name, "name"); requireNonNull(valueSupplier, "valueSupplier"); preflightResponseHeaders.put(name, valueSupplier); return this; } /** * Specifies that no preflight response headers should be added to a preflight response. * * @return {@link CorsServiceBuilder} to support method chaining. */ public CorsServiceBuilder disablePreflightResponseHeaders() { preflightResponseHeadersDisabled = true; return this; } /** * Builds a {@link CorsConfig} with settings specified by previous method calls. * * @return {@link CorsConfig} the configured CorsConfig instance. */ public <I extends HttpRequest, O extends HttpResponse> CorsService<I, O> build(Service<? super I, ? extends O> delegate) { return new CorsService<>(delegate, new CorsConfig(this)); } /** * Creates a new decorator that decorates a {@link Service} with a new {@link CorsService}. */ public <I extends HttpRequest, O extends HttpResponse> Function<Service<? super I, ? extends O>, CorsService<I, O>> newDecorator() { final CorsConfig config = new CorsConfig(this); return s -> new CorsService<>(s, config); } @Override public String toString() { return CorsConfig.toString(this, true, origins, anyOriginSupported, nullOriginAllowed, credentialsAllowed, shortCircuit, maxAge, exposedHeaders, allowedRequestMethods, allowedRequestHeaders, preflightResponseHeaders); } }