Basic CORS support

This commit adds a globally configurable CORS support.  Note that this differs from AWS per-bucket support.
pull/290/head
Falk Reimann 2018-12-21 23:19:50 +01:00 zatwierdzone przez Andrew Gaul
rodzic 3ba59d7370
commit e3277a4c1f
13 zmienionych plików z 892 dodań i 22 usunięć

Wyświetl plik

@ -25,6 +25,9 @@ ENV \
S3PROXY_IDENTITY="local-identity" \
S3PROXY_CREDENTIAL="local-credential" \
S3PROXY_CORS_ALLOW_ALL="false" \
S3PROXY_CORS_ALLOW_ORIGINS="" \
S3PROXY_CORS_ALLOW_METHODS="" \
S3PROXY_CORS_ALLOW_HEADERS="" \
S3PROXY_IGNORE_UNKNOWN_HEADERS="false" \
JCLOUDS_PROVIDER="filesystem" \
JCLOUDS_ENDPOINT="" \

Wyświetl plik

@ -103,7 +103,7 @@ S3Proxy has broad compatibility with the S3 API, however, it does not support:
* ACLs other than private and public-read
* BitTorrent hosting
* bucket logging
* cross-origin resource sharing, see [#142](https://github.com/gaul/s3proxy/issues/142)
* [CORS bucket operations](https://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html#how-do-i-enable-cors) like getting or setting the CORS configuration for a bucket. S3Proxy only supports a static configuration (see below).
* hosting static websites
* list objects v2, see [#168](https://github.com/gaul/s3proxy/issues/168)
* object server-side encryption
@ -117,6 +117,18 @@ S3Proxy emulates the following operations:
* copy multi-part objects, see [#76](https://github.com/gaul/s3proxy/issues/76)
S3Proxy has basic CORS preflight and actual request/response handling. It can be configured within the properties
file (and corresponding ENV variables for Docker):
```
s3proxy.cors-allow-origins=https://example\.com https://.+\.example\.com https://example\.cloud
s3proxy.cors-allow-methods=GET PUT
s3proxy.cors-allow-headers=Accept Content-Type
```
CORS cannot be configured per bucket. `s3proxy.cors-allow-all=true` will accept any origin and header.
Actual CORS requests are supported for GET, PUT and POST methods.
The wiki collects
[compatibility notes](https://github.com/gaul/s3proxy/wiki/Storage-backend-compatibility)
for specific storage backends.

Wyświetl plik

@ -280,13 +280,32 @@ final class AwsSignature {
signedHeaders = Splitter.on(';').splitToList(request.getParameter(
"X-Amz-SignedHeaders"));
}
/*
* CORS Preflight
*
* The signature is based on the canonical request, which includes the
* HTTP Method.
* For presigned URLs, the method must be replaced for OPTIONS request
* to match
*/
String method = request.getMethod();
if ("OPTIONS".equals(method)) {
String corsMethod = request.getHeader(
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
if (corsMethod != null) {
method = corsMethod;
}
}
String canonicalRequest = Joiner.on("\n").join(
request.getMethod(),
method,
uri,
buildCanonicalQueryString(request),
buildCanonicalHeaders(request, signedHeaders) + "\n",
Joiner.on(';').join(signedHeaders),
digest);
return getMessageDigest(
canonicalRequest.getBytes(StandardCharsets.UTF_8),
hashAlgorithm);

Wyświetl plik

@ -0,0 +1,163 @@
/*
* Copyright 2014-2018 Andrew Gaul <andrew@gaul.org>
*
* Licensed 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.gaul.s3proxy;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
final class CrossOriginResourceSharing {
private static final String HEADER_VALUE_SEPARATOR = ", ";
private static final String ALLOW_ANY_HEADER = "*";
private static final Logger logger = LoggerFactory.getLogger(
CrossOriginResourceSharing.class);
private final String allowedMethodsRaw;
private final String allowedHeadersRaw;
private final Set<Pattern> allowedOrigins;
private final Set<String> allowedMethods;
private final Set<String> allowedHeaders;
protected CrossOriginResourceSharing() {
// CORS Allow all
this(Lists.newArrayList(".*"), Lists.newArrayList("GET", "PUT", "POST"),
Lists.newArrayList(ALLOW_ANY_HEADER));
}
protected CrossOriginResourceSharing(Collection<String> allowedOrigins,
Collection<String> allowedMethods,
Collection<String> allowedHeaders) {
Set<Pattern> allowedPattern = new HashSet<Pattern>();
if (allowedOrigins != null) {
for (String origin : allowedOrigins) {
allowedPattern.add(Pattern.compile(
origin, Pattern.CASE_INSENSITIVE));
}
}
this.allowedOrigins = ImmutableSet.copyOf(allowedPattern);
if (allowedMethods == null) {
this.allowedMethods = ImmutableSet.of();
} else {
this.allowedMethods = ImmutableSet.copyOf(allowedMethods);
}
this.allowedMethodsRaw = Joiner.on(HEADER_VALUE_SEPARATOR).join(
this.allowedMethods);
if (allowedHeaders == null) {
this.allowedHeaders = ImmutableSet.of();
} else {
this.allowedHeaders = ImmutableSet.copyOf(allowedHeaders);
}
this.allowedHeadersRaw = Joiner.on(HEADER_VALUE_SEPARATOR).join(
this.allowedHeaders);
logger.info("CORS allowed origins: {}", allowedOrigins);
logger.info("CORS allowed methods: {}", allowedMethods);
logger.info("CORS allowed headers: {}", allowedHeaders);
}
public String getAllowedMethods() {
return this.allowedMethodsRaw;
}
public boolean isOriginAllowed(String origin) {
if (!Strings.isNullOrEmpty(origin)) {
for (Pattern pattern : this.allowedOrigins) {
Matcher matcher = pattern.matcher(origin);
if (matcher.matches()) {
logger.debug("CORS origin allowed: {}", origin);
return true;
}
}
}
logger.debug("CORS origin not allowed: {}", origin);
return false;
}
public boolean isMethodAllowed(String method) {
if (!Strings.isNullOrEmpty(method)) {
if (this.allowedMethods.contains(method)) {
logger.debug("CORS method allowed: {}", method);
return true;
}
}
logger.debug("CORS method not allowed: {}", method);
return false;
}
public boolean isEveryHeaderAllowed(String headers) {
boolean result = false;
if (!Strings.isNullOrEmpty(headers)) {
if (this.allowedHeadersRaw.equals(ALLOW_ANY_HEADER)) {
result = true;
} else {
for (String header : Splitter.on(HEADER_VALUE_SEPARATOR).split(
headers)) {
result = this.allowedHeaders.contains(header);
if (!result) {
// First not matching header breaks
break;
}
}
}
}
if (result) {
logger.debug("CORS headers allowed: {}", headers);
} else {
logger.debug("CORS headers not allowed: {}", headers);
}
return result;
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || !(object instanceof CrossOriginResourceSharing)) {
return false;
}
CrossOriginResourceSharing that = (CrossOriginResourceSharing) object;
return this.allowedOrigins.equals(that.allowedOrigins) &&
this.allowedMethodsRaw.equals(that.allowedMethodsRaw) &&
this.allowedHeadersRaw.equals(that.allowedHeadersRaw);
}
@Override
public int hashCode() {
return Objects.hash(this.allowedOrigins, this.allowedMethodsRaw,
this.allowedHeadersRaw);
}
}

Wyświetl plik

@ -46,6 +46,10 @@ enum S3ErrorCode {
INVALID_ARGUMENT(HttpServletResponse.SC_BAD_REQUEST, "Bad Request"),
INVALID_BUCKET_NAME(HttpServletResponse.SC_BAD_REQUEST,
"The specified bucket is not valid."),
INVALID_CORS_ORIGIN(HttpServletResponse.SC_BAD_REQUEST,
"Insufficient information. Origin request header needed."),
INVALID_CORS_METHOD(HttpServletResponse.SC_BAD_REQUEST,
"The specified Access-Control-Request-Method is not valid."),
INVALID_DIGEST(HttpServletResponse.SC_BAD_REQUEST, "Bad Request"),
INVALID_LOCATION_CONSTRAINT(HttpServletResponse.SC_BAD_REQUEST,
"The specified location constraint is not valid. For" +

Wyświetl plik

@ -25,7 +25,9 @@ import java.net.URISyntaxException;
import java.util.Objects;
import java.util.Properties;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
@ -114,7 +116,7 @@ public final class S3Proxy {
builder.authenticationType, builder.identity,
builder.credential, builder.virtualHost,
builder.v4MaxNonChunkedRequestSize,
builder.ignoreUnknownHeaders, builder.corsAllowAll,
builder.ignoreUnknownHeaders, builder.corsRules,
builder.servicePath);
server.setHandler(handler);
}
@ -133,7 +135,7 @@ public final class S3Proxy {
private String virtualHost;
private long v4MaxNonChunkedRequestSize = 32 * 1024 * 1024;
private boolean ignoreUnknownHeaders;
private boolean corsAllowAll;
private CrossOriginResourceSharing corsRules;
private int jettyMaxThreads = 200; // sourced from QueuedThreadPool()
Builder() {
@ -240,9 +242,23 @@ public final class S3Proxy {
String corsAllowAll = properties.getProperty(
S3ProxyConstants.PROPERTY_CORS_ALLOW_ALL);
if (corsAllowAll != null) {
builder.corsAllowAll(Boolean.parseBoolean(
corsAllowAll));
if (!Strings.isNullOrEmpty(corsAllowAll) && Boolean.parseBoolean(
corsAllowAll)) {
builder.corsRules(new CrossOriginResourceSharing());
} else {
String corsAllowOrigins = properties.getProperty(
S3ProxyConstants.PROPERTY_CORS_ALLOW_ORIGINS, "");
String corsAllowMethods = properties.getProperty(
S3ProxyConstants.PROPERTY_CORS_ALLOW_METHODS, "");
String corsAllowHeaders = properties.getProperty(
S3ProxyConstants.PROPERTY_CORS_ALLOW_HEADERS, "");
Splitter splitter = Splitter.on(" ").trimResults()
.omitEmptyStrings();
builder.corsRules(new CrossOriginResourceSharing(
Lists.newArrayList(splitter.split(corsAllowOrigins)),
Lists.newArrayList(splitter.split(corsAllowMethods)),
Lists.newArrayList(splitter.split(corsAllowHeaders))));
}
String jettyMaxThreads = properties.getProperty(
@ -304,8 +320,8 @@ public final class S3Proxy {
return this;
}
public Builder corsAllowAll(boolean corsAllowAll) {
this.corsAllowAll = corsAllowAll;
public Builder corsRules(CrossOriginResourceSharing corsRules) {
this.corsRules = corsRules;
return this;
}
@ -368,7 +384,7 @@ public final class S3Proxy {
that.v4MaxNonChunkedRequestSize) &&
Objects.equals(this.ignoreUnknownHeaders,
that.ignoreUnknownHeaders) &&
Objects.equals(this.corsAllowAll, that.corsAllowAll);
this.corsRules.equals(that.corsRules);
}
@Override
@ -376,7 +392,7 @@ public final class S3Proxy {
return Objects.hash(endpoint, secureEndpoint, keyStorePath,
keyStorePassword, virtualHost, servicePath,
v4MaxNonChunkedRequestSize, ignoreUnknownHeaders,
corsAllowAll);
corsRules);
}
}

Wyświetl plik

@ -32,6 +32,12 @@ public final class S3ProxyConstants {
/** When true, include "Access-Control-Allow-Origin: *" in all responses. */
public static final String PROPERTY_CORS_ALLOW_ALL =
"s3proxy.cors-allow-all";
public static final String PROPERTY_CORS_ALLOW_ORIGINS =
"s3proxy.cors-allow-origins";
public static final String PROPERTY_CORS_ALLOW_METHODS =
"s3proxy.cors-allow-methods";
public static final String PROPERTY_CORS_ALLOW_HEADERS =
"s3proxy.cors-allow-headers";
public static final String PROPERTY_CREDENTIAL =
"s3proxy.credential";
public static final String PROPERTY_IGNORE_UNKNOWN_HEADERS =

Wyświetl plik

@ -191,7 +191,7 @@ public class S3ProxyHandler {
private final Optional<String> virtualHost;
private final long v4MaxNonChunkedRequestSize;
private final boolean ignoreUnknownHeaders;
private final boolean corsAllowAll;
private final CrossOriginResourceSharing corsRules;
private final String servicePath;
private final XMLOutputFactory xmlOutputFactory =
XMLOutputFactory.newInstance();
@ -214,7 +214,7 @@ public class S3ProxyHandler {
AuthenticationType authenticationType, final String identity,
final String credential, @Nullable String virtualHost,
long v4MaxNonChunkedRequestSize, boolean ignoreUnknownHeaders,
boolean corsAllowAll, final String servicePath) {
CrossOriginResourceSharing corsRules, final String servicePath) {
if (authenticationType != AuthenticationType.NONE) {
anonymousIdentity = false;
blobStoreLocator = new BlobStoreLocator() {
@ -244,7 +244,7 @@ public class S3ProxyHandler {
this.virtualHost = Optional.fromNullable(virtualHost);
this.v4MaxNonChunkedRequestSize = v4MaxNonChunkedRequestSize;
this.ignoreUnknownHeaders = ignoreUnknownHeaders;
this.corsAllowAll = corsAllowAll;
this.corsRules = corsRules;
this.defaultBlobStore = blobStore;
xmlOutputFactory.setProperty("javax.xml.stream.isRepairingNamespaces",
Boolean.FALSE);
@ -326,7 +326,7 @@ public class S3ProxyHandler {
// treat it as anonymous, return all public accessible information
if (!anonymousIdentity &&
(method.equals("GET") || method.equals("HEAD") ||
method.equals("POST")) &&
method.equals("POST") || method.equals("OPTIONS")) &&
request.getHeader(HttpHeaders.AUTHORIZATION) == null &&
// v2 or /v4
request.getParameter("X-Amz-Algorithm") == null && // v4 query
@ -729,6 +729,10 @@ public class S3ProxyHandler {
path[2]);
return;
}
case "OPTIONS":
handleOptionsBlob(request, response, blobStore, path[1],
path[2]);
return;
default:
break;
}
@ -807,6 +811,24 @@ public class S3ProxyHandler {
return;
}
break;
case "OPTIONS":
if (uri.equals("/")) {
throw new S3Exception(S3ErrorCode.ACCESS_DENIED);
} else {
String containerName = path[1];
/*
* Only check access on bucket level. The preflight request
* might be for a PUT, so the object is not yet there.
*/
ContainerAccess access = blobStore.getContainerAccess(
containerName);
if (access == ContainerAccess.PRIVATE) {
throw new S3Exception(S3ErrorCode.ACCESS_DENIED);
}
handleOptionsBlob(request, response, blobStore, containerName,
"");
return;
}
default:
break;
}
@ -1322,6 +1344,14 @@ public class S3ProxyHandler {
PageSet<? extends StorageMetadata> set = blobStore.list(containerName,
options);
String corsOrigin = request.getHeader(HttpHeaders.ORIGIN);
if (!Strings.isNullOrEmpty(corsOrigin) &&
corsRules.isOriginAllowed(corsOrigin)) {
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
corsOrigin);
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET");
}
response.setCharacterEncoding(UTF_8);
try (Writer writer = response.getWriter()) {
response.setContentType(XML_CONTENT_TYPE);
@ -1519,6 +1549,48 @@ public class S3ProxyHandler {
addMetadataToResponse(request, response, metadata);
}
private void handleOptionsBlob(HttpServletRequest request,
HttpServletResponse response,
BlobStore blobStore, String containerName,
String blobName) throws IOException, S3Exception {
if (!blobStore.containerExists(containerName)) {
// Don't leak internal information, although authenticated
throw new S3Exception(S3ErrorCode.ACCESS_DENIED);
}
String corsOrigin = request.getHeader(HttpHeaders.ORIGIN);
if (Strings.isNullOrEmpty(corsOrigin)) {
throw new S3Exception(S3ErrorCode.INVALID_CORS_ORIGIN);
}
if (!corsRules.isOriginAllowed(corsOrigin)) {
throw new S3Exception(S3ErrorCode.ACCESS_DENIED);
}
String corsMethod = request.getHeader(
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
if (!corsRules.isMethodAllowed(corsMethod)) {
throw new S3Exception(S3ErrorCode.INVALID_CORS_METHOD);
}
String corsHeaders = request.getHeader(
HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
if (!Strings.isNullOrEmpty(corsHeaders)) {
if (corsRules.isEveryHeaderAllowed(corsHeaders)) {
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
corsHeaders);
} else {
throw new S3Exception(S3ErrorCode.ACCESS_DENIED);
}
}
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, corsOrigin);
response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN);
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,
corsRules.getAllowedMethods());
response.setStatus(HttpServletResponse.SC_OK);
}
private void handleGetBlob(HttpServletRequest request,
HttpServletResponse response, BlobStore blobStore,
String containerName, String blobName)
@ -1572,8 +1644,12 @@ public class S3ProxyHandler {
response.setStatus(status);
if (corsAllowAll) {
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
String corsOrigin = request.getHeader(HttpHeaders.ORIGIN);
if (!Strings.isNullOrEmpty(corsOrigin) &&
corsRules.isOriginAllowed(corsOrigin)) {
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
corsOrigin);
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET");
}
addMetadataToResponse(request, response, blob.getMetadata());
@ -1715,7 +1791,7 @@ public class S3ProxyHandler {
}
}
private static void handlePutBlob(HttpServletRequest request,
private void handlePutBlob(HttpServletRequest request,
HttpServletResponse response, InputStream is, BlobStore blobStore,
String containerName, String blobName)
throws IOException, S3Exception {
@ -1810,6 +1886,14 @@ public class S3ProxyHandler {
eTag = blobStore.putBlob(containerName, builder.build(),
options);
String corsOrigin = request.getHeader(HttpHeaders.ORIGIN);
if (!Strings.isNullOrEmpty(corsOrigin) &&
corsRules.isOriginAllowed(corsOrigin)) {
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
corsOrigin);
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "PUT");
}
response.addHeader(HttpHeaders.ETAG, maybeQuoteETag(eTag));
}
@ -1974,8 +2058,13 @@ public class S3ProxyHandler {
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
if (corsAllowAll) {
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
String corsOrigin = request.getHeader(HttpHeaders.ORIGIN);
if (!Strings.isNullOrEmpty(corsOrigin) &&
corsRules.isOriginAllowed(corsOrigin)) {
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
corsOrigin);
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,
"POST");
}
}

Wyświetl plik

@ -44,10 +44,10 @@ final class S3ProxyHandlerJetty extends AbstractHandler {
AuthenticationType authenticationType, final String identity,
final String credential, @Nullable String virtualHost,
long v4MaxNonChunkedRequestSize, boolean ignoreUnknownHeaders,
boolean corsAllowAll, String servicePath) {
CrossOriginResourceSharing corsRules, String servicePath) {
handler = new S3ProxyHandler(blobStore, authenticationType, identity,
credential, virtualHost, v4MaxNonChunkedRequestSize,
ignoreUnknownHeaders, corsAllowAll, servicePath);
ignoreUnknownHeaders, corsRules, servicePath);
}
private void sendS3Exception(HttpServletRequest request,

Wyświetl plik

@ -8,6 +8,9 @@ exec java \
-Ds3proxy.identity=${S3PROXY_IDENTITY} \
-Ds3proxy.credential=${S3PROXY_CREDENTIAL} \
-Ds3proxy.cors-allow-all=${S3PROXY_CORS_ALLOW_ALL} \
-Ds3proxy.cors-allow-origins="${S3PROXY_CORS_ALLOW_ORIGINS}" \
-Ds3proxy.cors-allow-methods="${S3PROXY_CORS_ALLOW_METHODS}" \
-Ds3proxy.cors-allow-headers="${S3PROXY_CORS_ALLOW_HEADERS}" \
-Ds3proxy.ignore-unknown-headers=${S3PROXY_IGNORE_UNKNOWN_HEADERS} \
-Djclouds.provider=${JCLOUDS_PROVIDER} \
-Djclouds.identity=${JCLOUDS_IDENTITY} \

Wyświetl plik

@ -0,0 +1,366 @@
/*
* Copyright 2014-2018 Andrew Gaul <andrew@gaul.org>
*
* Licensed 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.gaul.s3proxy;
import static org.assertj.core.api.Assertions.assertThat;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import com.amazonaws.HttpMethod;
import com.amazonaws.SDKGlobalConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.google.common.io.ByteSource;
import com.google.common.net.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.jclouds.blobstore.BlobStoreContext;
import org.jclouds.blobstore.domain.Blob;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public final class CrossOriginResourceSharingResponseTest {
static {
System.setProperty(
SDKGlobalConfiguration.DISABLE_CERT_CHECKING_SYSTEM_PROPERTY,
"true");
AwsSdkTest.disableSslVerification();
}
private URI s3Endpoint;
private EndpointConfiguration s3EndpointConfig;
private S3Proxy s3Proxy;
private BlobStoreContext context;
private String blobStoreType;
private String containerName;
private AWSCredentials awsCreds;
private AmazonS3 s3Client;
private String servicePath;
private CloseableHttpClient httpClient;
private URI presignedGET;
private URI publicGET;
@Before
public void setUp() throws Exception {
TestUtils.S3ProxyLaunchInfo info = TestUtils.startS3Proxy(
"s3proxy-cors.conf");
awsCreds = new BasicAWSCredentials(info.getS3Identity(),
info.getS3Credential());
context = info.getBlobStore().getContext();
s3Proxy = info.getS3Proxy();
s3Endpoint = info.getSecureEndpoint();
servicePath = info.getServicePath();
s3EndpointConfig = new EndpointConfiguration(
s3Endpoint.toString() + servicePath, "us-east-1");
s3Client = AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.withEndpointConfiguration(s3EndpointConfig)
.build();
httpClient = getHttpClient();
containerName = createRandomContainerName();
info.getBlobStore().createContainerInLocation(null, containerName);
s3Client.setBucketAcl(containerName,
CannedAccessControlList.PublicRead);
String blobName = "test";
ByteSource payload = ByteSource.wrap("blob-content".getBytes(
StandardCharsets.UTF_8));
Blob blob = info.getBlobStore().blobBuilder(blobName)
.payload(payload).contentLength(payload.size()).build();
info.getBlobStore().putBlob(containerName, blob);
Date expiration = new Date(System.currentTimeMillis() +
TimeUnit.HOURS.toMillis(1));
presignedGET = s3Client.generatePresignedUrl(containerName, blobName,
expiration, HttpMethod.GET).toURI();
publicGET = s3Client.getUrl(containerName, blobName).toURI();
}
@After
public void tearDown() throws Exception {
if (s3Proxy != null) {
s3Proxy.stop();
}
if (context != null) {
context.getBlobStore().deleteContainer(containerName);
context.close();
}
if (httpClient != null) {
httpClient.close();
}
}
@Test
public void testCorsPreflightNegative() throws Exception {
// No CORS headers
HttpOptions request = new HttpOptions(presignedGET);
HttpResponse response = httpClient.execute(request);
/*
* For non presigned URLs that should give a 400, but the
* Access-Control-Request-Method header is needed for presigned URLs
* to calculate the same signature. If this is missing it fails already
* with 403 - Signature mismatch before processing the OPTIONS request
* See testCorsPreflightPublicRead for that cases
*/
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_FORBIDDEN);
// Not allowed origin
request.reset();
request.setHeader(HttpHeaders.ORIGIN, "https://example.org");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
response = httpClient.execute(request);
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_FORBIDDEN);
// Not allowed method
request.reset();
request.setHeader(HttpHeaders.ORIGIN, "https://example.com");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PATCH");
response = httpClient.execute(request);
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_FORBIDDEN);
// Not allowed header
request.reset();
request.setHeader(HttpHeaders.ORIGIN, "https://example.com");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS,
"Accept-Encoding");
response = httpClient.execute(request);
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_FORBIDDEN);
// Not allowed header combination
request.reset();
request.setHeader(HttpHeaders.ORIGIN, "https://example.com");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS,
"Accept, Accept-Encoding");
response = httpClient.execute(request);
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_FORBIDDEN);
}
@Test
public void testCorsPreflight() throws Exception {
// Allowed origin and method
HttpOptions request = new HttpOptions(presignedGET);
request.setHeader(HttpHeaders.ORIGIN, "https://example.com");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
HttpResponse response = httpClient.execute(request);
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_OK);
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(response.getFirstHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN).getValue())
.isEqualTo("https://example.com");
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isTrue();
assertThat(response.getFirstHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS).getValue())
.isEqualTo("GET, PUT");
// Allowed origin, method and header
request.reset();
request.setHeader(HttpHeaders.ORIGIN, "https://example.com");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "Accept");
response = httpClient.execute(request);
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_OK);
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(response.getFirstHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN).getValue())
.isEqualTo("https://example.com");
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isTrue();
assertThat(response.getFirstHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS).getValue())
.isEqualTo("GET, PUT");
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)).isTrue();
assertThat(response.getFirstHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS).getValue())
.isEqualTo("Accept");
// Allowed origin, method and header combination
request.reset();
request.setHeader(HttpHeaders.ORIGIN, "https://example.com");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS,
"Accept, Content-Type");
response = httpClient.execute(request);
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_OK);
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(response.getFirstHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN).getValue())
.isEqualTo("https://example.com");
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isTrue();
assertThat(response.getFirstHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS).getValue())
.isEqualTo("GET, PUT");
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)).isTrue();
assertThat(response.getFirstHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS).getValue())
.isEqualTo("Accept, Content-Type");
}
@Test
public void testCorsPreflightPublicRead() throws Exception {
// No CORS headers
HttpOptions request = new HttpOptions(publicGET);
HttpResponse response = httpClient.execute(request);
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_BAD_REQUEST);
// Not allowed method
request.reset();
request.setHeader(HttpHeaders.ORIGIN, "https://example.com");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PATCH");
response = httpClient.execute(request);
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_BAD_REQUEST);
// Allowed origin and method
request.reset();
request.setHeader(HttpHeaders.ORIGIN, "https://example.com");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
request.setHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS,
"Accept, Content-Type");
response = httpClient.execute(request);
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_OK);
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(response.getFirstHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN).getValue())
.isEqualTo("https://example.com");
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isTrue();
assertThat(response.getFirstHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS).getValue())
.isEqualTo("GET, PUT");
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)).isTrue();
assertThat(response.getFirstHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS).getValue())
.isEqualTo("Accept, Content-Type");
}
@Test
public void testCorsActual() throws Exception {
HttpGet request = new HttpGet(presignedGET);
request.setHeader(HttpHeaders.ORIGIN, "https://example.com");
HttpResponse response = httpClient.execute(request);
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_OK);
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
assertThat(response.getFirstHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN).getValue())
.isEqualTo("https://example.com");
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isTrue();
assertThat(response.getFirstHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS).getValue())
.isEqualTo("GET");
}
@Test
public void testNonCors() throws Exception {
HttpGet request = new HttpGet(presignedGET);
HttpResponse response = httpClient.execute(request);
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_OK);
assertThat(response.containsHeader(
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse();
}
private static String createRandomContainerName() {
return "s3proxy-" + new Random().nextInt(Integer.MAX_VALUE);
}
private static CloseableHttpClient getHttpClient() throws
KeyManagementException, NoSuchAlgorithmException,
KeyStoreException {
// Relax SSL Certificate check
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(
null, new TrustStrategy() {
public boolean isTrusted(X509Certificate[] arg0,
String arg1) throws CertificateException {
return true;
}
}).build();
Registry<ConnectionSocketFactory> registry = RegistryBuilder
.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.INSTANCE)
.register("https", new SSLConnectionSocketFactory(sslContext,
NoopHostnameVerifier.INSTANCE)).build();
PoolingHttpClientConnectionManager connectionManager = new
PoolingHttpClientConnectionManager(registry);
return HttpClients.custom().setConnectionManager(connectionManager)
.build();
}
}

