1
0
Эх сурвалжийг харах

CORS: Support regular expressions for origin to match against

This commit adds regular expression support for the allow-origin
header depending on the value of the request `Origin` header.

The existing HttpRequestBuilder is also extended to support the
OPTIONS HTTP method.

Relates #5601
Closes #6891
Alexander Reelsen 11 жил өмнө
parent
commit
a1e335b1e9

+ 4 - 1
docs/reference/modules/http.asciidoc

@@ -42,7 +42,10 @@ i.e. whether a browser on another origin can do requests to
 Elasticsearch. Defaults to `true`.
 
 |`http.cors.allow-origin` |Which origins to allow. Defaults to `*`,
-i.e. any origin.
+i.e. any origin. If you prepend and append a `/` to the value, this will
+be treated as a regular expression, allowing you to support HTTP and HTTPs.
+for example using `/https?:\/\/localhost(:[0-9]+)?/` would return the
+request header appropriately in both cases.
 
 |`http.cors.max-age` |Browsers send a "preflight" OPTIONS-request to
 determine CORS settings. `max-age` defines how long the result should

+ 6 - 1
src/main/java/org/elasticsearch/http/netty/HttpRequestHandler.java

@@ -19,9 +19,12 @@
 
 package org.elasticsearch.http.netty;
 
+import org.elasticsearch.rest.support.RestUtils;
 import org.jboss.netty.channel.*;
 import org.jboss.netty.handler.codec.http.HttpRequest;
 
