Adding a Cross Origin Resource Sharing (CORS) handler.
This commit is contained in:
parent
a906c9681b
commit
6a954d5b47
@ -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.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
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