diff --git a/doc_source/request-signing.md b/doc_source/request-signing.md index 6a3dc61..2436d66 100644 --- a/doc_source/request-signing.md +++ b/doc_source/request-signing.md @@ -16,11 +16,12 @@ The latest versions of the Elasticsearch clients might include license or versio ## Java -The easiest way of sending a signed request is to use the [Amazon Web Services request signing interceptor](https://github.com/awslabs/aws-request-signing-apache-interceptor)\. The repository contains some examples to help you get started, or you can [download a sample project for OpenSearch Service on GitHub](https://github.com/awsdocs/amazon-opensearch-service-developer-guide/blob/master/sample_code/java/amazon-opensearch-docs-sample-client.zip)\. +The easiest way of sending a signed request is to use the [Amazon Web Services request signing interceptor](https://github.com/awslabs/aws-request-signing-apache-interceptor)\. The repository contains some examples to help you get started, or you can [refer a sample project for OpenSearch Service on GitHub](https://github.com/awsdocs/amazon-opensearch-service-developer-guide/tree/master/sample_code/java)\. The following example uses the [opensearch\-java](https://github.com/opensearch-project/opensearch-java) low\-level Java REST client to perform two unrelated actions: registering a snapshot repository and indexing a document\. You must provide values for `region` and `host`\. ``` +import com.amazonaws.http.AwsRequestSigningApacheInterceptor; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.HttpRequestInterceptor; @@ -29,10 +30,10 @@ import org.apache.http.nio.entity.NStringEntity; import org.opensearch.client.Request; import org.opensearch.client.Response; import org.opensearch.client.RestClient; -import com.amazonaws.auth.AWS4Signer; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.http.AWSRequestSigningApacheInterceptor; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.signer.Aws4Signer; + import java.io.IOException; public class AmazonOpenSearchServiceSample { @@ -47,7 +48,7 @@ public class AmazonOpenSearchServiceSample { private static String sampleDocument = "{" + "\"title\":\"Walk the Line\"," + "\"director\":\"James Mangold\"," + "\"year\":\"2005\"}"; private static String indexingPath = "/my-index/_doc"; - static final AWSCredentialsProvider credentialsProvider = new DefaultAWSCredentialsProviderChain(); + static final AwsCredentialsProvider credentialsProvider = DefaultCredentialsProvider.create(); public static void main(String[] args) throws IOException { RestClient searchClient = searchClient(serviceName, region); @@ -75,18 +76,17 @@ public class AmazonOpenSearchServiceSample { // Adds the interceptor to the OpenSearch REST client public static RestClient searchClient(String serviceName, String region) { - AWS4Signer signer = new AWS4Signer(); - signer.setServiceName(serviceName); - signer.setRegionName(region); - HttpRequestInterceptor interceptor = new AWSRequestSigningApacheInterceptor(serviceName, signer, credentialsProvider); + Aws4Signer signer = Aws4Signer.create(); + HttpRequestInterceptor interceptor = new AwsRequestSigningApacheInterceptor(serviceName, signer, credentialsProvider,region); return RestClient.builder(HttpHost.create(host)).setHttpClientConfigCallback(hacb -> hacb.addInterceptorLast(interceptor)).build(); } } ``` -If you prefer the high\-level REST client, which offers most of the same features and simpler code, try the following sample, which also uses the [Amazon Web Services Request Signing Interceptor](https://github.com/awslabs/aws-request-signing-apache-interceptor): +If you prefer the high\-level REST client, which offers most of the same features and simpler code, try the following sample, which also uses the [Amazon Web Services Request Signing Interceptor](https://github.com/awsdocs/amazon-opensearch-service-developer-guide/tree/master/sample_code/java): ``` +import com.amazonaws.http.AwsRequestSigningApacheInterceptor; import org.apache.http.HttpHost; import org.apache.http.HttpRequestInterceptor; import org.opensearch.action.index.IndexRequest; @@ -94,10 +94,10 @@ import org.opensearch.action.index.IndexResponse; import org.opensearch.client.RequestOptions; import org.opensearch.client.RestClient; import org.opensearch.client.RestHighLevelClient; -import com.amazonaws.auth.AWS4Signer; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.http.AWSRequestSigningApacheInterceptor; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.signer.Aws4Signer; + import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -111,7 +111,7 @@ public class AmazonOpenSearchServiceSample { private static String type = "_doc"; private static String id = "1"; - static final AWSCredentialsProvider credentialsProvider = new DefaultAWSCredentialsProviderChain(); + static final AwsCredentialsProvider credentialsProvider = DefaultCredentialsProvider.create(); public static void main(String[] args) throws IOException { RestHighLevelClient searchClient = searchClient(serviceName, region); @@ -130,13 +130,10 @@ public class AmazonOpenSearchServiceSample { // Adds the interceptor to the OpenSearch REST client public static RestHighLevelClient searchClient(String serviceName, String region) { - AWS4Signer signer = new AWS4Signer(); - signer.setServiceName(serviceName); - signer.setRegionName(region); - HttpRequestInterceptor interceptor = new AWSRequestSigningApacheInterceptor(serviceName, signer, credentialsProvider); + Aws4Signer signer = Aws4Signer.create(); + HttpRequestInterceptor interceptor = new AwsRequestSigningApacheInterceptor(serviceName, signer, credentialsProvider,region); return new RestHighLevelClient(RestClient.builder(HttpHost.create(host)).setHttpClientConfigCallback(hacb -> hacb.addInterceptorLast(interceptor))); } -} ``` **Tip** diff --git a/sample_code/java/LICENSE b/sample_code/java/LICENSE new file mode 100755 index 0000000..d645695 --- /dev/null +++ b/sample_code/java/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/sample_code/java/NOTICE b/sample_code/java/NOTICE new file mode 100755 index 0000000..e0848aa --- /dev/null +++ b/sample_code/java/NOTICE @@ -0,0 +1 @@ +Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/sample_code/java/amazon-opensearch-docs-sample-client.zip b/sample_code/java/amazon-opensearch-docs-sample-client.zip deleted file mode 100644 index f9e3a8e..0000000 Binary files a/sample_code/java/amazon-opensearch-docs-sample-client.zip and /dev/null differ diff --git a/sample_code/java/pom.xml b/sample_code/java/pom.xml new file mode 100755 index 0000000..87ca852 --- /dev/null +++ b/sample_code/java/pom.xml @@ -0,0 +1,89 @@ + + 4.0.0 + com.amazonaws + amazon-es-docs-sample-client + jar + 0.1.0 + amazon-os-sample-client + http://maven.apache.org + + + https://github.com/awsdocs/amazon-opensearch-service-developer-guide/tree/master/sample_code/java + + + + + Apache License, Version 2.0 + https://aws.amazon.com/apache2.0 + repo + + + + + 1.8 + 1.8 + 2.17.4 + + + + + org.junit + junit-bom + 5.7.2 + pom + import + + + + software.amazon.awssdk + bom + ${aws-java-sdk.version} + pom + import + + + + + + junit + junit + 4.13.2 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + software.amazon.awssdk + auth + + + org.opensearch.client + opensearch-rest-client + 1.3.1 + + + org.opensearch.client + opensearch-java + 0.1.0 + + + org.opensearch.client + opensearch-rest-high-level-client + 1.3.1 + + + com.amazonaws + aws-lambda-java-core + 1.2.1 + + + diff --git a/sample_code/java/src/main/java/com/amazonaws/http/AwsRequestSigningApacheInterceptor.java b/sample_code/java/src/main/java/com/amazonaws/http/AwsRequestSigningApacheInterceptor.java new file mode 100755 index 0000000..5b7fe60 --- /dev/null +++ b/sample_code/java/src/main/java/com/amazonaws/http/AwsRequestSigningApacheInterceptor.java @@ -0,0 +1,228 @@ +/* + * Copyright 2012-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.http; + +import static org.apache.http.protocol.HttpCoreContext.HTTP_TARGET_HOST; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +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.entity.BufferedHttpEntity; +import org.apache.http.message.BasicHeader; +import org.apache.http.protocol.HttpContext; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.signer.Signer; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.regions.Region; + +/** + * An {@link HttpRequestInterceptor} that signs requests using any AWS {@link Signer} + * and {@link AwsCredentialsProvider}. + */ +public class AwsRequestSigningApacheInterceptor implements HttpRequestInterceptor { + /** + * The service that we're connecting to. + */ + private final String service; + + /** + * The particular signer implementation. + */ + private final Signer signer; + + /** + * The source of AWS credentials for signing. + */ + private final AwsCredentialsProvider awsCredentialsProvider; + + /** + * The region signing region. + */ + private final Region region; + + /** + * + * @param service service that we're connecting to + * @param signer particular signer implementation + * @param awsCredentialsProvider source of AWS credentials for signing + * @param region signing region + */ + public AwsRequestSigningApacheInterceptor(final String service, + final Signer signer, + final AwsCredentialsProvider awsCredentialsProvider, + final Region region) { + this.service = service; + this.signer = signer; + this.awsCredentialsProvider = awsCredentialsProvider; + this.region = Objects.requireNonNull(region); + } + + /** + * + * @param service service that we're connecting to + * @param signer particular signer implementation + * @param awsCredentialsProvider source of AWS credentials for signing + * @param region signing region + */ + public AwsRequestSigningApacheInterceptor(final String service, + final Signer signer, + final AwsCredentialsProvider awsCredentialsProvider, + final String region) { + this(service, signer, awsCredentialsProvider, Region.of(region)); + } + + /** + * {@inheritDoc} + */ + @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); + } + + // Copy Apache HttpRequest to AWS Request + SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.fromValue(request.getRequestLine().getMethod())) + .uri(buildUri(context, uriBuilder)); + + if (request instanceof HttpEntityEnclosingRequest) { + HttpEntityEnclosingRequest httpEntityEnclosingRequest = + (HttpEntityEnclosingRequest) request; + if (httpEntityEnclosingRequest.getEntity() != null) { + InputStream content = httpEntityEnclosingRequest.getEntity().getContent(); + requestBuilder.contentStreamProvider(() -> content); + } + } + requestBuilder.rawQueryParameters(nvpToMapParams(uriBuilder.getQueryParams())); + requestBuilder.headers(headerArrayToMap(request.getAllHeaders())); + + ExecutionAttributes attributes = new ExecutionAttributes(); + attributes.putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS, awsCredentialsProvider.resolveCredentials()); + attributes.putAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, service); + attributes.putAttribute(AwsSignerExecutionAttribute.SIGNING_REGION, region); + + // Sign it + SdkHttpFullRequest signedRequest = signer.sign(requestBuilder.build(), attributes); + + // 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(); + basicHttpEntity.setContent(signedRequest.contentStreamProvider() + .orElseThrow(() -> new IllegalStateException("There must be content")) + .newStream()); + // wrap into repeatable entity to support retries + httpEntityEnclosingRequest.setEntity(new BufferedHttpEntity(basicHttpEntity)); + } + } + } + + private URI buildUri(final HttpContext context, URIBuilder uriBuilder) throws IOException { + try { + HttpHost host = (HttpHost) context.getAttribute(HTTP_TARGET_HOST); + + if (host != null) { + uriBuilder.setHost(host.getHostName()); + uriBuilder.setScheme(host.getSchemeName()); + uriBuilder.setPort(host.getPort()); + } + + return uriBuilder.build(); + } catch (URISyntaxException e) { + throw new IOException("Invalid URI", e); + } + } + + /** + * + * @param params list of HTTP query params as NameValuePairs + * @return a multimap of HTTP query params + */ + private static Map> nvpToMapParams(final List params) { + Map> parameterMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (NameValuePair nvp : params) { + List argsList = + parameterMap.computeIfAbsent(nvp.getName(), k -> new ArrayList<>()); + argsList.add(nvp.getValue()); + } + return parameterMap; + } + + /** + * @param headers modelled Header objects + * @return a Map of header entries + */ + private static Map> headerArrayToMap(final Header[] headers) { + Map> headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (Header header : headers) { + if (!skipHeader(header)) { + headersMap.put(header.getName(), headersMap + .getOrDefault(header.getName(), + new LinkedList<>(Collections.singletonList(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 modelled Header objects + */ + private static Header[] mapToHeaderArray(final Map> mapHeaders) { + Header[] headers = new Header[mapHeaders.size()]; + int i = 0; + for (Map.Entry> headerEntry : mapHeaders.entrySet()) { + for (String value : headerEntry.getValue()) { + headers[i++] = new BasicHeader(headerEntry.getKey(), value); + } + } + return headers; + } +} \ No newline at end of file diff --git a/sample_code/java/src/main/java/com/amazonaws/samples/AmazonOpenSearchServiceClient.java b/sample_code/java/src/main/java/com/amazonaws/samples/AmazonOpenSearchServiceClient.java new file mode 100644 index 0000000..8fdb616 --- /dev/null +++ b/sample_code/java/src/main/java/com/amazonaws/samples/AmazonOpenSearchServiceClient.java @@ -0,0 +1,93 @@ +package com.amazonaws.samples; +import com.amazonaws.http.AwsRequestSigningApacheInterceptor; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequestInterceptor; +import org.opensearch.OpenSearchStatusException; +import org.opensearch.action.bulk.BulkItemResponse; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.bulk.BulkResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.index.IndexResponse; +import org.opensearch.client.RequestOptions; +import org.opensearch.client.RestClient; +import org.opensearch.client.RestHighLevelClient; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.signer.Aws4Signer; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class AmazonOpenSearchServiceClient { + + private static final String serviceName = "es"; + private static final String region = "us-east-1"; + private static final String aesEndpoint = ""; // e.g. https://search-mydomain.us-west-1.es.amazonaws.com + private static final String type = "_doc"; + + static final AwsCredentialsProvider credentialsProvider = DefaultCredentialsProvider.create(); + + public static void main(String[] args) throws IOException { + + RestHighLevelClient aesClient = aesClient(); + + String index = "java-client-test-index"; + + // Create a document that simulates a simple log line from a web server + Map document = new HashMap<>(); + document.put("method", "GET"); + document.put("client_ip_address", "123.456.78.90"); + document.put("timestamp", "10/Oct/2000:14:56:14 -0700"); + + System.out.println("Demoing a single index request:"); + String id = "1"; + IndexRequest indexRequest = new IndexRequest(index, type, id).source(document); + IndexResponse indexResponse = aesClient.index(indexRequest, RequestOptions.DEFAULT); + System.out.println(indexResponse.toString()); + + System.out.println("Demoing a 1 MB bulk request:"); + BulkRequest bulkRequest = new BulkRequest(); + + // Add documents (the simple log line from earlier) to the request until it exceeds 1 MB + while (bulkRequest.estimatedSizeInBytes() < 1000000) { + // By not specifying an ID, these documents get auto-assigned IDs + bulkRequest.add(new IndexRequest(index, type).source(document)); + } + + try { + // Send the request and get the response + BulkResponse bulkResponse = aesClient.bulk(bulkRequest, RequestOptions.DEFAULT); + + // Check the response for failures + if (bulkResponse.hasFailures()) { + System.out.println("Encountered failures:"); + for (BulkItemResponse bulkItemResponse : bulkResponse) { + if (bulkItemResponse.isFailed()) { + System.out.println(bulkItemResponse.getFailureMessage()); + } + } + } + else { + System.out.println("No failures!"); + // Uncomment these lines for a line-by-line summary +// for (BulkItemResponse bulkItemResponse : bulkResponse) { +// System.out.println(bulkItemResponse.getResponse().toString()); +// } + } + } + + // Usually happens when the request size is too large + catch (OpenSearchStatusException e) { + System.out.println("Encountered exception:"); + System.out.println(e); + } + } + + // Adds the interceptor to the OpenSearch REST client + public static RestHighLevelClient aesClient() { + Aws4Signer signer = Aws4Signer.create(); + HttpRequestInterceptor interceptor = new AwsRequestSigningApacheInterceptor(serviceName, signer, credentialsProvider, region); + return new RestHighLevelClient(RestClient.builder(HttpHost.create(aesEndpoint)).setHttpClientConfigCallback(hacb -> hacb.addInterceptorLast(interceptor))); + } +} \ No newline at end of file diff --git a/sample_code/java/src/main/java/com/amazonaws/samples/AmazonOpenSearchServiceSample.java b/sample_code/java/src/main/java/com/amazonaws/samples/AmazonOpenSearchServiceSample.java new file mode 100644 index 0000000..dfae646 --- /dev/null +++ b/sample_code/java/src/main/java/com/amazonaws/samples/AmazonOpenSearchServiceSample.java @@ -0,0 +1,61 @@ +package com.amazonaws.samples; + +import com.amazonaws.http.AwsRequestSigningApacheInterceptor; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.entity.ContentType; +import org.apache.http.nio.entity.NStringEntity; +import org.opensearch.client.Request; +import org.opensearch.client.Response; +import org.opensearch.client.RestClient; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.signer.Aws4Signer; +import java.io.IOException; + +public class AmazonOpenSearchServiceSample { + + private static String serviceName = "es"; + private static String region = "us-west-1"; + private static String aesEndpoint = "https://domain.us-west-1.es.amazonaws.com"; + + private static String payload = "{ \"type\": \"s3\", \"settings\": { \"bucket\": \"your-bucket\", \"region\": \"us-west-1\", \"role_arn\": \"arn:aws:iam::123456789012:role/TheServiceRole\" } }"; + private static String snapshotPath = "/_snapshot/my-snapshot-repo"; + + private static String sampleDocument = "{" + "\"title\":\"Walk the Line\"," + "\"director\":\"James Mangold\"," + "\"year\":\"2005\"}"; + private static String indexingPath = "/my-index/_doc"; + + static final AwsCredentialsProvider credentialsProvider = DefaultCredentialsProvider.create(); + + public static void main(String[] args) throws IOException { + RestClient esClient = esClient(serviceName, region); + + // Register a snapshot repository + HttpEntity entity = new NStringEntity(payload, ContentType.APPLICATION_JSON); + Request request = new Request("PUT", snapshotPath); + request.setEntity(entity); + // request.addParameter(name, value); // optional parameters + Response response = esClient.performRequest(request); + System.out.println(response.toString()); + + // Index a document + entity = new NStringEntity(sampleDocument, ContentType.APPLICATION_JSON); + String id = "1"; + request = new Request("PUT", indexingPath + "/" + id); + request.setEntity(entity); + + // Using a String instead of an HttpEntity sets Content-Type to application/json automatically. + // request.setJsonEntity(sampleDocument); + + response = esClient.performRequest(request); + System.out.println(response.toString()); + } + + // Adds the interceptor to the ES REST client + public static RestClient esClient(String serviceName, String region) { + Aws4Signer signer = Aws4Signer.create(); + HttpRequestInterceptor interceptor = new AwsRequestSigningApacheInterceptor(serviceName, signer, credentialsProvider,region); + return RestClient.builder(HttpHost.create(aesEndpoint)).setHttpClientConfigCallback(hacb -> hacb.addInterceptorLast(interceptor)).build(); + } +} \ No newline at end of file diff --git a/sample_code/java/src/test/java/com/amazonaws/http/AwsRequestSigningApacheInterceptorTest.java b/sample_code/java/src/test/java/com/amazonaws/http/AwsRequestSigningApacheInterceptorTest.java new file mode 100755 index 0000000..5beaaec --- /dev/null +++ b/sample_code/java/src/test/java/com/amazonaws/http/AwsRequestSigningApacheInterceptorTest.java @@ -0,0 +1,251 @@ +/* + * Copyright 2012-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.http; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.GZIPOutputStream; + +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.ProtocolVersion; +import org.apache.http.RequestLine; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.message.BasicHttpEntityEnclosingRequest; +import org.apache.http.message.BasicHttpRequest; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.HttpCoreContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.signer.Signer; +import software.amazon.awssdk.http.ContentStreamProvider; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.utils.IoUtils; + +class AwsRequestSigningApacheInterceptorTest { + private AwsRequestSigningApacheInterceptor interceptor; + + @BeforeEach + void createInterceptor() { + AwsCredentialsProvider anonymousCredentialsProvider = StaticCredentialsProvider + .create(AnonymousCredentialsProvider.create().resolveCredentials()); + interceptor = new AwsRequestSigningApacheInterceptor("servicename", + new AddHeaderSigner("Signature", "wuzzle"), + anonymousCredentialsProvider, + Region.AF_SOUTH_1); + } + + @Test + void testGetSigner() throws Exception { + HttpEntityEnclosingRequest request = new BasicHttpEntityEnclosingRequest(new MockRequestLine("/query?a=b")); + request.addHeader("foo", "bar"); + request.addHeader("content-length", "0"); + + HttpCoreContext context = new HttpCoreContext(); + context.setTargetHost(HttpHost.create("localhost")); + + interceptor.process(request, context); + + assertEquals("bar", request.getFirstHeader("foo").getValue()); + assertEquals("wuzzle", request.getFirstHeader("Signature").getValue()); + assertNull(request.getFirstHeader("content-length")); + } + + @Test + void testPostSigner() throws Exception { + HttpEntityEnclosingRequest request = new BasicHttpEntityEnclosingRequest( + new MockRequestLine("POST", "/query?a=b")); + + String payload = "{\"test\": \"val\"}"; + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContentType("text/html; charset=UTF-8"); + final byte[] payloadData = payload.getBytes(); + httpEntity.setContent(new ByteArrayInputStream(payloadData)); + httpEntity.setContentLength(payloadData.length); + + request.setEntity(httpEntity); + + request.addHeader("foo", "bar"); + + HttpCoreContext context = new HttpCoreContext(); + context.setTargetHost(HttpHost.create("localhost")); + + interceptor.process(request, context); + + assertEquals("bar", request.getFirstHeader("foo").getValue()); + assertEquals("wuzzle", request.getFirstHeader("Signature").getValue()); + + assertEquals(Long.toString(httpEntity.getContentLength()), + request.getFirstHeader("signedContentLength").getValue()); + + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + request.getEntity().writeTo(outputStream); + assertEquals(payload, outputStream.toString()); + } + + @Test + void testRepeatableEntity() throws Exception { + HttpEntityEnclosingRequest request = new BasicHttpEntityEnclosingRequest( + new MockRequestLine("POST", "/")); + + String payload = "{\"test\": \"val\"}"; + request.setEntity(new StringEntity(payload)); + HttpCoreContext context = new HttpCoreContext(); + context.setTargetHost(HttpHost.create("localhost")); + interceptor.process(request, context); + + assertTrue(request.getEntity().isRepeatable()); + } + + @Test + void testBadRequest() throws Exception { + HttpRequest badRequest = new BasicHttpRequest("GET", "?#!@*%"); + assertThrows(IOException.class, () -> { + interceptor.process(badRequest, new BasicHttpContext()); + }); + } + + @Test + void testEncodedUriSigner() throws Exception { + String data = "I'm an entity"; + HttpEntityEnclosingRequest request = new BasicHttpEntityEnclosingRequest( + new MockRequestLine("/foo-2017-02-25%2Cfoo-2017-02-26/_search?a=b")); + request.setEntity(new StringEntity(data)); + request.addHeader("foo", "bar"); + request.addHeader("content-length", "0"); + + HttpCoreContext context = new HttpCoreContext(); + context.setTargetHost(HttpHost.create("localhost")); + + interceptor.process(request, context); + + assertEquals("bar", request.getFirstHeader("foo").getValue()); + assertEquals("wuzzle", request.getFirstHeader("Signature").getValue()); + assertNull(request.getFirstHeader("content-length")); + assertEquals("/foo-2017-02-25%2Cfoo-2017-02-26/_search", request.getFirstHeader("resourcePath").getValue()); + assertEquals(Long.toString(data.length()), request.getFirstHeader("signedContentLength").getValue()); + } + + @Test + void testGzipCompressedContent() throws Exception { + String data = "data"; + + HttpEntityEnclosingRequest request = new BasicHttpEntityEnclosingRequest( + new MockRequestLine("POST", "/query?a=b")); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream); + gzipOutputStream.write(data.getBytes("UTF-8")); + gzipOutputStream.close(); + + ByteArrayEntity entity = new ByteArrayEntity(outputStream.toByteArray(), + ContentType.DEFAULT_BINARY); + entity.setContentEncoding("gzip"); + request.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); + request.setEntity(entity); + + HttpCoreContext context = new HttpCoreContext(); + context.setTargetHost(HttpHost.create("localhost")); + + interceptor.process(request, context); + + assertEquals("wuzzle", request.getFirstHeader("Signature").getValue()); + + assertEquals(Long.toString(entity.getContentLength()), + request.getFirstHeader("signedContentLength").getValue()); + } + + private static final class AddHeaderSigner implements Signer { + private final String name; + private final String value; + + private AddHeaderSigner(final String name, final String value) { + this.name = name; + this.value = value; + } + + @Override + public SdkHttpFullRequest sign(final SdkHttpFullRequest request, final ExecutionAttributes ea) { + SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder() + .uri(request.getUri()) + .method(request.method()) + .headers(request.headers()) + .appendHeader(name, value) + .appendHeader("resourcePath", request.getUri().getRawPath()); + + if (request.contentStreamProvider().isPresent()) { + ContentStreamProvider contentStreamProvider = request.contentStreamProvider().get(); + requestBuilder.contentStreamProvider(contentStreamProvider); + requestBuilder.appendHeader("signedContentLength", + Long.toString(getContentLength(contentStreamProvider.newStream()))); + } + + return requestBuilder.build(); + } + + private static int getContentLength(final InputStream content) { + try { + return IoUtils.toByteArray(content).length; + } catch (IOException e) { + return -1; + } + } + } + + private static class MockRequestLine implements RequestLine { + private final String uri; + private final String method; + + MockRequestLine(final String uri) { + this("POST", uri); + } + + MockRequestLine(final String method, final String uri) { + this.method = method; + this.uri = uri; + } + + @Override + public String getMethod() { + return method; + } + + @Override + public String getUri() { + return uri; + } + + @Override + public ProtocolVersion getProtocolVersion() { + throw new UnsupportedOperationException("Not supported yet."); + } + } +}