Adding support for specifying preflight response headers.
Motivation: An intermediary like a load balancer might require that a Cross Origin Resource Sharing (CORS) preflight request have certain headers set. As a concrete example the Elastic Load Balancer (ELB) requires the 'Date' and 'Content-Length' header to be set or it will fail with a 502 error code. This works is an enhancement of https://github.com/netty/netty/pull/2290 Modifications: CorsConfig has been extended to make additional HTTP response headers configurable for preflight responses. Since some headers, like the 'Date' header need to be generated each time, m0wfo suggested using a Callable. Result: By default, the 'Date' and 'Content-Lenght' headers will be sent in a preflight response. This can be overriden and users can specify any headers that might be required by different intermediaries.
This commit is contained in:
parent
50f685fa49
commit
8d8bbec274
@ -15,13 +15,21 @@
|
|||||||
*/
|
*/
|
||||||
package io.netty.handler.codec.http.cors;
|
package io.netty.handler.codec.http.cors;
|
||||||
|
|
||||||
|
import io.netty.handler.codec.http.DefaultHttpHeaders;
|
||||||
|
import io.netty.handler.codec.http.HttpHeaders;
|
||||||
|
import io.netty.handler.codec.http.HttpHeaders.Names;
|
||||||
import io.netty.handler.codec.http.HttpMethod;
|
import io.netty.handler.codec.http.HttpMethod;
|
||||||
import io.netty.util.internal.StringUtil;
|
import io.netty.util.internal.StringUtil;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for Cross-Origin Resource Sharing (CORS).
|
* Configuration for Cross-Origin Resource Sharing (CORS).
|
||||||
@ -36,6 +44,7 @@ public final class CorsConfig {
|
|||||||
private final Set<HttpMethod> allowedRequestMethods;
|
private final Set<HttpMethod> allowedRequestMethods;
|
||||||
private final Set<String> allowedRequestHeaders;
|
private final Set<String> allowedRequestHeaders;
|
||||||
private final boolean allowNullOrigin;
|
private final boolean allowNullOrigin;
|
||||||
|
private final Map<CharSequence, Callable<?>> preflightHeaders;
|
||||||
|
|
||||||
private CorsConfig(final Builder builder) {
|
private CorsConfig(final Builder builder) {
|
||||||
origin = builder.origin;
|
origin = builder.origin;
|
||||||
@ -46,6 +55,7 @@ public final class CorsConfig {
|
|||||||
allowedRequestMethods = builder.requestMethods;
|
allowedRequestMethods = builder.requestMethods;
|
||||||
allowedRequestHeaders = builder.requestHeaders;
|
allowedRequestHeaders = builder.requestHeaders;
|
||||||
allowNullOrigin = builder.allowNullOrigin;
|
allowNullOrigin = builder.allowNullOrigin;
|
||||||
|
preflightHeaders = builder.preflightHeaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,6 +79,7 @@ public final class CorsConfig {
|
|||||||
/**
|
/**
|
||||||
* Web browsers may set the 'Origin' request header to 'null' if a resource is loaded
|
* Web browsers may set the 'Origin' request header to 'null' if a resource is loaded
|
||||||
* from the local file system.
|
* from the local file system.
|
||||||
|
*
|
||||||
* If isNullOriginAllowed is true then the server will response with the wildcard for the
|
* If isNullOriginAllowed is true then the server will response with the wildcard for the
|
||||||
* the CORS response header 'Access-Control-Allow-Origin'.
|
* the CORS response header 'Access-Control-Allow-Origin'.
|
||||||
*
|
*
|
||||||
@ -95,8 +106,8 @@ public final class CorsConfig {
|
|||||||
* <li>Last-Modified</li>
|
* <li>Last-Modified</li>
|
||||||
* <li>Pragma</li>
|
* <li>Pragma</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* To expose other headers they need to be specified which what this method enables by adding the headers
|
* To expose other headers they need to be specified, which is what this method enables by
|
||||||
* to the CORS 'Access-Control-Expose-Headers' response header.
|
* adding the headers names to the CORS 'Access-Control-Expose-Headers' response header.
|
||||||
*
|
*
|
||||||
* @return {@code List<String>} a list of the headers to expose.
|
* @return {@code List<String>} a list of the headers to expose.
|
||||||
*/
|
*/
|
||||||
@ -107,9 +118,17 @@ public final class CorsConfig {
|
|||||||
/**
|
/**
|
||||||
* Determines if cookies are supported for CORS requests.
|
* Determines if cookies are supported for CORS requests.
|
||||||
*
|
*
|
||||||
* By default cookies are not included in CORS requests but if isCredentialsAllowed returns true cookies will
|
* By default cookies are not included in CORS requests but if isCredentialsAllowed returns
|
||||||
* be added to CORS requests. Setting this value to true will set the CORS 'Access-Control-Allow-Credentials'
|
* true cookies will be added to CORS requests. Setting this value to true will set the
|
||||||
* response header to true.
|
* CORS 'Access-Control-Allow-Credentials' response header to true.
|
||||||
|
*
|
||||||
|
* 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>
|
||||||
|
* The default value for 'withCredentials' is false in which case no cookies are sent.
|
||||||
|
* Settning this to true will included cookies in cross origin requests.
|
||||||
*
|
*
|
||||||
* @return {@code true} if cookies are supported.
|
* @return {@code true} if cookies are supported.
|
||||||
*/
|
*/
|
||||||
@ -120,9 +139,10 @@ public final class CorsConfig {
|
|||||||
/**
|
/**
|
||||||
* Gets the maxAge setting.
|
* Gets the maxAge setting.
|
||||||
*
|
*
|
||||||
* When making a preflight request the client has to perform two request with can be inefficient. This setting
|
* When making a preflight request the client has to perform two request with can be inefficient.
|
||||||
* will set the CORS 'Access-Control-Max-Age' response header and enables the caching of the preflight response
|
* This setting will set the CORS 'Access-Control-Max-Age' response header and enables the
|
||||||
* for the specified time. During this time no preflight request will be made.
|
* caching of the preflight response for the specified time. During this time no preflight
|
||||||
|
* request will be made.
|
||||||
*
|
*
|
||||||
* @return {@code long} the time in seconds that a preflight request may be cached.
|
* @return {@code long} the time in seconds that a preflight request may be cached.
|
||||||
*/
|
*/
|
||||||
@ -132,10 +152,9 @@ public final class CorsConfig {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the allowed set of Request Methods. The Http methods that should be returned in the
|
* Returns the allowed set of Request Methods. The Http methods that should be returned in the
|
||||||
*
|
|
||||||
* CORS 'Access-Control-Request-Method' response header.
|
* CORS 'Access-Control-Request-Method' response header.
|
||||||
*
|
*
|
||||||
* @return {@code Set} strings that represent the allowed Request Methods.
|
* @return {@code Set} of {@link HttpMethod}s that represent the allowed Request Methods.
|
||||||
*/
|
*/
|
||||||
public Set<HttpMethod> allowedRequestMethods() {
|
public Set<HttpMethod> allowedRequestMethods() {
|
||||||
return Collections.unmodifiableSet(allowedRequestMethods);
|
return Collections.unmodifiableSet(allowedRequestMethods);
|
||||||
@ -144,15 +163,45 @@ public final class CorsConfig {
|
|||||||
/**
|
/**
|
||||||
* Returns the allowed set of Request Headers.
|
* Returns the allowed set of Request Headers.
|
||||||
*
|
*
|
||||||
* The header names returned from this method will be used to set the CORS 'Access-Control-Allow-Headers'
|
* The header names returned from this method will be used to set the CORS
|
||||||
* response header.
|
* 'Access-Control-Allow-Headers' response header.
|
||||||
*
|
*
|
||||||
* @return {@code Set} of strings that represent the allowed Request Headers.
|
* @return {@code Set<String>} of strings that represent the allowed Request Headers.
|
||||||
*/
|
*/
|
||||||
public Set<String> allowedRequestHeaders() {
|
public Set<String> allowedRequestHeaders() {
|
||||||
return Collections.unmodifiableSet(allowedRequestHeaders);
|
return Collections.unmodifiableSet(allowedRequestHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP response headers that should be added to a CORS preflight response.
|
||||||
|
*
|
||||||
|
* @return {@link HttpHeaders} the HTTP response headers to be added.
|
||||||
|
*/
|
||||||
|
public HttpHeaders preflightResponseHeaders() {
|
||||||
|
if (preflightHeaders.isEmpty()) {
|
||||||
|
return HttpHeaders.EMPTY_HEADERS;
|
||||||
|
}
|
||||||
|
final HttpHeaders preflightHeaders = new DefaultHttpHeaders();
|
||||||
|
for (Entry<CharSequence, Callable<?>> entry : this.preflightHeaders.entrySet()) {
|
||||||
|
final Object value = getValue(entry.getValue());
|
||||||
|
if (value instanceof Iterable) {
|
||||||
|
preflightHeaders.add(entry.getKey(), (Iterable<?>) value);
|
||||||
|
} else {
|
||||||
|
preflightHeaders.add(entry.getKey(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return preflightHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T getValue(final Callable<T> callable) {
|
||||||
|
try {
|
||||||
|
return callable.call();
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new IllegalStateException("Could not generate value for callable [" + callable + ']', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return StringUtil.simpleClassName(this) + "[enabled=" + enabled +
|
return StringUtil.simpleClassName(this) + "[enabled=" + enabled +
|
||||||
", origin=" + origin +
|
", origin=" + origin +
|
||||||
@ -160,17 +209,31 @@ public final class CorsConfig {
|
|||||||
", isCredentialsAllowed=" + allowCredentials +
|
", isCredentialsAllowed=" + allowCredentials +
|
||||||
", maxAge=" + maxAge +
|
", maxAge=" + maxAge +
|
||||||
", allowedRequestMethods=" + allowedRequestMethods +
|
", allowedRequestMethods=" + allowedRequestMethods +
|
||||||
", allowedRequestHeaders=" + allowedRequestHeaders + ']';
|
", allowedRequestHeaders=" + allowedRequestHeaders +
|
||||||
|
", preflightHeaders=" + preflightHeaders + ']';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Builder instance with it's origin set to '*'.
|
||||||
|
*
|
||||||
|
* @return Builder to support method chaining.
|
||||||
|
*/
|
||||||
public static Builder anyOrigin() {
|
public static Builder anyOrigin() {
|
||||||
return new Builder("*");
|
return new Builder("*");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link Builder} instance with the specified origin.
|
||||||
|
*
|
||||||
|
* @return {@link Builder} to support method chaining.
|
||||||
|
*/
|
||||||
public static Builder withOrigin(final String origin) {
|
public static Builder withOrigin(final String origin) {
|
||||||
return new Builder(origin);
|
return new Builder(origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder used to configure and build a CorsConfig instance.
|
||||||
|
*/
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
||||||
private final String origin;
|
private final String origin;
|
||||||
@ -181,49 +244,254 @@ public final class CorsConfig {
|
|||||||
private long maxAge;
|
private long maxAge;
|
||||||
private final Set<HttpMethod> requestMethods = new HashSet<HttpMethod>();
|
private final Set<HttpMethod> requestMethods = new HashSet<HttpMethod>();
|
||||||
private final Set<String> requestHeaders = new HashSet<String>();
|
private final Set<String> requestHeaders = new HashSet<String>();
|
||||||
|
private final Map<CharSequence, Callable<?>> preflightHeaders = new HashMap<CharSequence, Callable<?>>();
|
||||||
|
private boolean noPreflightHeaders;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Builder instance with the origin passed in.
|
||||||
|
*
|
||||||
|
* @param origin the origin to be used for this builder.
|
||||||
|
*/
|
||||||
public Builder(final String origin) {
|
public Builder(final String origin) {
|
||||||
this.origin = origin;
|
this.origin = origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 wildcard for the the CORS response header 'Access-Control-Allow-Origin'.
|
||||||
|
*
|
||||||
|
* @return {@link Builder} to support method chaining.
|
||||||
|
*/
|
||||||
public Builder allowNullOrigin() {
|
public Builder allowNullOrigin() {
|
||||||
allowNullOrigin = true;
|
allowNullOrigin = true;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables CORS support.
|
||||||
|
*
|
||||||
|
* @return {@link Builder} to support method chaining.
|
||||||
|
*/
|
||||||
public Builder disable() {
|
public Builder disable() {
|
||||||
enabled = false;
|
enabled = false;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the headers to be exposed to calling clients.
|
||||||
|
*
|
||||||
|
* During a simple CORS request, only certain response headers are made available by the
|
||||||
|
* browser, for example using:
|
||||||
|
* <pre>
|
||||||
|
* xhr.getResponseHeader("Content-Type");
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* 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>
|
||||||
|
*
|
||||||
|
* 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 Builder} to support method chaining.
|
||||||
|
*/
|
||||||
public Builder exposeHeaders(final String... headers) {
|
public Builder exposeHeaders(final String... headers) {
|
||||||
exposeHeaders.addAll(Arrays.asList(headers));
|
exposeHeaders.addAll(Arrays.asList(headers));
|
||||||
return this;
|
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.
|
||||||
|
*
|
||||||
|
* 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>
|
||||||
|
* The default value for 'withCredentials' is false in which case no cookies are sent.
|
||||||
|
* Settning this to true will included cookies in cross origin requests.
|
||||||
|
*
|
||||||
|
* @return {@link Builder} to support method chaining.
|
||||||
|
*/
|
||||||
public Builder allowCredentials() {
|
public Builder allowCredentials() {
|
||||||
allowCredentials = true;
|
allowCredentials = true;
|
||||||
return this;
|
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 max the maximum time, in seconds, that the preflight response may be cached.
|
||||||
|
* @return {@link Builder} to support method chaining.
|
||||||
|
*/
|
||||||
public Builder maxAge(final long max) {
|
public Builder maxAge(final long max) {
|
||||||
maxAge = max;
|
maxAge = max;
|
||||||
return this;
|
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 Builder} to support method chaining.
|
||||||
|
*/
|
||||||
public Builder allowedRequestMethods(final HttpMethod... methods) {
|
public Builder allowedRequestMethods(final HttpMethod... methods) {
|
||||||
requestMethods.addAll(Arrays.asList(methods));
|
requestMethods.addAll(Arrays.asList(methods));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the if headers that should be returned in the CORS 'Access-Control-Allow-Headers'
|
||||||
|
* response header.
|
||||||
|
*
|
||||||
|
* If a client specifies headers on the request, for example by calling:
|
||||||
|
* <pre>
|
||||||
|
* xhr.setRequestHeader('My-Custom-Header', "SomeValue");
|
||||||
|
* </pre>
|
||||||
|
* the server will recieve 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 Builder} to support method chaining.
|
||||||
|
*/
|
||||||
public Builder allowedRequestHeaders(final String... headers) {
|
public Builder allowedRequestHeaders(final String... headers) {
|
||||||
requestHeaders.addAll(Arrays.asList(headers));
|
requestHeaders.addAll(Arrays.asList(headers));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP response headers that should be added to a CORS preflight response.
|
||||||
|
*
|
||||||
|
* 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 Builder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public Builder preflightResponseHeader(final CharSequence name, final Object... values) {
|
||||||
|
if (values.length == 1) {
|
||||||
|
preflightHeaders.put(name, new ConstantValueGenerator(values[0]));
|
||||||
|
} else {
|
||||||
|
preflightResponseHeader(name, Arrays.asList(values));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP response headers that should be added to a CORS preflight response.
|
||||||
|
*
|
||||||
|
* 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 value the values for the HTTP header.
|
||||||
|
* @param <T> the type of values that the Iterable contains.
|
||||||
|
* @return {@link Builder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public <T> Builder preflightResponseHeader(final CharSequence name, final Iterable<T> value) {
|
||||||
|
preflightHeaders.put(name, new ConstantValueGenerator(value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP response headers that should be added to a CORS preflight response.
|
||||||
|
*
|
||||||
|
* An intermediary like a load balancer might require that a CORS preflight request
|
||||||
|
* have certain headers set. This enables such headers to be added.
|
||||||
|
*
|
||||||
|
* Some values must be dynamically created when the HTTP response is created, for
|
||||||
|
* example the 'Date' response header. This can be occomplished by using a Callable
|
||||||
|
* which will have its 'call' method invoked when the HTTP response is created.
|
||||||
|
*
|
||||||
|
* @param name the name of the HTTP header.
|
||||||
|
* @param valueGenerator a Callable which will be invoked at HTTP response creation.
|
||||||
|
* @param <T> the type of the value that the Callable can return.
|
||||||
|
* @return {@link Builder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public <T> Builder preflightResponseHeader(final String name, final Callable<T> valueGenerator) {
|
||||||
|
preflightHeaders.put(name, valueGenerator);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies that no preflight response headers should be added to a preflight response.
|
||||||
|
*
|
||||||
|
* @return {@link Builder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public Builder noPreflightResponseHeaders() {
|
||||||
|
noPreflightHeaders = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@link CorsConfig} with settings specified by previous method calls.
|
||||||
|
*
|
||||||
|
* @return {@link CorsConfig} the configured CorsConfig instance.
|
||||||
|
*/
|
||||||
public CorsConfig build() {
|
public CorsConfig build() {
|
||||||
|
if (preflightHeaders.isEmpty() && !noPreflightHeaders) {
|
||||||
|
preflightHeaders.put(Names.DATE, new DateValueGenerator());
|
||||||
|
preflightHeaders.put(Names.CONTENT_LENGTH, new ConstantValueGenerator("0"));
|
||||||
|
}
|
||||||
return new CorsConfig(this);
|
return new CorsConfig(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used for preflight HTTP response values that do not need to be
|
||||||
|
* generated, but instead the value is "static" in that the same value will be returned
|
||||||
|
* for each call.
|
||||||
|
*/
|
||||||
|
private static final class ConstantValueGenerator implements Callable<Object> {
|
||||||
|
|
||||||
|
private final Object value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sole constructor.
|
||||||
|
*
|
||||||
|
* @param value the value that will be returned when the call method is invoked.
|
||||||
|
*/
|
||||||
|
private ConstantValueGenerator(final Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
throw new IllegalArgumentException("value must not be null");
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object call() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This callable is used for the DATE preflight HTTP response HTTP header.
|
||||||
|
* It's value must be generated when the response is generated, hence will be
|
||||||
|
* different for every call.
|
||||||
|
*/
|
||||||
|
public static final class DateValueGenerator implements Callable<Date> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date call() throws Exception {
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package io.netty.handler.codec.http.cors;
|
package io.netty.handler.codec.http.cors;
|
||||||
|
|
||||||
import static io.netty.handler.codec.http.HttpHeaders.Names.*;
|
|
||||||
import static io.netty.handler.codec.http.HttpMethod.*;
|
|
||||||
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
|
|
||||||
|
|
||||||
import io.netty.channel.ChannelDuplexHandler;
|
import io.netty.channel.ChannelDuplexHandler;
|
||||||
import io.netty.channel.ChannelFutureListener;
|
import io.netty.channel.ChannelFutureListener;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
@ -30,7 +26,9 @@ import io.netty.handler.codec.http.HttpResponse;
|
|||||||
import io.netty.util.internal.logging.InternalLogger;
|
import io.netty.util.internal.logging.InternalLogger;
|
||||||
import io.netty.util.internal.logging.InternalLoggerFactory;
|
import io.netty.util.internal.logging.InternalLoggerFactory;
|
||||||
|
|
||||||
import java.util.Date;
|
import static io.netty.handler.codec.http.HttpHeaders.Names.*;
|
||||||
|
import static io.netty.handler.codec.http.HttpMethod.*;
|
||||||
|
import static io.netty.handler.codec.http.HttpResponseStatus.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles <a href="http://www.w3.org/TR/cors/">Cross Origin Resource Sharing</a> (CORS) requests.
|
* Handles <a href="http://www.w3.org/TR/cors/">Cross Origin Resource Sharing</a> (CORS) requests.
|
||||||
@ -64,16 +62,25 @@ public class CorsHandler extends ChannelDuplexHandler {
|
|||||||
private void handlePreflight(final ChannelHandlerContext ctx, final HttpRequest request) {
|
private void handlePreflight(final ChannelHandlerContext ctx, final HttpRequest request) {
|
||||||
final HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(), OK);
|
final HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(), OK);
|
||||||
if (setOrigin(response)) {
|
if (setOrigin(response)) {
|
||||||
HttpHeaders.setContentLength(response, 0);
|
|
||||||
HttpHeaders.setDate(response, new Date());
|
|
||||||
setAllowMethods(response);
|
setAllowMethods(response);
|
||||||
setAllowHeaders(response);
|
setAllowHeaders(response);
|
||||||
setAllowCredentials(response);
|
setAllowCredentials(response);
|
||||||
setMaxAge(response);
|
setMaxAge(response);
|
||||||
|
setPreflightHeaders(response);
|
||||||
}
|
}
|
||||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a non CORS specification feature which enables the setting of preflight
|
||||||
|
* response headers that might be required by intermediaries.
|
||||||
|
*
|
||||||
|
* @param response the HttpResponse to which the preflight response headers should be added.
|
||||||
|
*/
|
||||||
|
private void setPreflightHeaders(final HttpResponse response) {
|
||||||
|
response.headers().add(config.preflightResponseHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
private boolean setOrigin(final HttpResponse response) {
|
private boolean setOrigin(final HttpResponse response) {
|
||||||
final String origin = request.headers().get(ORIGIN);
|
final String origin = request.headers().get(ORIGIN);
|
||||||
if (origin != null) {
|
if (origin != null) {
|
||||||
|
@ -15,17 +15,16 @@
|
|||||||
*/
|
*/
|
||||||
package io.netty.handler.codec.http.cors;
|
package io.netty.handler.codec.http.cors;
|
||||||
|
|
||||||
import static io.netty.handler.codec.http.cors.CorsConfig.withOrigin;
|
import io.netty.handler.codec.http.HttpHeaders;
|
||||||
import static io.netty.handler.codec.http.cors.CorsConfig.anyOrigin;
|
import io.netty.handler.codec.http.HttpHeaders.Names;
|
||||||
import io.netty.handler.codec.http.HttpMethod;
|
import io.netty.handler.codec.http.HttpMethod;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
import static io.netty.handler.codec.http.cors.CorsConfig.*;
|
||||||
import static org.hamcrest.CoreMatchers.hasItems;
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
|
||||||
import static org.hamcrest.MatcherAssert.*;
|
import static org.hamcrest.MatcherAssert.*;
|
||||||
|
|
||||||
public class CorsConfigurationTest {
|
public class CorsConfigTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void disabled() {
|
public void disabled() {
|
||||||
@ -75,4 +74,34 @@ public class CorsConfigurationTest {
|
|||||||
assertThat(cors.allowedRequestHeaders(), hasItems("preflight-header1", "preflight-header2"));
|
assertThat(cors.allowedRequestHeaders(), hasItems("preflight-header1", "preflight-header2"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preflightResponseHeadersSingleValue() {
|
||||||
|
final CorsConfig cors = withOrigin("*").preflightResponseHeader("SingleValue", "value").build();
|
||||||
|
assertThat(cors.preflightResponseHeaders().get("SingleValue"), equalTo("value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preflightResponseHeadersMultipleValues() {
|
||||||
|
final CorsConfig cors = withOrigin("*").preflightResponseHeader("MultipleValues", "value1", "value2").build();
|
||||||
|
assertThat(cors.preflightResponseHeaders().getAll("MultipleValues"), hasItems("value1", "value2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultPreflightResponseHeaders() {
|
||||||
|
final CorsConfig cors = withOrigin("*").build();
|
||||||
|
assertThat(cors.preflightResponseHeaders().get(Names.DATE), is(notNullValue()));
|
||||||
|
assertThat(cors.preflightResponseHeaders().get(Names.CONTENT_LENGTH), is("0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void emptyPreflightResponseHeaders() {
|
||||||
|
final CorsConfig cors = withOrigin("*").noPreflightResponseHeaders().build();
|
||||||
|
assertThat(cors.preflightResponseHeaders(), equalTo(HttpHeaders.EMPTY_HEADERS));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test (expected = IllegalArgumentException.class)
|
||||||
|
public void shouldThrowIfValueIsNull() {
|
||||||
|
withOrigin("*").preflightResponseHeader("HeaderName", null).build();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -15,17 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
package io.netty.handler.codec.http.cors;
|
package io.netty.handler.codec.http.cors;
|
||||||
|
|
||||||
import static io.netty.handler.codec.http.HttpHeaders.Names.*;
|
|
||||||
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
|
|
||||||
import static io.netty.handler.codec.http.HttpMethod.GET;
|
|
||||||
import static io.netty.handler.codec.http.HttpMethod.OPTIONS;
|
|
||||||
import static org.hamcrest.CoreMatchers.*;
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
|
||||||
|
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.SimpleChannelInboundHandler;
|
import io.netty.channel.SimpleChannelInboundHandler;
|
||||||
import io.netty.channel.embedded.EmbeddedChannel;
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
|
||||||
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||||
import io.netty.handler.codec.http.DefaultHttpResponse;
|
import io.netty.handler.codec.http.DefaultHttpResponse;
|
||||||
import io.netty.handler.codec.http.FullHttpRequest;
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
@ -34,6 +26,15 @@ import io.netty.handler.codec.http.HttpResponse;
|
|||||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
import static io.netty.handler.codec.http.HttpHeaders.Names.*;
|
||||||
|
import static io.netty.handler.codec.http.HttpMethod.*;
|
||||||
|
import static io.netty.handler.codec.http.HttpVersion.*;
|
||||||
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
|
import static org.hamcrest.MatcherAssert.*;
|
||||||
|
|
||||||
public class CorsHandlerTest {
|
public class CorsHandlerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -77,6 +78,54 @@ public class CorsHandlerTest {
|
|||||||
assertThat(response.headers().getAll(ACCESS_CONTROL_ALLOW_HEADERS), hasItems("content-type", "xheader1"));
|
assertThat(response.headers().getAll(ACCESS_CONTROL_ALLOW_HEADERS), hasItems("content-type", "xheader1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preflightRequestWithDefaultHeaders() {
|
||||||
|
final CorsConfig config = CorsConfig.withOrigin("http://localhost:8888").build();
|
||||||
|
final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
|
||||||
|
assertThat(response.headers().get(CONTENT_LENGTH), is("0"));
|
||||||
|
assertThat(response.headers().get(DATE), is(notNullValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preflightRequestWithCustomHeader() {
|
||||||
|
final CorsConfig config = CorsConfig.withOrigin("http://localhost:8888")
|
||||||
|
.preflightResponseHeader("CustomHeader", "somevalue")
|
||||||
|
.build();
|
||||||
|
final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
|
||||||
|
assertThat(response.headers().get("CustomHeader"), equalTo("somevalue"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preflightRequestWithCustomHeaders() {
|
||||||
|
final CorsConfig config = CorsConfig.withOrigin("http://localhost:8888")
|
||||||
|
.preflightResponseHeader("CustomHeader", "value1", "value2")
|
||||||
|
.build();
|
||||||
|
final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
|
||||||
|
assertThat(response.headers().getAll("CustomHeader"), hasItems("value1", "value2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preflightRequestWithCustomHeadersIterable() {
|
||||||
|
final CorsConfig config = CorsConfig.withOrigin("http://localhost:8888")
|
||||||
|
.preflightResponseHeader("CustomHeader", Arrays.asList("value1", "value2"))
|
||||||
|
.build();
|
||||||
|
final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
|
||||||
|
assertThat(response.headers().getAll("CustomHeader"), hasItems("value1", "value2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preflightRequestWithValueGenerator() {
|
||||||
|
final CorsConfig config = CorsConfig.withOrigin("http://localhost:8888")
|
||||||
|
.preflightResponseHeader("GenHeader", new Callable<String>() {
|
||||||
|
@Override
|
||||||
|
public String call() throws Exception {
|
||||||
|
return "generatedValue";
|
||||||
|
}
|
||||||
|
}).build();
|
||||||
|
final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
|
||||||
|
assertThat(response.headers().get("GenHeader"), equalTo("generatedValue"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void preflightRequestWithNullOrigin() {
|
public void preflightRequestWithNullOrigin() {
|
||||||
final String origin = "null";
|
final String origin = "null";
|
||||||
|
Loading…
Reference in New Issue
Block a user