Skip to content

Commit 3792aba

Browse files
committed
HSEARCH-5464 Make AWS signing compatible with other client impls
1 parent 57ed41f commit 3792aba

File tree

24 files changed

+724
-195
lines changed

24 files changed

+724
-195
lines changed

backend/elasticsearch-aws/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
<dependencies>
2727
<dependency>
2828
<groupId>org.hibernate.search</groupId>
29-
<artifactId>hibernate-search-backend-elasticsearch</artifactId>
29+
<artifactId>hibernate-search-backend-elasticsearch-client-common</artifactId>
3030
</dependency>
3131
<dependency>
3232
<groupId>software.amazon.awssdk</groupId>

backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/AwsSigningRequestInterceptor.java

Lines changed: 0 additions & 156 deletions
This file was deleted.

backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/ElasticsearchAwsBeanConfigurer.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import org.hibernate.search.backend.elasticsearch.aws.cfg.ElasticsearchAwsCredentialsTypeNames;
88
import org.hibernate.search.backend.elasticsearch.aws.spi.ElasticsearchAwsCredentialsProvider;
9-
import org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurer;
9+
import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptorProvider;
1010
import org.hibernate.search.engine.environment.bean.BeanHolder;
1111
import org.hibernate.search.engine.environment.bean.spi.BeanConfigurationContext;
1212
import org.hibernate.search.engine.environment.bean.spi.BeanConfigurer;
@@ -17,8 +17,8 @@ public class ElasticsearchAwsBeanConfigurer implements BeanConfigurer {
1717
@Override
1818
public void configure(BeanConfigurationContext context) {
1919
context.define(
20-
ElasticsearchHttpClientConfigurer.class,
21-
beanResolver -> BeanHolder.of( new ElasticsearchAwsHttpClientConfigurer() )
20+
ElasticsearchRequestInterceptorProvider.class,
21+
beanResolver -> BeanHolder.of( new ElasticsearchAwsSigningInterceptorProvider() )
2222
);
2323
context.define(
2424
ElasticsearchAwsCredentialsProvider.class, ElasticsearchAwsCredentialsTypeNames.DEFAULT,
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
package org.hibernate.search.backend.elasticsearch.aws.impl;
66

77
import java.util.Locale;
8+
import java.util.Optional;
89
import java.util.regex.Pattern;
910

1011
import org.hibernate.search.backend.elasticsearch.aws.cfg.ElasticsearchAwsBackendSettings;
1112
import org.hibernate.search.backend.elasticsearch.aws.cfg.ElasticsearchAwsCredentialsTypeNames;
1213
import org.hibernate.search.backend.elasticsearch.aws.logging.impl.AwsLog;
1314
import org.hibernate.search.backend.elasticsearch.aws.spi.ElasticsearchAwsCredentialsProvider;
14-
import org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurationContext;
15-
import org.hibernate.search.backend.elasticsearch.client.rest.ElasticsearchHttpClientConfigurer;
15+
import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptor;
16+
import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptorProvider;
17+
import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptorProviderContext;
1618
import org.hibernate.search.engine.cfg.ConfigurationPropertySource;
1719
import org.hibernate.search.engine.cfg.spi.ConfigurationProperty;
1820
import org.hibernate.search.engine.cfg.spi.OptionalConfigurationProperty;
@@ -23,7 +25,7 @@
2325
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
2426
import software.amazon.awssdk.regions.Region;
2527

26-
public class ElasticsearchAwsHttpClientConfigurer implements ElasticsearchHttpClientConfigurer {
28+
public class ElasticsearchAwsSigningInterceptorProvider implements ElasticsearchRequestInterceptorProvider {
2729
private static final Pattern DISTRIBUTION_NAME_PATTERN = Pattern.compile( "([^\\d]+)?(?:(?<=^)|(?=$)|(?<=.):(?=.))(.+)?" );
2830
private static final ConfigurationProperty<Boolean> SIGNING_ENABLED =
2931
ConfigurationProperty.forKey( ElasticsearchAwsBackendSettings.SIGNING_ENABLED )
@@ -59,12 +61,12 @@ public class ElasticsearchAwsHttpClientConfigurer implements ElasticsearchHttpCl
5961
.build();
6062

6163
@Override
62-
public void configure(ElasticsearchHttpClientConfigurationContext context) {
64+
public Optional<ElasticsearchRequestInterceptor> provide(ElasticsearchRequestInterceptorProviderContext context) {
6365
ConfigurationPropertySource propertySource = context.configurationPropertySource();
6466

6567
if ( !SIGNING_ENABLED.get( propertySource ) ) {
6668
AwsLog.INSTANCE.signingDisabled();
67-
return;
69+
return Optional.empty();
6870
}
6971

7072
Region region = REGION.getAndMapOrThrow( propertySource, Region::of, AwsLog.INSTANCE::missingPropertyForSigning );
@@ -91,10 +93,10 @@ public void configure(ElasticsearchHttpClientConfigurationContext context) {
9193

9294
AwsLog.INSTANCE.signingEnabled( region, service, credentialsProvider );
9395

94-
AwsSigningRequestInterceptor signingInterceptor =
95-
new AwsSigningRequestInterceptor( region, service, credentialsProvider );
96+
ElasticsearchAwsSigningRequestInterceptor signingInterceptor =
97+
new ElasticsearchAwsSigningRequestInterceptor( region, service, credentialsProvider );
9698

97-
context.clientBuilder().addInterceptorLast( signingInterceptor );
99+
return Optional.of( signingInterceptor );
98100
}
99101

100102
private AwsCredentialsProvider createCredentialsProvider(BeanResolver beanResolver,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.search.backend.elasticsearch.aws.impl;
6+
7+
import java.io.IOException;
8+
9+
import org.hibernate.search.backend.elasticsearch.aws.logging.impl.AwsLog;
10+
import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptor;
11+
import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptorContext;
12+
13+
import software.amazon.awssdk.auth.credentials.AwsCredentials;
14+
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
15+
import software.amazon.awssdk.http.ContentStreamProvider;
16+
import software.amazon.awssdk.http.SdkHttpFullRequest;
17+
import software.amazon.awssdk.http.SdkHttpMethod;
18+
import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner;
19+
import software.amazon.awssdk.http.auth.spi.signer.SignedRequest;
20+
import software.amazon.awssdk.regions.Region;
21+
22+
class ElasticsearchAwsSigningRequestInterceptor implements ElasticsearchRequestInterceptor {
23+
24+
private final AwsV4HttpSigner signer;
25+
private final Region region;
26+
private final String service;
27+
private final AwsCredentialsProvider credentialsProvider;
28+
29+
ElasticsearchAwsSigningRequestInterceptor(Region region, String service, AwsCredentialsProvider credentialsProvider) {
30+
this.signer = AwsV4HttpSigner.create();
31+
this.region = region;
32+
this.service = service;
33+
this.credentialsProvider = credentialsProvider;
34+
}
35+
36+
@Override
37+
public void intercept(ElasticsearchRequestInterceptorContext requestContext) throws IOException {
38+
try ( HttpEntityContentStreamProvider contentStreamProvider =
39+
HttpEntityContentStreamProvider.create( requestContext ) ) {
40+
sign( requestContext, contentStreamProvider );
41+
}
42+
}
43+
44+
private void sign(ElasticsearchRequestInterceptorContext requestContext,
45+
HttpEntityContentStreamProvider contentStreamProvider) {
46+
SdkHttpFullRequest awsRequest = toAwsRequest( requestContext, contentStreamProvider );
47+
48+
if ( AwsLog.INSTANCE.isTraceEnabled() ) {
49+
AwsLog.INSTANCE.httpRequestBeforeSigning( requestContext );
50+
AwsLog.INSTANCE.awsRequestBeforeSigning( awsRequest );
51+
}
52+
53+
AwsCredentials credentials = credentialsProvider.resolveCredentials();
54+
AwsLog.INSTANCE.awsCredentials( credentials );
55+
56+
SignedRequest signedRequest = signer.sign( r -> r.identity( credentials )
57+
.request( awsRequest )
58+
.payload( awsRequest.contentStreamProvider().orElse( null ) )
59+
.putProperty( AwsV4HttpSigner.SERVICE_SIGNING_NAME, service )
60+
.putProperty( AwsV4HttpSigner.REGION_NAME, region.id() ) );
61+
62+
// The AWS SDK added some headers.
63+
// Let's just override the existing headers with whatever the AWS SDK came up with.
64+
// We don't expect signing to affect anything else (path, query, content, ...).
65+
requestContext.overrideHeaders( signedRequest.request().headers() );
66+
67+
if ( AwsLog.INSTANCE.isTraceEnabled() ) {
68+
AwsLog.INSTANCE.httpRequestAfterSigning( signedRequest );
69+
AwsLog.INSTANCE.awsRequestAfterSigning( requestContext );
70+
}
71+
}
72+
73+
private SdkHttpFullRequest toAwsRequest(
74+
ElasticsearchRequestInterceptorContext requestContext,
75+
ContentStreamProvider contentStreamProvider) {
76+
SdkHttpFullRequest.Builder awsRequestBuilder = SdkHttpFullRequest.builder();
77+
78+
awsRequestBuilder.host( requestContext.host() );
79+
awsRequestBuilder.port( requestContext.port() );
80+
awsRequestBuilder.protocol( requestContext.scheme() );
81+
82+
awsRequestBuilder.method( SdkHttpMethod.fromValue( requestContext.method() ) );
83+
84+
String path = requestContext.path();
85+
86+
// For some reason this is needed on Amazon OpenSearch Serverless
87+
if ( "aoss".equals( service ) ) {
88+
awsRequestBuilder.appendHeader( "x-amz-content-sha256", "required" );
89+
}
90+
91+
awsRequestBuilder.encodedPath( path );
92+
for ( var param : requestContext.queryParameters().entrySet() ) {
93+
awsRequestBuilder.appendRawQueryParameter( param.getKey(), param.getValue() );
94+
}
95+
96+
// Do NOT copy the headers, as the AWS SDK will sometimes sign some headers
97+
// that are not properly taken into account by the AWS servers (e.g. content-length).
98+
99+
awsRequestBuilder.contentStreamProvider( contentStreamProvider );
100+
101+
return awsRequestBuilder.build();
102+
}
103+
104+
}

backend/elasticsearch-aws/src/main/java/org/hibernate/search/backend/elasticsearch/aws/impl/HttpEntityContentStreamProvider.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,31 @@
99
import java.io.InputStream;
1010
import java.io.UncheckedIOException;
1111

12-
import org.apache.http.HttpEntity;
12+
import org.hibernate.search.backend.elasticsearch.client.common.spi.ElasticsearchRequestInterceptorContext;
13+
1314
import software.amazon.awssdk.http.ContentStreamProvider;
1415

1516
public class HttpEntityContentStreamProvider implements ContentStreamProvider, Closeable {
16-
private final HttpEntity entity;
17+
private final ElasticsearchRequestInterceptorContext requestContext;
1718
private InputStream previousStream;
1819

19-
public HttpEntityContentStreamProvider(HttpEntity entity) {
20-
this.entity = entity;
20+
public HttpEntityContentStreamProvider(ElasticsearchRequestInterceptorContext requestContext) {
21+
this.requestContext = requestContext;
22+
}
23+
24+
public static HttpEntityContentStreamProvider create(ElasticsearchRequestInterceptorContext requestContext) {
25+
if ( requestContext.hasContent() ) {
26+
return new HttpEntityContentStreamProvider( requestContext );
27+
}
28+
return null;
2129
}
2230

2331
@Override
2432
public InputStream newStream() {
2533
try {
2634
// Believe it or not, the AWS SDK expects us to close previous streams ourselves...
2735
close();
28-
InputStream newStream = entity.getContent();
36+
InputStream newStream = requestContext.content();
2937
previousStream = newStream;
3038
return newStream;
3139
}

0 commit comments

Comments
 (0)