+import java.util.regex.Pattern;
+
 
 /**
  *
@@ -30,9 +33,11 @@ import org.jboss.netty.handler.codec.http.HttpRequest;
 public class HttpRequestHandler extends SimpleChannelUpstreamHandler {
 
     private final NettyHttpServerTransport serverTransport;
+    private final Pattern corsPattern;
 
     public HttpRequestHandler(NettyHttpServerTransport serverTransport) {
         this.serverTransport = serverTransport;
+        this.corsPattern = RestUtils.getCorsSettingRegex(serverTransport.settings());
     }
 
     @Override
@@ -41,7 +46,7 @@ public class HttpRequestHandler extends SimpleChannelUpstreamHandler {
         // the netty HTTP handling always copy over the buffer to its own buffer, either in NioWorker internally
         // when reading, or using a cumalation buffer
         NettyHttpRequest httpRequest = new NettyHttpRequest(request, e.getChannel());
-        serverTransport.dispatchRequest(httpRequest, new NettyHttpChannel(serverTransport, e.getChannel(), httpRequest));
+        serverTransport.dispatchRequest(httpRequest, new NettyHttpChannel(serverTransport, e.getChannel(), httpRequest, corsPattern));
         super.messageReceived(ctx, e);
     }
 

+ 19 - 7
src/main/java/org/elasticsearch/http/netty/NettyHttpChannel.java

@@ -19,6 +19,7 @@
 
 package org.elasticsearch.http.netty;
 
+import com.google.common.base.Strings;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.UnicodeUtil;
 import org.elasticsearch.common.bytes.BytesReference;
@@ -40,6 +41,9 @@ import org.jboss.netty.handler.codec.http.*;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Pattern;
+
+import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*;
 
 /**
  *
@@ -57,12 +61,14 @@ public class NettyHttpChannel extends HttpChannel {
     private final NettyHttpServerTransport transport;
     private final Channel channel;
     private final org.jboss.netty.handler.codec.http.HttpRequest nettyRequest;
+    private Pattern corsPattern;
 
-    public NettyHttpChannel(NettyHttpServerTransport transport, Channel channel, NettyHttpRequest request) {
+    public NettyHttpChannel(NettyHttpServerTransport transport, Channel channel, NettyHttpRequest request, Pattern corsPattern) {
         super(request);
         this.transport = transport;
         this.channel = channel;
         this.nettyRequest = request.request();
+        this.corsPattern = corsPattern;
     }
 
     @Override
@@ -90,15 +96,21 @@ public class NettyHttpChannel extends HttpChannel {
         } else {
             resp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status);
         }
-        if (RestUtils.isBrowser(nettyRequest.headers().get(HttpHeaders.Names.USER_AGENT))) {
+        if (RestUtils.isBrowser(nettyRequest.headers().get(USER_AGENT))) {
             if (transport.settings().getAsBoolean("http.cors.enabled", true)) {
-                // Add support for cross-origin Ajax requests (CORS)
-                resp.headers().add("Access-Control-Allow-Origin", transport.settings().get("http.cors.allow-origin", "*"));
+                String originHeader = request.header(ORIGIN);
+                if (!Strings.isNullOrEmpty(originHeader)) {
+                    if (corsPattern == null) {
+                        resp.headers().add(ACCESS_CONTROL_ALLOW_ORIGIN, transport.settings().get("http.cors.allow-origin", "*"));
+                    } 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", transport.settings().getAsInt("http.cors.max-age", 1728000));
-                    resp.headers().add("Access-Control-Allow-Methods", transport.settings().get("http.cors.allow-methods", "OPTIONS, HEAD, GET, POST, PUT, DELETE"));
-                    resp.headers().add("Access-Control-Allow-Headers", transport.settings().get("http.cors.allow-headers", "X-Requested-With, Content-Type, Content-Length"));
+                    resp.headers().add(ACCESS_CONTROL_MAX_AGE, transport.settings().getAsInt("http.cors.max-age", 1728000));
+                    resp.headers().add(ACCESS_CONTROL_ALLOW_METHODS, transport.settings().get("http.cors.allow-methods", "OPTIONS, HEAD, GET, POST, PUT, DELETE"));
+                    resp.headers().add(ACCESS_CONTROL_ALLOW_HEADERS, transport.settings().get("http.cors.allow-headers", "X-Requested-With, Content-Type, Content-Length"));
                 }
             }
         }

+ 18 - 0
src/main/java/org/elasticsearch/rest/support/RestUtils.java

@@ -22,9 +22,11 @@ package org.elasticsearch.rest.support;
 import com.google.common.base.Charsets;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.path.PathTrie;
+import org.elasticsearch.common.settings.Settings;
 
 import java.nio.charset.Charset;
 import java.util.Map;
+import java.util.regex.Pattern;
 
 /**
  *
@@ -37,6 +39,7 @@ public class RestUtils {
             return RestUtils.decodeComponent(value);
         }
     };
+    public static final String HTTP_CORS_ALLOW_ORIGIN_SETTING = "http.cors.allow-origin";
 
     public static boolean isBrowser(@Nullable String userAgent) {
         if (userAgent == null) {
@@ -216,4 +219,19 @@ public class RestUtils {
             return Character.MAX_VALUE;
         }
     }
+
+    /**
+     * Determine if CORS setting is a regex
+     */
+    public static Pattern getCorsSettingRegex(Settings settings) {
+        String corsSetting = settings.get(HTTP_CORS_ALLOW_ORIGIN_SETTING, "*");
+        int len = corsSetting.length();
+        boolean isRegex = len > 2 &&  corsSetting.startsWith("/") && corsSetting.endsWith("/");
+
+        if (isRegex) {
+            return Pattern.compile(corsSetting.substring(1, corsSetting.length()-1));
+        }
+
+        return null;
+    }
 }

+ 50 - 0
src/test/java/org/elasticsearch/rest/CorsRegexDefaultTests.java

@@ -0,0 +1,50 @@
+/*
+ * 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.rest;
+
+import org.elasticsearch.test.ElasticsearchIntegrationTest;
+import org.elasticsearch.test.rest.client.http.HttpResponse;
+import org.junit.Test;
+
+import static org.elasticsearch.rest.CorsRegexTests.httpClient;
+import static org.hamcrest.Matchers.*;
+
+/**
+ *
+ */
+public class CorsRegexDefaultTests extends ElasticsearchIntegrationTest {
+
+    @Test
+    public void testCorsSettingDefaultBehaviour() throws Exception {
+        String corsValue = "http://localhost:9200";
+        HttpResponse response = httpClient().method("GET").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", corsValue).execute();
+
+        assertThat(response.getStatusCode(), is(200));
+        assertThat(response.getHeaders(), hasKey("Access-Control-Allow-Origin"));
+        assertThat(response.getHeaders().get("Access-Control-Allow-Origin"), is("*"));
+    }
+
+    @Test
+    public void testThatOmittingCorsHeaderDoesNotReturnAnything() throws Exception {
+        HttpResponse response = httpClient().method("GET").path("/").execute();
+
+        assertThat(response.getStatusCode(), is(200));
+        assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Origin")));
+    }
+}

+ 111 - 0
src/test/java/org/elasticsearch/rest/CorsRegexTests.java

@@ -0,0 +1,111 @@
+/*
+ * 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.rest;
+
+import org.apache.http.impl.client.HttpClients;
+import org.elasticsearch.common.logging.ESLogger;
+import org.elasticsearch.common.logging.Loggers;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.transport.InetSocketTransportAddress;
+import org.elasticsearch.http.HttpServerTransport;
+import org.elasticsearch.test.ElasticsearchIntegrationTest;
+import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
+import org.elasticsearch.test.rest.client.http.HttpResponse;
+import org.junit.Test;
+
+import java.net.InetSocketAddress;
+
+import static org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
+import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope;
+import static org.hamcrest.Matchers.*;
+
+/**
+ *
+ */
+@ClusterScope(scope = Scope.SUITE, numDataNodes = 1)
+public class CorsRegexTests extends ElasticsearchIntegrationTest {
+
+    protected static final ESLogger logger = Loggers.getLogger(CorsRegexTests.class);
+
+    @Override
+    protected Settings nodeSettings(int nodeOrdinal) {
+        return ImmutableSettings.settingsBuilder()
+                .put("http.cors.allow-origin", "/https?:\\/\\/localhost(:[0-9]+)?/")
+                .put("network.host", "127.0.0.1")
+                .put(super.nodeSettings(nodeOrdinal))
+                .build();
+    }
+
+    @Test
+    public void testThatRegularExpressionWorksOnMatch() throws Exception {
+        String corsValue = "http://localhost:9200";
+        HttpResponse response = httpClient().method("GET").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", corsValue).execute();
+        assertResponseWithOriginheader(response, corsValue);
+
+        corsValue = "https://localhost:9200";
+        response = httpClient().method("GET").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", corsValue).execute();
+        assertResponseWithOriginheader(response, corsValue);
+    }
+
+    @Test
+    public void testThatRegularExpressionReturnsNullOnNonMatch() throws Exception {
+        HttpResponse response = httpClient().method("GET").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", "http://evil-host:9200").execute();
+        assertResponseWithOriginheader(response, "null");
+    }
+
+    @Test
+    public void testThatSendingNoOriginHeaderReturnsNoAccessControlHeader() throws Exception {
+        HttpResponse response = httpClient().method("GET").path("/").addHeader("User-Agent", "Mozilla Bar").execute();
+        assertThat(response.getStatusCode(), is(200));
+        assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Origin")));
+    }
+
+    @Test
+    public void testThatRegularExpressionIsNotAppliedWithoutCorrectBrowserOnMatch() throws Exception {
+        HttpResponse response = httpClient().method("GET").path("/").execute();
+        assertThat(response.getStatusCode(), is(200));
+        assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Origin")));
+    }
+
+    @Test
+    public void testThatPreFlightRequestWorksOnMatch() throws Exception {
+        String corsValue = "http://localhost:9200";
+        HttpResponse response = httpClient().method("OPTIONS").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", corsValue).execute();
+        assertResponseWithOriginheader(response, corsValue);
+    }
+
+    @Test
+    public void testThatPreFlightRequestReturnsNullOnNonMatch() throws Exception {
+        HttpResponse response = httpClient().method("OPTIONS").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", "http://evil-host:9200").execute();
+        assertResponseWithOriginheader(response, "null");
+    }
+
+    public static HttpRequestBuilder httpClient() {
+        HttpServerTransport httpServerTransport = internalCluster().getDataNodeInstance(HttpServerTransport.class);
+        InetSocketAddress address = ((InetSocketTransportAddress) httpServerTransport.boundAddress().publishAddress()).address();
+        return new HttpRequestBuilder(HttpClients.createDefault()).host(address.getHostName()).port(address.getPort());
+    }
+
+    public static void assertResponseWithOriginheader(HttpResponse response, String expectedCorsHeader) {
+        assertThat(response.getStatusCode(), is(200));
+        assertThat(response.getHeaders(), hasKey("Access-Control-Allow-Origin"));
+        assertThat(response.getHeaders().get("Access-Control-Allow-Origin"), is(expectedCorsHeader));
+    }
+}