Wyświetl plik

@ -0,0 +1,171 @@
/*
* Copyright 2014-2018 Andrew Gaul <andrew@gaul.org>
*
* Licensed 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.gaul.s3proxy;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.common.collect.Lists;
import org.junit.Before;
import org.junit.Test;
public final class CrossOriginResourceSharingRuleTest {
private CrossOriginResourceSharing corsAll;
private CrossOriginResourceSharing corsCfg;
private CrossOriginResourceSharing corsOff;
@Before
public void setUp() throws Exception {
// CORS Allow All
corsAll = new CrossOriginResourceSharing();
// CORS Configured
corsCfg = new CrossOriginResourceSharing(
Lists.newArrayList("https://example\\.com",
"https://.+\\.example\\.com",
"https://example\\.cloud"),
Lists.newArrayList("GET", "PUT"),
Lists.newArrayList("Accept", "Content-Type"));
// CORS disabled
corsOff = new CrossOriginResourceSharing(null, null, null);
}
@Test
public void testCorsOffOrigin() throws Exception {
String probe = "";
assertThat(corsOff.isOriginAllowed(probe))
.as("check '%s' as origin", probe).isFalse();
probe = "https://example.com";
assertThat(corsOff.isOriginAllowed(probe))
.as("check '%s' as origin", probe).isFalse();
}
@Test
public void testCorsOffMethod() throws Exception {
String probe = "";
assertThat(corsOff.isMethodAllowed(probe))
.as("check '%s' as method", probe).isFalse();
probe = "GET";
assertThat(corsOff.isMethodAllowed(probe))
.as("check '%s' as method", probe).isFalse();
}
@Test
public void testCorsOffHeader() throws Exception {
String probe = "";
assertThat(corsOff.isEveryHeaderAllowed(probe))
.as("check '%s' as header", probe).isFalse();
probe = "Accept";
assertThat(corsOff.isEveryHeaderAllowed(probe))
.as("check '%s' as header", probe).isFalse();
probe = "Accept, Content-Type";
assertThat(corsOff.isEveryHeaderAllowed(probe))
.as("check '%s' as header", probe).isFalse();
}
@Test
public void testCorsAllOrigin() throws Exception {
String probe = "";
assertThat(corsAll.isOriginAllowed(probe))
.as("check '%s' as origin", probe).isFalse();
probe = "https://example.com";
assertThat(corsAll.isOriginAllowed(probe))
.as("check '%s' as origin", probe).isTrue();
probe = "https://sub.example.com";
assertThat(corsAll.isOriginAllowed(probe))
.as("check '%s' as origin", probe).isTrue();
}
@Test
public void testCorsAllMethod() throws Exception {
String probe = "";
assertThat(corsAll.isMethodAllowed(probe))
.as("check '%s' as method", probe).isFalse();
probe = "PATCH";
assertThat(corsAll.isMethodAllowed(probe))
.as("check '%s' as method", probe).isFalse();
probe = "GET";
assertThat(corsAll.isMethodAllowed(probe))
.as("check '%s' as method", probe).isTrue();
}
@Test
public void testCorsAllHeader() throws Exception {
String probe = "";
assertThat(corsAll.isEveryHeaderAllowed(probe))
.as("check '%s' as header", probe).isFalse();
probe = "Accept";
assertThat(corsAll.isEveryHeaderAllowed(probe))
.as("check '%s' as header", probe).isTrue();
probe = "Accept, Content-Type";
assertThat(corsAll.isEveryHeaderAllowed(probe))
.as("check '%s' as header", probe).isTrue();
}
@Test
public void testCorsCfgOrigin() throws Exception {
String probe = "";
assertThat(corsCfg.isOriginAllowed(probe))
.as("check '%s' as origin", probe).isFalse();
probe = "https://example.org";
assertThat(corsCfg.isOriginAllowed(probe))
.as("check '%s' as origin", probe).isFalse();
probe = "https://example.com";
assertThat(corsCfg.isOriginAllowed(probe))
.as("check '%s' as origin", probe).isTrue();
probe = "https://sub.example.com";
assertThat(corsCfg.isOriginAllowed(probe))
.as("check '%s' as origin", probe).isTrue();
probe = "https://example.cloud";
assertThat(corsCfg.isOriginAllowed(probe))
.as("check '%s' as origin", probe).isTrue();
}
@Test
public void testCorsCfgMethod() throws Exception {
String probe = "";
assertThat(corsCfg.isMethodAllowed(probe))
.as("check '%s' as method", probe).isFalse();
probe = "PATCH";
assertThat(corsCfg.isMethodAllowed(probe))
.as("check '%s' as method", probe).isFalse();
probe = "GET";
assertThat(corsCfg.isMethodAllowed(probe))
.as("check '%s' as method", probe).isTrue();
probe = "PUT";
assertThat(corsCfg.isMethodAllowed(probe))
.as("check '%s' as method", probe).isTrue();
}
@Test
public void testCorsCfgHeader() throws Exception {
String probe = "";
assertThat(corsCfg.isEveryHeaderAllowed(probe))
.as("check '%s' as header", probe).isFalse();
probe = "Accept-Language";
assertThat(corsCfg.isEveryHeaderAllowed(probe))
.as("check '%s' as header", probe).isFalse();
probe = "Accept, Accept-Encoding";
assertThat(corsCfg.isEveryHeaderAllowed(probe))
.as("check '%s' as header", probe).isFalse();
probe = "Accept";
assertThat(corsCfg.isEveryHeaderAllowed(probe))
.as("check '%s' as header", probe).isTrue();
probe = "Accept, Content-Type";
assertThat(corsCfg.isEveryHeaderAllowed(probe))
.as("check '%s' as header", probe).isTrue();
}
}

Wyświetl plik

@ -0,0 +1,18 @@
s3proxy.endpoint=http://127.0.0.1:0
s3proxy.secure-endpoint=https://127.0.0.1:0
# authorization must be aws-v2, aws-v4, aws-v2-or-v4, or none
s3proxy.authorization=aws-v2-or-v4
s3proxy.identity=local-identity
s3proxy.credential=local-credential
s3proxy.keystore-path=keystore.jks
s3proxy.keystore-password=password
s3proxy.cors-allow-origins=https://example\.com https://.+\.example\.com https://example\.cloud
s3proxy.cors-allow-methods=GET PUT
s3proxy.cors-allow-headers=Accept Content-Type
jclouds.provider=transient
jclouds.identity=remote-identity
jclouds.credential=remote-credential
# endpoint is optional for some providers
#jclouds.endpoint=http://127.0.0.1:8081
jclouds.filesystem.basedir=/tmp/blobstore