-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support serving of Virtual Host requests #51
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.collections; | ||
|
||
import jakarta.ws.rs.core.MultivaluedHashMap; | ||
import jakarta.ws.rs.core.MultivaluedMap; | ||
|
||
import java.util.List; | ||
import java.util.Locale; | ||
import java.util.function.BiFunction; | ||
|
||
public class MultiMapHelper | ||
{ | ||
public static <V> MultivaluedMap<String, V> lowercase(MultivaluedMap<String, V> map) | ||
{ | ||
return lowercase(map, (ignored, values) -> values); | ||
} | ||
|
||
public static <V> MultivaluedMap<String, V> lowercase(MultivaluedMap<String, V> map, BiFunction<String, List<V>, List<V>> valueMapper) | ||
{ | ||
MultivaluedMap<String, V> result = new MultivaluedHashMap<>(); | ||
map.forEach((name, values) -> result.put(name.toLowerCase(Locale.ROOT), valueMapper.apply(name.toLowerCase(Locale.ROOT), values))); | ||
return result; | ||
} | ||
|
||
private MultiMapHelper() {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
/* | ||
* 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.rest; | ||
|
||
import com.google.common.base.Splitter; | ||
import jakarta.ws.rs.core.MultivaluedMap; | ||
import jakarta.ws.rs.core.UriBuilder; | ||
import org.glassfish.jersey.server.ContainerRequest; | ||
|
||
import java.util.List; | ||
import java.util.Locale; | ||
import java.util.Optional; | ||
|
||
import static io.trino.s3.proxy.server.collections.MultiMapHelper.lowercase; | ||
import static java.util.Objects.requireNonNull; | ||
|
||
public record ParsedS3Request( | ||
String bucketName, | ||
String keyInBucket, | ||
MultivaluedMap<String, String> requestHeaders, | ||
MultivaluedMap<String, String> requestQueryParameters, | ||
String httpVerb) | ||
{ | ||
public ParsedS3Request | ||
{ | ||
requireNonNull(bucketName, "bucketName is null"); | ||
requireNonNull(keyInBucket, "keyInBucket is null"); | ||
requestHeaders = lowercase(requireNonNull(requestHeaders, "requestHeaders is null")); | ||
requireNonNull(requestQueryParameters, "requestQueryParameters is null"); | ||
requireNonNull(httpVerb, "httpVerb is null"); | ||
} | ||
|
||
public static ParsedS3Request fromRequest(String requestPath, MultivaluedMap<String, String> requestHeaders, MultivaluedMap<String, String> requestQueryParameters, String httpVerb, Optional<String> serverHostName) | ||
{ | ||
MultivaluedMap<String, String> headers = lowercase(requestHeaders); | ||
return serverHostName | ||
.flatMap(serverHostNameValue -> { | ||
String lowercaseServerHostName = serverHostNameValue.toLowerCase(Locale.ROOT); | ||
return Optional.ofNullable(headers.getFirst("host")) | ||
.map(value -> UriBuilder.fromUri("http://" + value.toLowerCase(Locale.ROOT)).build().getHost()) | ||
.filter(value -> value.endsWith(lowercaseServerHostName)) | ||
.map(value -> value.substring(0, value.length() - lowercaseServerHostName.length())) | ||
.map(value -> value.endsWith(".") ? value.substring(0, value.length() - 1) : value); | ||
}) | ||
.map(bucket -> new ParsedS3Request(bucket, requestPath, headers, requestQueryParameters, httpVerb)) | ||
.orElseGet(() -> { | ||
List<String> parts = Splitter.on("/").limit(2).splitToList(requestPath); | ||
if (parts.size() <= 1) { | ||
return new ParsedS3Request(requestPath, "", headers, requestQueryParameters, httpVerb); | ||
} | ||
return new ParsedS3Request(parts.get(0), parts.get(1), headers, requestQueryParameters, httpVerb); | ||
}); | ||
} | ||
|
||
public static ParsedS3Request fromRequest(String requestPath, ContainerRequest request, Optional<String> serverHostName) | ||
{ | ||
return fromRequest(requestPath, request.getHeaders(), request.getUriInfo().getQueryParameters(), request.getMethod(), serverHostName); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ | |
import jakarta.ws.rs.core.MultivaluedHashMap; | ||
import jakarta.ws.rs.core.MultivaluedMap; | ||
import jakarta.ws.rs.core.Response; | ||
import org.glassfish.jersey.server.ContainerRequest; | ||
import jakarta.ws.rs.core.UriBuilder; | ||
|
||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.Target; | ||
|
@@ -38,7 +38,6 @@ | |
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.Executors; | ||
|
||
import static com.google.common.base.MoreObjects.firstNonNull; | ||
import static com.google.common.util.concurrent.MoreExecutors.shutdownAndAwaitTermination; | ||
import static io.trino.s3.proxy.server.credentials.SigningController.formatRequestInstant; | ||
import static java.lang.annotation.ElementType.FIELD; | ||
|
@@ -75,14 +74,12 @@ public void shutDown() | |
} | ||
} | ||
|
||
public void proxyRequest(SigningMetadata signingMetadata, ContainerRequest request, AsyncResponse asyncResponse, String bucket) | ||
public void proxyRequest(SigningMetadata signingMetadata, ParsedS3Request request, AsyncResponse asyncResponse) | ||
{ | ||
String remotePath = rewriteRequestPath(request, bucket); | ||
|
||
URI remoteUri = remoteS3Facade.buildEndpoint(request.getUriInfo().getRequestUriBuilder(), remotePath, bucket, signingMetadata.region()); | ||
URI remoteUri = remoteS3Facade.buildEndpoint(UriBuilder.newInstance(), request.keyInBucket(), request.bucketName(), signingMetadata.region()); | ||
|
||
Request.Builder remoteRequestBuilder = new Request.Builder() | ||
.setMethod(request.getMethod()) | ||
.setMethod(request.httpVerb()) | ||
.setUri(remoteUri) | ||
.setFollowRedirects(true); | ||
|
||
|
@@ -91,9 +88,10 @@ public void proxyRequest(SigningMetadata signingMetadata, ContainerRequest reque | |
} | ||
|
||
MultivaluedMap<String, String> remoteRequestHeaders = new MultivaluedHashMap<>(); | ||
request.getRequestHeaders().forEach((key, value) -> { | ||
switch (key.toLowerCase()) { | ||
request.requestHeaders().forEach((key, value) -> { | ||
switch (key) { | ||
case "x-amz-security-token" -> {} // we add this below | ||
case "authorization" -> {} // we will create our own authorization header | ||
case "amz-sdk-invocation-id", "amz-sdk-request" -> {} // don't send these | ||
case "x-amz-date" -> remoteRequestHeaders.putSingle("X-Amz-Date", formatRequestInstant(Instant.now())); // use now for the remote request | ||
case "host" -> remoteRequestHeaders.putSingle("Host", buildRemoteHost(remoteUri)); // replace source host with the remote AWS host | ||
|
@@ -107,14 +105,13 @@ public void proxyRequest(SigningMetadata signingMetadata, ContainerRequest reque | |
.ifPresent(sessionToken -> remoteRequestHeaders.add("x-amz-security-token", sessionToken)); | ||
|
||
// set the new signed request auth header | ||
String encodedPath = firstNonNull(remoteUri.getRawPath(), ""); | ||
String signature = signingController.signRequest( | ||
signingMetadata, | ||
Credentials::requiredRemoteCredential, | ||
remoteUri, | ||
remoteRequestHeaders, | ||
request.getUriInfo().getQueryParameters(), | ||
request.getMethod(), | ||
request.requestQueryParameters(), | ||
request.httpVerb(), | ||
Optional.empty()); | ||
remoteRequestHeaders.putSingle("Authorization", signature); | ||
|
||
|
@@ -134,23 +131,4 @@ private static String buildRemoteHost(URI remoteUri) | |
} | ||
return remoteUri.getHost() + ":" + port; | ||
} | ||
|
||
private static String rewriteRequestPath(ContainerRequest request, String bucket) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe move There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where do you think we could move this to?
Happy to create a helper class though, perhaps There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure actually. Let's leave it. |
||
{ | ||
String path = "/" + request.getPath(false); | ||
if (!path.startsWith(TrinoS3ProxyRestConstants.S3_PATH)) { | ||
throw new WebApplicationException(Response.Status.BAD_REQUEST); | ||
} | ||
|
||
path = path.substring(TrinoS3ProxyRestConstants.S3_PATH.length()); | ||
if (path.startsWith("/" + bucket)) { | ||
path = path.substring(("/" + bucket).length()); | ||
} | ||
|
||
if (path.isEmpty() && bucket.isEmpty()) { | ||
path = "/"; | ||
} | ||
|
||
return path; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/* | ||
* 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.rest; | ||
|
||
import io.airlift.configuration.Config; | ||
import io.airlift.configuration.ConfigDescription; | ||
import jakarta.validation.constraints.NotNull; | ||
|
||
import java.util.Optional; | ||
|
||
public class TrinoS3ProxyConfig | ||
mosiac1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
private Optional<String> hostName = Optional.empty(); | ||
|
||
@Config("s3proxy.hostname") | ||
@ConfigDescription("Hostname to use for REST operations, virtual-host style addressing is only supported if this is set") | ||
public TrinoS3ProxyConfig setHostName(String hostName) | ||
{ | ||
this.hostName = Optional.ofNullable(hostName); | ||
return this; | ||
} | ||
|
||
@NotNull | ||
public Optional<String> getHostName() | ||
{ | ||
return hostName; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