More robust handling of CORS HTTP Access Control
Uses a refactored version of Netty's CORS implementation to provide more robust cross-origin resource request functionality. The CORS specific Elasticsearch parameters remain the same, just the underlying implementation has changed. It has also been refactored in a way that allows dropping in Netty's CORS handler as a replacement once Elasticsearch is upgraded to Netty 4.
This commit is contained in:
parent
6707a89c3b
commit
592f5499cd
|
@ -1140,4 +1140,5 @@ public class Strings {
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,7 @@
|
||||||
package org.elasticsearch.http.netty;
|
package org.elasticsearch.http.netty;
|
||||||
|
|
||||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||||
import org.elasticsearch.http.HttpTransportSettings;
|
|
||||||
import org.elasticsearch.http.netty.pipelining.OrderedUpstreamMessageEvent;
|
import org.elasticsearch.http.netty.pipelining.OrderedUpstreamMessageEvent;
|
||||||
import org.elasticsearch.rest.support.RestUtils;
|
|
||||||
import org.jboss.netty.channel.ChannelHandler;
|
import org.jboss.netty.channel.ChannelHandler;
|
||||||
import org.jboss.netty.channel.ChannelHandlerContext;
|
import org.jboss.netty.channel.ChannelHandlerContext;
|
||||||
import org.jboss.netty.channel.ExceptionEvent;
|
import org.jboss.netty.channel.ExceptionEvent;
|
||||||
|
@ -30,9 +28,6 @@ import org.jboss.netty.channel.MessageEvent;
|
||||||
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
|
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
|
||||||
import org.jboss.netty.handler.codec.http.HttpRequest;
|
import org.jboss.netty.handler.codec.http.HttpRequest;
|
||||||
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@ -40,15 +35,12 @@ import java.util.regex.Pattern;
|
||||||
public class HttpRequestHandler extends SimpleChannelUpstreamHandler {
|
public class HttpRequestHandler extends SimpleChannelUpstreamHandler {
|
||||||
|
|
||||||
private final NettyHttpServerTransport serverTransport;
|
private final NettyHttpServerTransport serverTransport;
|
||||||
private final Pattern corsPattern;
|
|
||||||
private final boolean httpPipeliningEnabled;
|
private final boolean httpPipeliningEnabled;
|
||||||
private final boolean detailedErrorsEnabled;
|
private final boolean detailedErrorsEnabled;
|
||||||
private final ThreadContext threadContext;
|
private final ThreadContext threadContext;
|
||||||
|
|
||||||
public HttpRequestHandler(NettyHttpServerTransport serverTransport, boolean detailedErrorsEnabled, ThreadContext threadContext) {
|
public HttpRequestHandler(NettyHttpServerTransport serverTransport, boolean detailedErrorsEnabled, ThreadContext threadContext) {
|
||||||
this.serverTransport = serverTransport;
|
this.serverTransport = serverTransport;
|
||||||
this.corsPattern = RestUtils
|
|
||||||
.checkCorsSettingForRegex(HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN.get(serverTransport.settings()));
|
|
||||||
this.httpPipeliningEnabled = serverTransport.pipelining;
|
this.httpPipeliningEnabled = serverTransport.pipelining;
|
||||||
this.detailedErrorsEnabled = detailedErrorsEnabled;
|
this.detailedErrorsEnabled = detailedErrorsEnabled;
|
||||||
this.threadContext = threadContext;
|
this.threadContext = threadContext;
|
||||||
|
@ -70,9 +62,9 @@ public class HttpRequestHandler extends SimpleChannelUpstreamHandler {
|
||||||
// when reading, or using a cumalation buffer
|
// when reading, or using a cumalation buffer
|
||||||
NettyHttpRequest httpRequest = new NettyHttpRequest(request, e.getChannel());
|
NettyHttpRequest httpRequest = new NettyHttpRequest(request, e.getChannel());
|
||||||
if (oue != null) {
|
if (oue != null) {
|
||||||
serverTransport.dispatchRequest(httpRequest, new NettyHttpChannel(serverTransport, httpRequest, corsPattern, oue, detailedErrorsEnabled));
|
serverTransport.dispatchRequest(httpRequest, new NettyHttpChannel(serverTransport, httpRequest, oue, detailedErrorsEnabled));
|
||||||
} else {
|
} else {
|
||||||
serverTransport.dispatchRequest(httpRequest, new NettyHttpChannel(serverTransport, httpRequest, corsPattern, detailedErrorsEnabled));
|
serverTransport.dispatchRequest(httpRequest, new NettyHttpChannel(serverTransport, httpRequest, detailedErrorsEnabled));
|
||||||
}
|
}
|
||||||
super.messageReceived(ctx, e);
|
super.messageReceived(ctx, e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,18 +19,17 @@
|
||||||
|
|
||||||
package org.elasticsearch.http.netty;
|
package org.elasticsearch.http.netty;
|
||||||
|
|
||||||
import org.elasticsearch.common.Strings;
|
|
||||||
import org.elasticsearch.common.bytes.BytesReference;
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
import org.elasticsearch.common.io.stream.BytesStreamOutput;
|
import org.elasticsearch.common.io.stream.BytesStreamOutput;
|
||||||
import org.elasticsearch.common.io.stream.ReleasableBytesStreamOutput;
|
import org.elasticsearch.common.io.stream.ReleasableBytesStreamOutput;
|
||||||
import org.elasticsearch.common.lease.Releasable;
|
import org.elasticsearch.common.lease.Releasable;
|
||||||
import org.elasticsearch.common.netty.ReleaseChannelFutureListener;
|
import org.elasticsearch.common.netty.ReleaseChannelFutureListener;
|
||||||
import org.elasticsearch.http.HttpChannel;
|
import org.elasticsearch.http.HttpChannel;
|
||||||
|
import org.elasticsearch.http.netty.cors.CorsHandler;
|
||||||
import org.elasticsearch.http.netty.pipelining.OrderedDownstreamChannelEvent;
|
import org.elasticsearch.http.netty.pipelining.OrderedDownstreamChannelEvent;
|
||||||
import org.elasticsearch.http.netty.pipelining.OrderedUpstreamMessageEvent;
|
import org.elasticsearch.http.netty.pipelining.OrderedUpstreamMessageEvent;
|
||||||
import org.elasticsearch.rest.RestResponse;
|
import org.elasticsearch.rest.RestResponse;
|
||||||
import org.elasticsearch.rest.RestStatus;
|
import org.elasticsearch.rest.RestStatus;
|
||||||
import org.elasticsearch.rest.support.RestUtils;
|
|
||||||
import org.jboss.netty.buffer.ChannelBuffer;
|
import org.jboss.netty.buffer.ChannelBuffer;
|
||||||
import org.jboss.netty.channel.Channel;
|
import org.jboss.netty.channel.Channel;
|
||||||
import org.jboss.netty.channel.ChannelFuture;
|
import org.jboss.netty.channel.ChannelFuture;
|
||||||
|
@ -40,28 +39,17 @@ import org.jboss.netty.handler.codec.http.CookieDecoder;
|
||||||
import org.jboss.netty.handler.codec.http.CookieEncoder;
|
import org.jboss.netty.handler.codec.http.CookieEncoder;
|
||||||
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
|
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
|
||||||
import org.jboss.netty.handler.codec.http.HttpHeaders;
|
import org.jboss.netty.handler.codec.http.HttpHeaders;
|
||||||
import org.jboss.netty.handler.codec.http.HttpMethod;
|
import org.jboss.netty.handler.codec.http.HttpResponse;
|
||||||
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
|
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
|
||||||
import org.jboss.netty.handler.codec.http.HttpVersion;
|
import org.jboss.netty.handler.codec.http.HttpVersion;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_CREDENTIALS;
|
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONNECTION;
|
||||||
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_HEADERS;
|
import static org.jboss.netty.handler.codec.http.HttpHeaders.Values.CLOSE;
|
||||||
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_METHODS;
|
import static org.jboss.netty.handler.codec.http.HttpHeaders.Values.KEEP_ALIVE;
|
||||||
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN;
|
|
||||||
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ENABLED;
|
|
||||||
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_MAX_AGE;
|
|
||||||
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_CREDENTIALS;
|
|
||||||
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_HEADERS;
|
|
||||||
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_METHODS;
|
|
||||||
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN;
|
|
||||||
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_MAX_AGE;
|
|
||||||
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ORIGIN;
|
|
||||||
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.USER_AGENT;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -72,18 +60,18 @@ public class NettyHttpChannel extends HttpChannel {
|
||||||
private final Channel channel;
|
private final Channel channel;
|
||||||
private final org.jboss.netty.handler.codec.http.HttpRequest nettyRequest;
|
private final org.jboss.netty.handler.codec.http.HttpRequest nettyRequest;
|
||||||
private OrderedUpstreamMessageEvent orderedUpstreamMessageEvent = null;
|
private OrderedUpstreamMessageEvent orderedUpstreamMessageEvent = null;
|
||||||
private Pattern corsPattern;
|
|
||||||
|
|
||||||
public NettyHttpChannel(NettyHttpServerTransport transport, NettyHttpRequest request, Pattern corsPattern, boolean detailedErrorsEnabled) {
|
public NettyHttpChannel(NettyHttpServerTransport transport, NettyHttpRequest request,
|
||||||
|
boolean detailedErrorsEnabled) {
|
||||||
super(request, detailedErrorsEnabled);
|
super(request, detailedErrorsEnabled);
|
||||||
this.transport = transport;
|
this.transport = transport;
|
||||||
this.channel = request.getChannel();
|
this.channel = request.getChannel();
|
||||||
this.nettyRequest = request.request();
|
this.nettyRequest = request.request();
|
||||||
this.corsPattern = corsPattern;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public NettyHttpChannel(NettyHttpServerTransport transport, NettyHttpRequest request, Pattern corsPattern, OrderedUpstreamMessageEvent orderedUpstreamMessageEvent, boolean detailedErrorsEnabled) {
|
public NettyHttpChannel(NettyHttpServerTransport transport, NettyHttpRequest request,
|
||||||
this(transport, request, corsPattern, detailedErrorsEnabled);
|
OrderedUpstreamMessageEvent orderedUpstreamMessageEvent, boolean detailedErrorsEnabled) {
|
||||||
|
this(transport, request, detailedErrorsEnabled);
|
||||||
this.orderedUpstreamMessageEvent = orderedUpstreamMessageEvent;
|
this.orderedUpstreamMessageEvent = orderedUpstreamMessageEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,48 +83,12 @@ public class NettyHttpChannel extends HttpChannel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendResponse(RestResponse response) {
|
public void sendResponse(RestResponse response) {
|
||||||
// Decide whether to close the connection or not.
|
// if the response object was created upstream, then use it;
|
||||||
boolean http10 = nettyRequest.getProtocolVersion().equals(HttpVersion.HTTP_1_0);
|
// otherwise, create a new one
|
||||||
boolean close =
|
HttpResponse resp = newResponse();
|
||||||
HttpHeaders.Values.CLOSE.equalsIgnoreCase(nettyRequest.headers().get(HttpHeaders.Names.CONNECTION)) ||
|
resp.setStatus(getStatus(response.status()));
|
||||||
(http10 && !HttpHeaders.Values.KEEP_ALIVE.equalsIgnoreCase(nettyRequest.headers().get(HttpHeaders.Names.CONNECTION)));
|
|
||||||
|
|
||||||
// Build the response object.
|
CorsHandler.setCorsResponseHeaders(nettyRequest, resp, transport.getCorsConfig());
|
||||||
HttpResponseStatus status = getStatus(response.status());
|
|
||||||
org.jboss.netty.handler.codec.http.HttpResponse resp;
|
|
||||||
if (http10) {
|
|
||||||
resp = new DefaultHttpResponse(HttpVersion.HTTP_1_0, status);
|
|
||||||
if (!close) {
|
|
||||||
resp.headers().add(HttpHeaders.Names.CONNECTION, "Keep-Alive");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status);
|
|
||||||
}
|
|
||||||
if (RestUtils.isBrowser(nettyRequest.headers().get(USER_AGENT))) {
|
|
||||||
if (SETTING_CORS_ENABLED.get(transport.settings())) {
|
|
||||||
String originHeader = request.header(ORIGIN);
|
|
||||||
if (!Strings.isNullOrEmpty(originHeader)) {
|
|
||||||
if (corsPattern == null) {
|
|
||||||
String allowedOrigins = SETTING_CORS_ALLOW_ORIGIN.get(transport.settings());
|
|
||||||
if (!Strings.isNullOrEmpty(allowedOrigins)) {
|
|
||||||
resp.headers().add(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigins);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resp.headers().add(ACCESS_CONTROL_ALLOW_ORIGIN, corsPattern.matcher(originHeader).matches() ? originHeader : "null");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nettyRequest.getMethod() == HttpMethod.OPTIONS) {
|
|
||||||
// Allow Ajax requests based on the CORS "preflight" request
|
|
||||||
resp.headers().add(ACCESS_CONTROL_MAX_AGE, SETTING_CORS_MAX_AGE.get(transport.settings()));
|
|
||||||
resp.headers().add(ACCESS_CONTROL_ALLOW_METHODS, SETTING_CORS_ALLOW_METHODS.get(transport.settings()));
|
|
||||||
resp.headers().add(ACCESS_CONTROL_ALLOW_HEADERS, SETTING_CORS_ALLOW_HEADERS.get(transport.settings()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SETTING_CORS_ALLOW_CREDENTIALS.get(transport.settings())) {
|
|
||||||
resp.headers().add(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String opaque = nettyRequest.headers().get("X-Opaque-Id");
|
String opaque = nettyRequest.headers().get("X-Opaque-Id");
|
||||||
if (opaque != null) {
|
if (opaque != null) {
|
||||||
|
@ -201,7 +153,7 @@ public class NettyHttpChannel extends HttpChannel {
|
||||||
addedReleaseListener = true;
|
addedReleaseListener = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (close) {
|
if (isCloseConnection()) {
|
||||||
future.addListener(ChannelFutureListener.CLOSE);
|
future.addListener(ChannelFutureListener.CLOSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,6 +164,36 @@ public class NettyHttpChannel extends HttpChannel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine if the request protocol version is HTTP 1.0
|
||||||
|
private boolean isHttp10() {
|
||||||
|
return nettyRequest.getProtocolVersion().equals(HttpVersion.HTTP_1_0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if the request connection should be closed on completion.
|
||||||
|
private boolean isCloseConnection() {
|
||||||
|
final boolean http10 = isHttp10();
|
||||||
|
return CLOSE.equalsIgnoreCase(nettyRequest.headers().get(CONNECTION)) ||
|
||||||
|
(http10 && !KEEP_ALIVE.equalsIgnoreCase(nettyRequest.headers().get(CONNECTION)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new {@link HttpResponse} to transmit the response for the netty request.
|
||||||
|
private HttpResponse newResponse() {
|
||||||
|
final boolean http10 = isHttp10();
|
||||||
|
final boolean close = isCloseConnection();
|
||||||
|
// Build the response object.
|
||||||
|
HttpResponseStatus status = HttpResponseStatus.OK; // default to initialize
|
||||||
|
org.jboss.netty.handler.codec.http.HttpResponse resp;
|
||||||
|
if (http10) {
|
||||||
|
resp = new DefaultHttpResponse(HttpVersion.HTTP_1_0, status);
|
||||||
|
if (!close) {
|
||||||
|
resp.headers().add(CONNECTION, "Keep-Alive");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status);
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
private static final HttpResponseStatus TOO_MANY_REQUESTS = new HttpResponseStatus(429, "Too Many Requests");
|
private static final HttpResponseStatus TOO_MANY_REQUESTS = new HttpResponseStatus(429, "Too Many Requests");
|
||||||
|
|
||||||
private HttpResponseStatus getStatus(RestStatus status) {
|
private HttpResponseStatus getStatus(RestStatus status) {
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
package org.elasticsearch.http.netty;
|
package org.elasticsearch.http.netty;
|
||||||
|
|
||||||
|
import org.elasticsearch.common.Strings;
|
||||||
import org.elasticsearch.common.SuppressForbidden;
|
import org.elasticsearch.common.SuppressForbidden;
|
||||||
import org.elasticsearch.common.component.AbstractLifecycleComponent;
|
import org.elasticsearch.common.component.AbstractLifecycleComponent;
|
||||||
import org.elasticsearch.common.inject.Inject;
|
import org.elasticsearch.common.inject.Inject;
|
||||||
|
@ -44,10 +45,13 @@ import org.elasticsearch.http.HttpRequest;
|
||||||
import org.elasticsearch.http.HttpServerAdapter;
|
import org.elasticsearch.http.HttpServerAdapter;
|
||||||
import org.elasticsearch.http.HttpServerTransport;
|
import org.elasticsearch.http.HttpServerTransport;
|
||||||
import org.elasticsearch.http.HttpStats;
|
import org.elasticsearch.http.HttpStats;
|
||||||
import org.elasticsearch.http.HttpTransportSettings;
|
import org.elasticsearch.http.netty.cors.CorsConfig;
|
||||||
|
import org.elasticsearch.http.netty.cors.CorsConfigBuilder;
|
||||||
|
import org.elasticsearch.http.netty.cors.CorsHandler;
|
||||||
import org.elasticsearch.http.netty.pipelining.HttpPipeliningHandler;
|
import org.elasticsearch.http.netty.pipelining.HttpPipeliningHandler;
|
||||||
import org.elasticsearch.monitor.jvm.JvmInfo;
|
import org.elasticsearch.monitor.jvm.JvmInfo;
|
||||||
import org.elasticsearch.threadpool.ThreadPool;
|
import org.elasticsearch.threadpool.ThreadPool;
|
||||||
|
import org.elasticsearch.rest.support.RestUtils;
|
||||||
import org.elasticsearch.transport.BindTransportException;
|
import org.elasticsearch.transport.BindTransportException;
|
||||||
import org.jboss.netty.bootstrap.ServerBootstrap;
|
import org.jboss.netty.bootstrap.ServerBootstrap;
|
||||||
import org.jboss.netty.channel.AdaptiveReceiveBufferSizePredictorFactory;
|
import org.jboss.netty.channel.AdaptiveReceiveBufferSizePredictorFactory;
|
||||||
|
@ -63,6 +67,7 @@ import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
|
||||||
import org.jboss.netty.channel.socket.oio.OioServerSocketChannelFactory;
|
import org.jboss.netty.channel.socket.oio.OioServerSocketChannelFactory;
|
||||||
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
|
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
|
||||||
import org.jboss.netty.handler.codec.http.HttpContentCompressor;
|
import org.jboss.netty.handler.codec.http.HttpContentCompressor;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpMethod;
|
||||||
import org.jboss.netty.handler.codec.http.HttpRequestDecoder;
|
import org.jboss.netty.handler.codec.http.HttpRequestDecoder;
|
||||||
import org.jboss.netty.handler.timeout.ReadTimeoutException;
|
import org.jboss.netty.handler.timeout.ReadTimeoutException;
|
||||||
|
|
||||||
|
@ -74,13 +79,34 @@ import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static org.elasticsearch.common.util.concurrent.EsExecutors.daemonThreadFactory;
|
||||||
import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_BLOCKING;
|
import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_BLOCKING;
|
||||||
import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_KEEP_ALIVE;
|
import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_KEEP_ALIVE;
|
||||||
import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_NO_DELAY;
|
import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_NO_DELAY;
|
||||||
import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_RECEIVE_BUFFER_SIZE;
|
import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_RECEIVE_BUFFER_SIZE;
|
||||||
import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_REUSE_ADDRESS;
|
import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_REUSE_ADDRESS;
|
||||||
import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_SEND_BUFFER_SIZE;
|
import static org.elasticsearch.common.network.NetworkService.TcpSettings.TCP_SEND_BUFFER_SIZE;
|
||||||
import static org.elasticsearch.common.util.concurrent.EsExecutors.daemonThreadFactory;
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_CREDENTIALS;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_HEADERS;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_METHODS;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ENABLED;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_MAX_AGE;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_COMPRESSION;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_COMPRESSION_LEVEL;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_DETAILED_ERRORS_ENABLED;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_CHUNK_SIZE;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_CONTENT_LENGTH;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_HEADER_SIZE;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_INITIAL_LINE_LENGTH;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_PORT;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_PUBLISH_PORT;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_RESET_COOKIES;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_PIPELINING;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_PIPELINING_MAX_EVENTS;
|
||||||
|
import static org.elasticsearch.http.netty.cors.CorsHandler.ANY_ORIGIN;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -146,6 +172,8 @@ public class NettyHttpServerTransport extends AbstractLifecycleComponent<HttpSer
|
||||||
|
|
||||||
protected volatile HttpServerAdapter httpServerAdapter;
|
protected volatile HttpServerAdapter httpServerAdapter;
|
||||||
|
|
||||||
|
private final CorsConfig corsConfig;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@SuppressForbidden(reason = "sets org.jboss.netty.epollBugWorkaround based on netty.epollBugWorkaround")
|
@SuppressForbidden(reason = "sets org.jboss.netty.epollBugWorkaround based on netty.epollBugWorkaround")
|
||||||
// TODO: why be confusing like this? just let the user do it with the netty parameter instead!
|
// TODO: why be confusing like this? just let the user do it with the netty parameter instead!
|
||||||
|
@ -158,25 +186,25 @@ public class NettyHttpServerTransport extends AbstractLifecycleComponent<HttpSer
|
||||||
if (settings.getAsBoolean("netty.epollBugWorkaround", false)) {
|
if (settings.getAsBoolean("netty.epollBugWorkaround", false)) {
|
||||||
System.setProperty("org.jboss.netty.epollBugWorkaround", "true");
|
System.setProperty("org.jboss.netty.epollBugWorkaround", "true");
|
||||||
}
|
}
|
||||||
ByteSizeValue maxContentLength = HttpTransportSettings.SETTING_HTTP_MAX_CONTENT_LENGTH.get(settings);
|
ByteSizeValue maxContentLength = SETTING_HTTP_MAX_CONTENT_LENGTH.get(settings);
|
||||||
this.maxChunkSize = HttpTransportSettings.SETTING_HTTP_MAX_CHUNK_SIZE.get(settings);
|
this.maxChunkSize = SETTING_HTTP_MAX_CHUNK_SIZE.get(settings);
|
||||||
this.maxHeaderSize = HttpTransportSettings.SETTING_HTTP_MAX_HEADER_SIZE.get(settings);
|
this.maxHeaderSize = SETTING_HTTP_MAX_HEADER_SIZE.get(settings);
|
||||||
this.maxInitialLineLength = HttpTransportSettings.SETTING_HTTP_MAX_INITIAL_LINE_LENGTH.get(settings);
|
this.maxInitialLineLength = SETTING_HTTP_MAX_INITIAL_LINE_LENGTH.get(settings);
|
||||||
this.resetCookies = HttpTransportSettings.SETTING_HTTP_RESET_COOKIES.get(settings);
|
this.resetCookies = SETTING_HTTP_RESET_COOKIES.get(settings);
|
||||||
this.maxCumulationBufferCapacity = settings.getAsBytesSize("http.netty.max_cumulation_buffer_capacity", null);
|
this.maxCumulationBufferCapacity = settings.getAsBytesSize("http.netty.max_cumulation_buffer_capacity", null);
|
||||||
this.maxCompositeBufferComponents = settings.getAsInt("http.netty.max_composite_buffer_components", -1);
|
this.maxCompositeBufferComponents = settings.getAsInt("http.netty.max_composite_buffer_components", -1);
|
||||||
this.workerCount = settings.getAsInt("http.netty.worker_count", EsExecutors.boundedNumberOfProcessors(settings) * 2);
|
this.workerCount = settings.getAsInt("http.netty.worker_count", EsExecutors.boundedNumberOfProcessors(settings) * 2);
|
||||||
this.blockingServer = settings.getAsBoolean("http.netty.http.blocking_server", TCP_BLOCKING.get(settings));
|
this.blockingServer = settings.getAsBoolean("http.netty.http.blocking_server", TCP_BLOCKING.get(settings));
|
||||||
this.port = HttpTransportSettings.SETTING_HTTP_PORT.get(settings);
|
this.port = SETTING_HTTP_PORT.get(settings);
|
||||||
this.bindHosts = settings.getAsArray("http.netty.bind_host", settings.getAsArray("http.bind_host", settings.getAsArray("http.host", null)));
|
this.bindHosts = settings.getAsArray("http.netty.bind_host", settings.getAsArray("http.bind_host", settings.getAsArray("http.host", null)));
|
||||||
this.publishHosts = settings.getAsArray("http.netty.publish_host", settings.getAsArray("http.publish_host", settings.getAsArray("http.host", null)));
|
this.publishHosts = settings.getAsArray("http.netty.publish_host", settings.getAsArray("http.publish_host", settings.getAsArray("http.host", null)));
|
||||||
this.publishPort = HttpTransportSettings.SETTING_HTTP_PUBLISH_PORT.get(settings);
|
this.publishPort = SETTING_HTTP_PUBLISH_PORT.get(settings);
|
||||||
this.tcpNoDelay = settings.getAsBoolean("http.netty.tcp_no_delay", TCP_NO_DELAY.get(settings));
|
this.tcpNoDelay = settings.getAsBoolean("http.netty.tcp_no_delay", TCP_NO_DELAY.get(settings));
|
||||||
this.tcpKeepAlive = settings.getAsBoolean("http.netty.tcp_keep_alive", TCP_KEEP_ALIVE.get(settings));
|
this.tcpKeepAlive = settings.getAsBoolean("http.netty.tcp_keep_alive", TCP_KEEP_ALIVE.get(settings));
|
||||||
this.reuseAddress = settings.getAsBoolean("http.netty.reuse_address", TCP_REUSE_ADDRESS.get(settings));
|
this.reuseAddress = settings.getAsBoolean("http.netty.reuse_address", TCP_REUSE_ADDRESS.get(settings));
|
||||||
this.tcpSendBufferSize = settings.getAsBytesSize("http.netty.tcp_send_buffer_size", TCP_SEND_BUFFER_SIZE.get(settings));
|
this.tcpSendBufferSize = settings.getAsBytesSize("http.netty.tcp_send_buffer_size", TCP_SEND_BUFFER_SIZE.get(settings));
|
||||||
this.tcpReceiveBufferSize = settings.getAsBytesSize("http.netty.tcp_receive_buffer_size", TCP_RECEIVE_BUFFER_SIZE.get(settings));
|
this.tcpReceiveBufferSize = settings.getAsBytesSize("http.netty.tcp_receive_buffer_size", TCP_RECEIVE_BUFFER_SIZE.get(settings));
|
||||||
this.detailedErrorsEnabled = HttpTransportSettings.SETTING_HTTP_DETAILED_ERRORS_ENABLED.get(settings);
|
this.detailedErrorsEnabled = SETTING_HTTP_DETAILED_ERRORS_ENABLED.get(settings);
|
||||||
|
|
||||||
long defaultReceiverPredictor = 512 * 1024;
|
long defaultReceiverPredictor = 512 * 1024;
|
||||||
if (JvmInfo.jvmInfo().getMem().getDirectMemoryMax().bytes() > 0) {
|
if (JvmInfo.jvmInfo().getMem().getDirectMemoryMax().bytes() > 0) {
|
||||||
|
@ -194,10 +222,11 @@ public class NettyHttpServerTransport extends AbstractLifecycleComponent<HttpSer
|
||||||
receiveBufferSizePredictorFactory = new AdaptiveReceiveBufferSizePredictorFactory((int) receivePredictorMin.bytes(), (int) receivePredictorMin.bytes(), (int) receivePredictorMax.bytes());
|
receiveBufferSizePredictorFactory = new AdaptiveReceiveBufferSizePredictorFactory((int) receivePredictorMin.bytes(), (int) receivePredictorMin.bytes(), (int) receivePredictorMax.bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.compression = HttpTransportSettings.SETTING_HTTP_COMPRESSION.get(settings);
|
this.compression = SETTING_HTTP_COMPRESSION.get(settings);
|
||||||
this.compressionLevel = HttpTransportSettings.SETTING_HTTP_COMPRESSION_LEVEL.get(settings);
|
this.compressionLevel = SETTING_HTTP_COMPRESSION_LEVEL.get(settings);
|
||||||
this.pipelining = HttpTransportSettings.SETTING_PIPELINING.get(settings);
|
this.pipelining = SETTING_PIPELINING.get(settings);
|
||||||
this.pipeliningMaxEvents = HttpTransportSettings.SETTING_PIPELINING_MAX_EVENTS.get(settings);
|
this.pipeliningMaxEvents = SETTING_PIPELINING_MAX_EVENTS.get(settings);
|
||||||
|
this.corsConfig = buildCorsConfig(settings);
|
||||||
|
|
||||||
// validate max content length
|
// validate max content length
|
||||||
if (maxContentLength.bytes() > Integer.MAX_VALUE) {
|
if (maxContentLength.bytes() > Integer.MAX_VALUE) {
|
||||||
|
@ -290,6 +319,39 @@ public class NettyHttpServerTransport extends AbstractLifecycleComponent<HttpSer
|
||||||
this.boundAddress = new BoundTransportAddress(boundAddresses.toArray(new TransportAddress[boundAddresses.size()]), new InetSocketTransportAddress(publishAddress));
|
this.boundAddress = new BoundTransportAddress(boundAddresses.toArray(new TransportAddress[boundAddresses.size()]), new InetSocketTransportAddress(publishAddress));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CorsConfig buildCorsConfig(Settings settings) {
|
||||||
|
if (SETTING_CORS_ENABLED.get(settings) == false) {
|
||||||
|
return CorsConfigBuilder.forOrigins().disable().build();
|
||||||
|
}
|
||||||
|
String origin = SETTING_CORS_ALLOW_ORIGIN.get(settings);
|
||||||
|
final CorsConfigBuilder builder;
|
||||||
|
if (Strings.isNullOrEmpty(origin)) {
|
||||||
|
builder = CorsConfigBuilder.forOrigins();
|
||||||
|
} else if (origin.equals(ANY_ORIGIN)) {
|
||||||
|
builder = CorsConfigBuilder.forAnyOrigin();
|
||||||
|
} else {
|
||||||
|
Pattern p = RestUtils.checkCorsSettingForRegex(origin);
|
||||||
|
if (p == null) {
|
||||||
|
builder = CorsConfigBuilder.forOrigins(RestUtils.corsSettingAsArray(origin));
|
||||||
|
} else {
|
||||||
|
builder = CorsConfigBuilder.forPattern(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (SETTING_CORS_ALLOW_CREDENTIALS.get(settings)) {
|
||||||
|
builder.allowCredentials();
|
||||||
|
}
|
||||||
|
String[] strMethods = settings.getAsArray(SETTING_CORS_ALLOW_METHODS.get(settings), new String[0]);
|
||||||
|
HttpMethod[] methods = Arrays.asList(strMethods)
|
||||||
|
.stream()
|
||||||
|
.map(HttpMethod::valueOf)
|
||||||
|
.toArray(size -> new HttpMethod[size]);
|
||||||
|
return builder.allowedRequestMethods(methods)
|
||||||
|
.maxAge(SETTING_CORS_MAX_AGE.get(settings))
|
||||||
|
.allowedRequestHeaders(settings.getAsArray(SETTING_CORS_ALLOW_HEADERS.get(settings), new String[0]))
|
||||||
|
.shortCircuit()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
private InetSocketTransportAddress bindAddress(final InetAddress hostAddress) {
|
private InetSocketTransportAddress bindAddress(final InetAddress hostAddress) {
|
||||||
final AtomicReference<Exception> lastException = new AtomicReference<>();
|
final AtomicReference<Exception> lastException = new AtomicReference<>();
|
||||||
final AtomicReference<InetSocketAddress> boundSocket = new AtomicReference<>();
|
final AtomicReference<InetSocketAddress> boundSocket = new AtomicReference<>();
|
||||||
|
@ -365,6 +427,10 @@ public class NettyHttpServerTransport extends AbstractLifecycleComponent<HttpSer
|
||||||
return new HttpStats(channels == null ? 0 : channels.numberOfOpenChannels(), channels == null ? 0 : channels.totalChannels());
|
return new HttpStats(channels == null ? 0 : channels.numberOfOpenChannels(), channels == null ? 0 : channels.totalChannels());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CorsConfig getCorsConfig() {
|
||||||
|
return corsConfig;
|
||||||
|
}
|
||||||
|
|
||||||
protected void dispatchRequest(HttpRequest request, HttpChannel channel) {
|
protected void dispatchRequest(HttpRequest request, HttpChannel channel) {
|
||||||
httpServerAdapter.dispatchRequest(request, channel, threadPool.getThreadContext());
|
httpServerAdapter.dispatchRequest(request, channel, threadPool.getThreadContext());
|
||||||
}
|
}
|
||||||
|
@ -430,6 +496,9 @@ public class NettyHttpServerTransport extends AbstractLifecycleComponent<HttpSer
|
||||||
httpChunkAggregator.setMaxCumulationBufferComponents(transport.maxCompositeBufferComponents);
|
httpChunkAggregator.setMaxCumulationBufferComponents(transport.maxCompositeBufferComponents);
|
||||||
}
|
}
|
||||||
pipeline.addLast("aggregator", httpChunkAggregator);
|
pipeline.addLast("aggregator", httpChunkAggregator);
|
||||||
|
if (SETTING_CORS_ENABLED.get(transport.settings())) {
|
||||||
|
pipeline.addLast("cors", new CorsHandler(transport.getCorsConfig()));
|
||||||
|
}
|
||||||
pipeline.addLast("encoder", new ESHttpResponseEncoder());
|
pipeline.addLast("encoder", new ESHttpResponseEncoder());
|
||||||
if (transport.compression) {
|
if (transport.compression) {
|
||||||
pipeline.addLast("encoder_compress", new HttpContentCompressor(transport.compressionLevel));
|
pipeline.addLast("encoder_compress", new HttpContentCompressor(transport.compressionLevel));
|
||||||
|
|
|
@ -0,0 +1,233 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch 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 org.elasticsearch.http.netty.cors;
|
||||||
|
|
||||||
|
import org.jboss.netty.handler.codec.http.DefaultHttpHeaders;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpHeaders;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpMethod;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for Cross-Origin Resource Sharing (CORS).
|
||||||
|
*
|
||||||
|
* This class was lifted from the Netty project:
|
||||||
|
* https://github.com/netty/netty
|
||||||
|
*/
|
||||||
|
public final class CorsConfig {
|
||||||
|
|
||||||
|
private final Optional<Set<String>> origins;
|
||||||
|
private final Optional<Pattern> pattern;
|
||||||
|
private final boolean anyOrigin;
|
||||||
|
private final boolean enabled;
|
||||||
|
private final boolean allowCredentials;
|
||||||
|
private final long maxAge;
|
||||||
|
private final Set<HttpMethod> allowedRequestMethods;
|
||||||
|
private final Set<String> allowedRequestHeaders;
|
||||||
|
private final boolean allowNullOrigin;
|
||||||
|
private final Map<CharSequence, Callable<?>> preflightHeaders;
|
||||||
|
private final boolean shortCircuit;
|
||||||
|
|
||||||
|
CorsConfig(final CorsConfigBuilder builder) {
|
||||||
|
origins = builder.origins.map(s -> new LinkedHashSet<>(s));
|
||||||
|
pattern = builder.pattern;
|
||||||
|
anyOrigin = builder.anyOrigin;
|
||||||
|
enabled = builder.enabled;
|
||||||
|
allowCredentials = builder.allowCredentials;
|
||||||
|
maxAge = builder.maxAge;
|
||||||
|
allowedRequestMethods = builder.requestMethods;
|
||||||
|
allowedRequestHeaders = builder.requestHeaders;
|
||||||
|
allowNullOrigin = builder.allowNullOrigin;
|
||||||
|
preflightHeaders = builder.preflightHeaders;
|
||||||
|
shortCircuit = builder.shortCircuit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if support for CORS is enabled.
|
||||||
|
*
|
||||||
|
* @return {@code true} if support for CORS is enabled, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isCorsSupportEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a wildcard origin, '*', is supported.
|
||||||
|
*
|
||||||
|
* @return {@code boolean} true if any origin is allowed.
|
||||||
|
*/
|
||||||
|
public boolean isAnyOriginSupported() {
|
||||||
|
return anyOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the set of allowed origins.
|
||||||
|
*
|
||||||
|
* @return {@code Set} the allowed origins.
|
||||||
|
*/
|
||||||
|
public Optional<Set<String>> origins() {
|
||||||
|
return origins;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the input origin is allowed by this configuration.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the origin is allowed, otherwise {@code false}
|
||||||
|
*/
|
||||||
|
public boolean isOriginAllowed(final String origin) {
|
||||||
|
if (origins.isPresent()) {
|
||||||
|
return origins.get().contains(origin);
|
||||||
|
} else if (pattern.isPresent()) {
|
||||||
|
return pattern.get().matcher(origin).matches();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Please note that cookie support needs to be enabled on the client side as well.
|
||||||
|
* The client needs to opt-in to send cookies by calling:
|
||||||
|
* <pre>
|
||||||
|
* xhr.withCredentials = true;
|
||||||
|
* </pre>
|
||||||
|
* The default value for 'withCredentials' is false in which case no cookies are sent.
|
||||||
|
* Settning this to true will included cookies in cross origin requests.
|
||||||
|
*
|
||||||
|
* @return {@code true} if cookies are supported.
|
||||||
|
*/
|
||||||
|
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} of {@link HttpMethod}s 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<String>} of strings that represent the allowed Request Headers.
|
||||||
|
*/
|
||||||
|
public Set<String> allowedRequestHeaders() {
|
||||||
|
return Collections.unmodifiableSet(allowedRequestHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP response headers that should be added to a CORS preflight response.
|
||||||
|
*
|
||||||
|
* @return {@link HttpHeaders} the HTTP response headers to be added.
|
||||||
|
*/
|
||||||
|
public HttpHeaders preflightResponseHeaders() {
|
||||||
|
if (preflightHeaders.isEmpty()) {
|
||||||
|
return HttpHeaders.EMPTY_HEADERS;
|
||||||
|
}
|
||||||
|
final HttpHeaders preflightHeaders = new DefaultHttpHeaders();
|
||||||
|
for (Map.Entry<CharSequence, Callable<?>> entry : this.preflightHeaders.entrySet()) {
|
||||||
|
final Object value = getValue(entry.getValue());
|
||||||
|
if (value instanceof Iterable) {
|
||||||
|
preflightHeaders.add(entry.getKey().toString(), (Iterable<?>) value);
|
||||||
|
} else {
|
||||||
|
preflightHeaders.add(entry.getKey().toString(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return preflightHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a CORS request should be rejected if it's invalid before being
|
||||||
|
* further processing.
|
||||||
|
*
|
||||||
|
* CORS headers are set after a request is processed. This may not always be desired
|
||||||
|
* and this setting will check that the Origin is valid and if it is not valid no
|
||||||
|
* further processing will take place, and a error will be returned to the calling client.
|
||||||
|
*
|
||||||
|
* @return {@code true} if a CORS request should short-curcuit upon receiving an invalid Origin header.
|
||||||
|
*/
|
||||||
|
public boolean isShortCircuit() {
|
||||||
|
return shortCircuit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T getValue(final Callable<T> callable) {
|
||||||
|
try {
|
||||||
|
return callable.call();
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new IllegalStateException("Could not generate value for callable [" + callable + ']', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "CorsConfig[enabled=" + enabled +
|
||||||
|
", origins=" + origins +
|
||||||
|
", anyOrigin=" + anyOrigin +
|
||||||
|
", isCredentialsAllowed=" + allowCredentials +
|
||||||
|
", maxAge=" + maxAge +
|
||||||
|
", allowedRequestMethods=" + allowedRequestMethods +
|
||||||
|
", allowedRequestHeaders=" + allowedRequestHeaders +
|
||||||
|
", preflightHeaders=" + preflightHeaders + ']';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,356 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch 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 org.elasticsearch.http.netty.cors;
|
||||||
|
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpMethod;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder used to configure and build a {@link CorsConfig} instance.
|
||||||
|
*
|
||||||
|
* This class was lifted from the Netty project:
|
||||||
|
* https://github.com/netty/netty
|
||||||
|
*/
|
||||||
|
public final class CorsConfigBuilder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Builder instance with it's origin set to '*'.
|
||||||
|
*
|
||||||
|
* @return Builder to support method chaining.
|
||||||
|
*/
|
||||||
|
public static CorsConfigBuilder forAnyOrigin() {
|
||||||
|
return new CorsConfigBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link CorsConfigBuilder} instance with the specified origin.
|
||||||
|
*
|
||||||
|
* @return {@link CorsConfigBuilder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public static CorsConfigBuilder forOrigin(final String origin) {
|
||||||
|
if ("*".equals(origin)) {
|
||||||
|
return new CorsConfigBuilder();
|
||||||
|
}
|
||||||
|
return new CorsConfigBuilder(origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a {@link CorsConfigBuilder} instance with the specified pattern origin.
|
||||||
|
*
|
||||||
|
* @param pattern the regular expression pattern to match incoming origins on.
|
||||||
|
* @return {@link CorsConfigBuilder} with the configured origin pattern.
|
||||||
|
*/
|
||||||
|
public static CorsConfigBuilder forPattern(final Pattern pattern) {
|
||||||
|
if (pattern == null) {
|
||||||
|
throw new IllegalArgumentException("CORS pattern cannot be null");
|
||||||
|
}
|
||||||
|
return new CorsConfigBuilder(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link CorsConfigBuilder} instance with the specified origins.
|
||||||
|
*
|
||||||
|
* @return {@link CorsConfigBuilder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public static CorsConfigBuilder forOrigins(final String... origins) {
|
||||||
|
return new CorsConfigBuilder(origins);
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<Set<String>> origins;
|
||||||
|
Optional<Pattern> pattern;
|
||||||
|
final boolean anyOrigin;
|
||||||
|
boolean allowNullOrigin;
|
||||||
|
boolean enabled = true;
|
||||||
|
boolean allowCredentials;
|
||||||
|
long maxAge;
|
||||||
|
final Set<HttpMethod> requestMethods = new HashSet<>();
|
||||||
|
final Set<String> requestHeaders = new HashSet<>();
|
||||||
|
final Map<CharSequence, Callable<?>> preflightHeaders = new HashMap<>();
|
||||||
|
private boolean noPreflightHeaders;
|
||||||
|
boolean shortCircuit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Builder instance with the origin passed in.
|
||||||
|
*
|
||||||
|
* @param origins the origin to be used for this builder.
|
||||||
|
*/
|
||||||
|
CorsConfigBuilder(final String... origins) {
|
||||||
|
this.origins = Optional.of(new LinkedHashSet<>(Arrays.asList(origins)));
|
||||||
|
pattern = Optional.empty();
|
||||||
|
anyOrigin = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Builder instance allowing any origin, "*" which is the
|
||||||
|
* wildcard origin.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
CorsConfigBuilder() {
|
||||||
|
anyOrigin = true;
|
||||||
|
origins = Optional.empty();
|
||||||
|
pattern = Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Builder instance allowing any origin that matches the pattern.
|
||||||
|
*
|
||||||
|
* @param pattern the pattern to match against for incoming origins.
|
||||||
|
*/
|
||||||
|
CorsConfigBuilder(final Pattern pattern) {
|
||||||
|
this.pattern = Optional.of(pattern);
|
||||||
|
origins = Optional.empty();
|
||||||
|
anyOrigin = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web browsers may set the 'Origin' request header to 'null' if a resource is loaded
|
||||||
|
* from the local file system. Calling this method will enable a successful CORS response
|
||||||
|
* with a wildcard for the the CORS response header 'Access-Control-Allow-Origin'.
|
||||||
|
*
|
||||||
|
* @return {@link CorsConfigBuilder} to support method chaining.
|
||||||
|
*/
|
||||||
|
CorsConfigBuilder allowNullOrigin() {
|
||||||
|
allowNullOrigin = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables CORS support.
|
||||||
|
*
|
||||||
|
* @return {@link CorsConfigBuilder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public CorsConfigBuilder disable() {
|
||||||
|
enabled = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default cookies are not included in CORS requests, but this method will enable cookies to
|
||||||
|
* be added to CORS requests. Calling this method will set the CORS 'Access-Control-Allow-Credentials'
|
||||||
|
* response header to true.
|
||||||
|
*
|
||||||
|
* Please note, that cookie support needs to be enabled on the client side as well.
|
||||||
|
* The client needs to opt-in to send cookies by calling:
|
||||||
|
* <pre>
|
||||||
|
* xhr.withCredentials = true;
|
||||||
|
* </pre>
|
||||||
|
* The default value for 'withCredentials' is false in which case no cookies are sent.
|
||||||
|
* Setting this to true will included cookies in cross origin requests.
|
||||||
|
*
|
||||||
|
* @return {@link CorsConfigBuilder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public CorsConfigBuilder allowCredentials() {
|
||||||
|
allowCredentials = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When making a preflight request the client has to perform two request with can be inefficient.
|
||||||
|
* This setting will set the CORS 'Access-Control-Max-Age' response header and enables the
|
||||||
|
* caching of the preflight response for the specified time. During this time no preflight
|
||||||
|
* request will be made.
|
||||||
|
*
|
||||||
|
* @param max the maximum time, in seconds, that the preflight response may be cached.
|
||||||
|
* @return {@link CorsConfigBuilder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public CorsConfigBuilder maxAge(final long max) {
|
||||||
|
maxAge = max;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the allowed set of HTTP Request Methods that should be returned in the
|
||||||
|
* CORS 'Access-Control-Request-Method' response header.
|
||||||
|
*
|
||||||
|
* @param methods the {@link HttpMethod}s that should be allowed.
|
||||||
|
* @return {@link CorsConfigBuilder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public CorsConfigBuilder allowedRequestMethods(final HttpMethod... methods) {
|
||||||
|
requestMethods.addAll(Arrays.asList(methods));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the if headers that should be returned in the CORS 'Access-Control-Allow-Headers'
|
||||||
|
* response header.
|
||||||
|
*
|
||||||
|
* If a client specifies headers on the request, for example by calling:
|
||||||
|
* <pre>
|
||||||
|
* xhr.setRequestHeader('My-Custom-Header', "SomeValue");
|
||||||
|
* </pre>
|
||||||
|
* the server will receive the above header name in the 'Access-Control-Request-Headers' of the
|
||||||
|
* preflight request. The server will then decide if it allows this header to be sent for the
|
||||||
|
* real request (remember that a preflight is not the real request but a request asking the server
|
||||||
|
* if it allow a request).
|
||||||
|
*
|
||||||
|
* @param headers the headers to be added to the preflight 'Access-Control-Allow-Headers' response header.
|
||||||
|
* @return {@link CorsConfigBuilder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public CorsConfigBuilder allowedRequestHeaders(final String... headers) {
|
||||||
|
requestHeaders.addAll(Arrays.asList(headers));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP response headers that should be added to a CORS preflight response.
|
||||||
|
*
|
||||||
|
* An intermediary like a load balancer might require that a CORS preflight request
|
||||||
|
* have certain headers set. This enables such headers to be added.
|
||||||
|
*
|
||||||
|
* @param name the name of the HTTP header.
|
||||||
|
* @param values the values for the HTTP header.
|
||||||
|
* @return {@link CorsConfigBuilder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public CorsConfigBuilder preflightResponseHeader(final CharSequence name, final Object... values) {
|
||||||
|
if (values.length == 1) {
|
||||||
|
preflightHeaders.put(name, new ConstantValueGenerator(values[0]));
|
||||||
|
} else {
|
||||||
|
preflightResponseHeader(name, Arrays.asList(values));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP response headers that should be added to a CORS preflight response.
|
||||||
|
*
|
||||||
|
* An intermediary like a load balancer might require that a CORS preflight request
|
||||||
|
* have certain headers set. This enables such headers to be added.
|
||||||
|
*
|
||||||
|
* @param name the name of the HTTP header.
|
||||||
|
* @param value the values for the HTTP header.
|
||||||
|
* @param <T> the type of values that the Iterable contains.
|
||||||
|
* @return {@link CorsConfigBuilder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public <T> CorsConfigBuilder preflightResponseHeader(final CharSequence name, final Iterable<T> value) {
|
||||||
|
preflightHeaders.put(name, new ConstantValueGenerator(value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP response headers that should be added to a CORS preflight response.
|
||||||
|
*
|
||||||
|
* An intermediary like a load balancer might require that a CORS preflight request
|
||||||
|
* have certain headers set. This enables such headers to be added.
|
||||||
|
*
|
||||||
|
* Some values must be dynamically created when the HTTP response is created, for
|
||||||
|
* example the 'Date' response header. This can be accomplished by using a Callable
|
||||||
|
* which will have its 'call' method invoked when the HTTP response is created.
|
||||||
|
*
|
||||||
|
* @param name the name of the HTTP header.
|
||||||
|
* @param valueGenerator a Callable which will be invoked at HTTP response creation.
|
||||||
|
* @param <T> the type of the value that the Callable can return.
|
||||||
|
* @return {@link CorsConfigBuilder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public <T> CorsConfigBuilder preflightResponseHeader(final CharSequence name, final Callable<T> valueGenerator) {
|
||||||
|
preflightHeaders.put(name, valueGenerator);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies that no preflight response headers should be added to a preflight response.
|
||||||
|
*
|
||||||
|
* @return {@link CorsConfigBuilder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public CorsConfigBuilder noPreflightResponseHeaders() {
|
||||||
|
noPreflightHeaders = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies that a CORS request should be rejected if it's invalid before being
|
||||||
|
* further processing.
|
||||||
|
*
|
||||||
|
* CORS headers are set after a request is processed. This may not always be desired
|
||||||
|
* and this setting will check that the Origin is valid and if it is not valid no
|
||||||
|
* further processing will take place, and a error will be returned to the calling client.
|
||||||
|
*
|
||||||
|
* @return {@link CorsConfigBuilder} to support method chaining.
|
||||||
|
*/
|
||||||
|
public CorsConfigBuilder shortCircuit() {
|
||||||
|
shortCircuit = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@link CorsConfig} with settings specified by previous method calls.
|
||||||
|
*
|
||||||
|
* @return {@link CorsConfig} the configured CorsConfig instance.
|
||||||
|
*/
|
||||||
|
public CorsConfig build() {
|
||||||
|
if (preflightHeaders.isEmpty() && !noPreflightHeaders) {
|
||||||
|
preflightHeaders.put("date", DateValueGenerator.INSTANCE);
|
||||||
|
preflightHeaders.put("content-length", new ConstantValueGenerator("0"));
|
||||||
|
}
|
||||||
|
return new CorsConfig(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used for preflight HTTP response values that do not need to be
|
||||||
|
* generated, but instead the value is "static" in that the same value will be returned
|
||||||
|
* for each call.
|
||||||
|
*/
|
||||||
|
private static final class ConstantValueGenerator implements Callable<Object> {
|
||||||
|
|
||||||
|
private final Object value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sole constructor.
|
||||||
|
*
|
||||||
|
* @param value the value that will be returned when the call method is invoked.
|
||||||
|
*/
|
||||||
|
private ConstantValueGenerator(final Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
throw new IllegalArgumentException("value must not be null");
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object call() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This callable is used for the DATE preflight HTTP response HTTP header.
|
||||||
|
* It's value must be generated when the response is generated, hence will be
|
||||||
|
* different for every call.
|
||||||
|
*/
|
||||||
|
private static final class DateValueGenerator implements Callable<Date> {
|
||||||
|
|
||||||
|
static final DateValueGenerator INSTANCE = new DateValueGenerator();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date call() throws Exception {
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch 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 org.elasticsearch.http.netty.cors;
|
||||||
|
|
||||||
|
import org.elasticsearch.common.Strings;
|
||||||
|
import org.elasticsearch.rest.support.RestUtils;
|
||||||
|
import org.jboss.netty.channel.ChannelFutureListener;
|
||||||
|
import org.jboss.netty.channel.ChannelHandlerContext;
|
||||||
|
import org.jboss.netty.channel.MessageEvent;
|
||||||
|
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
|
||||||
|
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpHeaders;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpMethod;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpRequest;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpResponse;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_CREDENTIALS;
|
||||||
|
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_HEADERS;
|
||||||
|
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_METHODS;
|
||||||
|
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN;
|
||||||
|
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_MAX_AGE;
|
||||||
|
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ORIGIN;
|
||||||
|
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.USER_AGENT;
|
||||||
|
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.VARY;
|
||||||
|
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
|
||||||
|
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.OK;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 CorsConfig}, please
|
||||||
|
* refer to this class for details about the configuration options available.
|
||||||
|
*
|
||||||
|
* This code was borrowed from Netty 4 and refactored to work for Elasticsearch's Netty 3 setup.
|
||||||
|
*/
|
||||||
|
public class CorsHandler extends SimpleChannelUpstreamHandler {
|
||||||
|
|
||||||
|
public static final String ANY_ORIGIN = "*";
|
||||||
|
private final CorsConfig config;
|
||||||
|
|
||||||
|
private HttpRequest request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance with the specified {@link CorsConfig}.
|
||||||
|
*/
|
||||||
|
public CorsHandler(final CorsConfig config) {
|
||||||
|
if (config == null) {
|
||||||
|
throw new IllegalArgumentException("Config cannot be null");
|
||||||
|
}
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e) throws Exception {
|
||||||
|
if (config.isCorsSupportEnabled() && e.getMessage() instanceof HttpRequest) {
|
||||||
|
request = (HttpRequest) e.getMessage();
|
||||||
|
if (RestUtils.isBrowser(request.headers().get(USER_AGENT))) {
|
||||||
|
if (isPreflightRequest(request)) {
|
||||||
|
handlePreflight(ctx, request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (config.isShortCircuit() && !validateOrigin()) {
|
||||||
|
forbidden(ctx, request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.messageReceived(ctx, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setCorsResponseHeaders(HttpRequest request, HttpResponse resp, CorsConfig config) {
|
||||||
|
if (!config.isCorsSupportEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String originHeader = request.headers().get(ORIGIN);
|
||||||
|
if (!Strings.isNullOrEmpty(originHeader)) {
|
||||||
|
final String originHeaderVal;
|
||||||
|
if (config.isAnyOriginSupported()) {
|
||||||
|
originHeaderVal = ANY_ORIGIN;
|
||||||
|
} else if (config.isOriginAllowed(originHeader)) {
|
||||||
|
originHeaderVal = originHeader;
|
||||||
|
} else {
|
||||||
|
originHeaderVal = null;
|
||||||
|
}
|
||||||
|
if (originHeaderVal != null) {
|
||||||
|
resp.headers().add(ACCESS_CONTROL_ALLOW_ORIGIN, originHeaderVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.isCredentialsAllowed()) {
|
||||||
|
resp.headers().add(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
setPreflightHeaders(response);
|
||||||
|
ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
|
||||||
|
} else {
|
||||||
|
forbidden(ctx, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void forbidden(final ChannelHandlerContext ctx, final HttpRequest request) {
|
||||||
|
ctx.getChannel().write(new DefaultHttpResponse(request.getProtocolVersion(), FORBIDDEN))
|
||||||
|
.addListener(ChannelFutureListener.CLOSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a non CORS specification feature which enables the setting of preflight
|
||||||
|
* response headers that might be required by intermediaries.
|
||||||
|
*
|
||||||
|
* @param response the HttpResponse to which the preflight response headers should be added.
|
||||||
|
*/
|
||||||
|
private void setPreflightHeaders(final HttpResponse response) {
|
||||||
|
response.headers().add(config.preflightResponseHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean setOrigin(final HttpResponse response) {
|
||||||
|
final String origin = request.headers().get(ORIGIN);
|
||||||
|
if (!Strings.isNullOrEmpty(origin)) {
|
||||||
|
if ("null".equals(origin) && config.isNullOriginAllowed()) {
|
||||||
|
setAnyOrigin(response);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (config.isAnyOriginSupported()) {
|
||||||
|
if (config.isCredentialsAllowed()) {
|
||||||
|
echoRequestOrigin(response);
|
||||||
|
setVaryHeader(response);
|
||||||
|
} else {
|
||||||
|
setAnyOrigin(response);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (config.isOriginAllowed(origin)) {
|
||||||
|
setOrigin(response, origin);
|
||||||
|
setVaryHeader(response);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateOrigin() {
|
||||||
|
if (config.isAnyOriginSupported()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String origin = request.headers().get(ORIGIN);
|
||||||
|
if (Strings.isNullOrEmpty(origin)) {
|
||||||
|
// Not a CORS request so we cannot validate it. It may be a non CORS request.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("null".equals(origin) && config.isNullOriginAllowed()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.isOriginAllowed(origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void echoRequestOrigin(final HttpResponse response) {
|
||||||
|
setOrigin(response, request.headers().get(ORIGIN));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setVaryHeader(final HttpResponse response) {
|
||||||
|
response.headers().set(VARY, ORIGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setAnyOrigin(final HttpResponse response) {
|
||||||
|
setOrigin(response, ANY_ORIGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setOrigin(final HttpResponse response, final String origin) {
|
||||||
|
response.headers().set(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAllowCredentials(final HttpResponse response) {
|
||||||
|
if (config.isCredentialsAllowed()
|
||||||
|
&& !response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN).equals(ANY_ORIGIN)) {
|
||||||
|
response.headers().set(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPreflightRequest(final HttpRequest request) {
|
||||||
|
final HttpHeaders headers = request.headers();
|
||||||
|
return request.getMethod().equals(HttpMethod.OPTIONS) &&
|
||||||
|
headers.contains(HttpHeaders.Names.ORIGIN) &&
|
||||||
|
headers.contains(HttpHeaders.Names.ACCESS_CONTROL_REQUEST_METHOD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAllowMethods(final HttpResponse response) {
|
||||||
|
response.headers().set(ACCESS_CONTROL_ALLOW_METHODS,
|
||||||
|
String.join(", ", config.allowedRequestMethods().stream()
|
||||||
|
.map(HttpMethod::getName)
|
||||||
|
.collect(Collectors.toList())).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -20,10 +20,12 @@
|
||||||
package org.elasticsearch.rest.support;
|
package org.elasticsearch.rest.support;
|
||||||
|
|
||||||
import org.elasticsearch.common.Nullable;
|
import org.elasticsearch.common.Nullable;
|
||||||
|
import org.elasticsearch.common.Strings;
|
||||||
import org.elasticsearch.common.path.PathTrie;
|
import org.elasticsearch.common.path.PathTrie;
|
||||||
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@ -238,4 +240,21 @@ public class RestUtils {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the CORS setting as an array of origins.
|
||||||
|
*
|
||||||
|
* @param corsSetting the CORS allow origin setting as configured by the user;
|
||||||
|
* should never pass null, but we check for it anyway.
|
||||||
|
* @return an array of origins if set, otherwise {@code null}.
|
||||||
|
*/
|
||||||
|
public static String[] corsSettingAsArray(String corsSetting) {
|
||||||
|
if (Strings.isNullOrEmpty(corsSetting)) {
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
return Arrays.asList(corsSetting.split(","))
|
||||||
|
.stream()
|
||||||
|
.map(String::trim)
|
||||||
|
.toArray(size -> new String[size]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.elasticsearch.common.network.NetworkService;
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.common.util.MockBigArrays;
|
import org.elasticsearch.common.util.MockBigArrays;
|
||||||
import org.elasticsearch.http.HttpTransportSettings;
|
import org.elasticsearch.http.HttpTransportSettings;
|
||||||
|
import org.elasticsearch.http.netty.cors.CorsHandler;
|
||||||
import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
|
import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
|
||||||
import org.elasticsearch.rest.RestResponse;
|
import org.elasticsearch.rest.RestResponse;
|
||||||
import org.elasticsearch.rest.RestStatus;
|
import org.elasticsearch.rest.RestStatus;
|
||||||
|
@ -51,11 +52,19 @@ import java.net.SocketAddress;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_CREDENTIALS;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_METHODS;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ENABLED;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.hamcrest.Matchers.notNullValue;
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
import static org.hamcrest.Matchers.nullValue;
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
|
||||||
public class NettyHttpChannelTests extends ESTestCase {
|
public class NettyHttpChannelTests extends ESTestCase {
|
||||||
|
|
||||||
|
private static final String ORIGIN = "remote-host";
|
||||||
|
|
||||||
private NetworkService networkService;
|
private NetworkService networkService;
|
||||||
private ThreadPool threadPool;
|
private ThreadPool threadPool;
|
||||||
private MockBigArrays bigArrays;
|
private MockBigArrays bigArrays;
|
||||||
|
@ -84,6 +93,57 @@ public class NettyHttpChannelTests extends ESTestCase {
|
||||||
Settings settings = Settings.builder()
|
Settings settings = Settings.builder()
|
||||||
.put(HttpTransportSettings.SETTING_CORS_ENABLED.getKey(), true)
|
.put(HttpTransportSettings.SETTING_CORS_ENABLED.getKey(), true)
|
||||||
.build();
|
.build();
|
||||||
|
HttpResponse response = execRequestWithCors(settings, ORIGIN);
|
||||||
|
// inspect response and validate
|
||||||
|
assertThat(response.headers().get(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN), nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testCorsEnabledWithAllowOrigins() {
|
||||||
|
final String originValue = ORIGIN;
|
||||||
|
// create a http transport with CORS enabled and allow origin configured
|
||||||
|
Settings settings = Settings.builder()
|
||||||
|
.put(SETTING_CORS_ENABLED.getKey(), true)
|
||||||
|
.put(SETTING_CORS_ALLOW_ORIGIN.getKey(), originValue)
|
||||||
|
.build();
|
||||||
|
HttpResponse response = execRequestWithCors(settings, originValue);
|
||||||
|
// inspect response and validate
|
||||||
|
assertThat(response.headers().get(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN), notNullValue());
|
||||||
|
String allowedOrigins = response.headers().get(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||||
|
assertThat(allowedOrigins, is(originValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testThatStringLiteralWorksOnMatch() {
|
||||||
|
final String originValue = ORIGIN;
|
||||||
|
Settings settings = Settings.builder()
|
||||||
|
.put(SETTING_CORS_ENABLED.getKey(), true)
|
||||||
|
.put(SETTING_CORS_ALLOW_ORIGIN.getKey(), originValue)
|
||||||
|
.put(SETTING_CORS_ALLOW_METHODS.getKey(), "get, options, post")
|
||||||
|
.put(SETTING_CORS_ALLOW_CREDENTIALS.getKey(), true)
|
||||||
|
.build();
|
||||||
|
HttpResponse response = execRequestWithCors(settings, originValue);
|
||||||
|
// inspect response and validate
|
||||||
|
assertThat(response.headers().get(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN), notNullValue());
|
||||||
|
String allowedOrigins = response.headers().get(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||||
|
assertThat(allowedOrigins, is(originValue));
|
||||||
|
assertThat(response.headers().get(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_CREDENTIALS), equalTo("true"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testThatAnyOriginWorks() {
|
||||||
|
final String originValue = CorsHandler.ANY_ORIGIN;
|
||||||
|
Settings settings = Settings.builder()
|
||||||
|
.put(SETTING_CORS_ENABLED.getKey(), true)
|
||||||
|
.put(SETTING_CORS_ALLOW_ORIGIN.getKey(), originValue)
|
||||||
|
.build();
|
||||||
|
HttpResponse response = execRequestWithCors(settings, originValue);
|
||||||
|
// inspect response and validate
|
||||||
|
assertThat(response.headers().get(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN), notNullValue());
|
||||||
|
String allowedOrigins = response.headers().get(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||||
|
assertThat(allowedOrigins, is(originValue));
|
||||||
|
assertThat(response.headers().get(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_CREDENTIALS), nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testHeadersSet() {
|
||||||
|
Settings settings = Settings.builder().build();
|
||||||
httpServerTransport = new NettyHttpServerTransport(settings, networkService, bigArrays, threadPool);
|
httpServerTransport = new NettyHttpServerTransport(settings, networkService, bigArrays, threadPool);
|
||||||
HttpRequest httpRequest = new TestHttpRequest();
|
HttpRequest httpRequest = new TestHttpRequest();
|
||||||
httpRequest.headers().add(HttpHeaders.Names.ORIGIN, "remote");
|
httpRequest.headers().add(HttpHeaders.Names.ORIGIN, "remote");
|
||||||
|
@ -93,24 +153,27 @@ public class NettyHttpChannelTests extends ESTestCase {
|
||||||
|
|
||||||
// send a response
|
// send a response
|
||||||
NettyHttpChannel channel = new NettyHttpChannel(httpServerTransport, request, null, randomBoolean());
|
NettyHttpChannel channel = new NettyHttpChannel(httpServerTransport, request, null, randomBoolean());
|
||||||
channel.sendResponse(new TestReponse());
|
TestReponse resp = new TestReponse();
|
||||||
|
final String customHeader = "custom-header";
|
||||||
|
final String customHeaderValue = "xyz";
|
||||||
|
resp.addHeader(customHeader, customHeaderValue);
|
||||||
|
channel.sendResponse(resp);
|
||||||
|
|
||||||
// inspect what was written
|
// inspect what was written
|
||||||
List<Object> writtenObjects = writeCapturingChannel.getWrittenObjects();
|
List<Object> writtenObjects = writeCapturingChannel.getWrittenObjects();
|
||||||
assertThat(writtenObjects.size(), is(1));
|
assertThat(writtenObjects.size(), is(1));
|
||||||
HttpResponse response = (HttpResponse) writtenObjects.get(0);
|
HttpResponse response = (HttpResponse) writtenObjects.get(0);
|
||||||
assertThat(response.headers().get(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN), nullValue());
|
assertThat(response.headers().get("non-existent-header"), nullValue());
|
||||||
|
assertThat(response.headers().get(customHeader), equalTo(customHeaderValue));
|
||||||
|
assertThat(response.headers().get(HttpHeaders.Names.CONTENT_LENGTH), equalTo(Integer.toString(resp.content().length())));
|
||||||
|
assertThat(response.headers().get(HttpHeaders.Names.CONTENT_TYPE), equalTo(resp.contentType()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testCorsEnabledWithAllowOrigins() {
|
private HttpResponse execRequestWithCors(final Settings settings, final String originValue) {
|
||||||
// create a http transport with CORS enabled and allow origin configured
|
// construct request and send it over the transport layer
|
||||||
Settings settings = Settings.builder()
|
|
||||||
.put(HttpTransportSettings.SETTING_CORS_ENABLED.getKey(), true)
|
|
||||||
.put(HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN.getKey(), "remote-host")
|
|
||||||
.build();
|
|
||||||
httpServerTransport = new NettyHttpServerTransport(settings, networkService, bigArrays, threadPool);
|
httpServerTransport = new NettyHttpServerTransport(settings, networkService, bigArrays, threadPool);
|
||||||
HttpRequest httpRequest = new TestHttpRequest();
|
HttpRequest httpRequest = new TestHttpRequest();
|
||||||
httpRequest.headers().add(HttpHeaders.Names.ORIGIN, "remote");
|
httpRequest.headers().add(HttpHeaders.Names.ORIGIN, ORIGIN);
|
||||||
httpRequest.headers().add(HttpHeaders.Names.USER_AGENT, "Mozilla fake");
|
httpRequest.headers().add(HttpHeaders.Names.USER_AGENT, "Mozilla fake");
|
||||||
WriteCapturingChannel writeCapturingChannel = new WriteCapturingChannel();
|
WriteCapturingChannel writeCapturingChannel = new WriteCapturingChannel();
|
||||||
NettyHttpRequest request = new NettyHttpRequest(httpRequest, writeCapturingChannel);
|
NettyHttpRequest request = new NettyHttpRequest(httpRequest, writeCapturingChannel);
|
||||||
|
@ -118,13 +181,10 @@ public class NettyHttpChannelTests extends ESTestCase {
|
||||||
NettyHttpChannel channel = new NettyHttpChannel(httpServerTransport, request, null, randomBoolean());
|
NettyHttpChannel channel = new NettyHttpChannel(httpServerTransport, request, null, randomBoolean());
|
||||||
channel.sendResponse(new TestReponse());
|
channel.sendResponse(new TestReponse());
|
||||||
|
|
||||||
// inspect what was written
|
// get the response
|
||||||
List<Object> writtenObjects = writeCapturingChannel.getWrittenObjects();
|
List<Object> writtenObjects = writeCapturingChannel.getWrittenObjects();
|
||||||
assertThat(writtenObjects.size(), is(1));
|
assertThat(writtenObjects.size(), is(1));
|
||||||
HttpResponse response = (HttpResponse) writtenObjects.get(0);
|
return (HttpResponse) writtenObjects.get(0);
|
||||||
assertThat(response.headers().get(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN), notNullValue());
|
|
||||||
String allowedOrigins = response.headers().get(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN);
|
|
||||||
assertThat(allowedOrigins, is("remote-host"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class WriteCapturingChannel implements Channel {
|
private static class WriteCapturingChannel implements Channel {
|
||||||
|
|
|
@ -16,12 +16,13 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.elasticsearch.rest;
|
package org.elasticsearch.rest;
|
||||||
|
|
||||||
import org.elasticsearch.common.network.NetworkModule;
|
import org.elasticsearch.common.network.NetworkModule;
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.node.Node;
|
|
||||||
import org.elasticsearch.test.ESIntegTestCase;
|
import org.elasticsearch.test.ESIntegTestCase;
|
||||||
|
import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
|
||||||
import org.elasticsearch.test.rest.client.http.HttpResponse;
|
import org.elasticsearch.test.rest.client.http.HttpResponse;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.hasKey;
|
import static org.hamcrest.Matchers.hasKey;
|
||||||
|
@ -31,7 +32,8 @@ import static org.hamcrest.Matchers.not;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class CorsRegexDefaultIT extends ESIntegTestCase {
|
@ClusterScope(scope = ESIntegTestCase.Scope.SUITE, numDataNodes = 1)
|
||||||
|
public class CorsNotSetIT extends ESIntegTestCase {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Settings nodeSettings(int nodeOrdinal) {
|
protected Settings nodeSettings(int nodeOrdinal) {
|
|
@ -26,8 +26,10 @@ import org.elasticsearch.test.ESIntegTestCase;
|
||||||
import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
|
import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
|
||||||
import org.elasticsearch.test.ESIntegTestCase.Scope;
|
import org.elasticsearch.test.ESIntegTestCase.Scope;
|
||||||
import org.elasticsearch.test.rest.client.http.HttpResponse;
|
import org.elasticsearch.test.rest.client.http.HttpResponse;
|
||||||
|
import org.jboss.netty.handler.codec.http.HttpHeaders;
|
||||||
|
|
||||||
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_CREDENTIALS;
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_CREDENTIALS;
|
||||||
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_METHODS;
|
||||||
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN;
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN;
|
||||||
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ENABLED;
|
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ENABLED;
|
||||||
import static org.hamcrest.Matchers.hasKey;
|
import static org.hamcrest.Matchers.hasKey;
|
||||||
|
@ -35,7 +37,7 @@ import static org.hamcrest.Matchers.is;
|
||||||
import static org.hamcrest.Matchers.not;
|
import static org.hamcrest.Matchers.not;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Test CORS where the allow origin value is a regular expression.
|
||||||
*/
|
*/
|
||||||
@ClusterScope(scope = Scope.SUITE, numDataNodes = 1)
|
@ClusterScope(scope = Scope.SUITE, numDataNodes = 1)
|
||||||
public class CorsRegexIT extends ESIntegTestCase {
|
public class CorsRegexIT extends ESIntegTestCase {
|
||||||
|
@ -48,6 +50,7 @@ public class CorsRegexIT extends ESIntegTestCase {
|
||||||
.put(super.nodeSettings(nodeOrdinal))
|
.put(super.nodeSettings(nodeOrdinal))
|
||||||
.put(SETTING_CORS_ALLOW_ORIGIN.getKey(), "/https?:\\/\\/localhost(:[0-9]+)?/")
|
.put(SETTING_CORS_ALLOW_ORIGIN.getKey(), "/https?:\\/\\/localhost(:[0-9]+)?/")
|
||||||
.put(SETTING_CORS_ALLOW_CREDENTIALS.getKey(), true)
|
.put(SETTING_CORS_ALLOW_CREDENTIALS.getKey(), true)
|
||||||
|
.put(SETTING_CORS_ALLOW_METHODS.getKey(), "get, options, post")
|
||||||
.put(SETTING_CORS_ENABLED.getKey(), true)
|
.put(SETTING_CORS_ENABLED.getKey(), true)
|
||||||
.put(NetworkModule.HTTP_ENABLED.getKey(), true)
|
.put(NetworkModule.HTTP_ENABLED.getKey(), true)
|
||||||
.build();
|
.build();
|
||||||
|
@ -65,9 +68,11 @@ public class CorsRegexIT extends ESIntegTestCase {
|
||||||
assertThat(response.getHeaders().get("Access-Control-Allow-Credentials"), is("true"));
|
assertThat(response.getHeaders().get("Access-Control-Allow-Credentials"), is("true"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testThatRegularExpressionReturnsNullOnNonMatch() throws Exception {
|
public void testThatRegularExpressionReturnsForbiddenOnNonMatch() throws Exception {
|
||||||
HttpResponse response = httpClient().method("GET").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", "http://evil-host:9200").execute();
|
HttpResponse response = httpClient().method("GET").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", "http://evil-host:9200").execute();
|
||||||
assertResponseWithOriginheader(response, "null");
|
// a rejected origin gets a FORBIDDEN - 403
|
||||||
|
assertThat(response.getStatusCode(), is(403));
|
||||||
|
assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Origin")));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testThatSendingNoOriginHeaderReturnsNoAccessControlHeader() throws Exception {
|
public void testThatSendingNoOriginHeaderReturnsNoAccessControlHeader() throws Exception {
|
||||||
|
@ -84,18 +89,33 @@ public class CorsRegexIT extends ESIntegTestCase {
|
||||||
|
|
||||||
public void testThatPreFlightRequestWorksOnMatch() throws Exception {
|
public void testThatPreFlightRequestWorksOnMatch() throws Exception {
|
||||||
String corsValue = "http://localhost:9200";
|
String corsValue = "http://localhost:9200";
|
||||||
HttpResponse response = httpClient().method("OPTIONS").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", corsValue).execute();
|
HttpResponse response = httpClient().method("OPTIONS")
|
||||||
|
.path("/")
|
||||||
|
.addHeader("User-Agent", "Mozilla Bar")
|
||||||
|
.addHeader("Origin", corsValue)
|
||||||
|
.addHeader(HttpHeaders.Names.ACCESS_CONTROL_REQUEST_METHOD, "GET")
|
||||||
|
.execute();
|
||||||
assertResponseWithOriginheader(response, corsValue);
|
assertResponseWithOriginheader(response, corsValue);
|
||||||
|
assertThat(response.getHeaders(), hasKey("Access-Control-Allow-Methods"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testThatPreFlightRequestReturnsNullOnNonMatch() throws Exception {
|
public void testThatPreFlightRequestReturnsNullOnNonMatch() throws Exception {
|
||||||
HttpResponse response = httpClient().method("OPTIONS").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", "http://evil-host:9200").execute();
|
HttpResponse response = httpClient().method("OPTIONS")
|
||||||
assertResponseWithOriginheader(response, "null");
|
.path("/")
|
||||||
|
.addHeader("User-Agent", "Mozilla Bar")
|
||||||
|
.addHeader("Origin", "http://evil-host:9200")
|
||||||
|
.addHeader(HttpHeaders.Names.ACCESS_CONTROL_REQUEST_METHOD, "GET")
|
||||||
|
.execute();
|
||||||
|
// a rejected origin gets a FORBIDDEN - 403
|
||||||
|
assertThat(response.getStatusCode(), is(403));
|
||||||
|
assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Origin")));
|
||||||
|
assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Methods")));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void assertResponseWithOriginheader(HttpResponse response, String expectedCorsHeader) {
|
protected static void assertResponseWithOriginheader(HttpResponse response, String expectedCorsHeader) {
|
||||||
assertThat(response.getStatusCode(), is(200));
|
assertThat(response.getStatusCode(), is(200));
|
||||||
assertThat(response.getHeaders(), hasKey("Access-Control-Allow-Origin"));
|
assertThat(response.getHeaders(), hasKey("Access-Control-Allow-Origin"));
|
||||||
assertThat(response.getHeaders().get("Access-Control-Allow-Origin"), is(expectedCorsHeader));
|
assertThat(response.getHeaders().get("Access-Control-Allow-Origin"), is(expectedCorsHeader));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue