Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@danielprinz
Last active February 28, 2024 23:18
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danielprinz/edd82f8bde7d66c3293ba0b20f395892 to your computer and use it in GitHub Desktop.
Save danielprinz/edd82f8bde7d66c3293ba0b20f395892 to your computer and use it in GitHub Desktop.
Request Signing Interceptor adapted for AWS SDK v2
import static org.apache.http.protocol.HttpCoreContext.HTTP_TARGET_HOST;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.http.Header;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
import lombok.RequiredArgsConstructor;
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;
/**
* AWS SDK v2 version of: https://github.com/awslabs/aws-request-signing-apache-interceptor/blob/master/src/main/java/com/amazonaws/http/AWSRequestSigningApacheInterceptor.java
*/
@RequiredArgsConstructor
public class AWSRequestSigningInterceptor implements HttpRequestInterceptor {
private final Aws4Signer signer;
private final Aws4SignerParams params;
@Override
public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
URIBuilder uriBuilder;
try {
uriBuilder = new URIBuilder(request.getRequestLine().getUri());
} catch (URISyntaxException e) {
throw new IOException("Invalid URI" , e);
}
final SdkHttpFullRequest.Builder signableRequestBuilder = SdkHttpFullRequest.builder();
final HttpHost host = (HttpHost) context.getAttribute(HTTP_TARGET_HOST);
if (host != null) {
signableRequestBuilder.uri(URI.create(host.toURI()));
}
final SdkHttpMethod httpMethod =
SdkHttpMethod.fromValue(request.getRequestLine().getMethod());
signableRequestBuilder.method(httpMethod);
try {
signableRequestBuilder.encodedPath(uriBuilder.build().getRawPath());
} catch (URISyntaxException e) {
throw new IOException("Invalid URI" , e);
}
if (request instanceof HttpEntityEnclosingRequest) {
HttpEntityEnclosingRequest httpEntityEnclosingRequest =
(HttpEntityEnclosingRequest) request;
if (httpEntityEnclosingRequest.getEntity() != null) {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
httpEntityEnclosingRequest.getEntity().writeTo(outputStream);
signableRequestBuilder.contentStreamProvider(() -> new ByteArrayInputStream(outputStream.toByteArray()));
}
}
// Append Parameters and Headers
nvpToMapParams(uriBuilder.getQueryParams()).forEach(signableRequestBuilder::appendRawQueryParameter);
headerArrayToMap(request.getAllHeaders()).forEach(signableRequestBuilder::appendHeader);
// Sign it
final SdkHttpFullRequest signedRequest = signer.sign(signableRequestBuilder.build(), params);
// Now copy everything back
request.setHeaders(mapToHeaderArray(signedRequest.headers()));
if (request instanceof HttpEntityEnclosingRequest) {
HttpEntityEnclosingRequest httpEntityEnclosingRequest =
(HttpEntityEnclosingRequest) request;
if (httpEntityEnclosingRequest.getEntity() != null) {
BasicHttpEntity basicHttpEntity = new BasicHttpEntity();
if (signedRequest.contentStreamProvider().isPresent()) {
basicHttpEntity.setContent(signedRequest.contentStreamProvider().get().newStream());
} else {
throw new RuntimeException("Empty content stream was not expected!");
}
httpEntityEnclosingRequest.setEntity(basicHttpEntity);
}
}
}
/**
*
* @param params list of HTTP query params as NameValuePairs
* @return a Multimap of HTTP query params
*/
private static Map<String, String> nvpToMapParams(final List<NameValuePair> params) {
Map<String, String> parameterMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (NameValuePair nvp : params) {
parameterMap.putIfAbsent(nvp.getName(), nvp.getValue());
}
return parameterMap;
}
/**
* @param headers modeled Header objects
* @return a Map of header entries
*/
private static Map<String, String> headerArrayToMap(final Header[] headers) {
Map<String, String> headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (Header header : headers) {
if (!skipHeader(header)) {
headersMap.put(header.getName(), header.getValue());
}
}
return headersMap;
}
/**
* @param header header line to check
* @return true if the given header should be excluded when signing
*/
private static boolean skipHeader(final Header header) {
return ("content-length".equalsIgnoreCase(header.getName())
&& "0".equals(header.getValue())) // Strip Content-Length: 0
|| "host".equalsIgnoreCase(header.getName()); // Host comes from endpoint
}
/**
* @param mapHeaders Map of header entries
* @return modeled Header objects
*/
private static Header[] mapToHeaderArray(final Map<String, List<String>> mapHeaders) {
Header[] headers = new Header[mapHeaders.size()];
int i = 0;
for (Map.Entry<String, List<String>> headerEntry : mapHeaders.entrySet()) {
for (String value : headerEntry.getValue()) {
headers[i++] = new BasicHeader(headerEntry.getKey(), value);
}
}
return headers;
}
}
@reinouts
Copy link

Hi @danielprinz - I'd like to adapt your code for my purposes. Under what license are you providing this code? Thanks!

@danielprinz
Copy link
Author

Hello @reinouts!
Feel free to use and adapt it. I would say the MIT License is appropriate.

Kind regards

@reinouts
Copy link

Thank you!

If you are using this code in an MIT-licensed project (in Github or elsewhere) please let me know, so that I can provide a PR at some point in the future.

@jakemraz
Copy link

I love it. really thanks!!!

@arkotamsetti
Copy link

Hi @danielprinz ,
Could please help me in understanding the DefaultCredentialsProvider.create().resolveCredentials() code snippet ?
Will it reads all three keys(aws_access_key_id,aws_secret_access_key and aws_session_token) ?
if yes then when token (aws_session_token) is got expired then how the SDK 2 gonna refresh it in AWSRequestSigningInterceptor.Java?
if no then what are all the keys that i going to read ?

@arkotamsetti
Copy link

I see below error when there is token(aws_session_token) expires

status line [HTTP/1.1 403 Forbidden]\n{"message":"The security token included in the request is expired"}\n\tat org.elasticsearch.client.RestClient.convertResponse(RestClient.java:302)\n\tat org.elasticsearch.client.RestClient.performRequest(RestClient.java:272)\n\tat org.elasticsearch.client.RestClient.performRequest(RestClient.java:246)\n\tat org.elasticsearch.client.RestHighLevelClient.internalPerformRequest(RestHighLevelClient.java:1613)\n\t...

@cuddihyge
Please anyone help me in resolving this

Thanks
AK

@arkotamsetti
Copy link

Hello @reinouts!
Feel free to use and adapt it. I would say the MIT License is appropriate.

Kind regards

Hi @danielprinz ,
Could please help me in understanding the DefaultCredentialsProvider.create().resolveCredentials() code snippet ?
Will it reads all three keys(aws_access_key_id,aws_secret_access_key and aws_session_token) ?
if yes then when token (aws_session_token) is got expired then how the SDK 2 gonna refresh it in AWSRequestSigningInterceptor.Java?
if no then what are all the keys that i going to read ?

@kasleet
Copy link

kasleet commented Nov 2, 2021

This will fail with an OutOfBounds exception when there are headers with multiple values.

private static Header[] mapToHeaderArray(final Map<String, List<String>> mapHeaders) {
    Header[] headers = new Header[mapHeaders.size()];
    int i = 0;
    for (Map.Entry<String, List<String>> headerEntry : mapHeaders.entrySet()) {
      for (String value : headerEntry.getValue()) {
        headers[i++] = new BasicHeader(headerEntry.getKey(), value);
      }
    }
    return headers;
  }

You are also caching the signing params, which contain the AWS credentials. After some time (~ 10 minutes), all requests will to ES will fail because the token has expired.

@kid1412621
Copy link

not working when enabled compression

@dblock
Copy link

dblock commented Jun 27, 2022

I've been trying to fix support with compression enabled, see https://github.com/dblock/opensearch-java-client-demo for a repro that does decompress gzip content and sets the correct headers, but that still doesn't work. I've engaged AWS SDK support at AWS.

@kid1412621
Copy link

I've been trying to fix support with compression enabled, see https://github.com/dblock/opensearch-java-client-demo for a repro that does decompress gzip content and sets the correct headers, but that still doesn't work. I've engaged AWS SDK support at AWS.

Great, thank you.

@dblock
Copy link

dblock commented Jul 7, 2022

Please head over to https://github.com/acm19/aws-request-signing-apache-interceptor. We have everything working except chunked request signing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment