Adding a Cross Origin Resource Sharing (CORS) handler.
This commit is contained in:
parent
46df4f800f
commit
75b0360867
@ -0,0 +1,229 @@
|
||||
/*
|
||||
* Copyright 2013 The Netty Project
|
||||
*
|
||||
* The Netty Project 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 io.netty.handler.codec.http.cors;
|
||||
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.util.internal.StringUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Configuration for Cross-Origin Resource Sharing (CORS).
|
||||
*/
|
||||
public final class CorsConfig {
|
||||
|
||||
private final String origin;
|
||||
private final boolean enabled;
|
||||
private final Set<String> exposeHeaders;
|
||||
private final boolean allowCredentials;
|
||||
private final long maxAge;
|
||||
private final Set<HttpMethod> allowedRequestMethods;
|
||||
private final Set<String> allowedRequestHeaders;
|
||||
private final boolean allowNullOrigin;
|
||||
|
||||
private CorsConfig(final Builder builder) {
|
||||
origin = builder.origin;
|
||||
enabled = builder.enabled;
|
||||
exposeHeaders = builder.exposeHeaders;
|
||||
allowCredentials = builder.allowCredentials;
|
||||
maxAge = builder.maxAge;
|
||||
allowedRequestMethods = builder.requestMethods;
|
||||
allowedRequestHeaders = builder.requestHeaders;
|
||||
allowNullOrigin = builder.allowNullOrigin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if support for CORS is enabled.
|
||||
*
|
||||
* @return {@code true} if support for CORS is enabled, false otherwise.
|
||||
*/
|
||||
public boolean isCorsSupportEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the allowed origin. This can either be a wildcard or an origin value.
|
||||
*
|
||||
* @return the value that will be used for the CORS response header 'Access-Control-Allow-Origin'
|
||||
*/
|
||||
public String origin() {
|
||||
return origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Web browsers may set the 'Origin' request header to 'null' if a resource is loaded
|
||||
* from the local file system.
|
||||
* If isNullOriginAllowed is true then the server will response with the wildcard for the
|
||||
* the CORS response header 'Access-Control-Allow-Origin'.
|
||||
*
|
||||
* @return {@code true} if a 'null' origin should be supported.
|
||||
*/
|
||||
public boolean isNullOriginAllowed() {
|
||||
return allowNullOrigin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of 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 what this method enables by adding the headers
|
||||
* to the CORS 'Access-Control-Expose-Headers' response header.
|
||||
*
|
||||
* @return {@code List<String>} a list of the headers to expose.
|
||||
*/
|
||||
public Set<String> exposedHeaders() {
|
||||
return Collections.unmodifiableSet(exposeHeaders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if cookies are supported for CORS requests.
|
||||
*
|
||||
* By default cookies are not included in CORS requests but if isCredentialsAllowed returns true cookies will
|
||||
* be added to CORS requests. Setting this value to true will set the CORS 'Access-Control-Allow-Credentials'
|
||||
* response header to true.
|
||||
*
|
||||
* @return {@code true} if cookies are supported.
|
||||
*/
|
||||
public boolean isCredentialsAllowed() {
|
||||
return allowCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maxAge setting.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @return {@code long} the time in seconds that a preflight request may be cached.
|
||||
*/
|
||||
public long maxAge() {
|
||||
return maxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the allowed set of Request Methods. The Http methods that should be returned in the
|
||||
*
|
||||
* CORS 'Access-Control-Request-Method' response header.
|
||||
*
|
||||
* @return {@code Set} strings that represent the allowed Request Methods.
|
||||
*/
|
||||
public Set<HttpMethod> allowedRequestMethods() {
|
||||
return Collections.unmodifiableSet(allowedRequestMethods);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'
|
||||
* response header.
|
||||
*
|
||||
* @return {@code Set} of strings that represent the allowed Request Headers.
|
||||
*/
|
||||
public Set<String> allowedRequestHeaders() {
|
||||
return Collections.unmodifiableSet(allowedRequestHeaders);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return StringUtil.simpleClassName(this) + "[enabled=" + enabled +
|
||||
", origin=" + origin +
|
||||
", exposedHeaders=" + exposeHeaders +
|
||||
", isCredentialsAllowed=" + allowCredentials +
|
||||
", maxAge=" + maxAge +
|
||||
", allowedRequestMethods=" + allowedRequestMethods +
|
||||
", allowedRequestHeaders=" + allowedRequestHeaders + ']';
|
||||
}
|
||||
|
||||
public static Builder anyOrigin() {
|
||||
return new Builder("*");
|
||||
}
|
||||
|
||||
public static Builder withOrigin(final String origin) {
|
||||
return new Builder(origin);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final String origin;
|
||||
private boolean allowNullOrigin;
|
||||
private boolean enabled = true;
|
||||
private boolean allowCredentials;
|
||||
private final Set<String> exposeHeaders = new HashSet<String>();
|
||||
private long maxAge;
|
||||
private final Set<HttpMethod> requestMethods = new HashSet<HttpMethod>();
|
||||
private final Set<String> requestHeaders = new HashSet<String>();
|
||||
|
||||
public Builder(final String origin) {
|
||||
this.origin = origin;
|
||||
}
|
||||
|
||||
public Builder allowNullOrigin() {
|
||||
allowNullOrigin = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder disable() {
|
||||
enabled = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder exposeHeaders(final String... headers) {
|
||||
exposeHeaders.addAll(Arrays.asList(headers));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder allowCredentials() {
|
||||
allowCredentials = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maxAge(final long max) {
|
||||
maxAge = max;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder allowedRequestMethods(final HttpMethod... methods) {
|
||||
requestMethods.addAll(Arrays.asList(methods));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder allowedRequestHeaders(final String... headers) {
|
||||
requestHeaders.addAll(Arrays.asList(headers));
|
||||
return this;
|
||||
}
|
||||
|
||||
public CorsConfig build() {
|
||||
return new CorsConfig(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2013 The Netty Project
|
||||
*
|
||||
* The Netty Project 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 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.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.handler.codec.http.DefaultHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.util.internal.logging.InternalLogger;
|
||||
import io.netty.util.internal.logging.InternalLoggerFactory;
|
||||
|
||||
/**
|
||||
* Handles <a href="http://www.w3.org/TR/cors/">Cross Origin Resource Sharing</a> (CORS) requests.
|
||||
* <p>
|
||||
* This handler can be configured using a {@link io.netty.handler.codec.http.cors.CorsConfig}, please
|
||||
* refer to this class for details about the configuration options available.
|
||||
*/
|
||||
public class CorsHandler extends ChannelDuplexHandler {
|
||||
|
||||
private static final InternalLogger logger = InternalLoggerFactory.getInstance(CorsHandler.class);
|
||||
private final CorsConfig config;
|
||||
|
||||
private HttpRequest request;
|
||||
|
||||
public CorsHandler(final CorsConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
if (config.isCorsSupportEnabled() && msg instanceof HttpRequest) {
|
||||
request = (HttpRequest) msg;
|
||||
if (isPreflightRequest(request)) {
|
||||
handlePreflight(ctx, request);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
|
||||
private void handlePreflight(final ChannelHandlerContext ctx, final HttpRequest request) {
|
||||
final HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(), OK);
|
||||
if (setOrigin(response)) {
|
||||
setAllowMethods(response);
|
||||
setAllowHeaders(response);
|
||||
setAllowCredentials(response);
|
||||
setMaxAge(response);
|
||||
}
|
||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
private boolean setOrigin(final HttpResponse response) {
|
||||
final String origin = request.headers().get(ORIGIN);
|
||||
if (origin != null) {
|
||||
if ("null".equals(origin) && config.isNullOriginAllowed()) {
|
||||
response.headers().set(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
|
||||
} else {
|
||||
response.headers().set(ACCESS_CONTROL_ALLOW_ORIGIN, config.origin());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setAllowCredentials(final HttpResponse response) {
|
||||
if (config.isCredentialsAllowed()) {
|
||||
response.headers().set(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPreflightRequest(final HttpRequest request) {
|
||||
final HttpHeaders headers = request.headers();
|
||||
return request.getMethod().equals(OPTIONS) &&
|
||||
headers.contains(ORIGIN) &&
|
||||
headers.contains(ACCESS_CONTROL_REQUEST_METHOD);
|
||||
}
|
||||
|
||||
private void setExposeHeaders(final HttpResponse response) {
|
||||
if (!config.exposedHeaders().isEmpty()) {
|
||||
response.headers().set(ACCESS_CONTROL_EXPOSE_HEADERS, config.exposedHeaders());
|
||||
}
|
||||
}
|
||||
|
||||
private void setAllowMethods(final HttpResponse response) {
|
||||
response.headers().set(ACCESS_CONTROL_ALLOW_METHODS, config.allowedRequestMethods());
|
||||
}
|
||||
|
||||
private void setAllowHeaders(final HttpResponse response) {
|
||||
response.headers().set(ACCESS_CONTROL_ALLOW_HEADERS, config.allowedRequestHeaders());
|
||||
}
|
||||
|
||||
private void setMaxAge(final HttpResponse response) {
|
||||
response.headers().set(ACCESS_CONTROL_MAX_AGE, config.maxAge());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise)
|
||||
throws Exception {
|
||||
if (config.isCorsSupportEnabled() && msg instanceof HttpResponse) {
|
||||
final HttpResponse response = (HttpResponse) msg;
|
||||
if (setOrigin(response)) {
|
||||
setAllowCredentials(response);
|
||||
setAllowHeaders(response);
|
||||
setExposeHeaders(response);
|
||||
}
|
||||
}
|
||||
ctx.writeAndFlush(msg, promise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception {
|
||||
logger.error("Caught error in CorsHandler", cause);
|
||||
ctx.fireExceptionCaught(cause);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2013 The Netty Project
|
||||
*
|
||||
* The Netty Project 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This package contains Cross Origin Resource Sharing (CORS) related classes.
|
||||
*/
|
||||
package io.netty.handler.codec.http.cors;
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2013 The Netty Project
|
||||
*
|
||||
* The Netty Project 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 io.netty.handler.codec.http.cors;
|
||||
|
||||
import static io.netty.handler.codec.http.cors.CorsConfig.withOrigin;
|
||||
import static io.netty.handler.codec.http.cors.CorsConfig.anyOrigin;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.hasItems;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.*;
|
||||
|
||||
public class CorsConfigurationTest {
|
||||
|
||||
@Test
|
||||
public void disabled() {
|
||||
final CorsConfig cors = withOrigin("*").disable().build();
|
||||
assertThat(cors.isCorsSupportEnabled(), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wildcardOrigin() {
|
||||
final CorsConfig cors = anyOrigin().build();
|
||||
assertThat(cors.origin(), is(equalTo("*")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void origin() {
|
||||
final CorsConfig cors = withOrigin("http://localhost:7888").build();
|
||||
assertThat(cors.origin(), is(equalTo("http://localhost:7888")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exposeHeaders() {
|
||||
final CorsConfig cors = withOrigin("*").exposeHeaders("custom-header1", "custom-header2").build();
|
||||
assertThat(cors.exposedHeaders(), hasItems("custom-header1", "custom-header2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowCredentials() {
|
||||
final CorsConfig cors = withOrigin("*").allowCredentials().build();
|
||||
assertThat(cors.isCredentialsAllowed(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maxAge() {
|
||||
final CorsConfig cors = withOrigin("*").maxAge(3000).build();
|
||||
assertThat(cors.maxAge(), is(3000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestMethods() {
|
||||
final CorsConfig cors = withOrigin("*").allowedRequestMethods(HttpMethod.POST, HttpMethod.GET).build();
|
||||
assertThat(cors.allowedRequestMethods(), hasItems(HttpMethod.POST, HttpMethod.GET));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestHeaders() {
|
||||
final CorsConfig cors = withOrigin("*").allowedRequestHeaders("preflight-header1", "preflight-header2").build();
|
||||
assertThat(cors.allowedRequestHeaders(), hasItems("preflight-header1", "preflight-header2"));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright 2013 The Netty Project
|
||||
*
|
||||
* The Netty Project 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 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.SimpleChannelInboundHandler;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
|
||||
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||
import io.netty.handler.codec.http.DefaultHttpResponse;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CorsHandlerTest {
|
||||
|
||||
@Test
|
||||
public void nonCorsRequest() {
|
||||
final HttpResponse response = simpleRequest(CorsConfig.anyOrigin().build(), null);
|
||||
assertThat(response.headers().contains(ACCESS_CONTROL_ALLOW_ORIGIN), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleRequestWithAnyOrigin() {
|
||||
final HttpResponse response = simpleRequest(CorsConfig.anyOrigin().build(), "http://localhost:7777");
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is("*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleRequestWithOrigin() {
|
||||
final String origin = "http://localhost:8888";
|
||||
final HttpResponse response = simpleRequest(CorsConfig.withOrigin(origin).build(), origin);
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(origin));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightDeleteRequestWithCustomHeaders() {
|
||||
final CorsConfig config = CorsConfig.withOrigin("http://localhost:8888")
|
||||
.allowedRequestMethods(HttpMethod.GET, HttpMethod.DELETE)
|
||||
.build();
|
||||
final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://localhost:8888"));
|
||||
assertThat(response.headers().getAll(ACCESS_CONTROL_ALLOW_METHODS), hasItems("GET", "DELETE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightGetRequestWithCustomHeaders() {
|
||||
final CorsConfig config = CorsConfig.withOrigin("http://localhost:8888")
|
||||
.allowedRequestMethods(HttpMethod.OPTIONS, HttpMethod.GET, HttpMethod.DELETE)
|
||||
.allowedRequestHeaders("content-type", "xheader1")
|
||||
.build();
|
||||
final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is("http://localhost:8888"));
|
||||
assertThat(response.headers().getAll(ACCESS_CONTROL_ALLOW_METHODS), hasItems("OPTIONS", "GET"));
|
||||
assertThat(response.headers().getAll(ACCESS_CONTROL_ALLOW_HEADERS), hasItems("content-type", "xheader1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestWithNullOrigin() {
|
||||
final String origin = "null";
|
||||
final CorsConfig config = CorsConfig.withOrigin(origin).allowNullOrigin().build();
|
||||
final HttpResponse response = preflightRequest(config, origin, "content-type, xheader1");
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(equalTo("*")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestAllowCredentials() {
|
||||
final String origin = "null";
|
||||
final CorsConfig config = CorsConfig.withOrigin(origin).allowCredentials().build();
|
||||
final HttpResponse response = preflightRequest(config, origin, "content-type, xheader1");
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(equalTo("true")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestDoNotAllowCredentials() {
|
||||
final CorsConfig config = CorsConfig.withOrigin("http://localhost:8888").build();
|
||||
final HttpResponse response = preflightRequest(config, "http://localhost:8888", "");
|
||||
// the only valid value for Access-Control-Allow-Credentials is true.
|
||||
assertThat(response.headers().contains(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleRequestCustomHeaders() {
|
||||
final CorsConfig config = CorsConfig.anyOrigin().exposeHeaders("custom1", "custom2").build();
|
||||
final HttpResponse response = simpleRequest(config, "http://localhost:7777", "");
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), equalTo("*"));
|
||||
assertThat(response.headers().getAll(ACCESS_CONTROL_EXPOSE_HEADERS), hasItems("custom1", "custom1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleRequestAllowCredentials() {
|
||||
final CorsConfig config = CorsConfig.anyOrigin().allowCredentials().build();
|
||||
final HttpResponse response = simpleRequest(config, "http://localhost:7777", "");
|
||||
assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_CREDENTIALS), equalTo("true"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleRequestDoNotAllowCredentials() {
|
||||
final CorsConfig config = CorsConfig.anyOrigin().build();
|
||||
final HttpResponse response = simpleRequest(config, "http://localhost:7777", "");
|
||||
assertThat(response.headers().contains(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleRequestExposeHeaders() {
|
||||
final CorsConfig config = CorsConfig.anyOrigin().exposeHeaders("one", "two").build();
|
||||
final HttpResponse response = simpleRequest(config, "http://localhost:7777", "");
|
||||
assertThat(response.headers().getAll(ACCESS_CONTROL_EXPOSE_HEADERS), hasItems("one", "two"));
|
||||
}
|
||||
|
||||
private static HttpResponse simpleRequest(final CorsConfig config, final String origin) {
|
||||
return simpleRequest(config, origin, null);
|
||||
}
|
||||
|
||||
private static HttpResponse simpleRequest(final CorsConfig config,
|
||||
final String origin,
|
||||
final String requestHeaders) {
|
||||
return simpleRequest(config, origin, requestHeaders, GET);
|
||||
}
|
||||
|
||||
private static HttpResponse simpleRequest(final CorsConfig config,
|
||||
final String origin,
|
||||
final String requestHeaders,
|
||||
final HttpMethod method) {
|
||||
final EmbeddedChannel channel = new EmbeddedChannel(new CorsHandler(config), new EchoHandler());
|
||||
final FullHttpRequest httpRequest = createHttpRequest(method);
|
||||
if (origin != null) {
|
||||
httpRequest.headers().set(ORIGIN, origin);
|
||||
}
|
||||
if (requestHeaders != null) {
|
||||
httpRequest.headers().set(ACCESS_CONTROL_REQUEST_HEADERS, requestHeaders);
|
||||
}
|
||||
channel.writeInbound(httpRequest);
|
||||
return (HttpResponse) channel.readOutbound();
|
||||
}
|
||||
|
||||
private static HttpResponse preflightRequest(final CorsConfig config,
|
||||
final String origin,
|
||||
final String requestHeaders) {
|
||||
final EmbeddedChannel channel = new EmbeddedChannel(new CorsHandler(config));
|
||||
final FullHttpRequest httpRequest = createHttpRequest(OPTIONS);
|
||||
httpRequest.headers().set(ORIGIN, origin);
|
||||
httpRequest.headers().set(ACCESS_CONTROL_REQUEST_METHOD, httpRequest.getMethod().toString());
|
||||
httpRequest.headers().set(ACCESS_CONTROL_REQUEST_HEADERS, requestHeaders);
|
||||
channel.writeInbound(httpRequest);
|
||||
return (HttpResponse) channel.readOutbound();
|
||||
}
|
||||
|
||||
private static FullHttpRequest createHttpRequest(HttpMethod method) {
|
||||
return new DefaultFullHttpRequest(HTTP_1_1, method, "/info");
|
||||
}
|
||||
|
||||
private static class EchoHandler extends SimpleChannelInboundHandler<Object> {
|
||||
|
||||
@Override
|
||||
public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
ctx.writeAndFlush(new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
102
example/src/main/java/io/netty/example/http/cors/HttpServer.java
Normal file
102
example/src/main/java/io/netty/example/http/cors/HttpServer.java
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2012 The Netty Project
|
||||
*
|
||||
* The Netty Project 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 io.netty.example.http.cors;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
|
||||
/**
|
||||
* This example server aims to demonstrate
|
||||
* <a href="http://www.w3.org/TR/cors/">Cross Origin Resource Sharing</a> (CORS) in Netty.
|
||||
* It does not have a client like most of the other examples, but instead has
|
||||
* a html page that is loaded to try out CORS support in a web brower.
|
||||
* <p>
|
||||
*
|
||||
* CORS is configured in {@link HttpServerInitializer} and by updating the config you can
|
||||
* try out various combinations, like using a specific origin instead of a
|
||||
* wildcard origin ('*').
|
||||
* <p>
|
||||
*
|
||||
* The file {@code src/main/resources/cors/cors.html} contains a very basic example client
|
||||
* which can be used to try out different configurations. For example, you can add
|
||||
* custom headers to force a CORS preflight request to make the request fail. Then
|
||||
* to enable a successful request, configure the CorsHandler to allow that/those
|
||||
* request headers.
|
||||
*
|
||||
* <h2>Testing CORS</h2>
|
||||
* You can either load the file {@code src/main/resources/cors/cors.html} using a web server
|
||||
* or load it from the file system using a web browser.
|
||||
*
|
||||
* <h3>Using a web server</h3>
|
||||
* To test CORS support you can serve the file {@code src/main/resources/cors/cors.html}
|
||||
* using a web server. You can then add a new host name to your systems hosts file, for
|
||||
* example if you are on Linux you may update /etc/hosts to add an addtional name
|
||||
* for you local system:
|
||||
* <pre>
|
||||
* 127.0.0.1 localhost domain1.com
|
||||
* </pre>
|
||||
* Now, you should be able to access {@code http://domain1.com/cors.html} depending on how you
|
||||
* have configured you local web server the exact url may differ.
|
||||
*
|
||||
* <h3>Using a web browser</h3>
|
||||
* Open the file {@code src/main/resources/cors/cors.html} in a web browser. You should see
|
||||
* loaded page and in the text area the following message:
|
||||
* <pre>
|
||||
* 'CORS is not working'
|
||||
* </pre>
|
||||
*
|
||||
* If you inspect the headers being sent using your browser you'll see that the 'Origin'
|
||||
* request header is {@code 'null'}. This is expected and happens when you load a file from the
|
||||
* local file system. Netty can handle this by configuring the CorsHandler which is done
|
||||
* in the {@link HttpServerInitializer}.
|
||||
*
|
||||
*/
|
||||
public class HttpServer {
|
||||
|
||||
private final int port;
|
||||
|
||||
public HttpServer(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public void run() throws Exception {
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup();
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup();
|
||||
try {
|
||||
ServerBootstrap b = new ServerBootstrap();
|
||||
b.group(bossGroup, workerGroup)
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.childHandler(new HttpServerInitializer());
|
||||
|
||||
b.bind(port).sync().channel().closeFuture().sync();
|
||||
} finally {
|
||||
bossGroup.shutdownGracefully();
|
||||
workerGroup.shutdownGracefully();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
int port;
|
||||
if (args.length > 0) {
|
||||
port = Integer.parseInt(args[0]);
|
||||
} else {
|
||||
port = 8080;
|
||||
}
|
||||
new HttpServer(port).run();
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2012 The Netty Project
|
||||
*
|
||||
* The Netty Project 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 io.netty.example.http.cors;
|
||||
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||
import io.netty.handler.codec.http.HttpRequestDecoder;
|
||||
import io.netty.handler.codec.http.HttpResponseEncoder;
|
||||
import io.netty.handler.codec.http.cors.CorsConfig;
|
||||
import io.netty.handler.codec.http.cors.CorsHandler;
|
||||
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||
|
||||
/**
|
||||
* Please refer to the {@link CorsConfig} javadocs for information about all the
|
||||
* configuration options available.
|
||||
*
|
||||
* Below are some of configuration discussed in this example:
|
||||
* <h3>Support only a specific origin</h3>
|
||||
* To support a single origin instead of the wildcard use the following:
|
||||
* <pre>
|
||||
* CorsConfig corsConfig = CorsConfig.withOrigin("http://domain1.com")
|
||||
* </pre>
|
||||
*
|
||||
* <h3>Enable loading from the file system</h3>
|
||||
* To enable the server to handle an origin specified as 'null', which happens
|
||||
* when a web browser loads a file from the local file system use the following:
|
||||
* <pre>
|
||||
* corsConfig.isNullOriginAllowed()
|
||||
* </pre>
|
||||
*
|
||||
* <h3>Enable request headers</h3>
|
||||
* To enable additional request headers:
|
||||
* <pre>
|
||||
* corsConfig.allowedRequestHeaders("custom-request-header")
|
||||
* </pre>
|
||||
*
|
||||
* <h3>Expose response headers</h3>
|
||||
* By default a browser only exposes the following simple header:
|
||||
* <ul>
|
||||
* <li>Cache-Control</li>
|
||||
* <li>Content-Language</li>
|
||||
* <li>Content-Type</li>
|
||||
* <li>Expires</li>
|
||||
* <li>Last-Modified</li>
|
||||
* <li>Pragma</li>
|
||||
* </ul>
|
||||
* Any of the above response headers can be retreived by:
|
||||
* <pre>
|
||||
* xhr.getResponseHeader("Content-Type");
|
||||
* </pre>
|
||||
* If you need to get access to other headers this must be enabled by the server, for example:
|
||||
* <pre>
|
||||
* corsConfig.exposedHeaders("custom-response-header");
|
||||
* </pre>
|
||||
*/
|
||||
public class HttpServerInitializer extends ChannelInitializer<SocketChannel> {
|
||||
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) throws Exception {
|
||||
ChannelPipeline pipeline = ch.pipeline();
|
||||
|
||||
CorsConfig corsConfig = CorsConfig.anyOrigin().build();
|
||||
|
||||
pipeline.addLast("decoder", new HttpRequestDecoder());
|
||||
pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
|
||||
pipeline.addLast("encoder", new HttpResponseEncoder());
|
||||
pipeline.addLast("chunkedWriter", new ChunkedWriteHandler());
|
||||
pipeline.addLast("cors", new CorsHandler(corsConfig));
|
||||
pipeline.addLast("handler", new OkResponseHandler());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2013 The Netty Project
|
||||
*
|
||||
* The Netty Project 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 io.netty.example.http.cors;
|
||||
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerAdapter;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.DefaultHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
|
||||
/**
|
||||
* A simple handler which will simple return a successful Http
|
||||
* response for any request.
|
||||
*/
|
||||
public class OkResponseHandler extends ChannelHandlerAdapter {
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
final DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
response.headers().set("custom-response-header", "Some value");
|
||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
42
example/src/main/resources/cors/cors.html
Normal file
42
example/src/main/resources/cors/cors.html
Normal file
@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Cross Origin Resource Sharing (CORS) Example</title>
|
||||
<link rel="stylesheet" href="css/cors.css">
|
||||
</head>
|
||||
<body onload="simpleGetRequest();">
|
||||
<h1>Repsonse from Server</h1>
|
||||
<textarea id="responseText"></textarea>
|
||||
<script>
|
||||
function simpleGetRequest() {
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', 'http://localhost:8080/cors');
|
||||
|
||||
// Uncomment to force a CORS preflight request.
|
||||
//xhr.setRequestHeader('custom-request-header', 'dummy value');
|
||||
|
||||
xhr.onerror = function() {
|
||||
getTextAreaElement().value = 'CORS is NOT working';
|
||||
}
|
||||
|
||||
xhr.onload = function() {
|
||||
getTextAreaElement().value = 'CORS is working';
|
||||
//var header = xhr.getResponseHeader("custom-response-header");
|
||||
//appendTextArea('custom-response-header=' + header);
|
||||
}
|
||||
|
||||
function getTextAreaElement() {
|
||||
return document.getElementById('responseText');
|
||||
}
|
||||
|
||||
function appendTextArea(newData) {
|
||||
var el = getTextAreaElement();
|
||||
el.value = el.value + '\n' + newData;
|
||||
}
|
||||
|
||||
xhr.send();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
4
example/src/main/resources/cors/css/cors.css
Normal file
4
example/src/main/resources/cors/css/cors.css
Normal file
@ -0,0 +1,4 @@
|
||||
textarea {
|
||||
width: 200px;
|
||||
height: 50px;
|
||||
}
|
Loading…
Reference in New Issue
Block a user