From 3f00b992ba5a945ab06968936d475424d8e6de9c Mon Sep 17 00:00:00 2001 From: AllanFly120 Date: Tue, 4 Jun 2019 09:41:46 -0700 Subject: [PATCH] feat(s3-request-presigner): provide a s3 request presigner (#266) --- packages/s3-request-presigner/.gitignore | 7 + packages/s3-request-presigner/.npmignore | 12 ++ packages/s3-request-presigner/LICENSE | 201 ++++++++++++++++++ packages/s3-request-presigner/README.md | 49 +++++ packages/s3-request-presigner/package.json | 31 +++ .../s3-request-presigner/src/constants.ts | 10 + .../s3-request-presigner/src/index.spec.ts | 84 ++++++++ packages/s3-request-presigner/src/index.ts | 46 ++++ packages/s3-request-presigner/tsconfig.json | 22 ++ .../s3-request-presigner/tsconfig.test.json | 10 + .../src/internalImports.ts | 4 + packages/types/src/client.ts | 5 + packages/util-format-url/src/index.ts | 4 +- 13 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 packages/s3-request-presigner/.gitignore create mode 100644 packages/s3-request-presigner/.npmignore create mode 100644 packages/s3-request-presigner/LICENSE create mode 100644 packages/s3-request-presigner/README.md create mode 100644 packages/s3-request-presigner/package.json create mode 100644 packages/s3-request-presigner/src/constants.ts create mode 100644 packages/s3-request-presigner/src/index.spec.ts create mode 100644 packages/s3-request-presigner/src/index.ts create mode 100644 packages/s3-request-presigner/tsconfig.json create mode 100644 packages/s3-request-presigner/tsconfig.test.json diff --git a/packages/s3-request-presigner/.gitignore b/packages/s3-request-presigner/.gitignore new file mode 100644 index 000000000000..b521d18d662b --- /dev/null +++ b/packages/s3-request-presigner/.gitignore @@ -0,0 +1,7 @@ +/node_modules/ +/build/ +/coverage/ +/docs/ +*.tgz +*.log +package-lock.json diff --git a/packages/s3-request-presigner/.npmignore b/packages/s3-request-presigner/.npmignore new file mode 100644 index 000000000000..8413290eff94 --- /dev/null +++ b/packages/s3-request-presigner/.npmignore @@ -0,0 +1,12 @@ +/src/ +/coverage/ +/docs/ +tsconfig.test.json + +*.spec.js +*.spec.d.ts +*.spec.js.map + +*.fixture.js +*.fixture.d.ts +*.fixture.js.map diff --git a/packages/s3-request-presigner/LICENSE b/packages/s3-request-presigner/LICENSE new file mode 100644 index 000000000000..e907b58668da --- /dev/null +++ b/packages/s3-request-presigner/LICENSE @@ -0,0 +1,201 @@ + 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 2019 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. + 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/packages/s3-request-presigner/README.md b/packages/s3-request-presigner/README.md new file mode 100644 index 000000000000..2e9c83fbb7c7 --- /dev/null +++ b/packages/s3-request-presigner/README.md @@ -0,0 +1,49 @@ +# @aws-sdk/s3-request-presigner + +[![NPM version](https://img.shields.io/npm/v/@aws-sdk/s3-request-presigner/preview.svg)](https://www.npmjs.com/package/@aws-sdk/s3-request-presigner) +[![NPM downloads](https://img.shields.io/npm/dm/@aws-sdk/s3-request-presigner/preview.svg)](https://www.npmjs.com/package/@aws-sdk/s3-request-presigner) + +This package provides a presigner based on signature V4 that will attempt to generate signed url for S3. + +JavaScript Example: + +```javascript +const S3Presigner = require("@aws-sdk/s3-request-presigner").S3RequestPresigner; +const browserSha256 = require("@aws-crypto/sha256-browser").Sha256; +const nodeSha256 = require("@aws-sdk/hash-node").Hash; +const signer = new S3Presigner({ + region: regionProvider, + credentials: credentialsProvider, + sha256: nodeSha256 //if the signer is used in browser, use `browserSha256` then +}); +const Day = 24 * 60 * 60 * 1000; +const expiration = new Date(Date.now() + 1 * Day); +const url = signer.presignRequest(request, expiration); +``` + +Typescript Example: + +```javascript +import { S3RequestPresigner } from "@aws-sdk/s3-request-presigner"; +import { Sha256 as browserSha256 } from "@aws-crypto/sha256-browser"; +import { Hash as nodeSha256 } from "@aws-sdk/hash-node"; +const signer = new S3RequestPresigner({ + region: regionProvider, + credentials: credentialsProvider, + sha256: nodeSha256 //if the signer is used in browser, use `browserSha256` then +}); +const Day = 24 * 60 * 60 * 1000; +const expiration = new Date(Date.now() + 1 * Day); +const url = signer.presignRequest(request, expiration); +``` + +To avoid redundant construction parameters when instantiate the s3 presigner, +you can simply spread the configurations of an existing s3 clients and supply to +the presigner's constructor. + +```javascript +//s3 is instantiated from S3Client from @aws-sdk/client-s3-* packages +const signer = new S3RequestPresigner({ + ...s3.config +}); +``` diff --git a/packages/s3-request-presigner/package.json b/packages/s3-request-presigner/package.json new file mode 100644 index 000000000000..d209599753e5 --- /dev/null +++ b/packages/s3-request-presigner/package.json @@ -0,0 +1,31 @@ +{ + "name": "@aws-sdk/s3-request-presigner", + "version": "0.1.0-preview.1", + "scripts": { + "prepublishOnly": "tsc", + "pretest": "tsc -p tsconfig.test.json", + "test": "jest" + }, + "main": "./build/index.js", + "types": "./build/index.d.ts", + "author": { + "name": "AWS SDK for JavaScript Team", + "email": "", + "url": "https://aws.amazon.com/javascript/" + }, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.8.0", + "@aws-sdk/util-create-request": "0.1.0-preview.1", + "@aws-sdk/signature-v4": "^0.1.0-preview.4", + "@aws-sdk/types": "^0.1.0-preview.3", + "@aws-sdk/util-format-url": "^0.1.0-preview.3" + }, + "devDependencies": { + "@types/jest": "^24.0.12", + "@types/node": "^12.0.2", + "@aws-sdk/client-s3-node": "0.1.0-preview.1", + "typescript": "^3.0.0", + "jest": "^24.7.1" + } +} diff --git a/packages/s3-request-presigner/src/constants.ts b/packages/s3-request-presigner/src/constants.ts new file mode 100644 index 000000000000..91c12f97f949 --- /dev/null +++ b/packages/s3-request-presigner/src/constants.ts @@ -0,0 +1,10 @@ +export const UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD"; +export const SHA256_HEADER = "X-Amz-Content-Sha256"; + +export const ALGORITHM_QUERY_PARAM = "X-Amz-Algorithm"; +export const CREDENTIAL_QUERY_PARAM = "X-Amz-Credential"; +export const AMZ_DATE_QUERY_PARAM = "X-Amz-Date"; +export const SIGNED_HEADERS_QUERY_PARAM = "X-Amz-SignedHeaders"; +export const EXPIRES_QUERY_PARAM = "X-Amz-Expires"; +export const HOST_HEADER = "host"; +export const ALGORITHM_IDENTIFIER = "AWS4-HMAC-SHA256"; diff --git a/packages/s3-request-presigner/src/index.spec.ts b/packages/s3-request-presigner/src/index.spec.ts new file mode 100644 index 000000000000..a59c81c1fbd4 --- /dev/null +++ b/packages/s3-request-presigner/src/index.spec.ts @@ -0,0 +1,84 @@ +import { S3RequestPresigner } from "./index"; +import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3-node"; +import { HttpRequest } from "@aws-sdk/types"; +import { + ALGORITHM_IDENTIFIER, + SHA256_HEADER, + ALGORITHM_QUERY_PARAM, + AMZ_DATE_QUERY_PARAM, + CREDENTIAL_QUERY_PARAM, + EXPIRES_QUERY_PARAM, + HOST_HEADER, + SIGNED_HEADERS_QUERY_PARAM, + UNSIGNED_PAYLOAD +} from "./constants"; + +describe("s3 presigner", () => { + const s3 = new S3Client({ + credentials: { + accessKeyId: "akid", + secretAccessKey: "skey" + }, + region: "us-bar-1" + }); + const expiration = Math.floor( + (new Date("2000-01-01T00:00:00.000Z").valueOf() + 60 * 60 * 1000) / 1000 + ); + const presigningOptions = { + signingDate: new Date("2000-01-01T00:00:00.000Z") + }; + const minimalRequest: HttpRequest = { + method: "GET", + protocol: "https:", + path: "/foo/bar/baz", + headers: { + host: "foo.s3.us-bar-1.amazonaws.com" + }, + hostname: "foo.s3.us-bar-1.amazonaws.com" + }; + + it("should not double uri encode the path", async () => { + const signer = new S3RequestPresigner({ + ...s3.config + }); + const signed = await signer.presignRequest( + minimalRequest, + expiration, + presigningOptions + ); + expect(signed.path).toEqual(minimalRequest.path); + }); + + it("should set the body digest to 'UNSIGNED_PAYLOAD'", async () => { + const signer = new S3RequestPresigner({ + ...s3.config + }); + const signed = await signer.presignRequest( + minimalRequest, + expiration, + presigningOptions + ); + expect(signed.query).toMatchObject({ [SHA256_HEADER]: UNSIGNED_PAYLOAD }); + }); + + it("should not change original request", async () => { + const signer = new S3RequestPresigner({ + ...s3.config + }); + const originalRequest = { ...minimalRequest }; + const signed = await signer.presignRequest( + minimalRequest, + expiration, + presigningOptions + ); + expect(signed.query).toMatchObject({ + [SHA256_HEADER]: UNSIGNED_PAYLOAD, + [ALGORITHM_QUERY_PARAM]: ALGORITHM_IDENTIFIER, + [CREDENTIAL_QUERY_PARAM]: "akid/20000101/us-bar-1/s3/aws4_request", + [AMZ_DATE_QUERY_PARAM]: "20000101T000000Z", + [EXPIRES_QUERY_PARAM]: "3600", + [SIGNED_HEADERS_QUERY_PARAM]: HOST_HEADER + }); + expect(minimalRequest).toMatchObject(originalRequest); + }); +}); diff --git a/packages/s3-request-presigner/src/index.ts b/packages/s3-request-presigner/src/index.ts new file mode 100644 index 000000000000..20d1c5b836b3 --- /dev/null +++ b/packages/s3-request-presigner/src/index.ts @@ -0,0 +1,46 @@ +import { + DateInput, + RequestPresigner, + RequestSigningArguments +} from "@aws-sdk/types"; +import { + SignatureV4, + SignatureV4Init, + SignatureV4CryptoInit +} from "@aws-sdk/signature-v4"; +import { HttpRequest } from "@aws-sdk/types"; +import { UNSIGNED_PAYLOAD, SHA256_HEADER } from "./constants"; + +/** + * PartialBy makes properties specified in K optional in interface T + * see: https://stackoverflow.com/questions/43159887/make-a-single-property-optional-in-typescript + * */ +type Omit = Pick>; +type PartialBy = Omit & Partial>; + +export class S3RequestPresigner implements RequestPresigner { + private readonly signer: SignatureV4; + constructor({ + service = "s3", + uriEscapePath = false, + ...rest + }: PartialBy< + SignatureV4Init & SignatureV4CryptoInit, + "service" | "uriEscapePath" + >) { + this.signer = new SignatureV4({ + uriEscapePath, + service, + ...rest + }); + } + + public async presignRequest( + requestToSign: HttpRequest, + expiration: DateInput, + options?: RequestSigningArguments + ): Promise> { + requestToSign.headers[SHA256_HEADER] = UNSIGNED_PAYLOAD; + return this.signer.presignRequest(requestToSign, expiration, options); + } +} diff --git a/packages/s3-request-presigner/tsconfig.json b/packages/s3-request-presigner/tsconfig.json new file mode 100644 index 000000000000..824ef5574bf4 --- /dev/null +++ b/packages/s3-request-presigner/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "declaration": true, + "strict": true, + "sourceMap": true, + "downlevelIteration": true, + "importHelpers": true, + "noEmitHelpers": true, + "lib": [ + "es5", + "es2015.promise", + "es2015.collection", + "es2015.iterable", + "es2015.symbol.wellknown", + "dom" + ], + "rootDir": "./src", + "outDir": "./build" + } +} diff --git a/packages/s3-request-presigner/tsconfig.test.json b/packages/s3-request-presigner/tsconfig.test.json new file mode 100644 index 000000000000..57f7d5b14080 --- /dev/null +++ b/packages/s3-request-presigner/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false, + "inlineSourceMap": true, + "inlineSources": true, + "rootDir": "./src", + "outDir": "./build" + } +} diff --git a/packages/service-types-generator/src/internalImports.ts b/packages/service-types-generator/src/internalImports.ts index e125db884a92..2050838edf39 100644 --- a/packages/service-types-generator/src/internalImports.ts +++ b/packages/service-types-generator/src/internalImports.ts @@ -324,6 +324,10 @@ export const IMPORTS: { [key: string]: Import } = { package: "@aws-sdk/route53-id-normalizer-middleware", version: "^0.1.0-preview.3" }, + "s3-request-presigner": { + package: "@aws-sdk/s3-request-presigner", + version: "^0.1.0-preview.1" + }, "service-error-classification": { package: "@aws-sdk/service-error-classification", version: "^0.1.0-preview.1" diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 500aff699a55..6b5c04816928 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -6,6 +6,8 @@ import { RequestSerializer } from "./marshaller"; import { HttpEndpoint, HttpHandler } from "./http"; import { Command } from "./command"; import { MetadataBearer } from "./response"; +import { Credentials } from "./credentials"; +import { Hash, HashConstructor } from "./crypto"; export interface ConfigurationPropertyDefinition< InputType, @@ -81,6 +83,7 @@ export interface ClientResolvedConfigurationBase< OutputTypes extends object, StreamType > { + credentials?: Provider; profile?: string; maxRedirects?: number; maxRetries?: number; @@ -99,6 +102,8 @@ export interface ClientResolvedConfigurationBase< _user_injected_http_handler?: boolean; httpHandler?: HttpHandler; handler?: Terminalware; + md5?: { new (): Hash }; + sha256?: HashConstructor; } /** diff --git a/packages/util-format-url/src/index.ts b/packages/util-format-url/src/index.ts index f061f6cb74d3..9da7451037fc 100644 --- a/packages/util-format-url/src/index.ts +++ b/packages/util-format-url/src/index.ts @@ -1,7 +1,9 @@ import { HttpRequest, QueryParameterBag } from "@aws-sdk/types"; import { buildQueryString } from "@aws-sdk/querystring-builder"; -export function formatUrl(request: HttpRequest): string { +export function formatUrl( + request: HttpRequest +): string { let { protocol, path, hostname, port, query } = request; if (protocol && protocol.substr(-1) !== ":") { protocol += ":";