Skip to content

Commit

Permalink
Implement support for request signing
Browse files Browse the repository at this point in the history
- Use AWS SDK signing code
- Added an initial test for a simple `aws s3 ls` based on this doc: https://min.io/docs/minio/linux/integrations/aws-cli-with-minio.html

Closes #5
  • Loading branch information
Randgalt committed May 19, 2024
1 parent 4b59e9d commit 0287a5f
Show file tree
Hide file tree
Showing 8 changed files with 337 additions and 10 deletions.
30 changes: 29 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@

<air.java.version>22.0.0</air.java.version>
<air.main.basedir>${project.basedir}</air.main.basedir>
<air.modernizer.java-version>8</air.modernizer.java-version>
<air.check.skip-spotbugs>true</air.check.skip-spotbugs>
<air.check.skip-pmd>true</air.check.skip-pmd>
<air.modernizer.java-version>8</air.modernizer.java-version>
<air.check.skip-modernizer>true</air.check.skip-modernizer>

<dep.airlift.version>245</dep.airlift.version>
<dep.aws-sdk.version>2.25.32</dep.aws-sdk.version>
</properties>

<dependencyManagement>
Expand All @@ -56,6 +58,32 @@
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>${dep.aws-sdk.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<configuration>
<licenseSets>
<licenseSet>
<excludes>
<exclude>**/io/trino/s3/proxy/server/credentials/Signer.java</exclude>
</excludes>
</licenseSet>
</licenseSets>
</configuration>
</plugin>
</plugins>
</build>
</project>
21 changes: 21 additions & 0 deletions trino-s3-proxy/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,27 @@
<artifactId>jakarta.ws.rs-api</artifactId>
</dependency>

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>auth</artifactId>
</dependency>

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>http-client-spi</artifactId>
</dependency>

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>regions</artifactId>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
package io.trino.s3.proxy.server;

import com.google.common.collect.ImmutableList;
import com.google.inject.Injector;
import com.google.inject.Module;
import io.airlift.bootstrap.Bootstrap;
import io.airlift.event.client.EventModule;
import io.airlift.http.server.HttpServerModule;
import io.airlift.http.server.testing.TestingHttpServer;
import io.airlift.jaxrs.JaxrsModule;
import io.airlift.json.JsonModule;
import io.airlift.log.Logger;
Expand All @@ -39,8 +41,13 @@ public static void main(String[] args)
.add(new JaxrsModule());

Bootstrap app = new Bootstrap(modules.build());
app.initialize();
Injector injector = app.initialize();

log.info("======== SERVER STARTED ========");

TestingHttpServer httpServer = injector.getInstance(TestingHttpServer.class);
log.info("");
log.info("Endpoint: %s", httpServer.getBaseUrl());
log.info("");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.credentials;

import static java.util.Objects.requireNonNull;

public record Credentials(Credential emulated, Credential real)
{
public Credentials
{
requireNonNull(emulated, "emulated is null");
requireNonNull(real, "real is null");
}

public record Credential(String accessKey, String secretKey)
{
public Credential
{
requireNonNull(accessKey, "accessKey is null");
requireNonNull(secretKey, "secretKey is null");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.s3.proxy.server;
package io.trino.s3.proxy.server.credentials;

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> credentials(String emulatedAccessKey);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package io.trino.s3.proxy.server.credentials;

import com.google.common.collect.ImmutableSet;
import com.google.common.net.HostAndPort;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.signer.Aws4Signer;
import software.amazon.awssdk.auth.signer.params.Aws4SignerParams;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.regions.Region;

import java.io.ByteArrayInputStream;
import java.net.URI;
import java.time.Clock;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;

final class Signer
{
private static final DateTimeFormatter AMZ_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'", Locale.US).withZone(ZoneId.of("Z"));

private static final Set<String> IGNORED_HEADERS = ImmutableSet.of(
"accept-encoding",
"authorization",
"user-agent",
"connection",
"content-length");

private static final Aws4Signer aws4Signer = Aws4Signer.create();

private Signer() {}

@SuppressWarnings("SameParameterValue")
static String sign(
String serviceName,
URI requestURI,
MultivaluedMap<String, String> requestHeaders,
MultivaluedMap<String, String> queryParameters,
String httpMethod,
String encodedPath,
String region,
String accessKey,
String secretKey,
Optional<byte[]> entity)
{
requestHeaders = lowercase(requestHeaders);
queryParameters = lowercase(queryParameters);

SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder()
.port(requestURI.getPort())
.protocol(requestURI.getScheme())
.host(HostAndPort.fromString(requestHeaders.getFirst("host")).getHost())
.method(SdkHttpMethod.fromValue(httpMethod))
.encodedPath(encodedPath);

entity.ifPresent(entityBytes -> requestBuilder.contentStreamProvider(() -> new ByteArrayInputStream(entityBytes)));

requestHeaders
.entrySet()
.stream()
.filter(entry -> !IGNORED_HEADERS.contains(entry.getKey()))
.forEach(entry -> entry.getValue().forEach(value -> requestBuilder.appendHeader(entry.getKey(), value)));

queryParameters.forEach(requestBuilder::putRawQueryParameter);

String xAmzDate = Optional.ofNullable(requestHeaders.getFirst("x-amz-date")).orElseThrow();
ZonedDateTime date = ZonedDateTime.parse(xAmzDate, AMZ_DATE_FORMAT);

Aws4SignerParams signerParams = Aws4SignerParams.builder()
.signingName(serviceName)
.signingRegion(Region.of(region))
.awsCredentials(AwsBasicCredentials.create(accessKey, secretKey))
.signingClockOverride(Clock.fixed(date.toInstant(), date.getZone()))
.build();

return aws4Signer.sign(requestBuilder.build(), signerParams).firstMatchingHeader("Authorization").orElseThrow();
}

private static MultivaluedMap<String, String> lowercase(MultivaluedMap<String, String> map)
{
MultivaluedMap<String, String> result = new MultivaluedHashMap<>();
map.forEach((name, values) -> result.put(name.toLowerCase(Locale.ROOT), values));
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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.credentials;

import com.google.inject.Inject;
import jakarta.ws.rs.core.MultivaluedMap;

import java.net.URI;
import java.util.Optional;

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 String signRequest(
URI requestURI,
MultivaluedMap<String, String> requestHeaders,
MultivaluedMap<String, String> queryParameters,
String httpMethod,
String encodedPath,
String region,
String accessKey)
{
// TODO
Credentials credentials = credentialsController.credentials(accessKey).orElseThrow();

return Signer.sign(
"s3",
requestURI,
requestHeaders,
queryParameters,
httpMethod,
encodedPath,
region,
accessKey,
credentials.emulated().secretKey(),
Optional.empty());
}
}
Loading

0 comments on commit 0287a5f

Please sign in to comment.