diff --git a/NOTICE.txt b/NOTICE.txt
new file mode 100644
index 00000000..b74f3478
--- /dev/null
+++ b/NOTICE.txt
@@ -0,0 +1 @@
+This product includes software developed by Minio. (https://github.com/minio/minio-java)
diff --git a/pom.xml b/pom.xml
index 81585780..a01a63f7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,11 +40,13 @@
22.0.0
${project.basedir}
- 8
true
true
+ 8
+ true
245
+ 2.25.32
@@ -56,6 +58,40 @@
pom
import
+
+
+ software.amazon.awssdk
+ bom
+ ${dep.aws-sdk.version}
+ pom
+ import
+
+
+
+
+
+ com.mycila
+ license-maven-plugin
+
+
+
+
+ **/io/trino/s3/proxy/server/minio/*
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+ **/io/trino/s3/proxy/server/minio/*
+
+
+
+
diff --git a/trino-s3-proxy/pom.xml b/trino-s3-proxy/pom.xml
index 28d9ab81..ecae50e8 100644
--- a/trino-s3-proxy/pom.xml
+++ b/trino-s3-proxy/pom.xml
@@ -64,6 +64,12 @@
jakarta.ws.rs-api
+
+ org.assertj
+ assertj-core
+ test
+
+
org.junit.jupiter
junit-jupiter-api
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/Credentials.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/Credentials.java
new file mode 100644
index 00000000..11d093e6
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/Credentials.java
@@ -0,0 +1,25 @@
+/*
+ * 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 io.trino.s3.proxy.server;
+
+import static java.util.Objects.requireNonNull;
+
+public record Credentials(String emulatedAccessKey, String emulatedSecretKey)
+{
+ public Credentials
+ {
+ requireNonNull(emulatedAccessKey, "emulatedAccessKey is null");
+ requireNonNull(emulatedSecretKey, "emulatedSecretKey is null");
+ }
+}
diff --git a/trino-s3-proxy/src/test/java/io/trino/s3/proxy/server/DummyTest.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/CredentialsController.java
similarity index 81%
rename from trino-s3-proxy/src/test/java/io/trino/s3/proxy/server/DummyTest.java
rename to trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/CredentialsController.java
index 00ca9ac6..f763fa54 100644
--- a/trino-s3-proxy/src/test/java/io/trino/s3/proxy/server/DummyTest.java
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/CredentialsController.java
@@ -13,13 +13,9 @@
*/
package io.trino.s3.proxy.server;
-import org.junit.jupiter.api.Test;
+import java.util.Optional;
-public class DummyTest
+public interface CredentialsController
{
- @Test
- public void testDummy()
- {
- // stub test for now
- }
+ Optional credentials(String emulatedAccessKey);
}
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/SigningController.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/SigningController.java
new file mode 100644
index 00000000..8cd239e3
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/SigningController.java
@@ -0,0 +1,57 @@
+/*
+ * 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 io.trino.s3.proxy.server;
+
+import com.google.inject.Inject;
+import io.trino.s3.proxy.server.minio.Signer;
+import io.trino.s3.proxy.server.minio.emulation.MinioRequest;
+import io.trino.s3.proxy.server.minio.emulation.MinioUrl;
+import jakarta.ws.rs.core.MultivaluedMap;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Map;
+
+import static java.util.Objects.requireNonNull;
+
+public class SigningController
+{
+ private final CredentialsController credentialsController;
+
+ @Inject
+ public SigningController(CredentialsController credentialsController)
+ {
+ this.credentialsController = requireNonNull(credentialsController, "credentialsController is null");
+ }
+
+ public Map signedRequestHeaders(String method, MultivaluedMap requestHeaders, String encodedPath, String encodedQuery, String region, String accessKey)
+ {
+ // TODO
+ Credentials credentials = credentialsController.credentials(accessKey).orElseThrow();
+
+ MinioUrl minioUrl = MinioUrl.build(encodedPath, encodedQuery);
+ MinioRequest minioRequest = MinioRequest.build(requestHeaders, method, minioUrl);
+
+ // TODO
+ String sha256 = minioRequest.headerValue("x-amz-content-sha256").orElseThrow();
+
+ try {
+ return Signer.signV4S3(minioRequest, region, accessKey, credentials.emulatedSecretKey(), sha256).headers();
+ }
+ catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ // TODO
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Digest.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Digest.java
new file mode 100644
index 00000000..d77a5265
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Digest.java
@@ -0,0 +1,155 @@
+/*
+ * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2015 MinIO, Inc.
+ *
+ * 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 io.trino.s3.proxy.server.minio;
+
+import com.google.common.io.BaseEncoding;
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.Locale;
+
+/** Various global static functions used. */
+public class Digest {
+ // MD5 hash of zero length byte array.
+ public static final String ZERO_MD5_HASH = "1B2M2Y8AsgTpgAmY7PhCfg==";
+ // SHA-256 hash of zero length byte array.
+ public static final String ZERO_SHA256_HASH =
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
+
+ /** Private constructor. */
+ private Digest() {}
+
+ /** Returns MD5 hash of byte array. */
+ public static String md5Hash(byte[] data, int length) throws NoSuchAlgorithmException {
+ MessageDigest md5Digest = MessageDigest.getInstance("MD5");
+ md5Digest.update(data, 0, length);
+ return Base64.getEncoder().encodeToString(md5Digest.digest());
+ }
+
+ /** Returns SHA-256 hash of byte array. */
+ public static String sha256Hash(byte[] data, int length) throws NoSuchAlgorithmException {
+ MessageDigest sha256Digest = MessageDigest.getInstance("SHA-256");
+ sha256Digest.update((byte[]) data, 0, length);
+ return BaseEncoding.base16().encode(sha256Digest.digest()).toLowerCase(Locale.US);
+ }
+
+ /** Returns SHA-256 hash of given string. */
+ public static String sha256Hash(String string) throws NoSuchAlgorithmException {
+ byte[] data = string.getBytes(StandardCharsets.UTF_8);
+ return sha256Hash(data, data.length);
+ }
+
+ /**
+ * Returns SHA-256 and MD5 hashes of given data and it's length.
+ *
+ * @param data must be {@link RandomAccessFile}, {@link BufferedInputStream} or byte array.
+ * @param len length of data to be read for hash calculation.
+ * @deprecated This method is no longer supported.
+ */
+ @Deprecated
+ public static String[] sha256Md5Hashes(Object data, int len)
+ throws NoSuchAlgorithmException, IOException, InsufficientDataException, InternalException {
+ MessageDigest sha256Digest = MessageDigest.getInstance("SHA-256");
+ MessageDigest md5Digest = MessageDigest.getInstance("MD5");
+
+ if (data instanceof BufferedInputStream || data instanceof RandomAccessFile) {
+ updateDigests(data, len, sha256Digest, md5Digest);
+ } else if (data instanceof byte[]) {
+ sha256Digest.update((byte[]) data, 0, len);
+ md5Digest.update((byte[]) data, 0, len);
+ } else {
+ throw new InternalException(
+ "Unknown data source to calculate SHA-256 hash. This should not happen, "
+ + "please report this issue at https://github.com/minio/minio-java/issues",
+ null);
+ }
+
+ return new String[] {
+ BaseEncoding.base16().encode(sha256Digest.digest()).toLowerCase(Locale.US),
+ BaseEncoding.base64().encode(md5Digest.digest())
+ };
+ }
+
+ /** Updated MessageDigest with bytes read from file and stream. */
+ private static int updateDigests(
+ Object inputStream, int len, MessageDigest sha256Digest, MessageDigest md5Digest)
+ throws IOException, InsufficientDataException {
+ RandomAccessFile file = null;
+ BufferedInputStream stream = null;
+ if (inputStream instanceof RandomAccessFile) {
+ file = (RandomAccessFile) inputStream;
+ } else if (inputStream instanceof BufferedInputStream) {
+ stream = (BufferedInputStream) inputStream;
+ }
+
+ // hold current position of file/stream to reset back to this position.
+ long pos = 0;
+ if (file != null) {
+ pos = file.getFilePointer();
+ } else {
+ stream.mark(len);
+ }
+
+ // 16KiB buffer for optimization
+ byte[] buf = new byte[16384];
+ int bytesToRead = buf.length;
+ int bytesRead = 0;
+ int totalBytesRead = 0;
+ while (totalBytesRead < len) {
+ if ((len - totalBytesRead) < bytesToRead) {
+ bytesToRead = len - totalBytesRead;
+ }
+
+ if (file != null) {
+ bytesRead = file.read(buf, 0, bytesToRead);
+ } else {
+ bytesRead = stream.read(buf, 0, bytesToRead);
+ }
+
+ if (bytesRead < 0) {
+ // reached EOF
+ throw new InsufficientDataException(
+ "Insufficient data. bytes read " + totalBytesRead + " expected " + len);
+ }
+
+ if (bytesRead > 0) {
+ if (sha256Digest != null) {
+ sha256Digest.update(buf, 0, bytesRead);
+ }
+
+ if (md5Digest != null) {
+ md5Digest.update(buf, 0, bytesRead);
+ }
+
+ totalBytesRead += bytesRead;
+ }
+ }
+
+ // reset back to saved position.
+ if (file != null) {
+ file.seek(pos);
+ } else {
+ stream.reset();
+ }
+
+ return totalBytesRead;
+ }
+}
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/InsufficientDataException.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/InsufficientDataException.java
new file mode 100644
index 00000000..8adc4e26
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/InsufficientDataException.java
@@ -0,0 +1,29 @@
+/*
+ * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2015 MinIO, Inc.
+ *
+ * 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 io.trino.s3.proxy.server.minio;
+
+/**
+ * Thrown to indicate that reading given InputStream gets EOFException before reading given length.
+ */
+public class InsufficientDataException extends MinioException {
+ private static final long serialVersionUID = -1619719290805056566L;
+
+ /** Constructs a new InsufficientDataException with given error message. */
+ public InsufficientDataException(String message) {
+ super(message);
+ }
+}
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/InternalException.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/InternalException.java
new file mode 100644
index 00000000..2049718c
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/InternalException.java
@@ -0,0 +1,29 @@
+/*
+ * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2015 MinIO, Inc.
+ *
+ * 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 io.trino.s3.proxy.server.minio;
+
+/**
+ * Thrown to indicate that unexpected internal library error occured while processing given request.
+ */
+public class InternalException extends MinioException {
+ private static final long serialVersionUID = 138336287983212416L;
+
+ /** Constructs a new InternalException with given error message. */
+ public InternalException(String message, String httpTrace) {
+ super(message, httpTrace);
+ }
+}
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/MinioException.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/MinioException.java
new file mode 100644
index 00000000..af6eb545
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/MinioException.java
@@ -0,0 +1,44 @@
+/*
+ * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2015 MinIO, Inc.
+ *
+ * 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 io.trino.s3.proxy.server.minio;
+
+/** Base Exception class for all minio-java exceptions. */
+public class MinioException extends Exception {
+ private static final long serialVersionUID = -7241010318779326306L;
+
+ String httpTrace = null;
+
+ /** Constructs a new MinioException. */
+ public MinioException() {
+ super();
+ }
+
+ /** Constructs a new MinioException with given error message. */
+ public MinioException(String message) {
+ super(message);
+ }
+
+ /** Constructs a new MinioException with given error message. */
+ public MinioException(String message, String httpTrace) {
+ super(message);
+ this.httpTrace = httpTrace;
+ }
+
+ public String httpTrace() {
+ return this.httpTrace;
+ }
+}
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/S3Escaper.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/S3Escaper.java
new file mode 100644
index 00000000..bb3e1123
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/S3Escaper.java
@@ -0,0 +1,110 @@
+/*
+ * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2016 MinIO, Inc.
+ *
+ * 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 io.trino.s3.proxy.server.minio;
+
+import com.google.common.escape.Escaper;
+import com.google.common.net.UrlEscapers;
+
+public class S3Escaper {
+ private static final Escaper ESCAPER = UrlEscapers.urlPathSegmentEscaper();
+
+ /** Returns S3 encoded string. */
+ public static String encode(String str) {
+ if (str == null) {
+ return "";
+ }
+
+ StringBuilder builder = new StringBuilder();
+ for (char ch : ESCAPER.escape(str).toCharArray()) {
+ switch (ch) {
+ case '!':
+ builder.append("%21");
+ break;
+ case '$':
+ builder.append("%24");
+ break;
+ case '&':
+ builder.append("%26");
+ break;
+ case '\'':
+ builder.append("%27");
+ break;
+ case '(':
+ builder.append("%28");
+ break;
+ case ')':
+ builder.append("%29");
+ break;
+ case '*':
+ builder.append("%2A");
+ break;
+ case '+':
+ builder.append("%2B");
+ break;
+ case ',':
+ builder.append("%2C");
+ break;
+ case '/':
+ builder.append("%2F");
+ break;
+ case ':':
+ builder.append("%3A");
+ break;
+ case ';':
+ builder.append("%3B");
+ break;
+ case '=':
+ builder.append("%3D");
+ break;
+ case '@':
+ builder.append("%40");
+ break;
+ case '[':
+ builder.append("%5B");
+ break;
+ case ']':
+ builder.append("%5D");
+ break;
+ default:
+ builder.append(ch);
+ }
+ }
+ return builder.toString();
+ }
+
+ /** Returns S3 encoded string of given path where multiple '/' are trimmed. */
+ public static String encodePath(String path) {
+ final StringBuilder encodedPath = new StringBuilder();
+ for (String pathSegment : path.split("/")) {
+ if (!pathSegment.isEmpty()) {
+ if (encodedPath.length() > 0) {
+ encodedPath.append("/");
+ }
+ encodedPath.append(S3Escaper.encode(pathSegment));
+ }
+ }
+
+ if (path.startsWith("/")) {
+ encodedPath.insert(0, "/");
+ }
+ if (path.endsWith("/")) {
+ encodedPath.append("/");
+ }
+
+ return encodedPath.toString();
+ }
+}
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Signer.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Signer.java
new file mode 100644
index 00000000..e721a762
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Signer.java
@@ -0,0 +1,444 @@
+/*
+ * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2015 MinIO, Inc.
+ *
+ * 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 io.trino.s3.proxy.server.minio;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.io.BaseEncoding;
+import io.trino.s3.proxy.server.minio.emulation.MinioHeaders;
+import io.trino.s3.proxy.server.minio.emulation.MinioUrl;
+import io.trino.s3.proxy.server.minio.emulation.MinioRequest;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+/**
+ * Amazon AWS S3 signature V4 signer.
+ */
+public class Signer
+{
+ //
+ // Excerpts from @lsegal - https://github.com/aws/aws-sdk-js/issues/659#issuecomment-120477258
+ //
+ // * User-Agent
+ // This is ignored from signing because signing this causes problems with generating pre-signed
+ // URLs (that are executed by other agents) or when customers pass requests through proxies, which
+ // may modify the user-agent.
+ //
+ // * Authorization
+ // Is skipped for obvious reasons.
+ //
+ // * Accept-Encoding
+ // Some S3 servers like Hitachi Content Platform do not honour this header for signature
+ // calculation.
+ //
+ private static final Set IGNORED_HEADERS =
+ ImmutableSet.of("accept-encoding", "authorization", "user-agent");
+ private static final Set PRESIGN_IGNORED_HEADERS =
+ ImmutableSet.of(
+ "accept-encoding",
+ "authorization",
+ "user-agent",
+ "content-md5",
+ "x-amz-content-sha256",
+ "x-amz-date",
+ "x-amz-security-token");
+
+ private MinioRequest request;
+ private String contentSha256;
+ private ZonedDateTime date;
+ private String region;
+ private String accessKey;
+ private String secretKey;
+ private String prevSignature;
+
+ private String scope;
+ private Map canonicalHeaders;
+ private String signedHeaders;
+ private MinioUrl url;
+ private String canonicalQueryString;
+ private String canonicalRequest;
+ private String canonicalRequestHash;
+ private String stringToSign;
+ private byte[] signingKey;
+ private String signature;
+ private String authorization;
+
+ /**
+ * Create new Signer object for V4.
+ *
+ * @param request HTTP Request object.
+ * @param contentSha256 SHA-256 hash of request payload.
+ * @param date Date to be used to sign the request.
+ * @param region Amazon AWS region for the request.
+ * @param accessKey Access Key string.
+ * @param secretKey Secret Key string.
+ * @param prevSignature Previous signature of chunk upload.
+ */
+ private Signer(
+ MinioRequest request,
+ String contentSha256,
+ ZonedDateTime date,
+ String region,
+ String accessKey,
+ String secretKey,
+ String prevSignature)
+ {
+ this.request = request;
+ this.contentSha256 = contentSha256;
+ this.date = date;
+ this.region = region;
+ this.accessKey = accessKey;
+ this.secretKey = secretKey;
+ this.prevSignature = prevSignature;
+ }
+
+ private void setScope(String serviceName)
+ {
+ this.scope =
+ this.date.format(Time.SIGNER_DATE_FORMAT)
+ + "/"
+ + this.region
+ + "/"
+ + serviceName
+ + "/aws4_request";
+ }
+
+ private void setCanonicalHeaders(Set ignored_headers)
+ {
+ this.canonicalHeaders = new TreeMap<>();
+
+ for (String name : request.headerNames()) {
+ String signedHeader = name.toLowerCase(Locale.US);
+ if (!ignored_headers.contains(signedHeader)) {
+ // Convert and add header values as per
+ // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
+ // * Header having multiple values should be converted to comma separated values.
+ // * Multi-spaced value of header should be trimmed to single spaced value.
+ this.canonicalHeaders.put(
+ signedHeader,
+ request.headerValues(name).stream()
+ .map(
+ value -> {
+ return value.replaceAll("( +)", " ");
+ })
+ .collect(Collectors.joining(",")));
+ }
+ }
+
+ this.signedHeaders = Joiner.on(";").join(this.canonicalHeaders.keySet());
+ }
+
+ private void setCanonicalQueryString()
+ {
+ String encodedQuery = this.url.encodedQuery();
+ if (encodedQuery.isEmpty()) {
+ this.canonicalQueryString = "";
+ return;
+ }
+
+ // Building a multimap which only order keys, ordering values is not performed
+ // until MinIO server supports it.
+ Multimap signedQueryParams =
+ MultimapBuilder.treeKeys().arrayListValues().build();
+
+ for (String queryParam : encodedQuery.split("&")) {
+ String[] tokens = queryParam.split("=");
+ if (tokens.length > 1) {
+ signedQueryParams.put(tokens[0], tokens[1]);
+ }
+ else {
+ signedQueryParams.put(tokens[0], "");
+ }
+ }
+
+ this.canonicalQueryString =
+ Joiner.on("&").withKeyValueSeparator("=").join(signedQueryParams.entries());
+ }
+
+ private void setCanonicalRequest()
+ throws NoSuchAlgorithmException
+ {
+ setCanonicalHeaders(IGNORED_HEADERS);
+ this.url = this.request.url();
+ setCanonicalQueryString();
+
+ // CanonicalRequest =
+ // HTTPRequestMethod + '\n' +
+ // CanonicalURI + '\n' +
+ // CanonicalQueryString + '\n' +
+ // CanonicalHeaders + '\n' +
+ // SignedHeaders + '\n' +
+ // HexEncode(Hash(RequestPayload))
+ this.canonicalRequest =
+ this.request.httpMethod()
+ + "\n"
+ + this.url.encodedPath()
+ + "\n"
+ + this.canonicalQueryString
+ + "\n"
+ + Joiner.on("\n").withKeyValueSeparator(":").join(this.canonicalHeaders)
+ + "\n\n"
+ + this.signedHeaders
+ + "\n"
+ + this.contentSha256;
+
+ this.canonicalRequestHash = Digest.sha256Hash(this.canonicalRequest);
+ }
+
+ private void setStringToSign()
+ {
+ this.stringToSign =
+ "AWS4-HMAC-SHA256"
+ + "\n"
+ + this.date.format(Time.AMZ_DATE_FORMAT)
+ + "\n"
+ + this.scope
+ + "\n"
+ + this.canonicalRequestHash;
+ }
+
+ private void setChunkStringToSign()
+ throws NoSuchAlgorithmException
+ {
+ this.stringToSign =
+ "AWS4-HMAC-SHA256-PAYLOAD"
+ + "\n"
+ + this.date.format(Time.AMZ_DATE_FORMAT)
+ + "\n"
+ + this.scope
+ + "\n"
+ + this.prevSignature
+ + "\n"
+ + Digest.sha256Hash("")
+ + "\n"
+ + this.contentSha256;
+ }
+
+ private void setSigningKey(String serviceName)
+ throws NoSuchAlgorithmException, InvalidKeyException
+ {
+ String aws4SecretKey = "AWS4" + this.secretKey;
+
+ byte[] dateKey =
+ sumHmac(
+ aws4SecretKey.getBytes(StandardCharsets.UTF_8),
+ this.date.format(Time.SIGNER_DATE_FORMAT).getBytes(StandardCharsets.UTF_8));
+
+ byte[] dateRegionKey = sumHmac(dateKey, this.region.getBytes(StandardCharsets.UTF_8));
+
+ byte[] dateRegionServiceKey =
+ sumHmac(dateRegionKey, serviceName.getBytes(StandardCharsets.UTF_8));
+
+ this.signingKey =
+ sumHmac(dateRegionServiceKey, "aws4_request".getBytes(StandardCharsets.UTF_8));
+ }
+
+ private void setSignature()
+ throws NoSuchAlgorithmException, InvalidKeyException
+ {
+ byte[] digest = sumHmac(this.signingKey, this.stringToSign.getBytes(StandardCharsets.UTF_8));
+ this.signature = BaseEncoding.base16().encode(digest).toLowerCase(Locale.US);
+ }
+
+ private void setAuthorization()
+ {
+ this.authorization =
+ "AWS4-HMAC-SHA256 Credential="
+ + this.accessKey
+ + "/"
+ + this.scope
+ + ", SignedHeaders="
+ + this.signedHeaders
+ + ", Signature="
+ + this.signature;
+ }
+
+ /**
+ * Returns chunk signature calculated using given arguments.
+ */
+ public static String getChunkSignature(
+ String chunkSha256, ZonedDateTime date, String region, String secretKey, String prevSignature)
+ throws NoSuchAlgorithmException, InvalidKeyException
+ {
+ Signer signer = new Signer(null, chunkSha256, date, region, null, secretKey, prevSignature);
+ signer.setScope("s3");
+ signer.setChunkStringToSign();
+ signer.setSigningKey("s3");
+ signer.setSignature();
+
+ return signer.signature;
+ }
+
+ /**
+ * Returns signed request object for given request, region, access key and secret key.
+ */
+ private static MinioHeaders signV4(
+ String serviceName,
+ MinioRequest request,
+ String region,
+ String accessKey,
+ String secretKey,
+ String contentSha256)
+ throws NoSuchAlgorithmException, InvalidKeyException
+ {
+ ZonedDateTime date = ZonedDateTime.parse(request.headerValue("x-amz-date").orElseThrow(), Time.AMZ_DATE_FORMAT);
+
+ Signer signer = new Signer(request, contentSha256, date, region, accessKey, secretKey, null);
+ signer.setScope(serviceName);
+ signer.setCanonicalRequest();
+ signer.setStringToSign();
+ signer.setSigningKey(serviceName);
+ signer.setSignature();
+ signer.setAuthorization();
+
+ return MinioHeaders.of("Authorization", signer.authorization);
+ }
+
+ /**
+ * Returns signed request of given request for S3 service.
+ */
+ public static MinioHeaders signV4S3(
+ MinioRequest request, String region, String accessKey, String secretKey, String contentSha256)
+ throws NoSuchAlgorithmException, InvalidKeyException
+ {
+ return signV4("s3", request, region, accessKey, secretKey, contentSha256);
+ }
+
+ /**
+ * Returns signed request of given request for STS service.
+ */
+ public static MinioHeaders signV4Sts(
+ MinioRequest request, String region, String accessKey, String secretKey, String contentSha256)
+ throws NoSuchAlgorithmException, InvalidKeyException
+ {
+ return signV4("sts", request, region, accessKey, secretKey, contentSha256);
+ }
+
+ private void setPresignCanonicalRequest(int expires)
+ throws NoSuchAlgorithmException
+ {
+ setCanonicalHeaders(PRESIGN_IGNORED_HEADERS);
+
+ List parts = new ArrayList<>();
+ parts.add(String.join("=", S3Escaper.encode("X-Amz-Algorithm"), S3Escaper.encode("AWS4-HMAC-SHA256")));
+ parts.add(String.join("=", S3Escaper.encode("X-Amz-Credential"), S3Escaper.encode(this.accessKey + "/" + this.scope)));
+ parts.add(String.join("=", S3Escaper.encode("X-Amz-Date"), S3Escaper.encode(this.date.format(Time.AMZ_DATE_FORMAT))));
+ parts.add(String.join("=", S3Escaper.encode("X-Amz-Expires"), S3Escaper.encode(Integer.toString(expires))));
+ parts.add(String.join("=", S3Escaper.encode("X-Amz-SignedHeaders"), S3Escaper.encode(this.signedHeaders)));
+ String query = String.join("&", parts);
+ this.url = this.url.appendQuery(query);
+
+ setCanonicalQueryString();
+
+ this.canonicalRequest =
+ this.request.httpMethod()
+ + "\n"
+ + this.url.encodedPath()
+ + "\n"
+ + this.canonicalQueryString
+ + "\n"
+ + Joiner.on("\n").withKeyValueSeparator(":").join(this.canonicalHeaders)
+ + "\n\n"
+ + this.signedHeaders
+ + "\n"
+ + this.contentSha256;
+
+ this.canonicalRequestHash = Digest.sha256Hash(this.canonicalRequest);
+ }
+
+ /**
+ * Returns pre-signed HttpUrl object for given request, region, access key, secret key and expires
+ * time.
+ */
+ public static MinioUrl presignV4(
+ MinioRequest request, String region, String accessKey, String secretKey, int expires)
+ throws NoSuchAlgorithmException, InvalidKeyException
+ {
+ String contentSha256 = "UNSIGNED-PAYLOAD";
+ ZonedDateTime date = ZonedDateTime.parse(request.headerValue("x-amz-date").orElseThrow(), Time.AMZ_DATE_FORMAT);
+
+ Signer signer = new Signer(request, contentSha256, date, region, accessKey, secretKey, null);
+ signer.setScope("s3");
+ signer.setPresignCanonicalRequest(expires);
+ signer.setStringToSign();
+ signer.setSigningKey("s3");
+ signer.setSignature();
+
+ String query = String.join("=", S3Escaper.encode("X-Amz-Signature"), S3Escaper.encode(signer.signature));
+
+ return signer
+ .url
+ .appendQuery(query);
+ }
+
+ /**
+ * Returns credential string of given access key, date and region.
+ */
+ public static String credential(String accessKey, ZonedDateTime date, String region)
+ {
+ return accessKey
+ + "/"
+ + date.format(Time.SIGNER_DATE_FORMAT)
+ + "/"
+ + region
+ + "/s3/aws4_request";
+ }
+
+ /**
+ * Returns pre-signed post policy string for given stringToSign, secret key, date and region.
+ */
+ public static String postPresignV4(
+ String stringToSign, String secretKey, ZonedDateTime date, String region)
+ throws NoSuchAlgorithmException, InvalidKeyException
+ {
+ Signer signer = new Signer(null, null, date, region, null, secretKey, null);
+ signer.stringToSign = stringToSign;
+ signer.setSigningKey("s3");
+ signer.setSignature();
+
+ return signer.signature;
+ }
+
+ /**
+ * Returns HMacSHA256 digest of given key and data.
+ */
+ public static byte[] sumHmac(byte[] key, byte[] data)
+ throws NoSuchAlgorithmException, InvalidKeyException
+ {
+ Mac mac = Mac.getInstance("HmacSHA256");
+
+ mac.init(new SecretKeySpec(key, "HmacSHA256"));
+ mac.update(data);
+
+ return mac.doFinal();
+ }
+}
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Time.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Time.java
new file mode 100644
index 00000000..a977f782
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Time.java
@@ -0,0 +1,46 @@
+/*
+ * MinIO Java SDK for Amazon S3 Compatible Cloud Storage,
+ * (C) 2020 MinIO, Inc.
+ *
+ * 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 io.trino.s3.proxy.server.minio;
+
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+
+/** Time formatters for S3 APIs. */
+public class Time {
+ public static final ZoneId UTC = ZoneId.of("Z");
+
+ public static final DateTimeFormatter AMZ_DATE_FORMAT =
+ DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'", Locale.US).withZone(UTC);
+
+ public static final DateTimeFormatter RESPONSE_DATE_FORMAT =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH':'mm':'ss'.'SSS'Z'", Locale.US).withZone(UTC);
+
+ // Formatted string is convertible to LocalDate only, not to LocalDateTime or ZonedDateTime.
+ // Below example shows how to use this to get ZonedDateTime.
+ // LocalDate.parse("20200225", SIGNER_DATE_FORMAT).atStartOfDay(UTC);
+ public static final DateTimeFormatter SIGNER_DATE_FORMAT =
+ DateTimeFormatter.ofPattern("yyyyMMdd", Locale.US).withZone(UTC);
+
+ public static final DateTimeFormatter HTTP_HEADER_DATE_FORMAT =
+ DateTimeFormatter.ofPattern("EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'", Locale.US).withZone(UTC);
+
+ public static final DateTimeFormatter EXPIRATION_DATE_FORMAT = RESPONSE_DATE_FORMAT;
+
+ private Time() {}
+}
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioHeaders.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioHeaders.java
new file mode 100644
index 00000000..fd2db5b7
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioHeaders.java
@@ -0,0 +1,31 @@
+/*
+ * 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 io.trino.s3.proxy.server.minio.emulation;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+public record MinioHeaders(Map headers)
+{
+ public MinioHeaders
+ {
+ headers = ImmutableMap.copyOf(headers);
+ }
+
+ public static MinioHeaders of(String name, String value)
+ {
+ return new MinioHeaders(ImmutableMap.of(name, value));
+ }
+}
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioRequest.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioRequest.java
new file mode 100644
index 00000000..9741457a
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioRequest.java
@@ -0,0 +1,38 @@
+/*
+ * 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 io.trino.s3.proxy.server.minio.emulation;
+
+import jakarta.ws.rs.core.MultivaluedMap;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+public interface MinioRequest
+{
+ Collection headerNames();
+
+ List headerValues(String name);
+
+ Optional headerValue(String name);
+
+ String httpMethod();
+
+ MinioUrl url();
+
+ static MinioRequest build(MultivaluedMap headers, String httpMethod, MinioUrl url)
+ {
+ return new MinioRequestImpl(headers, httpMethod, url);
+ }
+}
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioRequestImpl.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioRequestImpl.java
new file mode 100644
index 00000000..85bc4b46
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioRequestImpl.java
@@ -0,0 +1,73 @@
+/*
+ * 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 io.trino.s3.proxy.server.minio.emulation;
+
+import com.google.common.collect.ImmutableList;
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.MultivaluedMap;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+
+import static java.util.Objects.requireNonNull;
+
+class MinioRequestImpl
+ implements MinioRequest
+{
+ private final MultivaluedMap headers;
+ private final String httpMethod;
+ private final MinioUrl url;
+
+ MinioRequestImpl(MultivaluedMap headers, String httpMethod, MinioUrl url)
+ {
+ requireNonNull(headers, "headers is null");
+ this.httpMethod = requireNonNull(httpMethod, "httpMethod is null");
+ this.url = requireNonNull(url, "url is null");
+
+ this.headers = new MultivaluedHashMap<>();
+ headers.forEach((key, values) -> this.headers.put(key.toLowerCase(Locale.ROOT), values));
+ }
+
+ @Override
+ public Collection headerNames()
+ {
+ return headers.keySet();
+ }
+
+ @Override
+ public List headerValues(String name)
+ {
+ return headers.getOrDefault(name, ImmutableList.of());
+ }
+
+ @Override
+ public Optional headerValue(String name)
+ {
+ return Optional.ofNullable(headers.getFirst(name));
+ }
+
+ @Override
+ public String httpMethod()
+ {
+ return httpMethod;
+ }
+
+ @Override
+ public MinioUrl url()
+ {
+ return url;
+ }
+}
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioUrl.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioUrl.java
new file mode 100644
index 00000000..f22a9c48
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioUrl.java
@@ -0,0 +1,28 @@
+/*
+ * 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 io.trino.s3.proxy.server.minio.emulation;
+
+public interface MinioUrl
+{
+ String encodedQuery();
+
+ String encodedPath();
+
+ MinioUrl appendQuery(String query);
+
+ static MinioUrl build(String encodedPath, String encodedQuery)
+ {
+ return new MinioUrlImpl(encodedPath, encodedQuery);
+ }
+}
diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioUrlImpl.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioUrlImpl.java
new file mode 100644
index 00000000..5cc78ab2
--- /dev/null
+++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioUrlImpl.java
@@ -0,0 +1,47 @@
+/*
+ * 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 io.trino.s3.proxy.server.minio.emulation;
+
+import static java.util.Objects.requireNonNull;
+
+class MinioUrlImpl
+ implements MinioUrl
+{
+ private final String encodedPath;
+ private final String encodedQuery;
+
+ MinioUrlImpl(String encodedPath, String encodedQuery)
+ {
+ this.encodedPath = requireNonNull(encodedPath, "encodedPath is null");
+ this.encodedQuery = requireNonNull(encodedQuery, "encodedQuery is null");
+ }
+
+ @Override
+ public String encodedQuery()
+ {
+ return encodedQuery;
+ }
+
+ @Override
+ public String encodedPath()
+ {
+ return encodedPath;
+ }
+
+ @Override
+ public MinioUrl appendQuery(String query)
+ {
+ return new MinioUrlImpl(encodedPath, encodedQuery.isEmpty() ? query : (encodedPath + "&" + query));
+ }
+}
diff --git a/trino-s3-proxy/src/test/java/io/trino/s3/proxy/server/TestSigningController.java b/trino-s3-proxy/src/test/java/io/trino/s3/proxy/server/TestSigningController.java
new file mode 100644
index 00000000..87d330a1
--- /dev/null
+++ b/trino-s3-proxy/src/test/java/io/trino/s3/proxy/server/TestSigningController.java
@@ -0,0 +1,68 @@
+/*
+ * 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 io.trino.s3.proxy.server;
+
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.MultivaluedMap;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TestSigningController
+{
+ private static final Credentials CREDENTIALS = new Credentials("THIS_IS_AN_ACCESS_KEY", "THIS_IS_A_SECRET_KEY");
+
+ @Test
+ public void testRootLs()
+ {
+ CredentialsController credentialsController = accessKey -> Optional.of(CREDENTIALS);
+ SigningController signingController = new SigningController(credentialsController);
+
+ // values discovered from an AWS CLI request sent to a dummy local HTTP server
+ MultivaluedMap requestHeaders = new MultivaluedHashMap<>();
+ requestHeaders.putSingle("X-Amz-Date", "20240516T024511Z");
+ requestHeaders.putSingle("X-Amz-Content-SHA256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
+ requestHeaders.putSingle("X-Amz-Security-Token", "FwoGZXIvYXdzEP3//////////wEaDG79rlcAjsgKPP9N3SKIAu7/Zvngne5Ov6kGrDcIIPUZYkGpwNbj8zNnbWgOhiqmOCM3hrk4NuH17mP5n3nC7urlXZxaTCywKpAHpO3YsvLXcwjlfaYFA0Au4oejwSbU9ybIlzPzrqz7lVesgCfJOV+rj5F5UAh19d7RpRpA6Vy4nxGBTTlCNIVbkW9fp2Esql2/vsdh77rAG+j+BQegtegDCKBfen4gHMdvEOF6hyc4ne43eLXjpvUKxBgpI9MjOHtNHrDbOOBFXDDyknoESgE9Hsm12nDuVQhwrI/hhA4YB/MSIpl4FTgVs2sQP3K+v65tmyvIlpL6O78S6spMM9Tv/F4JLtksTzb90w46uZk9sxKC/RBkRijisM6tBjIrr/0znxnW3i5ggGAX4H/Z3aWlxSdzNs2UGWtqig9Plp3Xa9gG+zCKcXmDAA==");
+ requestHeaders.putSingle("Host", "localhost:10064");
+ requestHeaders.putSingle("User-Agent", "aws-cli/2.15.16 Python/3.11.7 Darwin/22.6.0 source/x86_64 prompt/off command/s3.ls");
+ requestHeaders.putSingle("Accept-Encoding", "identity");
+
+ Map signedHeaders = signingController.signedRequestHeaders("GET", requestHeaders, "/", "", "us-east-1", "THIS_IS_AN_ACCESS_KEY");
+
+ assertThat(signedHeaders).contains(Map.entry("Authorization", "AWS4-HMAC-SHA256 Credential=THIS_IS_AN_ACCESS_KEY/20240516/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=9a19c251bf4e1533174e80da59fa57c65b3149b611ec9a4104f6944767c25704"));
+ }
+
+ @Test
+ public void testBucketLs()
+ {
+ CredentialsController credentialsController = accessKey -> Optional.of(CREDENTIALS);
+ SigningController signingController = new SigningController(credentialsController);
+
+ // values discovered from an AWS CLI request sent to a dummy local HTTP server
+ MultivaluedMap requestHeaders = new MultivaluedHashMap<>();
+ requestHeaders.putSingle("X-Amz-Date", "20240516T034003Z");
+ requestHeaders.putSingle("X-Amz-Content-SHA256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
+ requestHeaders.putSingle("X-Amz-Security-Token", "FwoGZXIvYXdzEP3//////////wEaDG79rlcAjsgKPP9N3SKIAu7/Zvngne5Ov6kGrDcIIPUZYkGpwNbj8zNnbWgOhiqmOCM3hrk4NuH17mP5n3nC7urlXZxaTCywKpAHpO3YsvLXcwjlfaYFA0Au4oejwSbU9ybIlzPzrqz7lVesgCfJOV+rj5F5UAh19d7RpRpA6Vy4nxGBTTlCNIVbkW9fp2Esql2/vsdh77rAG+j+BQegtegDCKBfen4gHMdvEOF6hyc4ne43eLXjpvUKxBgpI9MjOHtNHrDbOOBFXDDyknoESgE9Hsm12nDuVQhwrI/hhA4YB/MSIpl4FTgVs2sQP3K+v65tmyvIlpL6O78S6spMM9Tv/F4JLtksTzb90w46uZk9sxKC/RBkRijisM6tBjIrr/0znxnW3i5ggGAX4H/Z3aWlxSdzNs2UGWtqig9Plp3Xa9gG+zCKcXmDAA==");
+ requestHeaders.putSingle("Host", "localhost:10064");
+ requestHeaders.putSingle("User-Agent", "aws-cli/2.15.16 Python/3.11.7 Darwin/22.6.0 source/x86_64 prompt/off command/s3.ls");
+ requestHeaders.putSingle("Accept-Encoding", "identity");
+
+ Map signedHeaders = signingController.signedRequestHeaders("GET", requestHeaders, "/mybucket", "list-type=2&prefix=foo%2Fbar&delimiter=%2F&encoding-type=url", "us-east-1", "THIS_IS_AN_ACCESS_KEY");
+
+ assertThat(signedHeaders).contains(Map.entry("Authorization", "AWS4-HMAC-SHA256 Credential=THIS_IS_AN_ACCESS_KEY/20240516/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=222d7b7fcd4d5560c944e8fecd9424ee3915d131c3ad9e000d65db93e87946c4"));
+ }
+}