diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfig.java b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfig.java new file mode 100644 index 0000000000..b818453d94 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfig.java @@ -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 exposeHeaders; + private final boolean allowCredentials; + private final long maxAge; + private final Set allowedRequestMethods; + private final Set 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: + *
+     * xhr.getResponseHeader("Content-Type");
+     * 
+ * The headers that are available by default are: + *
    + *
  • Cache-Control
  • + *
  • Content-Language
  • + *
  • Content-Type
  • + *
  • Expires
  • + *
  • Last-Modified
  • + *
  • Pragma
  • + *
+ * 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} a list of the headers to expose. + */ + public Set 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 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 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 exposeHeaders = new HashSet(); + private long maxAge; + private final Set requestMethods = new HashSet(); + private final Set requestHeaders = new HashSet(); + + 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); + } + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java new file mode 100644 index 0000000000..f08a6a4af6 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java @@ -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 Cross Origin Resource Sharing (CORS) requests. + *

+ * 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); + } +} + diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cors/package-info.java b/codec-http/src/main/java/io/netty/handler/codec/http/cors/package-info.java new file mode 100644 index 0000000000..b9b480a338 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cors/package-info.java @@ -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; diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsConfigurationTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsConfigurationTest.java new file mode 100644 index 0000000000..293f1b0c78 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsConfigurationTest.java @@ -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")); + } + +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java new file mode 100644 index 0000000000..dc7aa94db4 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java @@ -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 { + + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + ctx.writeAndFlush(new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK)); + } + } + +} diff --git a/example/src/main/java/io/netty/example/http/cors/HttpServer.java b/example/src/main/java/io/netty/example/http/cors/HttpServer.java new file mode 100644 index 0000000000..249d6fd2e3 --- /dev/null +++ b/example/src/main/java/io/netty/example/http/cors/HttpServer.java @@ -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 + * Cross Origin Resource Sharing (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. + *

+ * + * 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 ('*'). + *

+ * + * 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. + * + *

Testing CORS

+ * 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. + * + *

Using a web server

+ * 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: + *
+ * 127.0.0.1   localhost domain1.com
+ * 
+ * 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. + * + *

Using a web browser

+ * 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: + *
+ * 'CORS is not working'
+ * 
+ * + * 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(); + } +} diff --git a/example/src/main/java/io/netty/example/http/cors/HttpServerInitializer.java b/example/src/main/java/io/netty/example/http/cors/HttpServerInitializer.java new file mode 100644 index 0000000000..ec4778f4fa --- /dev/null +++ b/example/src/main/java/io/netty/example/http/cors/HttpServerInitializer.java @@ -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: + *

Support only a specific origin

+ * To support a single origin instead of the wildcard use the following: + *
+ * CorsConfig corsConfig = CorsConfig.withOrigin("http://domain1.com")
+ * 
+ * + *

Enable loading from the file system

+ * 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: + *
+ * corsConfig.isNullOriginAllowed()
+ * 
+ * + *

Enable request headers

+ * To enable additional request headers: + *
+ * corsConfig.allowedRequestHeaders("custom-request-header")
+ * 
+ * + *

Expose response headers

+ * By default a browser only exposes the following simple header: + *
    + *
  • Cache-Control
  • + *
  • Content-Language
  • + *
  • Content-Type
  • + *
  • Expires
  • + *
  • Last-Modified
  • + *
  • Pragma
  • + *
+ * Any of the above response headers can be retreived by: + *
+ * xhr.getResponseHeader("Content-Type");
+ * 
+ * If you need to get access to other headers this must be enabled by the server, for example: + *
+ * corsConfig.exposedHeaders("custom-response-header");
+ * 
+ */ +public class HttpServerInitializer extends ChannelInitializer { + + @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()); + } + +} diff --git a/example/src/main/java/io/netty/example/http/cors/OkResponseHandler.java b/example/src/main/java/io/netty/example/http/cors/OkResponseHandler.java new file mode 100644 index 0000000000..454b9239f5 --- /dev/null +++ b/example/src/main/java/io/netty/example/http/cors/OkResponseHandler.java @@ -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); + } +} diff --git a/example/src/main/resources/cors/cors.html b/example/src/main/resources/cors/cors.html new file mode 100644 index 0000000000..ad5a75b699 --- /dev/null +++ b/example/src/main/resources/cors/cors.html @@ -0,0 +1,42 @@ + + + + + Cross Origin Resource Sharing (CORS) Example + + + +

Repsonse from Server

+ + + + diff --git a/example/src/main/resources/cors/css/cors.css b/example/src/main/resources/cors/css/cors.css new file mode 100644 index 0000000000..3bb211e8c7 --- /dev/null +++ b/example/src/main/resources/cors/css/cors.css @@ -0,0 +1,4 @@ +textarea { + width: 200px; + height: 50px; +}