Skip to content

Commit b42457f

Browse files
committed
HSEARCH-5464 Make AWS signing compatible with other client impls
1 parent 53f8777 commit b42457f

File tree

22 files changed

+698
-195
lines changed

22 files changed

+698
-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: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
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;
1617
import org.hibernate.search.engine.cfg.ConfigurationPropertySource;
1718
import org.hibernate.search.engine.cfg.spi.ConfigurationProperty;
1819
import org.hibernate.search.engine.cfg.spi.OptionalConfigurationProperty;
@@ -23,7 +24,7 @@
2324
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
2425
import software.amazon.awssdk.regions.Region;
2526

26-
public class ElasticsearchAwsHttpClientConfigurer implements ElasticsearchHttpClientConfigurer {
27+
public class ElasticsearchAwsSigningInterceptorProvider implements ElasticsearchRequestInterceptorProvider {
2728
private static final Pattern DISTRIBUTION_NAME_PATTERN = Pattern.compile( "([^\\d]+)?(?:(?<=^)|(?=$)|(?<=.):(?=.))(.+)?" );
2829
private static final ConfigurationProperty<Boolean> SIGNING_ENABLED =
2930
ConfigurationProperty.forKey( ElasticsearchAwsBackendSettings.SIGNING_ENABLED )
@@ -59,12 +60,12 @@ public class ElasticsearchAwsHttpClientConfigurer implements ElasticsearchHttpCl
5960
.build();
6061

6162
@Override
62-
public void configure(ElasticsearchHttpClientConfigurationContext context) {
63+
public Optional<ElasticsearchRequestInterceptor> provide(Context context) {
6364
ConfigurationPropertySource propertySource = context.configurationPropertySource();
6465

6566
if ( !SIGNING_ENABLED.get( propertySource ) ) {
6667
AwsLog.INSTANCE.signingDisabled();
67-
return;
68+
return Optional.empty();
6869
}
6970

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

9293
AwsLog.INSTANCE.signingEnabled( region, service, credentialsProvider );
9394

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

97-
context.clientBuilder().addInterceptorLast( signingInterceptor );
98+
return Optional.of( signingInterceptor );
9899
}
99100

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

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.ElasticsearchRequestInterceptor;
13+
1314
import software.amazon.awssdk.http.ContentStreamProvider;
1415

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

19-
public HttpEntityContentStreamProvider(HttpEntity entity) {
20-
this.entity = entity;
20+
public HttpEntityContentStreamProvider(ElasticsearchRequestInterceptor.RequestContext requestContext) {
21+
this.requestContext = requestContext;
22+
}
23+
24+
public static HttpEntityContentStreamProvider create(ElasticsearchRequestInterceptor.RequestContext 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)