Adding a Cross Origin Resource Sharing (CORS) handler.

This commit is contained in:
Daniel Bevenius 2013-12-16 14:36:45 +01:00 committed by Norman Maurer
parent a906c9681b
commit 6a954d5b47
10 changed files with 919 additions and 0 deletions

View File

@ -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);
}
}
}

View File

@ -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.ChannelFutureListener;
import io.netty.channel.ChannelHandlerAdapter;
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 ChannelHandlerAdapter {
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);
}
}

View File

@ -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;

View File

@ -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"));
}
}

View File

@ -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.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
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 ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.writeAndFlush(new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK));
}
}
}

View 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();
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View 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>

View File

@ -0,0 +1,4 @@
textarea {
width: 200px;
height: 50px;
}