1
+ /*
2
+ * Copyright OpenSearch Contributors
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ package org .opensearch .flint .core .auth ;
7
+
8
+ import com .amazonaws .auth .AWSSessionCredentials ;
9
+ import com .amazonaws .auth .AWSCredentialsProvider ;
10
+ import com .amazonaws .services .glue .model .InvalidStateException ;
11
+ import org .apache .http .Header ;
12
+ import org .apache .http .HttpEntity ;
13
+ import org .apache .http .HttpEntityEnclosingRequest ;
14
+ import org .apache .http .HttpHost ;
15
+ import org .apache .http .HttpRequest ;
16
+ import org .apache .http .HttpRequestInterceptor ;
17
+ import org .apache .http .client .utils .URIBuilder ;
18
+ import org .apache .http .entity .BasicHttpEntity ;
19
+ import org .apache .http .message .BasicHeader ;
20
+ import org .apache .http .protocol .HttpContext ;
21
+ import software .amazon .awssdk .auth .credentials .AwsSessionCredentials ;
22
+ import software .amazon .awssdk .auth .signer .AwsSignerExecutionAttribute ;
23
+ import software .amazon .awssdk .core .interceptor .ExecutionAttributes ;
24
+ import software .amazon .awssdk .core .signer .Signer ;
25
+ import software .amazon .awssdk .http .SdkHttpFullRequest ;
26
+ import software .amazon .awssdk .http .SdkHttpMethod ;
27
+ import software .amazon .awssdk .regions .Region ;
28
+
29
+ import java .io .IOException ;
30
+ import java .io .InputStream ;
31
+ import java .io .UncheckedIOException ;
32
+ import java .net .URISyntaxException ;
33
+ import java .util .ArrayList ;
34
+ import java .util .List ;
35
+ import java .util .Map ;
36
+ import java .util .TreeMap ;
37
+ import java .util .logging .Level ;
38
+ import java .util .logging .Logger ;
39
+
40
+ import static org .apache .http .protocol .HttpCoreContext .HTTP_TARGET_HOST ;
41
+ import static org .opensearch .flint .core .auth .AWSRequestSigningApacheInterceptor .nvpToMapParams ;
42
+ import static org .opensearch .flint .core .auth .AWSRequestSigningApacheInterceptor .skipHeader ;
43
+
44
+ /**
45
+ * Interceptor for signing AWS requests according to Signature Version 4A.
46
+ * This interceptor processes HTTP requests, signs them with AWS credentials,
47
+ * and updates the request headers to include the signature.
48
+ */
49
+ public class AWSRequestSigV4ASigningApacheInterceptor implements HttpRequestInterceptor {
50
+ private static final Logger LOG = Logger .getLogger (AWSRequestSigV4ASigningApacheInterceptor .class .getName ());
51
+
52
+ private static final String HTTPS_PROTOCOL = "https" ;
53
+ private static final int HTTPS_PORT = 443 ;
54
+
55
+ private final String service ;
56
+ private final String region ;
57
+ private final Signer signer ;
58
+ private final AWSCredentialsProvider awsCredentialsProvider ;
59
+
60
+ /**
61
+ * Constructs an interceptor for AWS request signing with metadata access.
62
+ *
63
+ * @param service The AWS service name.
64
+ * @param region The AWS region for signing.
65
+ * @param signer The signer implementation.
66
+ * @param awsCredentialsProvider The credentials provider for metadata access.
67
+ */
68
+ public AWSRequestSigV4ASigningApacheInterceptor (String service , String region , Signer signer , AWSCredentialsProvider awsCredentialsProvider ) {
69
+ this .service = service ;
70
+ this .region = region ;
71
+ this .signer = signer ;
72
+ this .awsCredentialsProvider = awsCredentialsProvider ;
73
+ }
74
+
75
+ /**
76
+ * Processes and signs an HTTP request, updating its headers with the signature.
77
+ *
78
+ * @param request the HTTP request to process and sign.
79
+ * @param context the HTTP context associated with the request.
80
+ * @throws IOException if an I/O error occurs during request processing.
81
+ */
82
+ @ Override
83
+ public void process (HttpRequest request , HttpContext context ) throws IOException {
84
+ SdkHttpFullRequest requestToSign = buildSdkHttpRequest (request , context );
85
+ SdkHttpFullRequest signedRequest = signRequest (requestToSign );
86
+ updateRequestHeaders (request , signedRequest .headers ());
87
+ updateRequestEntity (request , signedRequest );
88
+ }
89
+
90
+ /**
91
+ * Builds an {@link SdkHttpFullRequest} from the Apache {@link HttpRequest}.
92
+ *
93
+ * @param request the HTTP request to process and sign.
94
+ * @param context the HTTP context associated with the request.
95
+ * @return an SDK HTTP request ready to be signed.
96
+ * @throws IOException if an error occurs while building the request.
97
+ */
98
+ private SdkHttpFullRequest buildSdkHttpRequest (HttpRequest request , HttpContext context ) throws IOException {
99
+ URIBuilder uriBuilder = parseUri (request );
100
+ SdkHttpFullRequest .Builder builder = SdkHttpFullRequest .builder ()
101
+ .method (SdkHttpMethod .fromValue (request .getRequestLine ().getMethod ()))
102
+ .protocol (HTTPS_PROTOCOL )
103
+ .port (HTTPS_PORT )
104
+ .headers (headerArrayToMap (request .getAllHeaders ()))
105
+ .rawQueryParameters (nvpToMapParams (uriBuilder .getQueryParams ()));
106
+
107
+ HttpHost host = (HttpHost ) context .getAttribute (HTTP_TARGET_HOST );
108
+ if (host == null ) {
109
+ throw new InvalidStateException ("Host must not be null" );
110
+ }
111
+ builder .host (host .getHostName ());
112
+ try {
113
+ builder .encodedPath (uriBuilder .build ().getRawPath ());
114
+ } catch (URISyntaxException e ) {
115
+ throw new IOException ("Invalid URI" , e );
116
+ }
117
+ setRequestEntity (request , builder );
118
+ return builder .build ();
119
+ }
120
+
121
+ /**
122
+ * Sets the request entity for the {@link SdkHttpFullRequest.Builder} if the original request contains an entity.
123
+ * This is used for requests that have a body, such as POST or PUT requests.
124
+ *
125
+ * @param request the original HTTP request.
126
+ * @param builder the SDK HTTP request builder.
127
+ */
128
+ private void setRequestEntity (HttpRequest request , SdkHttpFullRequest .Builder builder ) {
129
+ if (request instanceof HttpEntityEnclosingRequest ) {
130
+ HttpEntity entity = ((HttpEntityEnclosingRequest ) request ).getEntity ();
131
+ if (entity != null ) {
132
+ builder .contentStreamProvider (() -> {
133
+ try {
134
+ return entity .getContent ();
135
+ } catch (IOException e ) {
136
+ throw new UncheckedIOException (e );
137
+ }
138
+ });
139
+ }
140
+ }
141
+ }
142
+
143
+ private URIBuilder parseUri (HttpRequest request ) throws IOException {
144
+ try {
145
+ return new URIBuilder (request .getRequestLine ().getUri ());
146
+ } catch (URISyntaxException e ) {
147
+ throw new IOException ("Invalid URI" , e );
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Signs the given SDK HTTP request using the provided AWS credentials and signer.
153
+ *
154
+ * @param request the SDK HTTP request to sign.
155
+ * @return a signed SDK HTTP request.
156
+ */
157
+ private SdkHttpFullRequest signRequest (SdkHttpFullRequest request ) {
158
+ AWSSessionCredentials sessionCredentials = (AWSSessionCredentials ) awsCredentialsProvider .getCredentials ();
159
+ AwsSessionCredentials awsCredentials = AwsSessionCredentials .create (
160
+ sessionCredentials .getAWSAccessKeyId (),
161
+ sessionCredentials .getAWSSecretKey (),
162
+ sessionCredentials .getSessionToken ()
163
+ );
164
+
165
+ ExecutionAttributes executionAttributes = new ExecutionAttributes ()
166
+ .putAttribute (AwsSignerExecutionAttribute .AWS_CREDENTIALS , awsCredentials )
167
+ .putAttribute (AwsSignerExecutionAttribute .SERVICE_SIGNING_NAME , service )
168
+ .putAttribute (AwsSignerExecutionAttribute .SIGNING_REGION , Region .of (region ));
169
+
170
+ try {
171
+ return signer .sign (request , executionAttributes );
172
+ } catch (Exception e ) {
173
+ LOG .log (Level .SEVERE , "Error Sigv4a signing the request" , e );
174
+ throw e ;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Updates the HTTP request headers with the signed headers.
180
+ *
181
+ * @param request the original HTTP request.
182
+ * @param signedHeaders the headers after signing.
183
+ */
184
+ private void updateRequestHeaders (HttpRequest request , Map <String , List <String >> signedHeaders ) {
185
+ Header [] headers = convertHeaderMapToArray (signedHeaders );
186
+ request .setHeaders (headers );
187
+ }
188
+
189
+ /**
190
+ * Updates the request entity based on the signed request. This is used to update the request body after signing.
191
+ *
192
+ * @param request the original HTTP request.
193
+ * @param signedRequest the signed SDK HTTP request.
194
+ */
195
+ private void updateRequestEntity (HttpRequest request , SdkHttpFullRequest signedRequest ) {
196
+ if (request instanceof HttpEntityEnclosingRequest ) {
197
+ HttpEntityEnclosingRequest httpEntityEnclosingRequest = (HttpEntityEnclosingRequest ) request ;
198
+ signedRequest .contentStreamProvider ().ifPresent (provider -> {
199
+ InputStream contentStream = provider .newStream ();
200
+ BasicHttpEntity basicHttpEntity = new BasicHttpEntity ();
201
+ basicHttpEntity .setContent (contentStream );
202
+ signedRequest .firstMatchingHeader ("Content-Length" ).ifPresent (value ->
203
+ basicHttpEntity .setContentLength (Long .parseLong (value )));
204
+ signedRequest .firstMatchingHeader ("Content-Type" ).ifPresent (basicHttpEntity ::setContentType );
205
+ httpEntityEnclosingRequest .setEntity (basicHttpEntity );
206
+ });
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Converts an array of {@link Header} objects into a map, consolidating multiple values for the same header name.
212
+ *
213
+ * @param headers the array of {@link Header} objects to convert.
214
+ * @return a map where each key is a header name and each value is a list of header values.
215
+ */
216
+ private static Map <String , List <String >> headerArrayToMap (final Header [] headers ) {
217
+ Map <String , List <String >> headersMap = new TreeMap <>(String .CASE_INSENSITIVE_ORDER );
218
+ for (Header header : headers ) {
219
+ if (!skipHeader (header )) {
220
+ headersMap .computeIfAbsent (header .getName (), k -> new ArrayList <>()).add (header .getValue ());
221
+ }
222
+ }
223
+ return headersMap ;
224
+ }
225
+
226
+ /**
227
+ * Converts a map of headers back into an array of {@link Header} objects.
228
+ *
229
+ * @param mapHeaders the map of headers to convert.
230
+ * @return an array of {@link Header} objects.
231
+ */
232
+ private Header [] convertHeaderMapToArray (final Map <String , List <String >> mapHeaders ) {
233
+ return mapHeaders .entrySet ().stream ()
234
+ .map (entry -> new BasicHeader (entry .getKey (), String .join ("," , entry .getValue ())))
235
+ .toArray (Header []::new );
236
+ }
237
+ }
0 commit comments