├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── declaration.jsconfig.json ├── dist ├── aws4fetch.cjs.js ├── aws4fetch.esm.js ├── aws4fetch.esm.mjs ├── aws4fetch.umd.js ├── main.d.mts └── main.d.ts ├── example ├── .dev.vars ├── README.md ├── jsconfig.json ├── package-lock.json ├── package.json ├── src │ └── index.js └── wrangler.toml ├── jsconfig.json ├── package.json ├── rollup.config.mjs ├── src └── main.js └── test ├── README.md ├── aws-sig-v4-test-suite ├── get-header-key-duplicate │ ├── get-header-key-duplicate.authz │ ├── get-header-key-duplicate.creq │ ├── get-header-key-duplicate.req │ ├── get-header-key-duplicate.sreq │ └── get-header-key-duplicate.sts ├── get-header-value-multiline │ ├── get-header-value-multiline.authz │ ├── get-header-value-multiline.creq │ ├── get-header-value-multiline.req │ ├── get-header-value-multiline.sreq │ └── get-header-value-multiline.sts ├── get-header-value-order │ ├── get-header-value-order.authz │ ├── get-header-value-order.creq │ ├── get-header-value-order.req │ ├── get-header-value-order.sreq │ └── get-header-value-order.sts ├── get-header-value-trim │ ├── get-header-value-trim.authz │ ├── get-header-value-trim.creq │ ├── get-header-value-trim.req │ ├── get-header-value-trim.sreq │ └── get-header-value-trim.sts ├── get-unreserved │ ├── get-unreserved.authz │ ├── get-unreserved.creq │ ├── get-unreserved.req │ ├── get-unreserved.sreq │ └── get-unreserved.sts ├── get-utf8 │ ├── get-utf8.authz │ ├── get-utf8.creq │ ├── get-utf8.req │ ├── get-utf8.sreq │ └── get-utf8.sts ├── get-vanilla-empty-query-key │ ├── get-vanilla-empty-query-key.authz │ ├── get-vanilla-empty-query-key.creq │ ├── get-vanilla-empty-query-key.req │ ├── get-vanilla-empty-query-key.sreq │ └── get-vanilla-empty-query-key.sts ├── get-vanilla-query-order-key-case │ ├── get-vanilla-query-order-key-case.authz │ ├── get-vanilla-query-order-key-case.creq │ ├── get-vanilla-query-order-key-case.req │ ├── get-vanilla-query-order-key-case.sreq │ └── get-vanilla-query-order-key-case.sts ├── get-vanilla-query-order-key │ ├── get-vanilla-query-order-key.authz │ ├── get-vanilla-query-order-key.creq │ ├── get-vanilla-query-order-key.req │ ├── get-vanilla-query-order-key.sreq │ └── get-vanilla-query-order-key.sts ├── get-vanilla-query-order-value │ ├── get-vanilla-query-order-value.authz │ ├── get-vanilla-query-order-value.creq │ ├── get-vanilla-query-order-value.req │ ├── get-vanilla-query-order-value.sreq │ └── get-vanilla-query-order-value.sts ├── get-vanilla-query-unreserved │ ├── get-vanilla-query-unreserved.authz │ ├── get-vanilla-query-unreserved.creq │ ├── get-vanilla-query-unreserved.req │ ├── get-vanilla-query-unreserved.sreq │ └── get-vanilla-query-unreserved.sts ├── get-vanilla-query │ ├── get-vanilla-query.authz │ ├── get-vanilla-query.creq │ ├── get-vanilla-query.req │ ├── get-vanilla-query.sreq │ └── get-vanilla-query.sts ├── get-vanilla-utf8-query │ ├── get-vanilla-utf8-query.authz │ ├── get-vanilla-utf8-query.creq │ ├── get-vanilla-utf8-query.req │ ├── get-vanilla-utf8-query.sreq │ └── get-vanilla-utf8-query.sts ├── get-vanilla │ ├── get-vanilla.authz │ ├── get-vanilla.creq │ ├── get-vanilla.req │ ├── get-vanilla.sreq │ └── get-vanilla.sts ├── normalize-path │ ├── get-relative-relative │ │ ├── get-relative-relative.authz │ │ ├── get-relative-relative.creq │ │ ├── get-relative-relative.req │ │ ├── get-relative-relative.sreq │ │ └── get-relative-relative.sts │ ├── get-relative │ │ ├── get-relative.authz │ │ ├── get-relative.creq │ │ ├── get-relative.req │ │ ├── get-relative.sreq │ │ └── get-relative.sts │ ├── get-slash-dot-slash │ │ ├── get-slash-dot-slash.authz │ │ ├── get-slash-dot-slash.creq │ │ ├── get-slash-dot-slash.req │ │ ├── get-slash-dot-slash.sreq │ │ └── get-slash-dot-slash.sts │ ├── get-slash-pointless-dot │ │ ├── get-slash-pointless-dot.authz │ │ ├── get-slash-pointless-dot.creq │ │ ├── get-slash-pointless-dot.req │ │ ├── get-slash-pointless-dot.sreq │ │ └── get-slash-pointless-dot.sts │ ├── get-slash │ │ ├── get-slash.authz │ │ ├── get-slash.creq │ │ ├── get-slash.req │ │ ├── get-slash.sreq │ │ └── get-slash.sts │ ├── get-slashes │ │ ├── get-slashes.authz │ │ ├── get-slashes.creq │ │ ├── get-slashes.req │ │ ├── get-slashes.sreq │ │ └── get-slashes.sts │ ├── get-space │ │ ├── get-space.authz │ │ ├── get-space.creq │ │ ├── get-space.req │ │ ├── get-space.sreq │ │ └── get-space.sts │ └── normalize-path.txt ├── post-header-key-case │ ├── post-header-key-case.authz │ ├── post-header-key-case.creq │ ├── post-header-key-case.req │ ├── post-header-key-case.sreq │ └── post-header-key-case.sts ├── post-header-key-sort │ ├── post-header-key-sort.authz │ ├── post-header-key-sort.creq │ ├── post-header-key-sort.req │ ├── post-header-key-sort.sreq │ └── post-header-key-sort.sts ├── post-header-value-case │ ├── post-header-value-case.authz │ ├── post-header-value-case.creq │ ├── post-header-value-case.req │ ├── post-header-value-case.sreq │ └── post-header-value-case.sts ├── post-sts-token │ ├── post-sts-header-after │ │ ├── post-sts-header-after.authz │ │ ├── post-sts-header-after.creq │ │ ├── post-sts-header-after.req │ │ ├── post-sts-header-after.sreq │ │ └── post-sts-header-after.sts │ ├── post-sts-header-before │ │ ├── post-sts-header-before.authz │ │ ├── post-sts-header-before.creq │ │ ├── post-sts-header-before.req │ │ ├── post-sts-header-before.sreq │ │ └── post-sts-header-before.sts │ └── readme.txt ├── post-vanilla-empty-query-value │ ├── post-vanilla-empty-query-value.authz │ ├── post-vanilla-empty-query-value.creq │ ├── post-vanilla-empty-query-value.req │ ├── post-vanilla-empty-query-value.sreq │ └── post-vanilla-empty-query-value.sts ├── post-vanilla-query │ ├── post-vanilla-query.authz │ ├── post-vanilla-query.creq │ ├── post-vanilla-query.req │ ├── post-vanilla-query.sreq │ └── post-vanilla-query.sts ├── post-vanilla │ ├── post-vanilla.authz │ ├── post-vanilla.creq │ ├── post-vanilla.req │ ├── post-vanilla.sreq │ └── post-vanilla.sts ├── post-x-www-form-urlencoded-parameters │ ├── post-x-www-form-urlencoded-parameters.authz │ ├── post-x-www-form-urlencoded-parameters.creq │ ├── post-x-www-form-urlencoded-parameters.req │ ├── post-x-www-form-urlencoded-parameters.sreq │ └── post-x-www-form-urlencoded-parameters.sts └── post-x-www-form-urlencoded │ ├── post-x-www-form-urlencoded.authz │ ├── post-x-www-form-urlencoded.creq │ ├── post-x-www-form-urlencoded.req │ ├── post-x-www-form-urlencoded.sreq │ └── post-x-www-form-urlencoded.sts ├── awsTests.js ├── integration.js ├── node-commonjs.js ├── node-es.mjs ├── paramTests.js ├── serviceTests.js ├── suite.js └── test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'standard', 3 | rules: { 4 | 'space-before-function-paren': ['error', 'never'], 5 | 'comma-dangle': ['error', 'always-multiline'], 6 | }, 7 | env: { 8 | browser: true, 9 | }, 10 | ignorePatterns: ['dist', 'example/worker.js'], 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example/worker.js 3 | package-lock.json 4 | .wrangler 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Michael Hart (michael.hart.au@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws4fetch 2 | 3 | 4 | 5 | A compact (6.4kb minified, 2.5kb gzipped) [AWS](https://aws.amazon.com/) client for environments that support 6 | [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and 7 | [`SubtleCrypto`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) – that is, modern web browsers and 8 | JS platforms like [Cloudflare Workers](https://www.cloudflare.com/products/cloudflare-workers/). Also retries 9 | requests with an [exponential backoff with full jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) 10 | strategy by default. 11 | 12 | # Example 13 | 14 | ```js 15 | import { AwsClient } from 'aws4fetch' 16 | 17 | const aws = new AwsClient({ accessKeyId: MY_ACCESS_KEY, secretAccessKey: MY_SECRET_KEY }) 18 | 19 | // https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html 20 | const LAMBDA_FN_API = 'https://lambda.us-east-1.amazonaws.com/2015-03-31/functions' 21 | 22 | async function invokeMyLambda(event) { 23 | const res = await aws.fetch(`${LAMBDA_FN_API}/my-lambda/invocations`, { body: JSON.stringify(event) }) 24 | 25 | // `res` is a standard Response object: https://developer.mozilla.org/en-US/docs/Web/API/Response 26 | return res.json() 27 | } 28 | 29 | invokeMyLambda({my: 'event'}).then(json => console.log(json)) 30 | ``` 31 | 32 | You can see a more detailed example, a Cloudflare Worker script you can use as 33 | a replacement for [API Gateway](https://aws.amazon.com/api-gateway/), in the [`example`](./example) directory. 34 | 35 | # API 36 | 37 | `aws4fetch` exports two classes: `AwsClient` and `AwsV4Signer` 38 | 39 | ## `new AwsClient(options)` 40 | 41 | You can use the same instance of `AwsClient` for all your service calls as the service and region will be determined 42 | at fetch time – or you can create separate instances if you have different needs, eg no retrying for some service. 43 | 44 | ```js 45 | import { AwsClient } from 'aws4fetch' 46 | 47 | const aws = new AwsClient({ 48 | accessKeyId, // required, akin to AWS_ACCESS_KEY_ID 49 | secretAccessKey, // required, akin to AWS_SECRET_ACCESS_KEY 50 | sessionToken, // akin to AWS_SESSION_TOKEN if using temp credentials 51 | service, // AWS service, by default parsed at fetch time 52 | region, // AWS region, by default parsed at fetch time 53 | cache, // credential cache, defaults to `new Map()` 54 | retries, // number of retries before giving up, defaults to 10, set to 0 for no retrying 55 | initRetryMs, // defaults to 50 – timeout doubles each retry 56 | }) 57 | ``` 58 | 59 | ### `Promise aws.fetch(input[, init])` 60 | 61 | Has the same signature as the [global fetch function](https://developer.mozilla.org/en-US/docs/Web/API/fetch#syntax) 62 | 63 | ```js 64 | import { AwsClient } from 'aws4fetch' 65 | 66 | const aws = new AwsClient(opts) 67 | 68 | async function doFetch() { 69 | 70 | const response = await aws.fetch(url, { 71 | 72 | method, // if not supplied, will default to 'POST' if there's a body, otherwise 'GET' 73 | headers, // standard JS object literal, or Headers instance 74 | body, // optional, String or ArrayBuffer/ArrayBufferView – ie, remember to stringify your JSON 75 | 76 | // and any other standard fetch options, eg keepalive, etc 77 | 78 | // optional, largely if you want to override options in the AwsClient instance 79 | aws: { 80 | signQuery, // set to true to sign the query string instead of the Authorization header 81 | accessKeyId, // same as in AwsClient constructor above 82 | secretAccessKey, // same as in AwsClient constructor above 83 | sessionToken, // same as in AwsClient constructor above 84 | service, // same as in AwsClient constructor above 85 | region, // same as in AwsClient constructor above 86 | cache, // same as in AwsClient constructor above 87 | datetime, // defaults to now, to override use the form '20150830T123600Z' 88 | appendSessionToken, // set to true to add X-Amz-Security-Token after signing, defaults to true for iot 89 | allHeaders, // set to true to force all headers to be signed instead of the defaults 90 | singleEncode, // set to true to only encode %2F once (usually only needed for testing) 91 | }, 92 | }) 93 | 94 | console.log(await response.json()) 95 | } 96 | ``` 97 | 98 | NB: Due to the way bodies are handled in [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) 99 | instances, it's faster to invoke the function as above – using a URL as the `input` 100 | argument and passing the `body` in the `init` argument – instead of the form of 101 | invocation that uses a `Request` object directly as `input`. 102 | 103 | If you don't know which URL to call for the AWS service you want, the full list 104 | of AWS endpoints can be found here: 105 | https://docs.aws.amazon.com/general/latest/gr/rande.html 106 | 107 | And the APIs are documented here: https://docs.aws.amazon.com/ (the REST APIs 108 | are usually documented under "API Reference" for each service) 109 | 110 | ### `Promise aws.sign(input[, init])` 111 | 112 | Returns a Promise that resolves to an 113 | [AWS4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) 114 | signed [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) – 115 | has the same signature as `fetch`. Use this to create a `Request` you can send 116 | using `fetch()` yourself. 117 | 118 | ```js 119 | import { AwsClient } from 'aws4fetch' 120 | 121 | const aws = new AwsClient(opts) 122 | 123 | async function doFetch() { 124 | 125 | const request = await aws.sign(url, { 126 | 127 | method, // if not supplied, will default to 'POST' if there's a body, otherwise 'GET' 128 | headers, // standard JS object literal, or Headers instance 129 | body, // optional, String or ArrayBuffer/ArrayBufferView – ie, remember to stringify your JSON 130 | 131 | // and any other standard fetch options, eg keepalive, etc 132 | 133 | // optional, largely if you want to override options in the AwsClient instance 134 | aws: { 135 | signQuery, // set to true to sign the query string instead of the Authorization header 136 | accessKeyId, // same as in AwsClient constructor above 137 | secretAccessKey, // same as in AwsClient constructor above 138 | sessionToken, // same as in AwsClient constructor above 139 | service, // same as in AwsClient constructor above 140 | region, // same as in AwsClient constructor above 141 | cache, // same as in AwsClient constructor above 142 | datetime, // defaults to now, to override use the form '20150830T123600Z' 143 | appendSessionToken, // set to true to add X-Amz-Security-Token after signing, defaults to true for iot 144 | allHeaders, // set to true to force all headers to be signed instead of the defaults 145 | singleEncode, // set to true to only encode %2F once (usually only needed for testing) 146 | }, 147 | }) 148 | 149 | const response = await fetch(request) 150 | 151 | console.log(await response.json()) 152 | } 153 | ``` 154 | 155 | ## `new AwsV4Signer(options)` 156 | 157 | The underlying signing class for a request – use this if you just want to deal 158 | with the raw AWS4 signed method/url/headers/body. 159 | 160 | ```js 161 | import { AwsV4Signer } from 'aws4fetch' 162 | 163 | const signer = new AwsV4Signer({ 164 | url, // required, the AWS endpoint to sign 165 | accessKeyId, // required, akin to AWS_ACCESS_KEY_ID 166 | secretAccessKey, // required, akin to AWS_SECRET_ACCESS_KEY 167 | sessionToken, // akin to AWS_SESSION_TOKEN if using temp credentials 168 | method, // if not supplied, will default to 'POST' if there's a body, otherwise 'GET' 169 | headers, // standard JS object literal, or Headers instance 170 | body, // optional, String or ArrayBuffer/ArrayBufferView – ie, remember to stringify your JSON 171 | signQuery, // set to true to sign the query string instead of the Authorization header 172 | service, // AWS service, by default parsed at fetch time 173 | region, // AWS region, by default parsed at fetch time 174 | cache, // credential cache, defaults to `new Map()` 175 | datetime, // defaults to now, to override use the form '20150830T123600Z' 176 | appendSessionToken, // set to true to add X-Amz-Security-Token after signing, defaults to true for iot 177 | allHeaders, // set to true to force all headers to be signed instead of the defaults 178 | singleEncode, // set to true to only encode %2F once (usually only needed for testing) 179 | }) 180 | ``` 181 | 182 | ### `Promise<{ method, url, headers, body }> signer.sign()` 183 | 184 | Actually perform the signing of the request and return a Promise that resolves 185 | to an object containing the signed method, url, headers and body. 186 | 187 | `method` will be a `String`, `url` will be an instance of [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL), 188 | `headers` will be an instance of [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) and 189 | `body` will unchanged from the argument you supply to the constructor. 190 | 191 | ```js 192 | import { AwsV4Signer } from 'aws4fetch' 193 | 194 | const signer = new AwsV4Signer(opts) 195 | 196 | async function sign() { 197 | const { method, url, headers, body } = await signer.sign() 198 | 199 | console.log(method, url, [...headers], body) 200 | } 201 | ``` 202 | 203 | ### `Promise signer.authHeader()` 204 | 205 | Returns a Promise that resolves to the signed string to use in the 206 | [`Authorization` header](https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html#sigv4-add-signature-auth-header) 207 | 208 | Used by the `sign()` method – you shouldn't need to access this directly unless you're constructing your own requests. 209 | 210 | ### `Promise signer.signature()` 211 | 212 | Returns a Promise that resolves to the 213 | [hex signature](https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html#sigv4-calculate-signature) 214 | 215 | Used by the `sign()` method – you shouldn't need to access this directly unless you're constructing your own requests. 216 | 217 | # Installation 218 | 219 | With [npm](http://npmjs.org/) do: 220 | 221 | ``` 222 | npm install aws4fetch 223 | ``` 224 | -------------------------------------------------------------------------------- /declaration.jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./jsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "noEmit": false, 6 | "emitDeclarationOnly": true, 7 | "removeComments": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /dist/aws4fetch.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | /** 6 | * @license MIT 7 | * @copyright Michael Hart 2024 8 | */ 9 | const encoder = new TextEncoder(); 10 | const HOST_SERVICES = { 11 | appstream2: 'appstream', 12 | cloudhsmv2: 'cloudhsm', 13 | email: 'ses', 14 | marketplace: 'aws-marketplace', 15 | mobile: 'AWSMobileHubService', 16 | pinpoint: 'mobiletargeting', 17 | queue: 'sqs', 18 | 'git-codecommit': 'codecommit', 19 | 'mturk-requester-sandbox': 'mturk-requester', 20 | 'personalize-runtime': 'personalize', 21 | }; 22 | const UNSIGNABLE_HEADERS = new Set([ 23 | 'authorization', 24 | 'content-type', 25 | 'content-length', 26 | 'user-agent', 27 | 'presigned-expires', 28 | 'expect', 29 | 'x-amzn-trace-id', 30 | 'range', 31 | 'connection', 32 | ]); 33 | class AwsClient { 34 | constructor({ accessKeyId, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }) { 35 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 36 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 37 | this.accessKeyId = accessKeyId; 38 | this.secretAccessKey = secretAccessKey; 39 | this.sessionToken = sessionToken; 40 | this.service = service; 41 | this.region = region; 42 | this.cache = cache || new Map(); 43 | this.retries = retries != null ? retries : 10; 44 | this.initRetryMs = initRetryMs || 50; 45 | } 46 | async sign(input, init) { 47 | if (input instanceof Request) { 48 | const { method, url, headers, body } = input; 49 | init = Object.assign({ method, url, headers }, init); 50 | if (init.body == null && headers.has('Content-Type')) { 51 | init.body = body != null && headers.has('X-Amz-Content-Sha256') ? body : await input.clone().arrayBuffer(); 52 | } 53 | input = url; 54 | } 55 | const signer = new AwsV4Signer(Object.assign({ url: input.toString() }, init, this, init && init.aws)); 56 | const signed = Object.assign({}, init, await signer.sign()); 57 | delete signed.aws; 58 | try { 59 | return new Request(signed.url.toString(), signed) 60 | } catch (e) { 61 | if (e instanceof TypeError) { 62 | return new Request(signed.url.toString(), Object.assign({ duplex: 'half' }, signed)) 63 | } 64 | throw e 65 | } 66 | } 67 | async fetch(input, init) { 68 | for (let i = 0; i <= this.retries; i++) { 69 | const fetched = fetch(await this.sign(input, init)); 70 | if (i === this.retries) { 71 | return fetched 72 | } 73 | const res = await fetched; 74 | if (res.status < 500 && res.status !== 429) { 75 | return res 76 | } 77 | await new Promise(resolve => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i))); 78 | } 79 | throw new Error('An unknown error occurred, ensure retries is not negative') 80 | } 81 | } 82 | class AwsV4Signer { 83 | constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) { 84 | if (url == null) throw new TypeError('url is a required option') 85 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 86 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 87 | this.method = method || (body ? 'POST' : 'GET'); 88 | this.url = new URL(url); 89 | this.headers = new Headers(headers || {}); 90 | this.body = body; 91 | this.accessKeyId = accessKeyId; 92 | this.secretAccessKey = secretAccessKey; 93 | this.sessionToken = sessionToken; 94 | let guessedService, guessedRegion; 95 | if (!service || !region) { 96 | [guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers); 97 | } 98 | this.service = service || guessedService || ''; 99 | this.region = region || guessedRegion || 'us-east-1'; 100 | this.cache = cache || new Map(); 101 | this.datetime = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, ''); 102 | this.signQuery = signQuery; 103 | this.appendSessionToken = appendSessionToken || this.service === 'iotdevicegateway'; 104 | this.headers.delete('Host'); 105 | if (this.service === 's3' && !this.signQuery && !this.headers.has('X-Amz-Content-Sha256')) { 106 | this.headers.set('X-Amz-Content-Sha256', 'UNSIGNED-PAYLOAD'); 107 | } 108 | const params = this.signQuery ? this.url.searchParams : this.headers; 109 | params.set('X-Amz-Date', this.datetime); 110 | if (this.sessionToken && !this.appendSessionToken) { 111 | params.set('X-Amz-Security-Token', this.sessionToken); 112 | } 113 | this.signableHeaders = ['host', ...this.headers.keys()] 114 | .filter(header => allHeaders || !UNSIGNABLE_HEADERS.has(header)) 115 | .sort(); 116 | this.signedHeaders = this.signableHeaders.join(';'); 117 | this.canonicalHeaders = this.signableHeaders 118 | .map(header => header + ':' + (header === 'host' ? this.url.host : (this.headers.get(header) || '').replace(/\s+/g, ' '))) 119 | .join('\n'); 120 | this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, 'aws4_request'].join('/'); 121 | if (this.signQuery) { 122 | if (this.service === 's3' && !params.has('X-Amz-Expires')) { 123 | params.set('X-Amz-Expires', '86400'); 124 | } 125 | params.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256'); 126 | params.set('X-Amz-Credential', this.accessKeyId + '/' + this.credentialString); 127 | params.set('X-Amz-SignedHeaders', this.signedHeaders); 128 | } 129 | if (this.service === 's3') { 130 | try { 131 | this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, ' ')); 132 | } catch (e) { 133 | this.encodedPath = this.url.pathname; 134 | } 135 | } else { 136 | this.encodedPath = this.url.pathname.replace(/\/+/g, '/'); 137 | } 138 | if (!singleEncode) { 139 | this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, '/'); 140 | } 141 | this.encodedPath = encodeRfc3986(this.encodedPath); 142 | const seenKeys = new Set(); 143 | this.encodedSearch = [...this.url.searchParams] 144 | .filter(([k]) => { 145 | if (!k) return false 146 | if (this.service === 's3') { 147 | if (seenKeys.has(k)) return false 148 | seenKeys.add(k); 149 | } 150 | return true 151 | }) 152 | .map(pair => pair.map(p => encodeRfc3986(encodeURIComponent(p)))) 153 | .sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0) 154 | .map(pair => pair.join('=')) 155 | .join('&'); 156 | } 157 | async sign() { 158 | if (this.signQuery) { 159 | this.url.searchParams.set('X-Amz-Signature', await this.signature()); 160 | if (this.sessionToken && this.appendSessionToken) { 161 | this.url.searchParams.set('X-Amz-Security-Token', this.sessionToken); 162 | } 163 | } else { 164 | this.headers.set('Authorization', await this.authHeader()); 165 | } 166 | return { 167 | method: this.method, 168 | url: this.url, 169 | headers: this.headers, 170 | body: this.body, 171 | } 172 | } 173 | async authHeader() { 174 | return [ 175 | 'AWS4-HMAC-SHA256 Credential=' + this.accessKeyId + '/' + this.credentialString, 176 | 'SignedHeaders=' + this.signedHeaders, 177 | 'Signature=' + (await this.signature()), 178 | ].join(', ') 179 | } 180 | async signature() { 181 | const date = this.datetime.slice(0, 8); 182 | const cacheKey = [this.secretAccessKey, date, this.region, this.service].join(); 183 | let kCredentials = this.cache.get(cacheKey); 184 | if (!kCredentials) { 185 | const kDate = await hmac('AWS4' + this.secretAccessKey, date); 186 | const kRegion = await hmac(kDate, this.region); 187 | const kService = await hmac(kRegion, this.service); 188 | kCredentials = await hmac(kService, 'aws4_request'); 189 | this.cache.set(cacheKey, kCredentials); 190 | } 191 | return buf2hex(await hmac(kCredentials, await this.stringToSign())) 192 | } 193 | async stringToSign() { 194 | return [ 195 | 'AWS4-HMAC-SHA256', 196 | this.datetime, 197 | this.credentialString, 198 | buf2hex(await hash(await this.canonicalString())), 199 | ].join('\n') 200 | } 201 | async canonicalString() { 202 | return [ 203 | this.method.toUpperCase(), 204 | this.encodedPath, 205 | this.encodedSearch, 206 | this.canonicalHeaders + '\n', 207 | this.signedHeaders, 208 | await this.hexBodyHash(), 209 | ].join('\n') 210 | } 211 | async hexBodyHash() { 212 | let hashHeader = this.headers.get('X-Amz-Content-Sha256') || (this.service === 's3' && this.signQuery ? 'UNSIGNED-PAYLOAD' : null); 213 | if (hashHeader == null) { 214 | if (this.body && typeof this.body !== 'string' && !('byteLength' in this.body)) { 215 | throw new Error('body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header') 216 | } 217 | hashHeader = buf2hex(await hash(this.body || '')); 218 | } 219 | return hashHeader 220 | } 221 | } 222 | async function hmac(key, string) { 223 | const cryptoKey = await crypto.subtle.importKey( 224 | 'raw', 225 | typeof key === 'string' ? encoder.encode(key) : key, 226 | { name: 'HMAC', hash: { name: 'SHA-256' } }, 227 | false, 228 | ['sign'], 229 | ); 230 | return crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(string)) 231 | } 232 | async function hash(content) { 233 | return crypto.subtle.digest('SHA-256', typeof content === 'string' ? encoder.encode(content) : content) 234 | } 235 | const HEX_CHARS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; 236 | function buf2hex(arrayBuffer) { 237 | const buffer = new Uint8Array(arrayBuffer); 238 | let out = ''; 239 | for (let idx = 0; idx < buffer.length; idx++) { 240 | const n = buffer[idx]; 241 | out += HEX_CHARS[(n >>> 4) & 0xF]; 242 | out += HEX_CHARS[n & 0xF]; 243 | } 244 | return out 245 | } 246 | function encodeRfc3986(urlEncodedStr) { 247 | return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()) 248 | } 249 | function guessServiceRegion(url, headers) { 250 | const { hostname, pathname } = url; 251 | if (hostname.endsWith('.on.aws')) { 252 | const match = hostname.match(/^[^.]{1,63}\.lambda-url\.([^.]{1,63})\.on\.aws$/); 253 | return match != null ? ['lambda', match[1] || ''] : ['', ''] 254 | } 255 | if (hostname.endsWith('.r2.cloudflarestorage.com')) { 256 | return ['s3', 'auto'] 257 | } 258 | if (hostname.endsWith('.backblazeb2.com')) { 259 | const match = hostname.match(/^(?:[^.]{1,63}\.)?s3\.([^.]{1,63})\.backblazeb2\.com$/); 260 | return match != null ? ['s3', match[1] || ''] : ['', ''] 261 | } 262 | const match = hostname.replace('dualstack.', '').match(/([^.]{1,63})\.(?:([^.]{0,63})\.)?amazonaws\.com(?:\.cn)?$/); 263 | let service = (match && match[1]) || ''; 264 | let region = match && match[2]; 265 | if (region === 'us-gov') { 266 | region = 'us-gov-west-1'; 267 | } else if (region === 's3' || region === 's3-accelerate') { 268 | region = 'us-east-1'; 269 | service = 's3'; 270 | } else if (service === 'iot') { 271 | if (hostname.startsWith('iot.')) { 272 | service = 'execute-api'; 273 | } else if (hostname.startsWith('data.jobs.iot.')) { 274 | service = 'iot-jobs-data'; 275 | } else { 276 | service = pathname === '/mqtt' ? 'iotdevicegateway' : 'iotdata'; 277 | } 278 | } else if (service === 'autoscaling') { 279 | const targetPrefix = (headers.get('X-Amz-Target') || '').split('.')[0]; 280 | if (targetPrefix === 'AnyScaleFrontendService') { 281 | service = 'application-autoscaling'; 282 | } else if (targetPrefix === 'AnyScaleScalingPlannerFrontendService') { 283 | service = 'autoscaling-plans'; 284 | } 285 | } else if (region == null && service.startsWith('s3-')) { 286 | region = service.slice(3).replace(/^fips-|^external-1/, ''); 287 | service = 's3'; 288 | } else if (service.endsWith('-fips')) { 289 | service = service.slice(0, -5); 290 | } else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) { 291 | [service, region] = [region, service]; 292 | } 293 | return [HOST_SERVICES[service] || service, region || ''] 294 | } 295 | 296 | exports.AwsClient = AwsClient; 297 | exports.AwsV4Signer = AwsV4Signer; 298 | -------------------------------------------------------------------------------- /dist/aws4fetch.esm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * @copyright Michael Hart 2024 4 | */ 5 | const encoder = new TextEncoder(); 6 | const HOST_SERVICES = { 7 | appstream2: 'appstream', 8 | cloudhsmv2: 'cloudhsm', 9 | email: 'ses', 10 | marketplace: 'aws-marketplace', 11 | mobile: 'AWSMobileHubService', 12 | pinpoint: 'mobiletargeting', 13 | queue: 'sqs', 14 | 'git-codecommit': 'codecommit', 15 | 'mturk-requester-sandbox': 'mturk-requester', 16 | 'personalize-runtime': 'personalize', 17 | }; 18 | const UNSIGNABLE_HEADERS = new Set([ 19 | 'authorization', 20 | 'content-type', 21 | 'content-length', 22 | 'user-agent', 23 | 'presigned-expires', 24 | 'expect', 25 | 'x-amzn-trace-id', 26 | 'range', 27 | 'connection', 28 | ]); 29 | class AwsClient { 30 | constructor({ accessKeyId, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }) { 31 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 32 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 33 | this.accessKeyId = accessKeyId; 34 | this.secretAccessKey = secretAccessKey; 35 | this.sessionToken = sessionToken; 36 | this.service = service; 37 | this.region = region; 38 | this.cache = cache || new Map(); 39 | this.retries = retries != null ? retries : 10; 40 | this.initRetryMs = initRetryMs || 50; 41 | } 42 | async sign(input, init) { 43 | if (input instanceof Request) { 44 | const { method, url, headers, body } = input; 45 | init = Object.assign({ method, url, headers }, init); 46 | if (init.body == null && headers.has('Content-Type')) { 47 | init.body = body != null && headers.has('X-Amz-Content-Sha256') ? body : await input.clone().arrayBuffer(); 48 | } 49 | input = url; 50 | } 51 | const signer = new AwsV4Signer(Object.assign({ url: input.toString() }, init, this, init && init.aws)); 52 | const signed = Object.assign({}, init, await signer.sign()); 53 | delete signed.aws; 54 | try { 55 | return new Request(signed.url.toString(), signed) 56 | } catch (e) { 57 | if (e instanceof TypeError) { 58 | return new Request(signed.url.toString(), Object.assign({ duplex: 'half' }, signed)) 59 | } 60 | throw e 61 | } 62 | } 63 | async fetch(input, init) { 64 | for (let i = 0; i <= this.retries; i++) { 65 | const fetched = fetch(await this.sign(input, init)); 66 | if (i === this.retries) { 67 | return fetched 68 | } 69 | const res = await fetched; 70 | if (res.status < 500 && res.status !== 429) { 71 | return res 72 | } 73 | await new Promise(resolve => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i))); 74 | } 75 | throw new Error('An unknown error occurred, ensure retries is not negative') 76 | } 77 | } 78 | class AwsV4Signer { 79 | constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) { 80 | if (url == null) throw new TypeError('url is a required option') 81 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 82 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 83 | this.method = method || (body ? 'POST' : 'GET'); 84 | this.url = new URL(url); 85 | this.headers = new Headers(headers || {}); 86 | this.body = body; 87 | this.accessKeyId = accessKeyId; 88 | this.secretAccessKey = secretAccessKey; 89 | this.sessionToken = sessionToken; 90 | let guessedService, guessedRegion; 91 | if (!service || !region) { 92 | [guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers); 93 | } 94 | this.service = service || guessedService || ''; 95 | this.region = region || guessedRegion || 'us-east-1'; 96 | this.cache = cache || new Map(); 97 | this.datetime = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, ''); 98 | this.signQuery = signQuery; 99 | this.appendSessionToken = appendSessionToken || this.service === 'iotdevicegateway'; 100 | this.headers.delete('Host'); 101 | if (this.service === 's3' && !this.signQuery && !this.headers.has('X-Amz-Content-Sha256')) { 102 | this.headers.set('X-Amz-Content-Sha256', 'UNSIGNED-PAYLOAD'); 103 | } 104 | const params = this.signQuery ? this.url.searchParams : this.headers; 105 | params.set('X-Amz-Date', this.datetime); 106 | if (this.sessionToken && !this.appendSessionToken) { 107 | params.set('X-Amz-Security-Token', this.sessionToken); 108 | } 109 | this.signableHeaders = ['host', ...this.headers.keys()] 110 | .filter(header => allHeaders || !UNSIGNABLE_HEADERS.has(header)) 111 | .sort(); 112 | this.signedHeaders = this.signableHeaders.join(';'); 113 | this.canonicalHeaders = this.signableHeaders 114 | .map(header => header + ':' + (header === 'host' ? this.url.host : (this.headers.get(header) || '').replace(/\s+/g, ' '))) 115 | .join('\n'); 116 | this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, 'aws4_request'].join('/'); 117 | if (this.signQuery) { 118 | if (this.service === 's3' && !params.has('X-Amz-Expires')) { 119 | params.set('X-Amz-Expires', '86400'); 120 | } 121 | params.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256'); 122 | params.set('X-Amz-Credential', this.accessKeyId + '/' + this.credentialString); 123 | params.set('X-Amz-SignedHeaders', this.signedHeaders); 124 | } 125 | if (this.service === 's3') { 126 | try { 127 | this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, ' ')); 128 | } catch (e) { 129 | this.encodedPath = this.url.pathname; 130 | } 131 | } else { 132 | this.encodedPath = this.url.pathname.replace(/\/+/g, '/'); 133 | } 134 | if (!singleEncode) { 135 | this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, '/'); 136 | } 137 | this.encodedPath = encodeRfc3986(this.encodedPath); 138 | const seenKeys = new Set(); 139 | this.encodedSearch = [...this.url.searchParams] 140 | .filter(([k]) => { 141 | if (!k) return false 142 | if (this.service === 's3') { 143 | if (seenKeys.has(k)) return false 144 | seenKeys.add(k); 145 | } 146 | return true 147 | }) 148 | .map(pair => pair.map(p => encodeRfc3986(encodeURIComponent(p)))) 149 | .sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0) 150 | .map(pair => pair.join('=')) 151 | .join('&'); 152 | } 153 | async sign() { 154 | if (this.signQuery) { 155 | this.url.searchParams.set('X-Amz-Signature', await this.signature()); 156 | if (this.sessionToken && this.appendSessionToken) { 157 | this.url.searchParams.set('X-Amz-Security-Token', this.sessionToken); 158 | } 159 | } else { 160 | this.headers.set('Authorization', await this.authHeader()); 161 | } 162 | return { 163 | method: this.method, 164 | url: this.url, 165 | headers: this.headers, 166 | body: this.body, 167 | } 168 | } 169 | async authHeader() { 170 | return [ 171 | 'AWS4-HMAC-SHA256 Credential=' + this.accessKeyId + '/' + this.credentialString, 172 | 'SignedHeaders=' + this.signedHeaders, 173 | 'Signature=' + (await this.signature()), 174 | ].join(', ') 175 | } 176 | async signature() { 177 | const date = this.datetime.slice(0, 8); 178 | const cacheKey = [this.secretAccessKey, date, this.region, this.service].join(); 179 | let kCredentials = this.cache.get(cacheKey); 180 | if (!kCredentials) { 181 | const kDate = await hmac('AWS4' + this.secretAccessKey, date); 182 | const kRegion = await hmac(kDate, this.region); 183 | const kService = await hmac(kRegion, this.service); 184 | kCredentials = await hmac(kService, 'aws4_request'); 185 | this.cache.set(cacheKey, kCredentials); 186 | } 187 | return buf2hex(await hmac(kCredentials, await this.stringToSign())) 188 | } 189 | async stringToSign() { 190 | return [ 191 | 'AWS4-HMAC-SHA256', 192 | this.datetime, 193 | this.credentialString, 194 | buf2hex(await hash(await this.canonicalString())), 195 | ].join('\n') 196 | } 197 | async canonicalString() { 198 | return [ 199 | this.method.toUpperCase(), 200 | this.encodedPath, 201 | this.encodedSearch, 202 | this.canonicalHeaders + '\n', 203 | this.signedHeaders, 204 | await this.hexBodyHash(), 205 | ].join('\n') 206 | } 207 | async hexBodyHash() { 208 | let hashHeader = this.headers.get('X-Amz-Content-Sha256') || (this.service === 's3' && this.signQuery ? 'UNSIGNED-PAYLOAD' : null); 209 | if (hashHeader == null) { 210 | if (this.body && typeof this.body !== 'string' && !('byteLength' in this.body)) { 211 | throw new Error('body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header') 212 | } 213 | hashHeader = buf2hex(await hash(this.body || '')); 214 | } 215 | return hashHeader 216 | } 217 | } 218 | async function hmac(key, string) { 219 | const cryptoKey = await crypto.subtle.importKey( 220 | 'raw', 221 | typeof key === 'string' ? encoder.encode(key) : key, 222 | { name: 'HMAC', hash: { name: 'SHA-256' } }, 223 | false, 224 | ['sign'], 225 | ); 226 | return crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(string)) 227 | } 228 | async function hash(content) { 229 | return crypto.subtle.digest('SHA-256', typeof content === 'string' ? encoder.encode(content) : content) 230 | } 231 | const HEX_CHARS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; 232 | function buf2hex(arrayBuffer) { 233 | const buffer = new Uint8Array(arrayBuffer); 234 | let out = ''; 235 | for (let idx = 0; idx < buffer.length; idx++) { 236 | const n = buffer[idx]; 237 | out += HEX_CHARS[(n >>> 4) & 0xF]; 238 | out += HEX_CHARS[n & 0xF]; 239 | } 240 | return out 241 | } 242 | function encodeRfc3986(urlEncodedStr) { 243 | return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()) 244 | } 245 | function guessServiceRegion(url, headers) { 246 | const { hostname, pathname } = url; 247 | if (hostname.endsWith('.on.aws')) { 248 | const match = hostname.match(/^[^.]{1,63}\.lambda-url\.([^.]{1,63})\.on\.aws$/); 249 | return match != null ? ['lambda', match[1] || ''] : ['', ''] 250 | } 251 | if (hostname.endsWith('.r2.cloudflarestorage.com')) { 252 | return ['s3', 'auto'] 253 | } 254 | if (hostname.endsWith('.backblazeb2.com')) { 255 | const match = hostname.match(/^(?:[^.]{1,63}\.)?s3\.([^.]{1,63})\.backblazeb2\.com$/); 256 | return match != null ? ['s3', match[1] || ''] : ['', ''] 257 | } 258 | const match = hostname.replace('dualstack.', '').match(/([^.]{1,63})\.(?:([^.]{0,63})\.)?amazonaws\.com(?:\.cn)?$/); 259 | let service = (match && match[1]) || ''; 260 | let region = match && match[2]; 261 | if (region === 'us-gov') { 262 | region = 'us-gov-west-1'; 263 | } else if (region === 's3' || region === 's3-accelerate') { 264 | region = 'us-east-1'; 265 | service = 's3'; 266 | } else if (service === 'iot') { 267 | if (hostname.startsWith('iot.')) { 268 | service = 'execute-api'; 269 | } else if (hostname.startsWith('data.jobs.iot.')) { 270 | service = 'iot-jobs-data'; 271 | } else { 272 | service = pathname === '/mqtt' ? 'iotdevicegateway' : 'iotdata'; 273 | } 274 | } else if (service === 'autoscaling') { 275 | const targetPrefix = (headers.get('X-Amz-Target') || '').split('.')[0]; 276 | if (targetPrefix === 'AnyScaleFrontendService') { 277 | service = 'application-autoscaling'; 278 | } else if (targetPrefix === 'AnyScaleScalingPlannerFrontendService') { 279 | service = 'autoscaling-plans'; 280 | } 281 | } else if (region == null && service.startsWith('s3-')) { 282 | region = service.slice(3).replace(/^fips-|^external-1/, ''); 283 | service = 's3'; 284 | } else if (service.endsWith('-fips')) { 285 | service = service.slice(0, -5); 286 | } else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) { 287 | [service, region] = [region, service]; 288 | } 289 | return [HOST_SERVICES[service] || service, region || ''] 290 | } 291 | 292 | export { AwsClient, AwsV4Signer }; 293 | -------------------------------------------------------------------------------- /dist/aws4fetch.esm.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * @copyright Michael Hart 2024 4 | */ 5 | const encoder = new TextEncoder(); 6 | const HOST_SERVICES = { 7 | appstream2: 'appstream', 8 | cloudhsmv2: 'cloudhsm', 9 | email: 'ses', 10 | marketplace: 'aws-marketplace', 11 | mobile: 'AWSMobileHubService', 12 | pinpoint: 'mobiletargeting', 13 | queue: 'sqs', 14 | 'git-codecommit': 'codecommit', 15 | 'mturk-requester-sandbox': 'mturk-requester', 16 | 'personalize-runtime': 'personalize', 17 | }; 18 | const UNSIGNABLE_HEADERS = new Set([ 19 | 'authorization', 20 | 'content-type', 21 | 'content-length', 22 | 'user-agent', 23 | 'presigned-expires', 24 | 'expect', 25 | 'x-amzn-trace-id', 26 | 'range', 27 | 'connection', 28 | ]); 29 | class AwsClient { 30 | constructor({ accessKeyId, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }) { 31 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 32 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 33 | this.accessKeyId = accessKeyId; 34 | this.secretAccessKey = secretAccessKey; 35 | this.sessionToken = sessionToken; 36 | this.service = service; 37 | this.region = region; 38 | this.cache = cache || new Map(); 39 | this.retries = retries != null ? retries : 10; 40 | this.initRetryMs = initRetryMs || 50; 41 | } 42 | async sign(input, init) { 43 | if (input instanceof Request) { 44 | const { method, url, headers, body } = input; 45 | init = Object.assign({ method, url, headers }, init); 46 | if (init.body == null && headers.has('Content-Type')) { 47 | init.body = body != null && headers.has('X-Amz-Content-Sha256') ? body : await input.clone().arrayBuffer(); 48 | } 49 | input = url; 50 | } 51 | const signer = new AwsV4Signer(Object.assign({ url: input.toString() }, init, this, init && init.aws)); 52 | const signed = Object.assign({}, init, await signer.sign()); 53 | delete signed.aws; 54 | try { 55 | return new Request(signed.url.toString(), signed) 56 | } catch (e) { 57 | if (e instanceof TypeError) { 58 | return new Request(signed.url.toString(), Object.assign({ duplex: 'half' }, signed)) 59 | } 60 | throw e 61 | } 62 | } 63 | async fetch(input, init) { 64 | for (let i = 0; i <= this.retries; i++) { 65 | const fetched = fetch(await this.sign(input, init)); 66 | if (i === this.retries) { 67 | return fetched 68 | } 69 | const res = await fetched; 70 | if (res.status < 500 && res.status !== 429) { 71 | return res 72 | } 73 | await new Promise(resolve => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i))); 74 | } 75 | throw new Error('An unknown error occurred, ensure retries is not negative') 76 | } 77 | } 78 | class AwsV4Signer { 79 | constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) { 80 | if (url == null) throw new TypeError('url is a required option') 81 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 82 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 83 | this.method = method || (body ? 'POST' : 'GET'); 84 | this.url = new URL(url); 85 | this.headers = new Headers(headers || {}); 86 | this.body = body; 87 | this.accessKeyId = accessKeyId; 88 | this.secretAccessKey = secretAccessKey; 89 | this.sessionToken = sessionToken; 90 | let guessedService, guessedRegion; 91 | if (!service || !region) { 92 | [guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers); 93 | } 94 | this.service = service || guessedService || ''; 95 | this.region = region || guessedRegion || 'us-east-1'; 96 | this.cache = cache || new Map(); 97 | this.datetime = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, ''); 98 | this.signQuery = signQuery; 99 | this.appendSessionToken = appendSessionToken || this.service === 'iotdevicegateway'; 100 | this.headers.delete('Host'); 101 | if (this.service === 's3' && !this.signQuery && !this.headers.has('X-Amz-Content-Sha256')) { 102 | this.headers.set('X-Amz-Content-Sha256', 'UNSIGNED-PAYLOAD'); 103 | } 104 | const params = this.signQuery ? this.url.searchParams : this.headers; 105 | params.set('X-Amz-Date', this.datetime); 106 | if (this.sessionToken && !this.appendSessionToken) { 107 | params.set('X-Amz-Security-Token', this.sessionToken); 108 | } 109 | this.signableHeaders = ['host', ...this.headers.keys()] 110 | .filter(header => allHeaders || !UNSIGNABLE_HEADERS.has(header)) 111 | .sort(); 112 | this.signedHeaders = this.signableHeaders.join(';'); 113 | this.canonicalHeaders = this.signableHeaders 114 | .map(header => header + ':' + (header === 'host' ? this.url.host : (this.headers.get(header) || '').replace(/\s+/g, ' '))) 115 | .join('\n'); 116 | this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, 'aws4_request'].join('/'); 117 | if (this.signQuery) { 118 | if (this.service === 's3' && !params.has('X-Amz-Expires')) { 119 | params.set('X-Amz-Expires', '86400'); 120 | } 121 | params.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256'); 122 | params.set('X-Amz-Credential', this.accessKeyId + '/' + this.credentialString); 123 | params.set('X-Amz-SignedHeaders', this.signedHeaders); 124 | } 125 | if (this.service === 's3') { 126 | try { 127 | this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, ' ')); 128 | } catch (e) { 129 | this.encodedPath = this.url.pathname; 130 | } 131 | } else { 132 | this.encodedPath = this.url.pathname.replace(/\/+/g, '/'); 133 | } 134 | if (!singleEncode) { 135 | this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, '/'); 136 | } 137 | this.encodedPath = encodeRfc3986(this.encodedPath); 138 | const seenKeys = new Set(); 139 | this.encodedSearch = [...this.url.searchParams] 140 | .filter(([k]) => { 141 | if (!k) return false 142 | if (this.service === 's3') { 143 | if (seenKeys.has(k)) return false 144 | seenKeys.add(k); 145 | } 146 | return true 147 | }) 148 | .map(pair => pair.map(p => encodeRfc3986(encodeURIComponent(p)))) 149 | .sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0) 150 | .map(pair => pair.join('=')) 151 | .join('&'); 152 | } 153 | async sign() { 154 | if (this.signQuery) { 155 | this.url.searchParams.set('X-Amz-Signature', await this.signature()); 156 | if (this.sessionToken && this.appendSessionToken) { 157 | this.url.searchParams.set('X-Amz-Security-Token', this.sessionToken); 158 | } 159 | } else { 160 | this.headers.set('Authorization', await this.authHeader()); 161 | } 162 | return { 163 | method: this.method, 164 | url: this.url, 165 | headers: this.headers, 166 | body: this.body, 167 | } 168 | } 169 | async authHeader() { 170 | return [ 171 | 'AWS4-HMAC-SHA256 Credential=' + this.accessKeyId + '/' + this.credentialString, 172 | 'SignedHeaders=' + this.signedHeaders, 173 | 'Signature=' + (await this.signature()), 174 | ].join(', ') 175 | } 176 | async signature() { 177 | const date = this.datetime.slice(0, 8); 178 | const cacheKey = [this.secretAccessKey, date, this.region, this.service].join(); 179 | let kCredentials = this.cache.get(cacheKey); 180 | if (!kCredentials) { 181 | const kDate = await hmac('AWS4' + this.secretAccessKey, date); 182 | const kRegion = await hmac(kDate, this.region); 183 | const kService = await hmac(kRegion, this.service); 184 | kCredentials = await hmac(kService, 'aws4_request'); 185 | this.cache.set(cacheKey, kCredentials); 186 | } 187 | return buf2hex(await hmac(kCredentials, await this.stringToSign())) 188 | } 189 | async stringToSign() { 190 | return [ 191 | 'AWS4-HMAC-SHA256', 192 | this.datetime, 193 | this.credentialString, 194 | buf2hex(await hash(await this.canonicalString())), 195 | ].join('\n') 196 | } 197 | async canonicalString() { 198 | return [ 199 | this.method.toUpperCase(), 200 | this.encodedPath, 201 | this.encodedSearch, 202 | this.canonicalHeaders + '\n', 203 | this.signedHeaders, 204 | await this.hexBodyHash(), 205 | ].join('\n') 206 | } 207 | async hexBodyHash() { 208 | let hashHeader = this.headers.get('X-Amz-Content-Sha256') || (this.service === 's3' && this.signQuery ? 'UNSIGNED-PAYLOAD' : null); 209 | if (hashHeader == null) { 210 | if (this.body && typeof this.body !== 'string' && !('byteLength' in this.body)) { 211 | throw new Error('body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header') 212 | } 213 | hashHeader = buf2hex(await hash(this.body || '')); 214 | } 215 | return hashHeader 216 | } 217 | } 218 | async function hmac(key, string) { 219 | const cryptoKey = await crypto.subtle.importKey( 220 | 'raw', 221 | typeof key === 'string' ? encoder.encode(key) : key, 222 | { name: 'HMAC', hash: { name: 'SHA-256' } }, 223 | false, 224 | ['sign'], 225 | ); 226 | return crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(string)) 227 | } 228 | async function hash(content) { 229 | return crypto.subtle.digest('SHA-256', typeof content === 'string' ? encoder.encode(content) : content) 230 | } 231 | const HEX_CHARS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; 232 | function buf2hex(arrayBuffer) { 233 | const buffer = new Uint8Array(arrayBuffer); 234 | let out = ''; 235 | for (let idx = 0; idx < buffer.length; idx++) { 236 | const n = buffer[idx]; 237 | out += HEX_CHARS[(n >>> 4) & 0xF]; 238 | out += HEX_CHARS[n & 0xF]; 239 | } 240 | return out 241 | } 242 | function encodeRfc3986(urlEncodedStr) { 243 | return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()) 244 | } 245 | function guessServiceRegion(url, headers) { 246 | const { hostname, pathname } = url; 247 | if (hostname.endsWith('.on.aws')) { 248 | const match = hostname.match(/^[^.]{1,63}\.lambda-url\.([^.]{1,63})\.on\.aws$/); 249 | return match != null ? ['lambda', match[1] || ''] : ['', ''] 250 | } 251 | if (hostname.endsWith('.r2.cloudflarestorage.com')) { 252 | return ['s3', 'auto'] 253 | } 254 | if (hostname.endsWith('.backblazeb2.com')) { 255 | const match = hostname.match(/^(?:[^.]{1,63}\.)?s3\.([^.]{1,63})\.backblazeb2\.com$/); 256 | return match != null ? ['s3', match[1] || ''] : ['', ''] 257 | } 258 | const match = hostname.replace('dualstack.', '').match(/([^.]{1,63})\.(?:([^.]{0,63})\.)?amazonaws\.com(?:\.cn)?$/); 259 | let service = (match && match[1]) || ''; 260 | let region = match && match[2]; 261 | if (region === 'us-gov') { 262 | region = 'us-gov-west-1'; 263 | } else if (region === 's3' || region === 's3-accelerate') { 264 | region = 'us-east-1'; 265 | service = 's3'; 266 | } else if (service === 'iot') { 267 | if (hostname.startsWith('iot.')) { 268 | service = 'execute-api'; 269 | } else if (hostname.startsWith('data.jobs.iot.')) { 270 | service = 'iot-jobs-data'; 271 | } else { 272 | service = pathname === '/mqtt' ? 'iotdevicegateway' : 'iotdata'; 273 | } 274 | } else if (service === 'autoscaling') { 275 | const targetPrefix = (headers.get('X-Amz-Target') || '').split('.')[0]; 276 | if (targetPrefix === 'AnyScaleFrontendService') { 277 | service = 'application-autoscaling'; 278 | } else if (targetPrefix === 'AnyScaleScalingPlannerFrontendService') { 279 | service = 'autoscaling-plans'; 280 | } 281 | } else if (region == null && service.startsWith('s3-')) { 282 | region = service.slice(3).replace(/^fips-|^external-1/, ''); 283 | service = 's3'; 284 | } else if (service.endsWith('-fips')) { 285 | service = service.slice(0, -5); 286 | } else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) { 287 | [service, region] = [region, service]; 288 | } 289 | return [HOST_SERVICES[service] || service, region || ''] 290 | } 291 | 292 | export { AwsClient, AwsV4Signer }; 293 | -------------------------------------------------------------------------------- /dist/aws4fetch.umd.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.aws4fetch = {})); 5 | })(this, (function (exports) { 'use strict'; 6 | 7 | /** 8 | * @license MIT 9 | * @copyright Michael Hart 2024 10 | */ 11 | const encoder = new TextEncoder(); 12 | const HOST_SERVICES = { 13 | appstream2: 'appstream', 14 | cloudhsmv2: 'cloudhsm', 15 | email: 'ses', 16 | marketplace: 'aws-marketplace', 17 | mobile: 'AWSMobileHubService', 18 | pinpoint: 'mobiletargeting', 19 | queue: 'sqs', 20 | 'git-codecommit': 'codecommit', 21 | 'mturk-requester-sandbox': 'mturk-requester', 22 | 'personalize-runtime': 'personalize', 23 | }; 24 | const UNSIGNABLE_HEADERS = new Set([ 25 | 'authorization', 26 | 'content-type', 27 | 'content-length', 28 | 'user-agent', 29 | 'presigned-expires', 30 | 'expect', 31 | 'x-amzn-trace-id', 32 | 'range', 33 | 'connection', 34 | ]); 35 | class AwsClient { 36 | constructor({ accessKeyId, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }) { 37 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 38 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 39 | this.accessKeyId = accessKeyId; 40 | this.secretAccessKey = secretAccessKey; 41 | this.sessionToken = sessionToken; 42 | this.service = service; 43 | this.region = region; 44 | this.cache = cache || new Map(); 45 | this.retries = retries != null ? retries : 10; 46 | this.initRetryMs = initRetryMs || 50; 47 | } 48 | async sign(input, init) { 49 | if (input instanceof Request) { 50 | const { method, url, headers, body } = input; 51 | init = Object.assign({ method, url, headers }, init); 52 | if (init.body == null && headers.has('Content-Type')) { 53 | init.body = body != null && headers.has('X-Amz-Content-Sha256') ? body : await input.clone().arrayBuffer(); 54 | } 55 | input = url; 56 | } 57 | const signer = new AwsV4Signer(Object.assign({ url: input.toString() }, init, this, init && init.aws)); 58 | const signed = Object.assign({}, init, await signer.sign()); 59 | delete signed.aws; 60 | try { 61 | return new Request(signed.url.toString(), signed) 62 | } catch (e) { 63 | if (e instanceof TypeError) { 64 | return new Request(signed.url.toString(), Object.assign({ duplex: 'half' }, signed)) 65 | } 66 | throw e 67 | } 68 | } 69 | async fetch(input, init) { 70 | for (let i = 0; i <= this.retries; i++) { 71 | const fetched = fetch(await this.sign(input, init)); 72 | if (i === this.retries) { 73 | return fetched 74 | } 75 | const res = await fetched; 76 | if (res.status < 500 && res.status !== 429) { 77 | return res 78 | } 79 | await new Promise(resolve => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i))); 80 | } 81 | throw new Error('An unknown error occurred, ensure retries is not negative') 82 | } 83 | } 84 | class AwsV4Signer { 85 | constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) { 86 | if (url == null) throw new TypeError('url is a required option') 87 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 88 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 89 | this.method = method || (body ? 'POST' : 'GET'); 90 | this.url = new URL(url); 91 | this.headers = new Headers(headers || {}); 92 | this.body = body; 93 | this.accessKeyId = accessKeyId; 94 | this.secretAccessKey = secretAccessKey; 95 | this.sessionToken = sessionToken; 96 | let guessedService, guessedRegion; 97 | if (!service || !region) { 98 | [guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers); 99 | } 100 | this.service = service || guessedService || ''; 101 | this.region = region || guessedRegion || 'us-east-1'; 102 | this.cache = cache || new Map(); 103 | this.datetime = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, ''); 104 | this.signQuery = signQuery; 105 | this.appendSessionToken = appendSessionToken || this.service === 'iotdevicegateway'; 106 | this.headers.delete('Host'); 107 | if (this.service === 's3' && !this.signQuery && !this.headers.has('X-Amz-Content-Sha256')) { 108 | this.headers.set('X-Amz-Content-Sha256', 'UNSIGNED-PAYLOAD'); 109 | } 110 | const params = this.signQuery ? this.url.searchParams : this.headers; 111 | params.set('X-Amz-Date', this.datetime); 112 | if (this.sessionToken && !this.appendSessionToken) { 113 | params.set('X-Amz-Security-Token', this.sessionToken); 114 | } 115 | this.signableHeaders = ['host', ...this.headers.keys()] 116 | .filter(header => allHeaders || !UNSIGNABLE_HEADERS.has(header)) 117 | .sort(); 118 | this.signedHeaders = this.signableHeaders.join(';'); 119 | this.canonicalHeaders = this.signableHeaders 120 | .map(header => header + ':' + (header === 'host' ? this.url.host : (this.headers.get(header) || '').replace(/\s+/g, ' '))) 121 | .join('\n'); 122 | this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, 'aws4_request'].join('/'); 123 | if (this.signQuery) { 124 | if (this.service === 's3' && !params.has('X-Amz-Expires')) { 125 | params.set('X-Amz-Expires', '86400'); 126 | } 127 | params.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256'); 128 | params.set('X-Amz-Credential', this.accessKeyId + '/' + this.credentialString); 129 | params.set('X-Amz-SignedHeaders', this.signedHeaders); 130 | } 131 | if (this.service === 's3') { 132 | try { 133 | this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, ' ')); 134 | } catch (e) { 135 | this.encodedPath = this.url.pathname; 136 | } 137 | } else { 138 | this.encodedPath = this.url.pathname.replace(/\/+/g, '/'); 139 | } 140 | if (!singleEncode) { 141 | this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, '/'); 142 | } 143 | this.encodedPath = encodeRfc3986(this.encodedPath); 144 | const seenKeys = new Set(); 145 | this.encodedSearch = [...this.url.searchParams] 146 | .filter(([k]) => { 147 | if (!k) return false 148 | if (this.service === 's3') { 149 | if (seenKeys.has(k)) return false 150 | seenKeys.add(k); 151 | } 152 | return true 153 | }) 154 | .map(pair => pair.map(p => encodeRfc3986(encodeURIComponent(p)))) 155 | .sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0) 156 | .map(pair => pair.join('=')) 157 | .join('&'); 158 | } 159 | async sign() { 160 | if (this.signQuery) { 161 | this.url.searchParams.set('X-Amz-Signature', await this.signature()); 162 | if (this.sessionToken && this.appendSessionToken) { 163 | this.url.searchParams.set('X-Amz-Security-Token', this.sessionToken); 164 | } 165 | } else { 166 | this.headers.set('Authorization', await this.authHeader()); 167 | } 168 | return { 169 | method: this.method, 170 | url: this.url, 171 | headers: this.headers, 172 | body: this.body, 173 | } 174 | } 175 | async authHeader() { 176 | return [ 177 | 'AWS4-HMAC-SHA256 Credential=' + this.accessKeyId + '/' + this.credentialString, 178 | 'SignedHeaders=' + this.signedHeaders, 179 | 'Signature=' + (await this.signature()), 180 | ].join(', ') 181 | } 182 | async signature() { 183 | const date = this.datetime.slice(0, 8); 184 | const cacheKey = [this.secretAccessKey, date, this.region, this.service].join(); 185 | let kCredentials = this.cache.get(cacheKey); 186 | if (!kCredentials) { 187 | const kDate = await hmac('AWS4' + this.secretAccessKey, date); 188 | const kRegion = await hmac(kDate, this.region); 189 | const kService = await hmac(kRegion, this.service); 190 | kCredentials = await hmac(kService, 'aws4_request'); 191 | this.cache.set(cacheKey, kCredentials); 192 | } 193 | return buf2hex(await hmac(kCredentials, await this.stringToSign())) 194 | } 195 | async stringToSign() { 196 | return [ 197 | 'AWS4-HMAC-SHA256', 198 | this.datetime, 199 | this.credentialString, 200 | buf2hex(await hash(await this.canonicalString())), 201 | ].join('\n') 202 | } 203 | async canonicalString() { 204 | return [ 205 | this.method.toUpperCase(), 206 | this.encodedPath, 207 | this.encodedSearch, 208 | this.canonicalHeaders + '\n', 209 | this.signedHeaders, 210 | await this.hexBodyHash(), 211 | ].join('\n') 212 | } 213 | async hexBodyHash() { 214 | let hashHeader = this.headers.get('X-Amz-Content-Sha256') || (this.service === 's3' && this.signQuery ? 'UNSIGNED-PAYLOAD' : null); 215 | if (hashHeader == null) { 216 | if (this.body && typeof this.body !== 'string' && !('byteLength' in this.body)) { 217 | throw new Error('body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header') 218 | } 219 | hashHeader = buf2hex(await hash(this.body || '')); 220 | } 221 | return hashHeader 222 | } 223 | } 224 | async function hmac(key, string) { 225 | const cryptoKey = await crypto.subtle.importKey( 226 | 'raw', 227 | typeof key === 'string' ? encoder.encode(key) : key, 228 | { name: 'HMAC', hash: { name: 'SHA-256' } }, 229 | false, 230 | ['sign'], 231 | ); 232 | return crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(string)) 233 | } 234 | async function hash(content) { 235 | return crypto.subtle.digest('SHA-256', typeof content === 'string' ? encoder.encode(content) : content) 236 | } 237 | const HEX_CHARS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; 238 | function buf2hex(arrayBuffer) { 239 | const buffer = new Uint8Array(arrayBuffer); 240 | let out = ''; 241 | for (let idx = 0; idx < buffer.length; idx++) { 242 | const n = buffer[idx]; 243 | out += HEX_CHARS[(n >>> 4) & 0xF]; 244 | out += HEX_CHARS[n & 0xF]; 245 | } 246 | return out 247 | } 248 | function encodeRfc3986(urlEncodedStr) { 249 | return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()) 250 | } 251 | function guessServiceRegion(url, headers) { 252 | const { hostname, pathname } = url; 253 | if (hostname.endsWith('.on.aws')) { 254 | const match = hostname.match(/^[^.]{1,63}\.lambda-url\.([^.]{1,63})\.on\.aws$/); 255 | return match != null ? ['lambda', match[1] || ''] : ['', ''] 256 | } 257 | if (hostname.endsWith('.r2.cloudflarestorage.com')) { 258 | return ['s3', 'auto'] 259 | } 260 | if (hostname.endsWith('.backblazeb2.com')) { 261 | const match = hostname.match(/^(?:[^.]{1,63}\.)?s3\.([^.]{1,63})\.backblazeb2\.com$/); 262 | return match != null ? ['s3', match[1] || ''] : ['', ''] 263 | } 264 | const match = hostname.replace('dualstack.', '').match(/([^.]{1,63})\.(?:([^.]{0,63})\.)?amazonaws\.com(?:\.cn)?$/); 265 | let service = (match && match[1]) || ''; 266 | let region = match && match[2]; 267 | if (region === 'us-gov') { 268 | region = 'us-gov-west-1'; 269 | } else if (region === 's3' || region === 's3-accelerate') { 270 | region = 'us-east-1'; 271 | service = 's3'; 272 | } else if (service === 'iot') { 273 | if (hostname.startsWith('iot.')) { 274 | service = 'execute-api'; 275 | } else if (hostname.startsWith('data.jobs.iot.')) { 276 | service = 'iot-jobs-data'; 277 | } else { 278 | service = pathname === '/mqtt' ? 'iotdevicegateway' : 'iotdata'; 279 | } 280 | } else if (service === 'autoscaling') { 281 | const targetPrefix = (headers.get('X-Amz-Target') || '').split('.')[0]; 282 | if (targetPrefix === 'AnyScaleFrontendService') { 283 | service = 'application-autoscaling'; 284 | } else if (targetPrefix === 'AnyScaleScalingPlannerFrontendService') { 285 | service = 'autoscaling-plans'; 286 | } 287 | } else if (region == null && service.startsWith('s3-')) { 288 | region = service.slice(3).replace(/^fips-|^external-1/, ''); 289 | service = 's3'; 290 | } else if (service.endsWith('-fips')) { 291 | service = service.slice(0, -5); 292 | } else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) { 293 | [service, region] = [region, service]; 294 | } 295 | return [HOST_SERVICES[service] || service, region || ''] 296 | } 297 | 298 | exports.AwsClient = AwsClient; 299 | exports.AwsV4Signer = AwsV4Signer; 300 | 301 | Object.defineProperty(exports, '__esModule', { value: true }); 302 | 303 | })); 304 | -------------------------------------------------------------------------------- /dist/main.d.mts: -------------------------------------------------------------------------------- 1 | export class AwsClient { 2 | constructor({ accessKeyId, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }: { 3 | accessKeyId: string; 4 | secretAccessKey: string; 5 | sessionToken?: string; 6 | service?: string; 7 | region?: string; 8 | cache?: Map; 9 | retries?: number; 10 | initRetryMs?: number; 11 | }); 12 | accessKeyId: string; 13 | secretAccessKey: string; 14 | sessionToken: string | undefined; 15 | service: string | undefined; 16 | region: string | undefined; 17 | cache: Map; 18 | retries: number; 19 | initRetryMs: number; 20 | sign(input: Request | { 21 | toString: () => string; 22 | }, init?: (RequestInit & { 23 | aws?: { 24 | accessKeyId?: string; 25 | secretAccessKey?: string; 26 | sessionToken?: string; 27 | service?: string; 28 | region?: string; 29 | cache?: Map; 30 | datetime?: string; 31 | signQuery?: boolean; 32 | appendSessionToken?: boolean; 33 | allHeaders?: boolean; 34 | singleEncode?: boolean; 35 | }; 36 | }) | null | undefined): Promise; 37 | fetch(input: Request | { 38 | toString: () => string; 39 | }, init?: (RequestInit & { 40 | aws?: { 41 | accessKeyId?: string; 42 | secretAccessKey?: string; 43 | sessionToken?: string; 44 | service?: string; 45 | region?: string; 46 | cache?: Map; 47 | datetime?: string; 48 | signQuery?: boolean; 49 | appendSessionToken?: boolean; 50 | allHeaders?: boolean; 51 | singleEncode?: boolean; 52 | }; 53 | }) | null | undefined): Promise; 54 | } 55 | export class AwsV4Signer { 56 | constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }: { 57 | method?: string; 58 | url: string; 59 | headers?: HeadersInit; 60 | body?: BodyInit | null; 61 | accessKeyId: string; 62 | secretAccessKey: string; 63 | sessionToken?: string; 64 | service?: string; 65 | region?: string; 66 | cache?: Map; 67 | datetime?: string; 68 | signQuery?: boolean; 69 | appendSessionToken?: boolean; 70 | allHeaders?: boolean; 71 | singleEncode?: boolean; 72 | }); 73 | method: string; 74 | url: URL; 75 | headers: Headers; 76 | body: BodyInit | null | undefined; 77 | accessKeyId: string; 78 | secretAccessKey: string; 79 | sessionToken: string | undefined; 80 | service: string; 81 | region: string; 82 | cache: Map; 83 | datetime: string; 84 | signQuery: boolean | undefined; 85 | appendSessionToken: boolean; 86 | signableHeaders: string[]; 87 | signedHeaders: string; 88 | canonicalHeaders: string; 89 | credentialString: string; 90 | encodedPath: string; 91 | encodedSearch: string; 92 | sign(): Promise<{ 93 | method: string; 94 | url: URL; 95 | headers: Headers; 96 | body?: BodyInit | null; 97 | }>; 98 | authHeader(): Promise; 99 | signature(): Promise; 100 | stringToSign(): Promise; 101 | canonicalString(): Promise; 102 | hexBodyHash(): Promise; 103 | } 104 | -------------------------------------------------------------------------------- /dist/main.d.ts: -------------------------------------------------------------------------------- 1 | export class AwsClient { 2 | constructor({ accessKeyId, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }: { 3 | accessKeyId: string; 4 | secretAccessKey: string; 5 | sessionToken?: string; 6 | service?: string; 7 | region?: string; 8 | cache?: Map; 9 | retries?: number; 10 | initRetryMs?: number; 11 | }); 12 | accessKeyId: string; 13 | secretAccessKey: string; 14 | sessionToken: string | undefined; 15 | service: string | undefined; 16 | region: string | undefined; 17 | cache: Map; 18 | retries: number; 19 | initRetryMs: number; 20 | sign(input: Request | { 21 | toString: () => string; 22 | }, init?: (RequestInit & { 23 | aws?: { 24 | accessKeyId?: string; 25 | secretAccessKey?: string; 26 | sessionToken?: string; 27 | service?: string; 28 | region?: string; 29 | cache?: Map; 30 | datetime?: string; 31 | signQuery?: boolean; 32 | appendSessionToken?: boolean; 33 | allHeaders?: boolean; 34 | singleEncode?: boolean; 35 | }; 36 | }) | null | undefined): Promise; 37 | fetch(input: Request | { 38 | toString: () => string; 39 | }, init?: (RequestInit & { 40 | aws?: { 41 | accessKeyId?: string; 42 | secretAccessKey?: string; 43 | sessionToken?: string; 44 | service?: string; 45 | region?: string; 46 | cache?: Map; 47 | datetime?: string; 48 | signQuery?: boolean; 49 | appendSessionToken?: boolean; 50 | allHeaders?: boolean; 51 | singleEncode?: boolean; 52 | }; 53 | }) | null | undefined): Promise; 54 | } 55 | export class AwsV4Signer { 56 | constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }: { 57 | method?: string; 58 | url: string; 59 | headers?: HeadersInit; 60 | body?: BodyInit | null; 61 | accessKeyId: string; 62 | secretAccessKey: string; 63 | sessionToken?: string; 64 | service?: string; 65 | region?: string; 66 | cache?: Map; 67 | datetime?: string; 68 | signQuery?: boolean; 69 | appendSessionToken?: boolean; 70 | allHeaders?: boolean; 71 | singleEncode?: boolean; 72 | }); 73 | method: string; 74 | url: URL; 75 | headers: Headers; 76 | body: BodyInit | null | undefined; 77 | accessKeyId: string; 78 | secretAccessKey: string; 79 | sessionToken: string | undefined; 80 | service: string; 81 | region: string; 82 | cache: Map; 83 | datetime: string; 84 | signQuery: boolean | undefined; 85 | appendSessionToken: boolean; 86 | signableHeaders: string[]; 87 | signedHeaders: string; 88 | canonicalHeaders: string; 89 | credentialString: string; 90 | encodedPath: string; 91 | encodedSearch: string; 92 | sign(): Promise<{ 93 | method: string; 94 | url: URL; 95 | headers: Headers; 96 | body?: BodyInit | null; 97 | }>; 98 | authHeader(): Promise; 99 | signature(): Promise; 100 | stringToSign(): Promise; 101 | canonicalString(): Promise; 102 | hexBodyHash(): Promise; 103 | } 104 | -------------------------------------------------------------------------------- /example/.dev.vars: -------------------------------------------------------------------------------- 1 | AWS_SECRET_ACCESS_KEY = "EXAMPLEKEY..." -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # aws4fetch Example: Calling AWS services from Cloudflare Workers 2 | 3 | `src/index.js` contains an example script you can use in a 4 | [Cloudflare Worker](https://workers.cloudflare.com/) to call an 5 | AWS Lambda function, including caching – you could use this in place of an API 6 | Gateway integration, for example. 7 | 8 | The relevant portions of the code are the import/setup: 9 | 10 | ```js 11 | import { AwsClient } from 'aws4fetch' 12 | 13 | // ... 14 | 15 | // Assume AWS_* vars have added to your environment 16 | // https://developers.cloudflare.com/workers/reference/apis/environment-variables/#secrets 17 | aws = new AwsClient({ 18 | accessKeyId: env.AWS_ACCESS_KEY_ID, 19 | secretAccessKey: env.AWS_SECRET_ACCESS_KEY, 20 | }) 21 | 22 | // https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html 23 | const LAMBDA_INVOKE_URL = `https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/${LAMBDA_FN}/invocations` 24 | 25 | const lambdaResponse = await aws.fetch(LAMBDA_INVOKE_URL, { 26 | method: 'POST', 27 | headers: { 'Content-Type': 'application/json' }, 28 | body: JSON.stringify(await toLambdaEvent(request)), 29 | }) 30 | ``` 31 | 32 | The conversion from the incoming [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) 33 | to an API-Gateway-style Lambda event: 34 | 35 | ```js 36 | async function toLambdaEvent(request) { 37 | const url = new URL(request.url) 38 | return { 39 | httpMethod: request.method, 40 | path: url.pathname, 41 | queryStringParameters: Object.fromEntries([...url.searchParams]), 42 | headers: Object.fromEntries([...request.headers]), 43 | body: ['GET', 'HEAD'].includes(request.method) ? undefined : await request.text(), 44 | } 45 | } 46 | ``` 47 | 48 | And the call to Lambda itself, converting the response into a 49 | [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) you can return from the worker: 50 | 51 | ```js 52 | const lambdaResponse = await aws.fetch(LAMBDA_INVOKE_URL, { 53 | method: 'POST', 54 | headers: { 'Content-Type': 'application/json' }, 55 | body: JSON.stringify(await toLambdaEvent(request)), 56 | }) 57 | 58 | if (!lambdaResponse.ok) { 59 | console.error(await lambdaResponse.text()) 60 | return Response.json({ error: `Lambda API returned ${lambdaResponse.status}` }, { status: 500 }) 61 | } 62 | 63 | const { statusCode: status, headers, body } = await lambdaResponse.json() 64 | 65 | return new Response(body, { status, headers }) 66 | ``` 67 | 68 | ## Deploying to Cloudflare 69 | 70 | ```console 71 | npm install 72 | npx wrangler deploy 73 | ``` 74 | 75 | -------------------------------------------------------------------------------- /example/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext"], 5 | "moduleResolution": "Bundler", 6 | "types": ["@cloudflare/workers-types"], 7 | "checkJs": true, 8 | "noEmit": true, 9 | "strict": true, 10 | "allowUnreachableCode": false, 11 | "allowUnusedLabels": false, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitOverride": true, 14 | "noImplicitReturns": true, 15 | "noPropertyAccessFromIndexSignature": true, 16 | "noUncheckedIndexedAccess": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true 19 | }, 20 | "include": ["src/**/*"] 21 | } 22 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws4fetch-example", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "aws4fetch": "^1.0.18" 6 | }, 7 | "devDependencies": { 8 | "@cloudflare/workers-types": "^4.20240718.0", 9 | "wrangler": "^3.65.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import { AwsClient } from 'aws4fetch' 2 | 3 | const LAMBDA_FN = 'my-api-function' 4 | 5 | /** @type {AwsClient | undefined} */ 6 | let aws 7 | 8 | /** @type {ExportedHandler} */ 9 | export default { 10 | async fetch(request, env, ctx) { 11 | const { method, url } = request 12 | 13 | if (method === 'OPTIONS') { 14 | return new Response('', { 15 | headers: { 16 | 'Access-Control-Allow-Origin': '*', 17 | 'Access-Control-Allow-Headers': 'Content-Type,Authorization', 18 | 'Access-Control-Allow-Methods': 'GET,OPTIONS,POST', 19 | 'Access-Control-Max-Age': '86400', 20 | }, 21 | }) 22 | } 23 | 24 | // https://developers.cloudflare.com/workers/reference/apis/cache/ 25 | const cache = caches.default 26 | 27 | const isCacheable = ['GET', 'HEAD'].includes(method) 28 | if (isCacheable) { 29 | const response = await cache.match(url) 30 | if (response != null) { 31 | return response 32 | } 33 | } 34 | 35 | if (aws == null) { 36 | // Assume AWS_* vars have added to your environment 37 | // https://developers.cloudflare.com/workers/reference/apis/environment-variables/#secrets 38 | aws = new AwsClient({ 39 | accessKeyId: env.AWS_ACCESS_KEY_ID, 40 | secretAccessKey: env.AWS_SECRET_ACCESS_KEY, 41 | }) 42 | } 43 | 44 | // https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html 45 | const LAMBDA_INVOKE_URL = `https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/${LAMBDA_FN}/invocations` 46 | 47 | const lambdaResponse = await aws.fetch(LAMBDA_INVOKE_URL, { 48 | method: 'POST', 49 | headers: { 'Content-Type': 'application/json' }, 50 | body: JSON.stringify(await toLambdaEvent(request)), 51 | }) 52 | 53 | if (!lambdaResponse.ok) { 54 | console.error(await lambdaResponse.text()) 55 | return Response.json({ error: `Lambda API returned ${lambdaResponse.status}` }, { status: 500 }) 56 | } 57 | 58 | const { statusCode: status, headers, body } = await lambdaResponse.json() 59 | 60 | const response = new Response(body, { status, headers }) 61 | 62 | if (isCacheable && response.headers.has('Cache-Control')) { 63 | ctx.waitUntil(cache.put(url, response.clone())) 64 | } 65 | 66 | return response 67 | }, 68 | } 69 | 70 | /** 71 | * @param {Request} request 72 | */ 73 | async function toLambdaEvent(request) { 74 | const url = new URL(request.url) 75 | return { 76 | httpMethod: request.method, 77 | path: url.pathname, 78 | queryStringParameters: Object.fromEntries([...url.searchParams]), 79 | headers: Object.fromEntries([...request.headers]), 80 | body: ['GET', 'HEAD'].includes(request.method) ? undefined : await request.text(), 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /example/wrangler.toml: -------------------------------------------------------------------------------- 1 | #:schema ../../node_modules/wrangler/config-schema.json 2 | name = "aws4fetch-example" 3 | main = "src/index.js" 4 | compatibility_date = "2024-07-01" 5 | 6 | [vars] 7 | AWS_ACCESS_KEY_ID = "AKIAIOS..." 8 | 9 | # Add this via `npx wrangler secret put AWS_SECRET_ACCESS_KEY` 10 | # AWS_SECRET_ACCESS_KEY = "EXAMPLEKEY..." -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2016", 4 | "moduleResolution": "Bundler", 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "checkJs": true, 8 | "noEmit": true, 9 | "strict": true, 10 | "allowUnreachableCode": false, 11 | "allowUnusedLabels": false, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitOverride": true, 14 | "noImplicitReturns": true, 15 | "noPropertyAccessFromIndexSignature": true, 16 | "noUncheckedIndexedAccess": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "types": [] 20 | }, 21 | "include": ["src/**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws4fetch", 3 | "version": "1.0.20", 4 | "description": "A compact AWS client for modern JS environments", 5 | "author": "Michael Hart (https://github.com/mhart)", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/mhart/aws4fetch.git" 10 | }, 11 | "main": "dist/aws4fetch.cjs.js", 12 | "module": "dist/aws4fetch.esm.js", 13 | "browser": "dist/aws4fetch.umd.js", 14 | "exports": { 15 | ".": { 16 | "import": { 17 | "types": "./dist/main.d.mts", 18 | "default": "./dist/aws4fetch.esm.mjs" 19 | }, 20 | "worker": { 21 | "types": "./dist/main.d.mts", 22 | "default": "./dist/aws4fetch.esm.js" 23 | }, 24 | "browser": { 25 | "types": "./dist/main.d.ts", 26 | "default": "./dist/aws4fetch.umd.js" 27 | }, 28 | "require": { 29 | "types": "./dist/main.d.ts", 30 | "default": "./dist/aws4fetch.cjs.js" 31 | }, 32 | "default": { 33 | "types": "./dist/main.d.ts", 34 | "default": "./dist/aws4fetch.umd.js" 35 | } 36 | } 37 | }, 38 | "types": "dist/main.d.ts", 39 | "files": [ 40 | "dist" 41 | ], 42 | "scripts": { 43 | "declaration": "tsc -p declaration.jsconfig.json && cp dist/main.d.ts dist/main.d.mts", 44 | "build": "npm run declaration && rollup -c", 45 | "prepare": "npm run build", 46 | "lint": "eslint --ext .js,.cjs,.mjs --ignore-pattern rollup.config.mjs .", 47 | "format": "eslint --ext .js,.cjs,.mjs --ignore-pattern rollup.config.mjs --fix .", 48 | "test": "node test/test.js", 49 | "test-node": "node test/node-es.mjs && node test/node-commonjs.js", 50 | "integration": "node test/integration.js" 51 | }, 52 | "devDependencies": { 53 | "eslint": "^8.57.0", 54 | "eslint-config-standard": "^17.1.0", 55 | "eslint-plugin-import": "^2.29.1", 56 | "eslint-plugin-node": "^11.1.0", 57 | "eslint-plugin-promise": "^6.5.0", 58 | "puppeteer": "^22.13.1", 59 | "rollup": "^4.18.1", 60 | "rollup-plugin-cleanup": "^3.2.1", 61 | "typescript": "^5.5.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import pkg from './package.json' with { type: "json" } 2 | import cleanup from 'rollup-plugin-cleanup' 3 | 4 | export default { 5 | input: 'src/main.js', 6 | plugins: [ 7 | cleanup(), 8 | ], 9 | output: [ 10 | { format: 'es', file: pkg.module }, 11 | { format: 'es', file: pkg.exports['.'].import.default }, 12 | { format: 'cjs', file: pkg.main, esModule: true }, 13 | { format: 'umd', file: pkg.browser, name: pkg.name, esModule: true }, 14 | ], 15 | } 16 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * @copyright Michael Hart 2024 4 | */ 5 | 6 | const encoder = new TextEncoder() 7 | 8 | /** @type {Record} */ 9 | const HOST_SERVICES = { 10 | appstream2: 'appstream', 11 | cloudhsmv2: 'cloudhsm', 12 | email: 'ses', 13 | marketplace: 'aws-marketplace', 14 | mobile: 'AWSMobileHubService', 15 | pinpoint: 'mobiletargeting', 16 | queue: 'sqs', 17 | 'git-codecommit': 'codecommit', 18 | 'mturk-requester-sandbox': 'mturk-requester', 19 | 'personalize-runtime': 'personalize', 20 | } 21 | 22 | // https://github.com/aws/aws-sdk-js/blob/cc29728c1c4178969ebabe3bbe6b6f3159436394/lib/signers/v4.js#L190-L198 23 | const UNSIGNABLE_HEADERS = new Set([ 24 | 'authorization', 25 | 'content-type', 26 | 'content-length', 27 | 'user-agent', 28 | 'presigned-expires', 29 | 'expect', 30 | 'x-amzn-trace-id', 31 | 'range', 32 | 'connection', 33 | ]) 34 | 35 | export class AwsClient { 36 | /** 37 | * @param {{ 38 | * accessKeyId: string 39 | * secretAccessKey: string 40 | * sessionToken?: string 41 | * service?: string 42 | * region?: string 43 | * cache?: Map 44 | * retries?: number 45 | * initRetryMs?: number 46 | * }} options 47 | */ 48 | constructor({ accessKeyId, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }) { 49 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 50 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 51 | this.accessKeyId = accessKeyId 52 | this.secretAccessKey = secretAccessKey 53 | this.sessionToken = sessionToken 54 | this.service = service 55 | this.region = region 56 | /** @type {Map} */ 57 | this.cache = cache || new Map() 58 | this.retries = retries != null ? retries : 10 // Up to 25.6 secs 59 | this.initRetryMs = initRetryMs || 50 60 | } 61 | 62 | /** 63 | * @typedef {RequestInit & { 64 | * aws?: { 65 | * accessKeyId?: string 66 | * secretAccessKey?: string 67 | * sessionToken?: string 68 | * service?: string 69 | * region?: string 70 | * cache?: Map 71 | * datetime?: string 72 | * signQuery?: boolean 73 | * appendSessionToken?: boolean 74 | * allHeaders?: boolean 75 | * singleEncode?: boolean 76 | * } 77 | * }} AwsRequestInit 78 | * 79 | * @param {Request | { toString: () => string }} input 80 | * @param {?AwsRequestInit} [init] 81 | * @returns {Promise} 82 | */ 83 | async sign(input, init) { 84 | if (input instanceof Request) { 85 | const { method, url, headers, body } = input 86 | init = Object.assign({ method, url, headers }, init) 87 | if (init.body == null && headers.has('Content-Type')) { 88 | init.body = body != null && headers.has('X-Amz-Content-Sha256') ? body : await input.clone().arrayBuffer() 89 | } 90 | input = url 91 | } 92 | const signer = new AwsV4Signer(Object.assign({ url: input.toString() }, init, this, init && init.aws)) 93 | const signed = Object.assign({}, init, await signer.sign()) 94 | delete signed.aws 95 | try { 96 | return new Request(signed.url.toString(), signed) 97 | } catch (e) { 98 | if (e instanceof TypeError) { 99 | // https://bugs.chromium.org/p/chromium/issues/detail?id=1360943 100 | return new Request(signed.url.toString(), Object.assign({ duplex: 'half' }, signed)) 101 | } 102 | throw e 103 | } 104 | } 105 | 106 | /** 107 | * @param {Request | { toString: () => string }} input 108 | * @param {?AwsRequestInit} [init] 109 | * @returns {Promise} 110 | */ 111 | async fetch(input, init) { 112 | for (let i = 0; i <= this.retries; i++) { 113 | const fetched = fetch(await this.sign(input, init)) 114 | if (i === this.retries) { 115 | return fetched // No need to await if we're returning anyway 116 | } 117 | const res = await fetched 118 | if (res.status < 500 && res.status !== 429) { 119 | return res 120 | } 121 | await new Promise(resolve => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i))) 122 | } 123 | throw new Error('An unknown error occurred, ensure retries is not negative') 124 | } 125 | } 126 | 127 | export class AwsV4Signer { 128 | /** 129 | * @param {{ 130 | * method?: string 131 | * url: string 132 | * headers?: HeadersInit 133 | * body?: BodyInit | null 134 | * accessKeyId: string 135 | * secretAccessKey: string 136 | * sessionToken?: string 137 | * service?: string 138 | * region?: string 139 | * cache?: Map 140 | * datetime?: string 141 | * signQuery?: boolean 142 | * appendSessionToken?: boolean 143 | * allHeaders?: boolean 144 | * singleEncode?: boolean 145 | * }} options 146 | */ 147 | constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) { 148 | if (url == null) throw new TypeError('url is a required option') 149 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 150 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 151 | 152 | this.method = method || (body ? 'POST' : 'GET') 153 | this.url = new URL(url) 154 | this.headers = new Headers(headers || {}) 155 | this.body = body 156 | 157 | this.accessKeyId = accessKeyId 158 | this.secretAccessKey = secretAccessKey 159 | this.sessionToken = sessionToken 160 | 161 | let guessedService, guessedRegion 162 | if (!service || !region) { 163 | ;[guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers) 164 | } 165 | this.service = service || guessedService || '' 166 | this.region = region || guessedRegion || 'us-east-1' 167 | 168 | /** @type {Map} */ 169 | this.cache = cache || new Map() 170 | this.datetime = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, '') 171 | this.signQuery = signQuery 172 | this.appendSessionToken = appendSessionToken || this.service === 'iotdevicegateway' 173 | 174 | this.headers.delete('Host') // Can't be set in insecure env anyway 175 | 176 | if (this.service === 's3' && !this.signQuery && !this.headers.has('X-Amz-Content-Sha256')) { 177 | this.headers.set('X-Amz-Content-Sha256', 'UNSIGNED-PAYLOAD') 178 | } 179 | 180 | const params = this.signQuery ? this.url.searchParams : this.headers 181 | 182 | params.set('X-Amz-Date', this.datetime) 183 | if (this.sessionToken && !this.appendSessionToken) { 184 | params.set('X-Amz-Security-Token', this.sessionToken) 185 | } 186 | 187 | // headers are always lowercase in keys() 188 | this.signableHeaders = ['host', ...this.headers.keys()] 189 | .filter(header => allHeaders || !UNSIGNABLE_HEADERS.has(header)) 190 | .sort() 191 | 192 | this.signedHeaders = this.signableHeaders.join(';') 193 | 194 | // headers are always trimmed: 195 | // https://fetch.spec.whatwg.org/#concept-header-value-normalize 196 | this.canonicalHeaders = this.signableHeaders 197 | .map(header => header + ':' + (header === 'host' ? this.url.host : (this.headers.get(header) || '').replace(/\s+/g, ' '))) 198 | .join('\n') 199 | 200 | this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, 'aws4_request'].join('/') 201 | 202 | if (this.signQuery) { 203 | if (this.service === 's3' && !params.has('X-Amz-Expires')) { 204 | params.set('X-Amz-Expires', '86400') // 24 hours 205 | } 206 | params.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256') 207 | params.set('X-Amz-Credential', this.accessKeyId + '/' + this.credentialString) 208 | params.set('X-Amz-SignedHeaders', this.signedHeaders) 209 | } 210 | 211 | if (this.service === 's3') { 212 | try { 213 | this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, ' ')) 214 | } catch (e) { 215 | this.encodedPath = this.url.pathname 216 | } 217 | } else { 218 | this.encodedPath = this.url.pathname.replace(/\/+/g, '/') 219 | } 220 | if (!singleEncode) { 221 | this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, '/') 222 | } 223 | this.encodedPath = encodeRfc3986(this.encodedPath) 224 | 225 | const seenKeys = new Set() 226 | this.encodedSearch = [...this.url.searchParams] 227 | .filter(([k]) => { 228 | if (!k) return false // no empty keys 229 | if (this.service === 's3') { 230 | if (seenKeys.has(k)) return false // first val only for S3 231 | seenKeys.add(k) 232 | } 233 | return true 234 | }) 235 | .map(pair => pair.map(p => encodeRfc3986(encodeURIComponent(p)))) 236 | // @ts-expect-error "k1 is possibly undefined" due to overzealous noUncheckedIndexedAccess 237 | .sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0) 238 | .map(pair => pair.join('=')) 239 | .join('&') 240 | } 241 | 242 | /** 243 | * @returns {Promise<{ 244 | * method: string 245 | * url: URL 246 | * headers: Headers 247 | * body?: BodyInit | null 248 | * }>} 249 | */ 250 | async sign() { 251 | if (this.signQuery) { 252 | this.url.searchParams.set('X-Amz-Signature', await this.signature()) 253 | if (this.sessionToken && this.appendSessionToken) { 254 | this.url.searchParams.set('X-Amz-Security-Token', this.sessionToken) 255 | } 256 | } else { 257 | this.headers.set('Authorization', await this.authHeader()) 258 | } 259 | 260 | return { 261 | method: this.method, 262 | url: this.url, 263 | headers: this.headers, 264 | body: this.body, 265 | } 266 | } 267 | 268 | /** 269 | * @returns {Promise} 270 | */ 271 | async authHeader() { 272 | return [ 273 | 'AWS4-HMAC-SHA256 Credential=' + this.accessKeyId + '/' + this.credentialString, 274 | 'SignedHeaders=' + this.signedHeaders, 275 | 'Signature=' + (await this.signature()), 276 | ].join(', ') 277 | } 278 | 279 | /** 280 | * @returns {Promise} 281 | */ 282 | async signature() { 283 | const date = this.datetime.slice(0, 8) 284 | const cacheKey = [this.secretAccessKey, date, this.region, this.service].join() 285 | let kCredentials = this.cache.get(cacheKey) 286 | if (!kCredentials) { 287 | const kDate = await hmac('AWS4' + this.secretAccessKey, date) 288 | const kRegion = await hmac(kDate, this.region) 289 | const kService = await hmac(kRegion, this.service) 290 | kCredentials = await hmac(kService, 'aws4_request') 291 | this.cache.set(cacheKey, kCredentials) 292 | } 293 | return buf2hex(await hmac(kCredentials, await this.stringToSign())) 294 | } 295 | 296 | /** 297 | * @returns {Promise} 298 | */ 299 | async stringToSign() { 300 | return [ 301 | 'AWS4-HMAC-SHA256', 302 | this.datetime, 303 | this.credentialString, 304 | buf2hex(await hash(await this.canonicalString())), 305 | ].join('\n') 306 | } 307 | 308 | /** 309 | * @returns {Promise} 310 | */ 311 | async canonicalString() { 312 | return [ 313 | this.method.toUpperCase(), 314 | this.encodedPath, 315 | this.encodedSearch, 316 | this.canonicalHeaders + '\n', 317 | this.signedHeaders, 318 | await this.hexBodyHash(), 319 | ].join('\n') 320 | } 321 | 322 | /** 323 | * @returns {Promise} 324 | */ 325 | async hexBodyHash() { 326 | let hashHeader = this.headers.get('X-Amz-Content-Sha256') || (this.service === 's3' && this.signQuery ? 'UNSIGNED-PAYLOAD' : null) 327 | if (hashHeader == null) { 328 | if (this.body && typeof this.body !== 'string' && !('byteLength' in this.body)) { 329 | throw new Error('body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header') 330 | } 331 | hashHeader = buf2hex(await hash(this.body || '')) 332 | } 333 | return hashHeader 334 | } 335 | } 336 | 337 | /** 338 | * @param {string | BufferSource} key 339 | * @param {string} string 340 | * @returns {Promise} 341 | */ 342 | async function hmac(key, string) { 343 | const cryptoKey = await crypto.subtle.importKey( 344 | 'raw', 345 | typeof key === 'string' ? encoder.encode(key) : key, 346 | { name: 'HMAC', hash: { name: 'SHA-256' } }, 347 | false, 348 | ['sign'], 349 | ) 350 | return crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(string)) 351 | } 352 | 353 | /** 354 | * @param {string | BufferSource} content 355 | * @returns {Promise} 356 | */ 357 | async function hash(content) { 358 | return crypto.subtle.digest('SHA-256', typeof content === 'string' ? encoder.encode(content) : content) 359 | } 360 | 361 | const HEX_CHARS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'] 362 | 363 | /** 364 | * @param {ArrayBufferLike} arrayBuffer 365 | * @returns {string} 366 | */ 367 | function buf2hex(arrayBuffer) { 368 | const buffer = new Uint8Array(arrayBuffer) 369 | let out = '' 370 | for (let idx = 0; idx < buffer.length; idx++) { 371 | const n = buffer[idx] 372 | // @ts-expect-error "n is possibly undefined" due to overzealous noUncheckedIndexedAccess 373 | out += HEX_CHARS[(n >>> 4) & 0xF] 374 | // @ts-expect-error "n is possibly undefined" due to overzealous noUncheckedIndexedAccess 375 | out += HEX_CHARS[n & 0xF] 376 | } 377 | return out 378 | } 379 | 380 | /** 381 | * @param {string} urlEncodedStr 382 | * @returns {string} 383 | */ 384 | function encodeRfc3986(urlEncodedStr) { 385 | return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()) 386 | } 387 | 388 | /** 389 | * @param {URL} url 390 | * @param {Headers} headers 391 | * @returns {[string, string]} [service, region] 392 | */ 393 | function guessServiceRegion(url, headers) { 394 | const { hostname, pathname } = url 395 | 396 | if (hostname.endsWith('.on.aws')) { 397 | const match = hostname.match(/^[^.]{1,63}\.lambda-url\.([^.]{1,63})\.on\.aws$/) 398 | return match != null ? ['lambda', match[1] || ''] : ['', ''] 399 | } 400 | if (hostname.endsWith('.r2.cloudflarestorage.com')) { 401 | return ['s3', 'auto'] 402 | } 403 | if (hostname.endsWith('.backblazeb2.com')) { 404 | const match = hostname.match(/^(?:[^.]{1,63}\.)?s3\.([^.]{1,63})\.backblazeb2\.com$/) 405 | return match != null ? ['s3', match[1] || ''] : ['', ''] 406 | } 407 | const match = hostname.replace('dualstack.', '').match(/([^.]{1,63})\.(?:([^.]{0,63})\.)?amazonaws\.com(?:\.cn)?$/) 408 | let service = (match && match[1]) || '' 409 | let region = match && match[2] 410 | 411 | if (region === 'us-gov') { 412 | region = 'us-gov-west-1' 413 | } else if (region === 's3' || region === 's3-accelerate') { 414 | region = 'us-east-1' 415 | service = 's3' 416 | } else if (service === 'iot') { 417 | if (hostname.startsWith('iot.')) { 418 | service = 'execute-api' 419 | } else if (hostname.startsWith('data.jobs.iot.')) { 420 | service = 'iot-jobs-data' 421 | } else { 422 | service = pathname === '/mqtt' ? 'iotdevicegateway' : 'iotdata' 423 | } 424 | } else if (service === 'autoscaling') { 425 | const targetPrefix = (headers.get('X-Amz-Target') || '').split('.')[0] 426 | if (targetPrefix === 'AnyScaleFrontendService') { 427 | service = 'application-autoscaling' 428 | } else if (targetPrefix === 'AnyScaleScalingPlannerFrontendService') { 429 | service = 'autoscaling-plans' 430 | } 431 | } else if (region == null && service.startsWith('s3-')) { 432 | region = service.slice(3).replace(/^fips-|^external-1/, '') 433 | service = 's3' 434 | } else if (service.endsWith('-fips')) { 435 | service = service.slice(0, -5) 436 | } else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) { 437 | ;[service, region] = [region, service] 438 | } 439 | 440 | return [HOST_SERVICES[service] || service, region || ''] 441 | } 442 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | Source for `aws-sig-v4-test-suite`: 2 | 3 | https://docs.aws.amazon.com/general/latest/gr/signature-v4-test-suite.html 4 | 5 | Specifically this link: 6 | 7 | https://docs.aws.amazon.com/general/latest/gr/samples/aws-sig-v4-test-suite.zip 8 | -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-key-duplicate/get-header-key-duplicate.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=c9d5ea9f3f72853aea855b47ea873832890dbdd183b4468f858259531a5138ea -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-key-duplicate/get-header-key-duplicate.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | my-header1:value2,value2,value1 6 | x-amz-date:20150830T123600Z 7 | 8 | host;my-header1;x-amz-date 9 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-key-duplicate/get-header-key-duplicate.req: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value2 4 | My-Header1:value2 5 | My-Header1:value1 6 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-key-duplicate/get-header-key-duplicate.sreq: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value2 4 | My-Header1:value2 5 | My-Header1:value1 6 | X-Amz-Date:20150830T123600Z 7 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=c9d5ea9f3f72853aea855b47ea873832890dbdd183b4468f858259531a5138ea -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-key-duplicate/get-header-key-duplicate.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | dc7f04a3abfde8d472b0ab1a418b741b7c67174dad1551b4117b15527fbe966c -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-multiline/get-header-value-multiline.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=ba17b383a53190154eb5fa66a1b836cc297cc0a3d70a5d00705980573d8ff790 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-multiline/get-header-value-multiline.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | my-header1:value1,value2,value3 6 | x-amz-date:20150830T123600Z 7 | 8 | host;my-header1;x-amz-date 9 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-multiline/get-header-value-multiline.req: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value1 4 | value2 5 | value3 6 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-multiline/get-header-value-multiline.sreq: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value1 4 | value2 5 | value3 6 | X-Amz-Date:20150830T123600Z 7 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=ba17b383a53190154eb5fa66a1b836cc297cc0a3d70a5d00705980573d8ff790 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-multiline/get-header-value-multiline.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | b7b6cbfd8a0430b78891e986784da2630c8a135a8595cec25b26ea94f926ee55 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-order/get-header-value-order.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=08c7e5a9acfcfeb3ab6b2185e75ce8b1deb5e634ec47601a50643f830c755c01 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-order/get-header-value-order.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | my-header1:value4,value1,value3,value2 6 | x-amz-date:20150830T123600Z 7 | 8 | host;my-header1;x-amz-date 9 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-order/get-header-value-order.req: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value4 4 | My-Header1:value1 5 | My-Header1:value3 6 | My-Header1:value2 7 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-order/get-header-value-order.sreq: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value4 4 | My-Header1:value1 5 | My-Header1:value3 6 | My-Header1:value2 7 | X-Amz-Date:20150830T123600Z 8 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=08c7e5a9acfcfeb3ab6b2185e75ce8b1deb5e634ec47601a50643f830c755c01 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-order/get-header-value-order.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 31ce73cd3f3d9f66977ad3dd957dc47af14df92fcd8509f59b349e9137c58b86 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-trim/get-header-value-trim.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;my-header2;x-amz-date, Signature=acc3ed3afb60bb290fc8d2dd0098b9911fcaa05412b367055dee359757a9c736 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-trim/get-header-value-trim.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | my-header1:value1 6 | my-header2:"a b c" 7 | x-amz-date:20150830T123600Z 8 | 9 | host;my-header1;my-header2;x-amz-date 10 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-trim/get-header-value-trim.req: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1: value1 4 | My-Header2: "a b c" 5 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-trim/get-header-value-trim.sreq: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1: value1 4 | My-Header2: "a b c" 5 | X-Amz-Date:20150830T123600Z 6 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;my-header2;x-amz-date, Signature=acc3ed3afb60bb290fc8d2dd0098b9911fcaa05412b367055dee359757a9c736 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-header-value-trim/get-header-value-trim.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | a726db9b0df21c14f559d0a978e563112acb1b9e05476f0a6a1c7d68f28605c7 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-unreserved/get-unreserved.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=07ef7494c76fa4850883e2b006601f940f8a34d404d0cfa977f52a65bbf5f24f -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-unreserved/get-unreserved.creq: -------------------------------------------------------------------------------- 1 | GET 2 | /-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-unreserved/get-unreserved.req: -------------------------------------------------------------------------------- 1 | GET /-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-unreserved/get-unreserved.sreq: -------------------------------------------------------------------------------- 1 | GET /-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=07ef7494c76fa4850883e2b006601f940f8a34d404d0cfa977f52a65bbf5f24f -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-unreserved/get-unreserved.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 6a968768eefaa713e2a6b16b589a8ea192661f098f37349f4e2c0082757446f9 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-utf8/get-utf8.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=8318018e0b0f223aa2bbf98705b62bb787dc9c0e678f255a891fd03141be5d85 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-utf8/get-utf8.creq: -------------------------------------------------------------------------------- 1 | GET 2 | /%E1%88%B4 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-utf8/get-utf8.req: -------------------------------------------------------------------------------- 1 | GET /ሴ HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-utf8/get-utf8.sreq: -------------------------------------------------------------------------------- 1 | GET /ሴ HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=8318018e0b0f223aa2bbf98705b62bb787dc9c0e678f255a891fd03141be5d85 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-utf8/get-utf8.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 2a0a97d02205e45ce2e994789806b19270cfbbb0921b278ccf58f5249ac42102 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-empty-query-key/get-vanilla-empty-query-key.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=a67d582fa61cc504c4bae71f336f98b97f1ea3c7a6bfe1b6e45aec72011b9aeb -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-empty-query-key/get-vanilla-empty-query-key.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | Param1=value1 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-empty-query-key/get-vanilla-empty-query-key.req: -------------------------------------------------------------------------------- 1 | GET /?Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-empty-query-key/get-vanilla-empty-query-key.sreq: -------------------------------------------------------------------------------- 1 | GET /?Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=a67d582fa61cc504c4bae71f336f98b97f1ea3c7a6bfe1b6e45aec72011b9aeb -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-empty-query-key/get-vanilla-empty-query-key.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 1e24db194ed7d0eec2de28d7369675a243488e08526e8c1c73571282f7c517ab -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | Param1=value1&Param2=value2 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.req: -------------------------------------------------------------------------------- 1 | GET /?Param2=value2&Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.sreq: -------------------------------------------------------------------------------- 1 | GET /?Param2=value2&Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-key-case/get-vanilla-query-order-key-case.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 816cd5b414d056048ba4f7c5386d6e0533120fb1fcfa93762cf0fc39e2cf19e0 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-key/get-vanilla-query-order-key.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=eedbc4e291e521cf13422ffca22be7d2eb8146eecf653089df300a15b2382bd1 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-key/get-vanilla-query-order-key.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | Param1=Value1&Param1=value2 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-key/get-vanilla-query-order-key.req: -------------------------------------------------------------------------------- 1 | GET /?Param1=value2&Param1=Value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-key/get-vanilla-query-order-key.sreq: -------------------------------------------------------------------------------- 1 | GET /?Param1=value2&Param1=Value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=eedbc4e291e521cf13422ffca22be7d2eb8146eecf653089df300a15b2382bd1 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-key/get-vanilla-query-order-key.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 704b4cef673542d84cdff252633f065e8daeba5f168b77116f8b1bcaf3d38f89 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-value/get-vanilla-query-order-value.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5772eed61e12b33fae39ee5e7012498b51d56abc0abb7c60486157bd471c4694 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-value/get-vanilla-query-order-value.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | Param1=value1&Param1=value2 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-value/get-vanilla-query-order-value.req: -------------------------------------------------------------------------------- 1 | GET /?Param1=value2&Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-value/get-vanilla-query-order-value.sreq: -------------------------------------------------------------------------------- 1 | GET /?Param1=value2&Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5772eed61e12b33fae39ee5e7012498b51d56abc0abb7c60486157bd471c4694 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-order-value/get-vanilla-query-order-value.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | c968629d70850097a2d8781c9bf7edcb988b04cac14cca9be4acc3595f884606 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-unreserved/get-vanilla-query-unreserved.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=9c3e54bfcdf0b19771a7f523ee5669cdf59bc7cc0884027167c21bb143a40197 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-unreserved/get-vanilla-query-unreserved.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | -._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-unreserved/get-vanilla-query-unreserved.req: -------------------------------------------------------------------------------- 1 | GET /?-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-unreserved/get-vanilla-query-unreserved.sreq: -------------------------------------------------------------------------------- 1 | GET /?-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=9c3e54bfcdf0b19771a7f523ee5669cdf59bc7cc0884027167c21bb143a40197 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query-unreserved/get-vanilla-query-unreserved.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | c30d4703d9f799439be92736156d47ccfb2d879ddf56f5befa6d1d6aab979177 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query/get-vanilla-query.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query/get-vanilla-query.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query/get-vanilla-query.req: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query/get-vanilla-query.sreq: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-query/get-vanilla-query.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-utf8-query/get-vanilla-utf8-query.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=2cdec8eed098649ff3a119c94853b13c643bcf08f8b0a1d91e12c9027818dd04 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-utf8-query/get-vanilla-utf8-query.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | %E1%88%B4=bar 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-utf8-query/get-vanilla-utf8-query.req: -------------------------------------------------------------------------------- 1 | GET /?ሴ=bar HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-utf8-query/get-vanilla-utf8-query.sreq: -------------------------------------------------------------------------------- 1 | GET /?ሴ=bar HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=2cdec8eed098649ff3a119c94853b13c643bcf08f8b0a1d91e12c9027818dd04 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla-utf8-query/get-vanilla-utf8-query.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | eb30c5bed55734080471a834cc727ae56beb50e5f39d1bff6d0d38cb192a7073 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla/get-vanilla.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla/get-vanilla.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla/get-vanilla.req: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla/get-vanilla.sreq: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/get-vanilla/get-vanilla.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-relative-relative/get-relative-relative.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-relative-relative/get-relative-relative.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-relative-relative/get-relative-relative.req: -------------------------------------------------------------------------------- 1 | GET /example1/example2/../.. HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-relative-relative/get-relative-relative.sreq: -------------------------------------------------------------------------------- 1 | GET /example1/example2/../.. HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-relative-relative/get-relative-relative.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-relative/get-relative.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-relative/get-relative.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-relative/get-relative.req: -------------------------------------------------------------------------------- 1 | GET /example/.. HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-relative/get-relative.sreq: -------------------------------------------------------------------------------- 1 | GET /example/.. HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-relative/get-relative.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash-dot-slash/get-slash-dot-slash.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash-dot-slash/get-slash-dot-slash.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash-dot-slash/get-slash-dot-slash.req: -------------------------------------------------------------------------------- 1 | GET /./ HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash-dot-slash/get-slash-dot-slash.sreq: -------------------------------------------------------------------------------- 1 | GET /./ HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash-dot-slash/get-slash-dot-slash.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash-pointless-dot/get-slash-pointless-dot.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=ef75d96142cf21edca26f06005da7988e4f8dc83a165a80865db7089db637ec5 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash-pointless-dot/get-slash-pointless-dot.creq: -------------------------------------------------------------------------------- 1 | GET 2 | /example 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash-pointless-dot/get-slash-pointless-dot.req: -------------------------------------------------------------------------------- 1 | GET /./example HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash-pointless-dot/get-slash-pointless-dot.sreq: -------------------------------------------------------------------------------- 1 | GET /./example HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=ef75d96142cf21edca26f06005da7988e4f8dc83a165a80865db7089db637ec5 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash-pointless-dot/get-slash-pointless-dot.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 214d50c111a8edc4819da6a636336472c916b5240f51e9a51b5c3305180cf702 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash/get-slash.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash/get-slash.creq: -------------------------------------------------------------------------------- 1 | GET 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash/get-slash.req: -------------------------------------------------------------------------------- 1 | GET // HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash/get-slash.sreq: -------------------------------------------------------------------------------- 1 | GET // HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slash/get-slash.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slashes/get-slashes.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=9a624bd73a37c9a373b5312afbebe7a714a789de108f0bdfe846570885f57e84 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slashes/get-slashes.creq: -------------------------------------------------------------------------------- 1 | GET 2 | /example/ 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slashes/get-slashes.req: -------------------------------------------------------------------------------- 1 | GET //example// HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slashes/get-slashes.sreq: -------------------------------------------------------------------------------- 1 | GET //example// HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=9a624bd73a37c9a373b5312afbebe7a714a789de108f0bdfe846570885f57e84 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-slashes/get-slashes.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | cb96b4ac96d501f7c5c15bc6d67b3035061cfced4af6585ad927f7e6c985c015 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-space/get-space.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=652487583200325589f1fba4c7e578f72c47cb61beeca81406b39ddec1366741 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-space/get-space.creq: -------------------------------------------------------------------------------- 1 | GET 2 | /example%20space/ 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-space/get-space.req: -------------------------------------------------------------------------------- 1 | GET /example space/ HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-space/get-space.sreq: -------------------------------------------------------------------------------- 1 | GET /example space/ HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=652487583200325589f1fba4c7e578f72c47cb61beeca81406b39ddec1366741 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/get-space/get-space.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 63ee75631ed7234ae61b5f736dfc7754cdccfedbff4b5128a915706ee9390d86 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/normalize-path/normalize-path.txt: -------------------------------------------------------------------------------- 1 | A note about signing requests to Amazon S3: 2 | 3 | In exception to this, you do not normalize URI paths for requests to Amazon S3. For example, if you have a bucket with an object named my-object//example//photo.user, use that path. Normalizing the path to my-object/example/photo.user will cause the request to fail. For more information, see Task 1: Create a Canonical Request in the Amazon Simple Storage Service API Reference: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#canonical-request -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-key-case/post-header-key-case.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-key-case/post-header-key-case.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-key-case/post-header-key-case.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-key-case/post-header-key-case.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-key-case/post-header-key-case.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 553f88c9e4d10fc9e109e2aeb65f030801b70c2f6468faca261d401ae622fc87 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-key-sort/post-header-key-sort.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=c5410059b04c1ee005303aed430f6e6645f61f4dc9e1461ec8f8916fdf18852c -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-key-sort/post-header-key-sort.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | host:example.amazonaws.com 5 | my-header1:value1 6 | x-amz-date:20150830T123600Z 7 | 8 | host;my-header1;x-amz-date 9 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-key-sort/post-header-key-sort.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value1 4 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-key-sort/post-header-key-sort.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:value1 4 | X-Amz-Date:20150830T123600Z 5 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=c5410059b04c1ee005303aed430f6e6645f61f4dc9e1461ec8f8916fdf18852c -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-key-sort/post-header-key-sort.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 9368318c2967cf6de74404b30c65a91e8f6253e0a8659d6d5319f1a812f87d65 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-value-case/post-header-value-case.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=cdbc9802e29d2942e5e10b5bccfdd67c5f22c7c4e8ae67b53629efa58b974b7d -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-value-case/post-header-value-case.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | host:example.amazonaws.com 5 | my-header1:VALUE1 6 | x-amz-date:20150830T123600Z 7 | 8 | host;my-header1;x-amz-date 9 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-value-case/post-header-value-case.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:VALUE1 4 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-value-case/post-header-value-case.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | My-Header1:VALUE1 4 | X-Amz-Date:20150830T123600Z 5 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;my-header1;x-amz-date, Signature=cdbc9802e29d2942e5e10b5bccfdd67c5f22c7c4e8ae67b53629efa58b974b7d -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-header-value-case/post-header-value-case.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | d51ced243e649e3de6ef63afbbdcbca03131a21a7103a1583706a64618606a93 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-sts-token/post-sts-header-after/post-sts-header-after.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-sts-token/post-sts-header-after/post-sts-header-after.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-sts-token/post-sts-header-after/post-sts-header-after.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-sts-token/post-sts-header-after/post-sts-header-after.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | X-Amz-Security-Token:AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGdQrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA== 5 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-sts-token/post-sts-header-after/post-sts-header-after.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 553f88c9e4d10fc9e109e2aeb65f030801b70c2f6468faca261d401ae622fc87 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-sts-token/post-sts-header-before/post-sts-header-before.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=85d96828115b5dc0cfc3bd16ad9e210dd772bbebba041836c64533a82be05ead -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-sts-token/post-sts-header-before/post-sts-header-before.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | x-amz-security-token:AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGdQrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA== 7 | 8 | host;x-amz-date;x-amz-security-token 9 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-sts-token/post-sts-header-before/post-sts-header-before.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | X-Amz-Security-Token:AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGdQrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA== -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-sts-token/post-sts-header-before/post-sts-header-before.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | X-Amz-Security-Token:AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGdQrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA== 5 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=85d96828115b5dc0cfc3bd16ad9e210dd772bbebba041836c64533a82be05ead -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-sts-token/post-sts-header-before/post-sts-header-before.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | c237e1b440d4c63c32ca95b5b99481081cb7b13c7e40434868e71567c1a882f6 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-sts-token/readme.txt: -------------------------------------------------------------------------------- 1 | A note about using temporary security credentials: 2 | 3 | You can use temporary security credentials provided by the AWS Security Token Service (AWS STS) to sign a request. The process is the same as using long-term credentials but requires an additional HTTP header or query string parameter for the security token. The name of the header or query string parameter is X-Amz-Security-Token, and the value is the session token (the string that you received from AWS STS when you obtained temporary security credentials). 4 | 5 | When you add X-Amz-Security-Token, some services require that you include this parameter in the canonical (signed) request. For other services, you add this parameter at the end, after you calculate the signature. For details see the API reference documentation for that service. 6 | 7 | The test suite has 2 examples: 8 | 9 | post-sts-header-before - The X-Amz-Security-Token header is part of the canonical request. 10 | 11 | post-sts-header-after - The X-Amz-Security-Token header is added to the request after you calculate the signature. 12 | 13 | The test suite uses this example value for X-Amz-Security-Token: 14 | 15 | AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGdQrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA== -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla-empty-query-value/post-vanilla-empty-query-value.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=28038455d6de14eafc1f9222cf5aa6f1a96197d7deb8263271d420d138af7f11 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla-empty-query-value/post-vanilla-empty-query-value.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | Param1=value1 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla-empty-query-value/post-vanilla-empty-query-value.req: -------------------------------------------------------------------------------- 1 | POST /?Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla-empty-query-value/post-vanilla-empty-query-value.sreq: -------------------------------------------------------------------------------- 1 | POST /?Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=28038455d6de14eafc1f9222cf5aa6f1a96197d7deb8263271d420d138af7f11 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla-empty-query-value/post-vanilla-empty-query-value.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 9d659678c1756bb3113e2ce898845a0a79dbbc57b740555917687f1b3340fbbd -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla-query/post-vanilla-query.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=28038455d6de14eafc1f9222cf5aa6f1a96197d7deb8263271d420d138af7f11 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla-query/post-vanilla-query.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | Param1=value1 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla-query/post-vanilla-query.req: -------------------------------------------------------------------------------- 1 | POST /?Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla-query/post-vanilla-query.sreq: -------------------------------------------------------------------------------- 1 | POST /?Param1=value1 HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=28038455d6de14eafc1f9222cf5aa6f1a96197d7deb8263271d420d138af7f11 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla-query/post-vanilla-query.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 9d659678c1756bb3113e2ce898845a0a79dbbc57b740555917687f1b3340fbbd -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla/post-vanilla.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla/post-vanilla.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | host:example.amazonaws.com 5 | x-amz-date:20150830T123600Z 6 | 7 | host;x-amz-date 8 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla/post-vanilla.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla/post-vanilla.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host:example.amazonaws.com 3 | X-Amz-Date:20150830T123600Z 4 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5da7c1a2acd57cee7505fc6676e4e544621c30862966e37dddb68e92efbe5d6b -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-vanilla/post-vanilla.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 553f88c9e4d10fc9e109e2aeb65f030801b70c2f6468faca261d401ae622fc87 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-x-www-form-urlencoded-parameters/post-x-www-form-urlencoded-parameters.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=1a72ec8f64bd914b0e42e42607c7fbce7fb2c7465f63e3092b3b0d39fa77a6fe -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-x-www-form-urlencoded-parameters/post-x-www-form-urlencoded-parameters.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | content-type:application/x-www-form-urlencoded; charset=utf8 5 | host:example.amazonaws.com 6 | x-amz-date:20150830T123600Z 7 | 8 | content-type;host;x-amz-date 9 | 9095672bbd1f56dfc5b65f3e153adc8731a4a654192329106275f4c7b24d0b6e -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-x-www-form-urlencoded-parameters/post-x-www-form-urlencoded-parameters.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Content-Type:application/x-www-form-urlencoded; charset=utf8 3 | Host:example.amazonaws.com 4 | X-Amz-Date:20150830T123600Z 5 | 6 | Param1=value1 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-x-www-form-urlencoded-parameters/post-x-www-form-urlencoded-parameters.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Content-Type:application/x-www-form-urlencoded; charset=utf8 3 | Host:example.amazonaws.com 4 | X-Amz-Date:20150830T123600Z 5 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=1a72ec8f64bd914b0e42e42607c7fbce7fb2c7465f63e3092b3b0d39fa77a6fe 6 | 7 | Param1=value1 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-x-www-form-urlencoded-parameters/post-x-www-form-urlencoded-parameters.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 2e1cf7ed91881a30569e46552437e4156c823447bf1781b921b5d486c568dd1c -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-x-www-form-urlencoded/post-x-www-form-urlencoded.authz: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=ff11897932ad3f4e8b18135d722051e5ac45fc38421b1da7b9d196a0fe09473a -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-x-www-form-urlencoded/post-x-www-form-urlencoded.creq: -------------------------------------------------------------------------------- 1 | POST 2 | / 3 | 4 | content-type:application/x-www-form-urlencoded 5 | host:example.amazonaws.com 6 | x-amz-date:20150830T123600Z 7 | 8 | content-type;host;x-amz-date 9 | 9095672bbd1f56dfc5b65f3e153adc8731a4a654192329106275f4c7b24d0b6e -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-x-www-form-urlencoded/post-x-www-form-urlencoded.req: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Content-Type:application/x-www-form-urlencoded 3 | Host:example.amazonaws.com 4 | X-Amz-Date:20150830T123600Z 5 | 6 | Param1=value1 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-x-www-form-urlencoded/post-x-www-form-urlencoded.sreq: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Content-Type:application/x-www-form-urlencoded 3 | Host:example.amazonaws.com 4 | X-Amz-Date:20150830T123600Z 5 | Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=ff11897932ad3f4e8b18135d722051e5ac45fc38421b1da7b9d196a0fe09473a 6 | 7 | Param1=value1 -------------------------------------------------------------------------------- /test/aws-sig-v4-test-suite/post-x-www-form-urlencoded/post-x-www-form-urlencoded.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 42a5e5bb34198acb3e84da4f085bb7927f2bc277ca766e6d19c73c2154021281 -------------------------------------------------------------------------------- /test/awsTests.js: -------------------------------------------------------------------------------- 1 | import { AwsClient, AwsV4Signer } from '../src/main' 2 | 3 | export default async(fixtures) => { 4 | return Promise.all(fixtures.map(awsTest)) 5 | } 6 | 7 | const accessKeyId = 'AKIDEXAMPLE' 8 | const secretAccessKey = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' 9 | const service = 'service' 10 | const datetime = '20150830T123600Z' 11 | 12 | async function awsTest({ test, method, url, headers, body, canonicalString, stringToSign, authHeader }) { 13 | const reqHeaders = new Headers() 14 | headers.forEach(([name, value]) => { 15 | try { 16 | reqHeaders.append(name, value) // Cloudflare doesn't trim value 17 | reqHeaders.set(name, reqHeaders.get(name).replace(/, /g, ',')) // Multiple values 18 | } catch (e) { 19 | throw new Error(`append() failed for '${name}': '${value}'`) 20 | } 21 | }) 22 | 23 | const sessionToken = reqHeaders.get('X-Amz-Security-Token') 24 | 25 | const signer = new AwsV4Signer({ 26 | method, 27 | url, 28 | headers: reqHeaders, 29 | body, 30 | accessKeyId, 31 | secretAccessKey, 32 | sessionToken, 33 | service, 34 | datetime, 35 | allHeaders: true, 36 | singleEncode: true, 37 | }) 38 | 39 | const reqCanonicalString = await signer.canonicalString() 40 | console.assert(reqCanonicalString === canonicalString, `${test}: Expected canonicalString ${canonicalString}, got ${reqCanonicalString}`) 41 | 42 | const reqStringToSign = await signer.stringToSign() 43 | console.assert(reqStringToSign === stringToSign, `${test}: Expected stringToSign ${stringToSign}, got ${reqStringToSign}`) 44 | 45 | const reqAuthHeader = await signer.authHeader() 46 | console.assert(reqAuthHeader === authHeader, `${test}: Expected authHeader ${authHeader}, got ${reqAuthHeader}`) 47 | 48 | const client = new AwsClient({ accessKeyId, secretAccessKey, sessionToken, service }) 49 | 50 | const inputReq = new Request(url, { method, headers: reqHeaders, body }) 51 | const req1 = await client.sign(inputReq, { aws: { datetime, allHeaders: true, singleEncode: true } }) 52 | 53 | const clientAuthHeader = req1.headers.get('Authorization') 54 | console.assert(clientAuthHeader === authHeader, `${test}: Expected authHeader ${authHeader}, got ${clientAuthHeader}`) 55 | 56 | console.assert(req1.method === method, `${test}: Expected method ${method}, got ${req1.method}`) 57 | const body1 = await req1.text() 58 | console.assert(body1 === (body || ''), `${test}: Expected body ${body}, got ${body1}`) 59 | 60 | const req2 = await client.sign(url, { method, headers: reqHeaders, body, aws: { datetime, allHeaders: true, singleEncode: true } }) 61 | 62 | const client2AuthHeader = req2.headers.get('Authorization') 63 | console.assert(client2AuthHeader === authHeader, `${test}: Expected authHeader ${authHeader}, got ${client2AuthHeader}`) 64 | 65 | console.assert(req2.method === method, `${test}: Expected method ${method}, got ${req2.method}`) 66 | const body2 = await req2.text() 67 | console.assert(body2 === (body || ''), `${test}: Expected body ${body}, got ${body2}`) 68 | } 69 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const os = require('os') 4 | const https = require('https') 5 | const puppeteer = require('puppeteer') 6 | const rollup = require('rollup') 7 | 8 | https.globalAgent.maxSockets = 10 9 | 10 | ;(async() => { 11 | const bigStr = [...Array(256).keys()].slice(1).map(c => String.fromCharCode(c)).join('').replace(/#/g, '') 12 | 13 | const paths = [ 14 | '/ü', 15 | '/€', 16 | '/%41', 17 | '/!\'()*@%21%27%28%29%2A', 18 | '/%2a', 19 | '/%2f%2f', 20 | '/%2b%2b', 21 | '/++', 22 | '/ü%41', 23 | '/ü%41?a=%41ü', 24 | '/€ü%41?€ü=%41€ü', 25 | '/%2f?a=/&/=%2f', 26 | '/?a=b&a=B&a=b&a=c', 27 | '//a/b/..//c/.?a=b', 28 | '//a/b/..//c/./?a=b', 29 | '/?a=A&*=a&@=b', 30 | '/?a-=a&a=b&a=B&a=b&a=c', 31 | `/${bigStr}//${encodeURIComponent(bigStr)}?${bigStr}=${bigStr}`, 32 | ] 33 | 34 | const tests = [{ 35 | url: 'https://runtime.sagemaker.us-east-1.amazonaws.com/a=b~ and c * \' (whatever)!?a=b~ and c * \' @(whatever)!', 36 | method: 'post', 37 | headers: { 38 | 'Content-Type': 'application/x-amz-json-1.1', 39 | Accept: 'application/json', 40 | 'Accept-Encoding': 'gzip, deflate, br', 41 | }, 42 | body: '{}', 43 | }, { 44 | url: 'https://runtime.sagemaker.us-east-1.amazonaws.com/a=b~ and c * \' (whatever)!?a=b~ and c * \' @(whatever)!', 45 | signQuery: true, 46 | method: 'POST', 47 | headers: { 48 | 'Content-Type': 'application/x-amz-json-1.1', 49 | Accept: 'application/json', 50 | 'Accept-Encoding': 'gzip, deflate, br', 51 | 'X-Amz-Target': 'SageMaker.ListEndpoints', 52 | }, 53 | body: '{}', 54 | }, { 55 | url: 'https://s3.amazonaws.com/test//`@$^&*()-_+[]{}\\|;:.,<>€ü%41=b~ and c * \' //(whatever)!?€ü`@$^&*()-_+[]{}\\|;:.,<>=`@$^&*()-_+[]{}\\|;:.,<>%41€üab~ and c * \' (whatever)!', 56 | method: 'POST', 57 | body: '', 58 | }, { 59 | url: 'https://s3.amazonaws.com/test//`@$^&*()-_+[]{}\\|;:.,<>€ü%41=b~ and c * \' //(whatever)!?€ü`@$^&*()-_+[]{}\\|;:.,<>=`@$^&*()-_+[]{}\\|;:.,<>%41€üab~ and c * \' (whatever)!', 60 | signQuery: true, 61 | method: 'POST', 62 | body: '', 63 | }, { 64 | url: 'https://test.s3.amazonaws.com//`@$^&*()-_+[]{}\\|;:.,<>€ü%41=b~ and c * \' //(whatever)!?€ü`@$^&*()-_+[]{}\\|;:.,<>=`@$^&*()-_+[]{}\\|;:.,<>%41€üab~ and c * \' (whatever)!', 65 | method: 'POST', 66 | body: '', 67 | }, { 68 | url: 'https://test.s3.amazonaws.com//`@$^&*()-_+[]{}\\|;:.,<>€ü%41=b~ and c * \' //(whatever)!?€ü`@$^&*()-_+[]{}\\|;:.,<>=`@$^&*()-_+[]{}\\|;:.,<>%41€üab~ and c * \' (whatever)!', 69 | signQuery: true, 70 | method: 'POST', 71 | body: '', 72 | }] 73 | 74 | paths.forEach(p => tests.push({ url: `https://s3.amazonaws.com/test${p}` })) 75 | paths.forEach(p => tests.push({ url: `https://s3.amazonaws.com/test${p}`, signQuery: true })) 76 | paths.forEach(p => tests.push({ url: `https://test.s3.amazonaws.com/test${p}` })) 77 | paths.forEach(p => tests.push({ url: `https://test.s3.amazonaws.com/test${p}`, signQuery: true })) 78 | paths.forEach(p => tests.push({ url: `https://runtime.sagemaker.us-east-1.amazonaws.com/test${p}` })) 79 | paths.forEach(p => tests.push({ url: `https://runtime.sagemaker.us-east-1.amazonaws.com/test${p}`, signQuery: true })) 80 | 81 | tests.forEach(test => { 82 | test.accessKeyId = process.env.AWS_ACCESS_KEY_ID 83 | test.secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY 84 | test.sessionToken = process.env.AWS_SESSION_TOKEN 85 | }) 86 | 87 | let okTests = [{ 88 | url: 'https://s3.us-east-1.amazonaws.com/', 89 | }, { 90 | url: 'https://sqs.us-east-1.amazonaws.com/?Action=ListQueues', 91 | }, { 92 | url: 'https://iam.amazonaws.com/?Action=ListGroups&Version=2010-05-08', 93 | }, { 94 | url: 'https://ec2.us-east-1.amazonaws.com/?Action=DescribeRegions&Version=2014-06-15', 95 | }, { 96 | url: 'https://sns.us-east-1.amazonaws.com/?Action=ListTopics&Version=2010-03-31', 97 | }, { 98 | url: 'https://sts.us-east-1.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15', 99 | }, { 100 | url: 'https://cloudsearch.us-east-1.amazonaws.com/?Action=ListDomainNames&Version=2013-01-01', 101 | }, { 102 | url: 'https://email.us-east-1.amazonaws.com/?Action=ListIdentities&Version=2010-12-01', 103 | }, { 104 | url: 'https://autoscaling.us-east-1.amazonaws.com/?Action=DescribeAutoScalingInstances&Version=2011-01-01', 105 | }, { 106 | url: 'https://elasticloadbalancing.us-east-1.amazonaws.com/?Action=DescribeLoadBalancers&Version=2012-06-01', 107 | }, { 108 | url: 'https://cloudformation.us-east-1.amazonaws.com/?Action=ListStacks&Version=2010-05-15', 109 | }, { 110 | url: 'https://elasticbeanstalk.us-east-1.amazonaws.com/?Action=ListAvailableSolutionStacks&Version=2010-12-01', 111 | }, { 112 | url: 'https://rds.us-east-1.amazonaws.com/?Action=DescribeDBInstances&Version=2012-09-17', 113 | }, { 114 | url: 'https://monitoring.us-east-1.amazonaws.com/?Action=ListMetrics&Version=2010-08-01', 115 | }, { 116 | url: 'https://redshift.us-east-1.amazonaws.com/?Action=DescribeClusters&Version=2012-12-01', 117 | }, { 118 | url: 'https://cloudfront.amazonaws.com/2014-05-31/distribution', 119 | }, { 120 | url: 'https://elasticache.us-east-1.amazonaws.com/?Action=DescribeCacheClusters&Version=2014-07-15', 121 | }, { 122 | url: 'https://elasticmapreduce.us-east-1.amazonaws.com/?Action=ListClusters&Version=2009-03-31', 123 | }, { 124 | url: 'https://route53.amazonaws.com/2013-04-01/hostedzone', 125 | }, { 126 | url: 'https://cognito-sync.us-east-1.amazonaws.com/identitypools', 127 | }, { 128 | url: 'https://elastictranscoder.us-east-1.amazonaws.com/2012-09-25/pipelines', 129 | }, { 130 | url: 'https://lambda.us-east-1.amazonaws.com/2014-11-13/functions/', 131 | }, { 132 | url: 'https://ecs.us-east-1.amazonaws.com/?Action=ListClusters&Version=2014-11-13', 133 | }, { 134 | url: 'https://glacier.us-east-1.amazonaws.com/-/vaults', 135 | headers: { 136 | 'X-Amz-Glacier-Version': '2012-06-01', 137 | 'Accept-Encoding': 'gzip, deflate, br', 138 | }, 139 | }, { 140 | url: 'https://dynamodb.us-east-1.amazonaws.com/', 141 | headers: { 142 | 'Content-Type': 'application/x-amz-json-1.0', 143 | 'X-Amz-Target': 'DynamoDB_20120810.ListTables', 144 | 'Accept-Encoding': 'gzip, deflate, br', 145 | }, 146 | body: '{}', 147 | }, { 148 | url: 'https://appstream2.us-east-1.amazonaws.com/', 149 | headers: { 150 | 'Content-Type': 'application/x-amz-json-1.1', 151 | 'X-Amz-Target': 'PhotonAdminProxyService.DescribeFleets', 152 | 'Accept-Encoding': 'gzip, deflate, br', 153 | }, 154 | body: '{}', 155 | }, { 156 | url: 'https://storagegateway.us-east-1.amazonaws.com/', 157 | headers: { 158 | 'Content-Type': 'application/x-amz-json-1.1', 159 | 'X-Amz-Target': 'StorageGateway_20120630.ListGateways', 160 | 'Accept-Encoding': 'gzip, deflate, br', 161 | }, 162 | body: '{}', 163 | }, { 164 | url: 'https://datapipeline.us-east-1.amazonaws.com/', 165 | headers: { 166 | 'Content-Type': 'application/x-amz-json-1.1', 167 | 'X-Amz-Target': 'DataPipeline.ListPipelines', 168 | 'Accept-Encoding': 'gzip, deflate, br', 169 | }, 170 | body: '{}', 171 | }, { 172 | url: 'https://opsworks.us-east-1.amazonaws.com/', 173 | headers: { 174 | 'Content-Type': 'application/x-amz-json-1.1', 175 | 'X-Amz-Target': 'OpsWorks_20130218.DescribeStacks', 176 | 'Accept-Encoding': 'gzip, deflate, br', 177 | }, 178 | body: '{}', 179 | }, { 180 | url: 'https://route53domains.us-east-1.amazonaws.com/', 181 | headers: { 182 | 'Content-Type': 'application/x-amz-json-1.1', 183 | 'X-Amz-Target': 'Route53Domains_v20140515.ListDomains', 184 | 'Accept-Encoding': 'gzip, deflate, br', 185 | }, 186 | body: '{}', 187 | }, { 188 | url: 'https://kinesis.us-east-1.amazonaws.com/', 189 | headers: { 190 | 'Content-Type': 'application/x-amz-json-1.1', 191 | 'X-Amz-Target': 'Kinesis_20131202.ListStreams', 192 | 'Accept-Encoding': 'gzip, deflate, br', 193 | }, 194 | body: '{}', 195 | }, { 196 | url: 'https://cloudtrail.us-east-1.amazonaws.com/', 197 | headers: { 198 | 'Content-Type': 'application/x-amz-json-1.1', 199 | 'X-Amz-Target': 'CloudTrail_20131101.DescribeTrails', 200 | 'Accept-Encoding': 'gzip, deflate, br', 201 | }, 202 | body: '{}', 203 | }, { 204 | url: 'https://logs.us-east-1.amazonaws.com/', 205 | headers: { 206 | 'Content-Type': 'application/x-amz-json-1.1', 207 | 'X-Amz-Target': 'Logs_20140328.DescribeLogGroups', 208 | 'Accept-Encoding': 'gzip, deflate, br', 209 | }, 210 | body: '{}', 211 | }, { 212 | url: 'https://codedeploy.us-east-1.amazonaws.com/', 213 | headers: { 214 | 'Content-Type': 'application/x-amz-json-1.1', 215 | 'X-Amz-Target': 'CodeDeploy_20141006.ListApplications', 216 | 'Accept-Encoding': 'gzip, deflate, br', 217 | }, 218 | body: '{}', 219 | }, { 220 | url: 'https://directconnect.us-east-1.amazonaws.com/', 221 | headers: { 222 | 'Content-Type': 'application/x-amz-json-1.1', 223 | 'X-Amz-Target': 'OvertureService.DescribeConnections', 224 | 'Accept-Encoding': 'gzip, deflate, br', 225 | }, 226 | body: '{}', 227 | }, { 228 | url: 'https://kms.us-east-1.amazonaws.com/', 229 | headers: { 230 | 'Content-Type': 'application/x-amz-json-1.1', 231 | 'X-Amz-Target': 'TrentService.ListKeys', 232 | 'Accept-Encoding': 'gzip, deflate, br', 233 | }, 234 | body: '{}', 235 | }, { 236 | url: 'https://config.us-east-1.amazonaws.com/', 237 | headers: { 238 | 'Content-Type': 'application/x-amz-json-1.1', 239 | 'X-Amz-Target': 'StarlingDoveService.DescribeDeliveryChannels', 240 | 'Accept-Encoding': 'gzip, deflate, br', 241 | }, 242 | body: '{}', 243 | }, { 244 | url: 'https://cloudhsmv2.us-east-1.amazonaws.com/', 245 | headers: { 246 | 'Content-Type': 'application/x-amz-json-1.1', 247 | 'X-Amz-Target': 'BaldrApiService.DescribeClusters', 248 | 'Accept-Encoding': 'gzip, deflate, br', 249 | }, 250 | body: '{}', 251 | }, { 252 | url: 'https://swf.us-east-1.amazonaws.com/', 253 | headers: { 254 | 'Content-Type': 'application/x-amz-json-1.0', 255 | 'X-Amz-Target': 'SimpleWorkflowService.ListDomains', 256 | 'Accept-Encoding': 'gzip, deflate, br', 257 | }, 258 | body: '{"registrationStatus":"REGISTERED"}', 259 | }, { 260 | url: 'https://cognito-identity.us-east-1.amazonaws.com/', 261 | headers: { 262 | 'Content-Type': 'application/x-amz-json-1.1', 263 | 'X-Amz-Target': 'AWSCognitoIdentityService.ListIdentityPools', 264 | 'Accept-Encoding': 'gzip, deflate, br', 265 | }, 266 | body: '{"MaxResults": 1}', 267 | }] 268 | 269 | okTests = okTests.concat(okTests.map(test => Object.assign({ signQuery: true }, test))) 270 | 271 | okTests.forEach(test => { 272 | test.accessKeyId = process.env.AWS_ACCESS_KEY_ID 273 | test.secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY 274 | test.sessionToken = process.env.AWS_SESSION_TOKEN 275 | }) 276 | 277 | const browser = await puppeteer.launch() 278 | 279 | try { 280 | const signed = await getSignedTests(tests, browser) 281 | const responses = await Promise.all(signed.map(request)) 282 | 283 | responses.map((r, i) => /InvalidSignatureException|SignatureDoesNotMatch/.test(r.body) 284 | ? { 285 | path: signed[i].path, 286 | canonicalString: signed[i].canonicalString, 287 | body: r.body.replace(/&/g, '&'), 288 | } 289 | : null) 290 | .filter(Boolean).forEach(({ path, canonicalString, body }) => { 291 | console.log(path) 292 | console.log(canonicalString) 293 | console.log(body) 294 | }) 295 | } catch (e) { 296 | console.error(e) 297 | } 298 | 299 | try { 300 | const signed = await getSignedTests(okTests, browser) 301 | const responses = await Promise.all(signed.map(request)) 302 | 303 | responses.map((r, i) => r.statusCode !== 200 304 | ? { 305 | path: signed[i].path, 306 | canonicalString: signed[i].canonicalString, 307 | body: r.body.replace(/&/g, '&'), 308 | } 309 | : null) 310 | .filter(Boolean).forEach(({ path, canonicalString, body }) => { 311 | console.log(path) 312 | console.log(canonicalString) 313 | console.log(body) 314 | }) 315 | } catch (e) { 316 | console.error(e) 317 | } 318 | 319 | await browser.close() 320 | 321 | console.log('Tests complete') 322 | })() 323 | 324 | async function getSignedTests(tests, browser) { 325 | const rollupFile = path.join(os.tmpdir(), 'aws4fetch.integration.test.js') 326 | fs.writeFileSync(rollupFile, ` 327 | import { AwsV4Signer } from '${path.join(__dirname, '..', 'src', 'main')}' 328 | Promise.all(${JSON.stringify(tests)}.map(async(options) => { 329 | let signer = new AwsV4Signer(options) 330 | let canonicalString = await signer.canonicalString() 331 | let { method, url, headers, body } = await signer.sign() 332 | return { 333 | canonicalString, 334 | method, 335 | host: url.host, 336 | path: url.pathname + url.search, 337 | headers: [...headers].reduce((obj, [key, val]) => { obj[key] = val; return obj }, {}), 338 | body, 339 | } 340 | })) 341 | `) 342 | const bundle = await rollup.rollup({ input: rollupFile }) 343 | const { output: [{ code }] } = await bundle.generate({ format: 'es' }) 344 | const page = await browser.newPage() 345 | await page.goto('file:///dev/null') 346 | return page.evaluate(code) 347 | } 348 | 349 | const RETRY_ERRS = ['EADDRINFO', 'ETIMEDOUT', 'ECONNRESET', 'ESOCKETTIMEDOUT', 'ENOTFOUND', 'EMFILE'] 350 | 351 | async function request(options) { 352 | options.retries = options.retries || 0 353 | return new Promise((resolve, reject) => { 354 | const onError = err => { 355 | if (RETRY_ERRS.includes(err.code) && options.retries < 5) { 356 | options.retries++ 357 | return request(options).then(resolve).catch(reject) 358 | } 359 | reject(err) 360 | } 361 | https.request(options, res => { 362 | const bufs = [] 363 | res.on('error', onError) 364 | res.on('data', bufs.push.bind(bufs)) 365 | res.on('end', () => { 366 | resolve({ 367 | statusCode: res.statusCode, 368 | headers: res.headers, 369 | body: Buffer.concat(bufs).toString('utf8'), 370 | }) 371 | }) 372 | }).on('error', onError).end(options.body) 373 | }) 374 | } 375 | -------------------------------------------------------------------------------- /test/node-commonjs.js: -------------------------------------------------------------------------------- 1 | const { AwsV4Signer } = require('aws4fetch') 2 | typeof AwsV4Signer === 'function' && console.log('CommonJS: Success!') 3 | -------------------------------------------------------------------------------- /test/node-es.mjs: -------------------------------------------------------------------------------- 1 | import { AwsV4Signer } from 'aws4fetch' 2 | typeof AwsV4Signer === 'function' && console.log('ESM: Success!') 3 | -------------------------------------------------------------------------------- /test/paramTests.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { AwsClient, AwsV4Signer } from '../src/main' 4 | 5 | const decoder = new TextDecoder() 6 | 7 | const keys = { 8 | accessKeyId: 'AKIDEXAMPLE', 9 | secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', 10 | } 11 | 12 | const datetime = '20150830T123600Z' 13 | const url = 'https://queue.amazonaws.com/' 14 | 15 | export default async() => { 16 | let signer = new AwsV4Signer({ ...keys, url }) 17 | let hash = await signer.hexBodyHash() 18 | assertEqual(hash, 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') 19 | 20 | signer = new AwsV4Signer({ ...keys, url, body: 'a' }) 21 | hash = await signer.hexBodyHash() 22 | assertEqual(hash, 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb') 23 | 24 | signer = new AwsV4Signer({ ...keys, url, body: new ArrayBuffer(0) }) 25 | hash = await signer.hexBodyHash() 26 | assertEqual(hash, 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') 27 | 28 | const bodies = [ 29 | new ArrayBuffer(8), 30 | new Int8Array(8), 31 | new Int16Array(4), 32 | new Int32Array(2), 33 | new Uint8Array(8), 34 | new Uint16Array(4), 35 | new Uint32Array(2), 36 | new Uint8ClampedArray(8), 37 | new Float32Array(2), 38 | new Float64Array(1), 39 | new DataView(new ArrayBuffer(8)), 40 | ] 41 | 42 | for (const body of bodies) { 43 | signer = new AwsV4Signer({ ...keys, url, body }) 44 | hash = await signer.hexBodyHash() 45 | assertEqual(hash, 'af5570f5a1810b7af78caf4bc70a660f0df51e42baf91d4de5b2328de0e83dfc') 46 | } 47 | 48 | const client = new AwsClient(keys) 49 | 50 | const buf = await (await client.sign(url, { body: 'a' })).arrayBuffer() 51 | assertEqual(decoder.decode(buf), 'a') 52 | 53 | for (const body of bodies) { 54 | const authorization = (await client.sign(url, { 55 | body, 56 | aws: { datetime }, 57 | })).headers.get('authorization') 58 | assertEqual(authorization, 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/sqs/aws4_request, SignedHeaders=host;x-amz-date, Signature=07135e035abf2ecd131815a03fd5dffc64f7324cc79eaa6400a6d68373a8863d') 59 | } 60 | 61 | const extraBodies = [ 62 | new Blob(), 63 | new FormData(), 64 | new URLSearchParams(), 65 | new ReadableStream(), 66 | ] 67 | 68 | for (const body of extraBodies) { 69 | const authorization = (await client.sign(url, { 70 | body, 71 | headers: { 'X-Amz-Content-Sha256': 'af5570f5a1810b7af78caf4bc70a660f0df51e42baf91d4de5b2328de0e83dfc' }, 72 | aws: { datetime }, 73 | })).headers.get('authorization') 74 | assertEqual(authorization, 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/sqs/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=28cde03d679fcd9082587d2705ea318df3d61f4d5d7006668b039b66f06c750c') 75 | } 76 | } 77 | 78 | function assertEqual(actual, expected) { 79 | console.assert(actual === expected, `Expected ${expected} got ${actual}`) 80 | } 81 | -------------------------------------------------------------------------------- /test/serviceTests.js: -------------------------------------------------------------------------------- 1 | import { AwsV4Signer } from '../src/main' 2 | 3 | export default async() => { 4 | fixtures().forEach(({ url, service, region }) => { 5 | const signer = new AwsV4Signer({ url, accessKeyId: 'a', secretAccessKey: 'a' }) 6 | console.assert(signer.service === service, `Expected service ${service}, got ${signer.service} for url ${url}`) 7 | console.assert(signer.region === region, `Expected region ${region}, got ${signer.region} for url ${url}`) 8 | }) 9 | } 10 | 11 | function fixtures() { 12 | const csv = ` 13 | aa-custom-endpoint.execute-api.us-east-1.amazonaws.com,execute-api,us-east-1 14 | aa-custom-endpoint.iot.us-east-1.amazonaws.com/topics,iotdata,us-east-1 15 | aa-custom-endpoint.iot.us-east-1.amazonaws.com/things,iotdata,us-east-1 16 | aa-custom-endpoint.iot.us-east-1.amazonaws.com/mqtt,iotdevicegateway,us-east-1 17 | aa-custom-endpoint.us-east-1.cloudsearch.amazonaws.com,cloudsearch,us-east-1 18 | aa-custom-endpoint.us-east-1.es.amazonaws.com,es,us-east-1 19 | aa-custom-endpoint.appsync.us-east-1.amazonaws.com,appsync,us-east-1 20 | aa-custom-bucket.s3.amazonaws.com,s3,us-east-1 21 | aa-custom-bucket.s3-accelerate.amazonaws.com,s3,us-east-1 22 | aa-custom-bucket.s3-accelerate.dualstack.amazonaws.com,s3,us-east-1 23 | ap-northeast-1.queue.amazonaws.com,sqs,ap-northeast-1 24 | api.pricing.us-east-1.amazonaws.com,pricing,us-east-1 25 | appstream2.us-west-2.amazonaws.com,appstream,us-west-2 26 | autoscaling.amazonaws.com,autoscaling,us-east-1 27 | ca-central-1.queue.amazonaws.com,sqs,ca-central-1 28 | cloudfront.amazonaws.com,cloudfront,us-east-1 29 | cloudhsmv2.us-west-2.amazonaws.com,cloudhsm,us-west-2 30 | cn-north-1.queue.amazonaws.com,sqs,cn-north-1 31 | codebuild-fips.us-west-2.amazonaws.com,codebuild,us-west-2 32 | data.iot.us-east-1.amazonaws.com,iotdata,us-east-1 33 | data.jobs.iot.us-east-1.amazonaws.com,iot-jobs-data,us-east-1 34 | data.mediastore.us-west-2.amazonaws.com,mediastore,us-west-2 35 | ec2.amazonaws.com,ec2,us-east-1 36 | elasticloadbalancing.amazonaws.com,elasticloadbalancing,us-east-1 37 | elasticmapreduce.amazonaws.com,elasticmapreduce,us-east-1 38 | email.us-west-2.amazonaws.com,ses,us-west-2 39 | entitlement.marketplace.us-east-1.amazonaws.com,aws-marketplace,us-east-1 40 | eu-west-3.queue.amazonaws.com,sqs,eu-west-3 41 | iam.amazonaws.com,iam,us-east-1 42 | iam.us-gov.amazonaws.com,iam,us-gov-west-1 43 | importexport.amazonaws.com,importexport,us-east-1 44 | iot.us-west-2.amazonaws.com,execute-api,us-west-2 45 | kms-fips.us-west-2.amazonaws.com,kms,us-west-2 46 | ls.amazonaws.com,ls,us-east-1 47 | metering.marketplace.us-east-1.amazonaws.com,aws-marketplace,us-east-1 48 | mobile.us-east-1.amazonaws.com,AWSMobileHubService,us-east-1 49 | models.lex.us-east-1.amazonaws.com,lex,us-east-1 50 | mturk-requester-sandbox.us-east-1.amazonaws.com,mturk-requester,us-east-1 51 | personalize-runtime.us-west-2.amazonaws.com,personalize,us-west-2 52 | pinpoint.us-east-1.amazonaws.com,mobiletargeting,us-east-1 53 | queue.amazonaws.com,sqs,us-east-1 54 | route53.amazonaws.com,route53,us-east-1 55 | route53domains.us-east-1.amazonaws.com,route53domains,us-east-1 56 | runtime.lex.us-east-1.amazonaws.com,lex,us-east-1 57 | runtime.sagemaker.us-west-2.amazonaws.com,sagemaker,us-west-2 58 | s3-external-1.amazonaws.com,s3,us-east-1 59 | s3-us-gov-west-1.amazonaws.com,s3,us-gov-west-1 60 | s3-us-west-2.amazonaws.com,s3,us-west-2 61 | s3-fips-us-gov-west-1.amazonaws.com,s3,us-gov-west-1 62 | s3.amazonaws.com,s3,us-east-1 63 | s3.cn-north-1.amazonaws.com.cn,s3,cn-north-1 64 | s3.cn-northwest-1.amazonaws.com,s3,cn-northwest-1 65 | s3.dualstack.us-west-2.amazonaws.com,s3,us-west-2 66 | s3.us-west-2.amazonaws.com,s3,us-west-2 67 | sa-east-1.queue.amazonaws.com,sqs,sa-east-1 68 | sdb.amazonaws.com,sdb,us-east-1 69 | streams.dynamodb.cn-north-1.amazonaws.com.cn,dynamodb,cn-north-1 70 | streams.dynamodb.us-gov-west-1.amazonaws.com,dynamodb,us-gov-west-1 71 | streams.dynamodb.us-west-2.amazonaws.com,dynamodb,us-west-2 72 | sts.amazonaws.com,sts,us-east-1 73 | us-west-2.queue.amazonaws.com,sqs,us-west-2 74 | waf-regional.us-west-2.amazonaws.com,waf-regional,us-west-2 75 | waf.amazonaws.com,waf,us-east-1 76 | aa-custom-bucket.s3.us-west-001.backblazeb2.com,s3,us-west-001 77 | s3.us-west-001.backblazeb2.com,s3,us-west-001 78 | 12345678.r2.cloudflarestorage.com,s3,auto 79 | aa-custom-bucket.12345678.r2.cloudflarestorage.com,s3,auto 80 | ibvt72cx3dkyksnw7jktvkwhme0legmv.lambda-url.us-east-1.on.aws,lambda,us-east-1 81 | ` 82 | return csv.trim().split('\n').map(line => { 83 | const [url, service, region] = line.split(',') 84 | return { url: `https://${url}`, service, region } 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /test/suite.js: -------------------------------------------------------------------------------- 1 | import serviceTests from './serviceTests' 2 | import awsTests from './awsTests' 3 | import paramTests from './paramTests' 4 | 5 | Promise.all([ 6 | serviceTests(), 7 | awsTests(self.AWS_FIXTURES), 8 | paramTests(), 9 | ]) 10 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const puppeteer = require('puppeteer') 4 | const rollup = require('rollup') 5 | 6 | ;(async() => { 7 | const bundle = await rollup.rollup({ input: path.join(__dirname, 'suite.js') }) 8 | const { output: [{ code }] } = await bundle.generate({ format: 'es', intro: `self.AWS_FIXTURES = ${JSON.stringify(awsFixtures())};` }) 9 | 10 | const browser = await puppeteer.launch() 11 | const page = await browser.newPage() 12 | await page.goto('file:///dev/null') 13 | 14 | const errs = [] 15 | page.on('console', msg => { 16 | if (msg.type() === 'assert') { 17 | errs.push(msg.text()) 18 | } 19 | }) 20 | 21 | try { 22 | await page.evaluate(code) 23 | } catch (e) { 24 | errs.push(e) 25 | } 26 | 27 | await browser.close() 28 | 29 | if (!errs.length) { 30 | console.log('All tests passed') 31 | } else { 32 | errs.forEach(err => console.error(err)) 33 | process.exit(1) 34 | } 35 | })() 36 | 37 | function awsFixtures() { 38 | return matchingFiles(path.join(__dirname, 'aws-sig-v4-test-suite'), /\.req$/).map(file => { 39 | const test = file.split('/').pop().split('.')[0] 40 | const [preamble, body] = fs.readFileSync(file, 'utf8').trim().split('\n\n') 41 | const lines = (preamble + '\n').split('\n') 42 | const methodPath = lines[0].split(' ') 43 | const method = methodPath[0] 44 | const pathname = methodPath.slice(1, -1).join(' ') 45 | const headerLines = lines.slice(1).join('\n').split(':') 46 | const headers = [] 47 | let url = '' 48 | for (let i = 0; i < headerLines.length - 1; i++) { 49 | const name = headerLines[i] 50 | const newlineIx = headerLines[i + 1].lastIndexOf('\n') 51 | const value = headerLines[i + 1].slice(0, newlineIx) 52 | headerLines[i + 1] = headerLines[i + 1].slice(newlineIx + 1) 53 | if (name.toLowerCase() === 'host') { 54 | url = `https://${value}${pathname}` 55 | } else { 56 | value.split('\n').forEach(v => headers.push([name, v])) 57 | } 58 | } 59 | const canonicalString = fs.readFileSync(file.replace(/\.req$/, '.creq'), 'utf8').trim() 60 | const stringToSign = fs.readFileSync(file.replace(/\.req$/, '.sts'), 'utf8').trim() 61 | const authHeader = fs.readFileSync(file.replace(/\.req$/, '.authz'), 'utf8').trim() 62 | 63 | return { test, method, url, headers, body, canonicalString, stringToSign, authHeader } 64 | }) 65 | } 66 | 67 | function matchingFiles(dir, regex) { 68 | const ls = fs.readdirSync(dir).map(file => path.join(dir, file)) 69 | const dirs = ls.filter(file => fs.lstatSync(file).isDirectory()) 70 | let files = ls.filter(regex.test.bind(regex)) 71 | dirs.forEach(dir => { files = files.concat(matchingFiles(dir, regex)) }) 72 | return files 73 | } 74 | --------------------------------------------------------------------------------