+ 35 - 2
src/test/java/org/elasticsearch/rest/util/RestUtilsTests.java

@@ -19,15 +19,18 @@
 
 package org.elasticsearch.rest.util;
 
+import org.elasticsearch.common.settings.ImmutableSettings;
 import org.elasticsearch.rest.support.RestUtils;
 import org.elasticsearch.test.ElasticsearchTestCase;
 import org.junit.Test;
 
+import java.util.Locale;
 import java.util.Map;
+import java.util.regex.Pattern;
 
 import static com.google.common.collect.Maps.newHashMap;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.equalTo;
+import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder;
+import static org.hamcrest.Matchers.*;
 
 /**
  *
@@ -122,4 +125,34 @@ public class RestUtilsTests extends ElasticsearchTestCase {
         assertThat(params.get("p1"), equalTo("v1"));
     }
 
+    @Test
+    public void testCorsSettingIsARegex() {
+        assertCorsSettingRegex("/foo/", Pattern.compile("foo"));
+        assertCorsSettingRegex("/.*/", Pattern.compile(".*"));
+        assertCorsSettingRegex("/https?:\\/\\/localhost(:[0-9]+)?/", Pattern.compile("https?:\\/\\/localhost(:[0-9]+)?"));
+        assertCorsSettingRegexMatches("/https?:\\/\\/localhost(:[0-9]+)?/", true, "http://localhost:9200", "http://localhost:9215", "https://localhost:9200", "https://localhost");
+        assertCorsSettingRegexMatches("/https?:\\/\\/localhost(:[0-9]+)?/", false, "htt://localhost:9200", "http://localhost:9215/foo", "localhost:9215");
+        assertCorsSettingRegexIsNull("//");
+        assertCorsSettingRegexIsNull("/");
+        assertCorsSettingRegexIsNull("/foo");
+        assertCorsSettingRegexIsNull("foo");
+        assertCorsSettingRegexIsNull("");
+        assertThat(RestUtils.getCorsSettingRegex(ImmutableSettings.EMPTY), is(nullValue()));
+    }
+
+    private void assertCorsSettingRegexIsNull(String settingsValue) {
+        assertThat(RestUtils.getCorsSettingRegex(settingsBuilder().put("http.cors.allow-origin", settingsValue).build()), is(nullValue()));
+    }
+
+    private void assertCorsSettingRegex(String settingsValue, Pattern pattern) {
+        assertThat(RestUtils.getCorsSettingRegex(settingsBuilder().put("http.cors.allow-origin", settingsValue).build()).toString(), is(pattern.toString()));
+    }
+
+    private void assertCorsSettingRegexMatches(String settingsValue, boolean expectMatch, String ... candidates) {
+        Pattern pattern = RestUtils.getCorsSettingRegex(settingsBuilder().put("http.cors.allow-origin", settingsValue).build());
+        for (String candidate : candidates) {
+            assertThat(String.format(Locale.ROOT, "Expected pattern %s to match against %s: %s", settingsValue, candidate, expectMatch),
+                    pattern.matcher(candidate).matches(), is(expectMatch));
+        }
+    }
 }

+ 5 - 3
src/test/java/org/elasticsearch/test/rest/client/http/HttpRequestBuilder.java

@@ -20,16 +20,13 @@ package org.elasticsearch.test.rest.client.http;
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.Maps;
-import org.apache.http.client.config.RequestConfig;
 import org.apache.http.client.methods.*;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.lucene.util.IOUtils;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.logging.ESLogger;
 import org.elasticsearch.common.logging.Loggers;
 import org.elasticsearch.common.transport.InetSocketTransportAddress;
-import org.elasticsearch.common.transport.TransportAddress;
 import org.elasticsearch.http.HttpServerTransport;
 
 import java.io.IOException;
@@ -147,6 +144,11 @@ public class HttpRequestBuilder {
             return new HttpHead(buildUri());
         }
 
+        if (HttpOptions.METHOD_NAME.equalsIgnoreCase(method)) {
+            checkBodyNotSupported();
+            return new HttpOptions(buildUri());
+        }
+
         if (HttpDeleteWithEntity.METHOD_NAME.equalsIgnoreCase(method)) {
             return addOptionalBody(new HttpDeleteWithEntity(buildUri()));
         }