kopia lustrzana https://github.com/gaul/s3proxy
Basic CORS support
This commit adds a globally configurable CORS support. Note that this differs from AWS per-bucket support.pull/290/head
rodzic
3ba59d7370
commit
e3277a4c1f
|
|
@ -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="" \
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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" +
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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} \
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
Ładowanie…
Reference in New Issue