├── .gitignore ├── .github └── FUNDING.yml ├── test ├── aws-sig-v4-test-suite │ ├── get-utf8 │ │ ├── get-utf8.req │ │ ├── get-utf8.sts │ │ ├── get-utf8.creq │ │ ├── get-utf8.authz │ │ └── get-utf8.sreq │ ├── get-vanilla │ │ ├── get-vanilla.req │ │ ├── get-vanilla.sts │ │ ├── get-vanilla.creq │ │ ├── get-vanilla.authz │ │ └── get-vanilla.sreq │ ├── post-vanilla │ │ ├── post-vanilla.req │ │ ├── post-vanilla.sts │ │ ├── post-vanilla.creq │ │ ├── post-vanilla.authz │ │ └── post-vanilla.sreq │ ├── get-vanilla-query │ │ ├── get-vanilla-query.req │ │ ├── get-vanilla-query.sts │ │ ├── get-vanilla-query.creq │ │ ├── get-vanilla-query.authz │ │ └── get-vanilla-query.sreq │ ├── normalize-path │ │ ├── get-slash │ │ │ ├── get-slash.req │ │ │ ├── get-slash.sts │ │ │ ├── get-slash.creq │ │ │ ├── get-slash.authz │ │ │ └── get-slash.sreq │ │ ├── get-relative │ │ │ ├── get-relative.req │ │ │ ├── get-relative.sts │ │ │ ├── get-relative.creq │ │ │ ├── get-relative.authz │ │ │ └── get-relative.sreq │ │ ├── get-slashes │ │ │ ├── get-slashes.req │ │ │ ├── get-slashes.sts │ │ │ ├── get-slashes.creq │ │ │ ├── get-slashes.authz │ │ │ └── get-slashes.sreq │ │ ├── get-space │ │ │ ├── get-space.req │ │ │ ├── get-space.sts │ │ │ ├── get-space.authz │ │ │ ├── get-space.creq │ │ │ └── get-space.sreq │ │ ├── get-slash-dot-slash │ │ │ ├── get-slash-dot-slash.req │ │ │ ├── get-slash-dot-slash.sts │ │ │ ├── get-slash-dot-slash.creq │ │ │ ├── get-slash-dot-slash.authz │ │ │ └── get-slash-dot-slash.sreq │ │ ├── get-slash-pointless-dot │ │ │ ├── get-slash-pointless-dot.req │ │ │ ├── get-slash-pointless-dot.sts │ │ │ ├── get-slash-pointless-dot.creq │ │ │ ├── get-slash-pointless-dot.authz │ │ │ └── get-slash-pointless-dot.sreq │ │ ├── get-relative-relative │ │ │ ├── get-relative-relative.req │ │ │ ├── get-relative-relative.sts │ │ │ ├── get-relative-relative.creq │ │ │ ├── get-relative-relative.authz │ │ │ └── get-relative-relative.sreq │ │ └── normalize-path.txt │ ├── post-header-key-case │ │ ├── post-header-key-case.req │ │ ├── post-header-key-case.sts │ │ ├── post-header-key-case.creq │ │ ├── post-header-key-case.authz │ │ └── post-header-key-case.sreq │ ├── get-vanilla-utf8-query │ │ ├── get-vanilla-utf8-query.req │ │ ├── get-vanilla-utf8-query.sts │ │ ├── get-vanilla-utf8-query.creq │ │ ├── get-vanilla-utf8-query.authz │ │ └── get-vanilla-utf8-query.sreq │ ├── post-vanilla-query │ │ ├── post-vanilla-query.req │ │ ├── post-vanilla-query.sts │ │ ├── post-vanilla-query.authz │ │ ├── post-vanilla-query.creq │ │ └── post-vanilla-query.sreq │ ├── post-sts-token │ │ ├── post-sts-header-after │ │ │ ├── post-sts-header-after.req │ │ │ ├── post-sts-header-after.sts │ │ │ ├── post-sts-header-after.creq │ │ │ ├── post-sts-header-after.authz │ │ │ └── post-sts-header-after.sreq │ │ ├── post-sts-header-before │ │ │ ├── post-sts-header-before.sts │ │ │ ├── post-sts-header-before.authz │ │ │ ├── post-sts-header-before.req │ │ │ ├── post-sts-header-before.creq │ │ │ └── post-sts-header-before.sreq │ │ └── readme.txt │ ├── post-header-key-sort │ │ ├── post-header-key-sort.req │ │ ├── post-header-key-sort.sts │ │ ├── post-header-key-sort.authz │ │ ├── post-header-key-sort.creq │ │ └── post-header-key-sort.sreq │ ├── get-vanilla-empty-query-key │ │ ├── get-vanilla-empty-query-key.req │ │ ├── get-vanilla-empty-query-key.sts │ │ ├── get-vanilla-empty-query-key.creq │ │ ├── get-vanilla-empty-query-key.authz │ │ └── get-vanilla-empty-query-key.sreq │ ├── post-header-value-case │ │ ├── post-header-value-case.req │ │ ├── post-header-value-case.sts │ │ ├── post-header-value-case.authz │ │ ├── post-header-value-case.creq │ │ └── post-header-value-case.sreq │ ├── post-vanilla-empty-query-value │ │ ├── post-vanilla-empty-query-value.req │ │ ├── post-vanilla-empty-query-value.sts │ │ ├── post-vanilla-empty-query-value.creq │ │ ├── post-vanilla-empty-query-value.authz │ │ └── post-vanilla-empty-query-value.sreq │ ├── get-vanilla-query-order-key │ │ ├── get-vanilla-query-order-key.req │ │ ├── get-vanilla-query-order-key.sts │ │ ├── get-vanilla-query-order-key.authz │ │ ├── get-vanilla-query-order-key.creq │ │ └── get-vanilla-query-order-key.sreq │ ├── get-vanilla-query-order-value │ │ ├── get-vanilla-query-order-value.req │ │ ├── get-vanilla-query-order-value.sts │ │ ├── get-vanilla-query-order-value.authz │ │ ├── get-vanilla-query-order-value.creq │ │ └── get-vanilla-query-order-value.sreq │ ├── get-header-value-trim │ │ ├── get-header-value-trim.req │ │ ├── get-header-value-trim.sts │ │ ├── get-header-value-trim.authz │ │ ├── get-header-value-trim.creq │ │ └── get-header-value-trim.sreq │ ├── get-unreserved │ │ ├── get-unreserved.req │ │ ├── get-unreserved.sts │ │ ├── get-unreserved.authz │ │ ├── get-unreserved.creq │ │ └── get-unreserved.sreq │ ├── get-vanilla-query-order-key-case │ │ ├── get-vanilla-query-order-key-case.req │ │ ├── get-vanilla-query-order-key-case.sts │ │ ├── get-vanilla-query-order-key-case.authz │ │ ├── get-vanilla-query-order-key-case.creq │ │ └── get-vanilla-query-order-key-case.sreq │ ├── get-header-value-multiline │ │ ├── get-header-value-multiline.req │ │ ├── get-header-value-multiline.sts │ │ ├── get-header-value-multiline.authz │ │ ├── get-header-value-multiline.creq │ │ └── get-header-value-multiline.sreq │ ├── get-header-key-duplicate │ │ ├── get-header-key-duplicate.req │ │ ├── get-header-key-duplicate.sts │ │ ├── get-header-key-duplicate.authz │ │ ├── get-header-key-duplicate.creq │ │ └── get-header-key-duplicate.sreq │ ├── get-header-value-order │ │ ├── get-header-value-order.sts │ │ ├── get-header-value-order.req │ │ ├── get-header-value-order.authz │ │ ├── get-header-value-order.creq │ │ └── get-header-value-order.sreq │ ├── get-vanilla-query-unreserved │ │ ├── get-vanilla-query-unreserved.sts │ │ ├── get-vanilla-query-unreserved.authz │ │ ├── get-vanilla-query-unreserved.req │ │ ├── get-vanilla-query-unreserved.creq │ │ └── get-vanilla-query-unreserved.sreq │ ├── post-x-www-form-urlencoded │ │ ├── post-x-www-form-urlencoded.req │ │ ├── post-x-www-form-urlencoded.sts │ │ ├── post-x-www-form-urlencoded.authz │ │ ├── post-x-www-form-urlencoded.creq │ │ └── post-x-www-form-urlencoded.sreq │ └── post-x-www-form-urlencoded-parameters │ │ ├── post-x-www-form-urlencoded-parameters.sts │ │ ├── post-x-www-form-urlencoded-parameters.req │ │ ├── post-x-www-form-urlencoded-parameters.authz │ │ ├── post-x-www-form-urlencoded-parameters.creq │ │ └── post-x-www-form-urlencoded-parameters.sreq ├── suite.js ├── README.md ├── test.js ├── paramTests.js ├── awsTests.js ├── serviceTests.js └── integration.js ├── .vscode └── settings.json ├── declaration.tsconfig.json ├── .eslintrc.js ├── tsconfig.json ├── rollup.config.js ├── example ├── package.json ├── index.js ├── README.md └── package-lock.json ├── LICENSE ├── package.json ├── dist ├── main.d.ts ├── aws4fetch.esm.js ├── aws4fetch.cjs.js └── aws4fetch.umd.js ├── README.md └── src └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example/worker.js 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mhart 4 | -------------------------------------------------------------------------------- /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-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/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/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/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/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/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-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-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/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/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/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/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 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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-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-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-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-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/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/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/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-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-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/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-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/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/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/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-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/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.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 9368318c2967cf6de74404b30c65a91e8f6253e0a8659d6d5319f1a812f87d65 -------------------------------------------------------------------------------- /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.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | a726db9b0df21c14f559d0a978e563112acb1b9e05476f0a6a1c7d68f28605c7 -------------------------------------------------------------------------------- /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.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/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/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-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/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 -------------------------------------------------------------------------------- /declaration.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "noEmit": false, 6 | "emitDeclarationOnly": true, 7 | "removeComments": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /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-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-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/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-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.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.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/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/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.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | 42a5e5bb34198acb3e84da4f085bb7927f2bc277ca766e6d19c73c2154021281 -------------------------------------------------------------------------------- /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-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/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/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.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-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/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.sts: -------------------------------------------------------------------------------- 1 | AWS4-HMAC-SHA256 2 | 20150830T123600Z 3 | 20150830/us-east-1/service/aws4_request 4 | c237e1b440d4c63c32ca95b5b99481081cb7b13c7e40434868e71567c1a882f6 -------------------------------------------------------------------------------- /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/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-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/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-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/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-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-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/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/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-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/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-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/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/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-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/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/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/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-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/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-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/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-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/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-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/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-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-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-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-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/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/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-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-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-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/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-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/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/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-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/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-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/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/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/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-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-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-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-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-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-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/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-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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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-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/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/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/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/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-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 -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2016", 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "allowJs": true, 7 | "noEmit": true, 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true, 11 | "noUnusedLocals": true 12 | }, 13 | "include": ["src/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /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-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/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/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/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/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/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/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/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/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-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 -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.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: 'cjs', file: pkg.main }, 12 | { format: 'umd', file: pkg.browser, name: pkg.name }, 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /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/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/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 -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws4fetch-example", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "aws4fetch": "^1.0.9" 6 | }, 7 | "devDependencies": { 8 | "@rollup/plugin-node-resolve": "^8.0.0", 9 | "rollup": "^2.10.5" 10 | }, 11 | "scripts": { 12 | "build": "rollup index.js --format es -p @rollup/plugin-node-resolve -o worker.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /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-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-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-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-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-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/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/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/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/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/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/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-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-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-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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws4fetch", 3 | "version": "1.0.13", 4 | "description": "A compact AWS client for modern JS environments", 5 | "author": "Michael Hart (https://github.com/mhart)", 6 | "license": "MIT", 7 | "repository": "github:mhart/aws4fetch", 8 | "main": "dist/aws4fetch.cjs.js", 9 | "module": "dist/aws4fetch.esm.js", 10 | "browser": "dist/aws4fetch.umd.js", 11 | "types": "dist/main.d.ts", 12 | "files": [ 13 | "dist" 14 | ], 15 | "scripts": { 16 | "declaration": "tsc -p declaration.tsconfig.json", 17 | "build": "npm run declaration && rollup -c", 18 | "prepare": "npm run build", 19 | "lint": "eslint .", 20 | "test": "node test/test.js", 21 | "integration": "node test/integration.js" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^7.12.1", 25 | "eslint-config-standard": "^16.0.1", 26 | "eslint-plugin-import": "^2.22.1", 27 | "eslint-plugin-node": "^11.1.0", 28 | "eslint-plugin-promise": "^4.2.1", 29 | "eslint-plugin-standard": "^4.0.2", 30 | "puppeteer": "^5.4.1", 31 | "rollup": "^2.32.1", 32 | "rollup-plugin-cleanup": "^3.2.1", 33 | "typescript": "^4.0.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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== -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { AwsClient } from 'aws4fetch' 2 | 3 | // Assume AWS_* vars have added to your environment 4 | // https://developers.cloudflare.com/workers/reference/apis/environment-variables/#secrets 5 | const aws = new AwsClient({ accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY }) // eslint-disable-line no-undef 6 | 7 | const LAMBDA_FN = 'my-api-function' 8 | 9 | // https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html 10 | const LAMBDA_INVOKE_URL = `https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/${LAMBDA_FN}/invocations` 11 | 12 | addEventListener('fetch', event => event.respondWith(handle(event))) 13 | 14 | async function handle(event) { 15 | const { request } = event 16 | const { method, url } = request 17 | 18 | if (method === 'OPTIONS') { 19 | return new Response('', { 20 | headers: { 21 | 'Access-Control-Allow-Origin': '*', 22 | 'Access-Control-Allow-Headers': 'Content-Type,Authorization', 23 | 'Access-Control-Allow-Methods': 'GET,OPTIONS,POST', 24 | 'Access-Control-Max-Age': '86400', 25 | }, 26 | }) 27 | } 28 | 29 | // https://developers.cloudflare.com/workers/reference/apis/cache/ 30 | const cache = caches.default 31 | 32 | const isCacheable = ['GET', 'HEAD'].includes(method) 33 | if (isCacheable) { 34 | const response = await cache.match(url) 35 | if (response != null) { 36 | return response 37 | } 38 | } 39 | 40 | const lambdaResponse = await aws.fetch(LAMBDA_INVOKE_URL, { 41 | method: 'POST', 42 | headers: { 'Content-Type': 'application/json' }, 43 | body: JSON.stringify(await toLambdaEvent(request)), 44 | }) 45 | 46 | if (lambdaResponse.status !== 200) { 47 | return new Response(JSON.stringify({ error: `Lambda returned ${lambdaResponse.status}` }), { 48 | status: 500, 49 | headers: { 'Content-Type': 'application/json' }, 50 | }) 51 | } 52 | 53 | const { statusCode: status, headers, body } = await lambdaResponse.json() 54 | 55 | const response = new Response(body, { status, headers }) 56 | 57 | if (isCacheable && response.headers.has('Cache-Control')) { 58 | event.waitUntil(cache.put(url, response.clone())) 59 | } 60 | 61 | return response 62 | } 63 | 64 | async function toLambdaEvent(request) { 65 | const url = new URL(request.url) 66 | return { 67 | httpMethod: request.method, 68 | path: url.pathname, 69 | queryStringParameters: [...url.searchParams].reduce((obj, [key, val]) => ({ ...obj, [key]: val }), {}), 70 | headers: [...request.headers].reduce((obj, [key, val]) => ({ ...obj, [key]: val }), {}), 71 | body: ['GET', 'HEAD'].includes(request.method) ? undefined : await request.text(), 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # aws4fetch Example: Calling AWS services from Cloudflare Workers 2 | 3 | `index.js` contains an example script you can use in a 4 | [CF worker](https://www.cloudflare.com/products/cloudflare-workers/) 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 | // Assume AWS_* vars have been uploaded via the Cloudflare Worker Secrets Vault 14 | // https://developers.cloudflare.com/workers/api/resource-bindings/secrets-vault/ 15 | const aws = new AwsClient({ accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY }) 16 | 17 | const LAMBDA_FN = 'my-api-function' 18 | 19 | // https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html 20 | const LAMBDA_INVOKE_URL = `https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/${LAMBDA_FN}/invocations` 21 | ``` 22 | 23 | The conversion from the incoming [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) 24 | to an API-Gateway-style Lambda event: 25 | 26 | ```js 27 | async function toLambdaEvent(request) { 28 | const url = new URL(request.url); 29 | return { 30 | httpMethod: request.method, 31 | path: url.pathname, 32 | queryStringParameters: [...url.searchParams].reduce((obj, [key, val]) => ({ ...obj, [key]: val }), {}), 33 | headers: [...request.headers].reduce((obj, [key, val]) => ({ ...obj, [key]: val }), {}), 34 | body: ['GET', 'HEAD'].includes(request.method) ? undefined : await request.text(), 35 | } 36 | } 37 | ``` 38 | 39 | And the call to Lambda itself, converting the response into a 40 | [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) you can return from the worker: 41 | 42 | ```js 43 | const lambdaResponse = await aws.fetch(LAMBDA_INVOKE_URL, { 44 | method: 'POST', 45 | headers: { 'Content-Type': 'application/json' }, 46 | body: JSON.stringify(await toLambdaEvent(request)), 47 | }) 48 | 49 | if (lambdaResponse.status !== 200) { 50 | return new Response(JSON.stringify({ error: `Lambda returned ${lambdaResponse.status}` }), { 51 | status: 500, 52 | headers: { 'Content-Type': 'application/json' }, 53 | }) 54 | } 55 | 56 | const { statusCode: status, headers, body } = await lambdaResponse.json() 57 | 58 | const response = new Response(body, { status, headers }) 59 | ``` 60 | 61 | ## Bundling and deploying 62 | 63 | To bundle the code up: 64 | 65 | ```console 66 | npm install 67 | npm run build 68 | ``` 69 | 70 | Or run the `rollup` command manually: 71 | 72 | ``` 73 | rollup index.js --format es -p @rollup/plugin-node-resolve -o worker.js 74 | ``` 75 | 76 | You could also use [`webpack`](https://webpack.js.org/), [`parcel`](https://parceljs.org/) or any other JS bundler. 77 | 78 | You could then copy/paste the `worker.js` code into the Cloudflare Worker editor, or upload it directly 79 | [using the API](https://developers.cloudflare.com/workers/api/): 80 | 81 | ```console 82 | curl -X PUT "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/workers/scripts/${CF_SCRIPT}" \ 83 | -H 'Content-Type: application/javascript' \ 84 | -H "X-Auth-Email: ${CF_EMAIL}" \ 85 | -H "X-Auth-Key: ${CF_API_KEY}" \ 86 | --data-binary "@./worker.js" 87 | ``` 88 | -------------------------------------------------------------------------------- /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: RequestInfo, init?: (RequestInit & { 21 | aws?: { 22 | accessKeyId?: string | undefined; 23 | secretAccessKey?: string | undefined; 24 | sessionToken?: string | undefined; 25 | service?: string | undefined; 26 | region?: string | undefined; 27 | cache?: Map | undefined; 28 | datetime?: string | undefined; 29 | signQuery?: boolean | undefined; 30 | appendSessionToken?: boolean | undefined; 31 | allHeaders?: boolean | undefined; 32 | singleEncode?: boolean | undefined; 33 | } | undefined; 34 | }) | null | undefined): Promise; 35 | fetch(input: RequestInfo, init?: (RequestInit & { 36 | aws?: { 37 | accessKeyId?: string | undefined; 38 | secretAccessKey?: string | undefined; 39 | sessionToken?: string | undefined; 40 | service?: string | undefined; 41 | region?: string | undefined; 42 | cache?: Map | undefined; 43 | datetime?: string | undefined; 44 | signQuery?: boolean | undefined; 45 | appendSessionToken?: boolean | undefined; 46 | allHeaders?: boolean | undefined; 47 | singleEncode?: boolean | undefined; 48 | } | undefined; 49 | }) | null | undefined): Promise; 50 | } 51 | export class AwsV4Signer { 52 | constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }: { 53 | method?: string; 54 | url: string; 55 | headers?: HeadersInit; 56 | body?: BodyInit | null; 57 | accessKeyId: string; 58 | secretAccessKey: string; 59 | sessionToken?: string; 60 | service?: string; 61 | region?: string; 62 | cache?: Map; 63 | datetime?: string; 64 | signQuery?: boolean; 65 | appendSessionToken?: boolean; 66 | allHeaders?: boolean; 67 | singleEncode?: boolean; 68 | }); 69 | method: string; 70 | url: URL; 71 | headers: Headers; 72 | body: string | ArrayBuffer | ArrayBufferView | Blob | FormData | URLSearchParams | ReadableStream | null | undefined; 73 | accessKeyId: string; 74 | secretAccessKey: string; 75 | sessionToken: string | undefined; 76 | service: string; 77 | region: string; 78 | cache: Map; 79 | datetime: string; 80 | signQuery: boolean | undefined; 81 | appendSessionToken: boolean; 82 | signableHeaders: string[]; 83 | signedHeaders: string; 84 | canonicalHeaders: string; 85 | credentialString: string; 86 | encodedPath: string; 87 | encodedSearch: string; 88 | sign(): Promise<{ 89 | method: string; 90 | url: URL; 91 | headers: Headers; 92 | body?: BodyInit | null; 93 | }>; 94 | authHeader(): Promise; 95 | signature(): Promise; 96 | stringToSign(): Promise; 97 | canonicalString(): Promise; 98 | hexBodyHash(): Promise; 99 | } 100 | -------------------------------------------------------------------------------- /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 | ` 77 | return csv.trim().split('\n').map(line => { 78 | const [url, service, region] = line.split(',') 79 | return { url: `https://${url}`, service, region } 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /example/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws4fetch-example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@rollup/plugin-node-resolve": { 8 | "version": "8.0.0", 9 | "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-8.0.0.tgz", 10 | "integrity": "sha512-5poJCChrkVggXXND/sQ7yNqwjUNT4fP31gpRWCnSNnlXuUXTCMHT33xZrTGxgjm5Rl18MHj7iEzlCT8rYWwQSA==", 11 | "dev": true, 12 | "requires": { 13 | "@rollup/pluginutils": "^3.0.8", 14 | "@types/resolve": "0.0.8", 15 | "builtin-modules": "^3.1.0", 16 | "deep-freeze": "^0.0.1", 17 | "deepmerge": "^4.2.2", 18 | "is-module": "^1.0.0", 19 | "resolve": "^1.14.2" 20 | } 21 | }, 22 | "@rollup/pluginutils": { 23 | "version": "3.0.10", 24 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.10.tgz", 25 | "integrity": "sha512-d44M7t+PjmMrASHbhgpSbVgtL6EFyX7J4mYxwQ/c5eoaE6N2VgCgEcWVzNnwycIloti+/MpwFr8qfw+nRw00sw==", 26 | "dev": true, 27 | "requires": { 28 | "@types/estree": "0.0.39", 29 | "estree-walker": "^1.0.1", 30 | "picomatch": "^2.2.2" 31 | }, 32 | "dependencies": { 33 | "estree-walker": { 34 | "version": "1.0.1", 35 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", 36 | "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", 37 | "dev": true 38 | } 39 | } 40 | }, 41 | "@types/estree": { 42 | "version": "0.0.39", 43 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", 44 | "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", 45 | "dev": true 46 | }, 47 | "@types/node": { 48 | "version": "14.0.5", 49 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.5.tgz", 50 | "integrity": "sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA==", 51 | "dev": true 52 | }, 53 | "@types/resolve": { 54 | "version": "0.0.8", 55 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", 56 | "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", 57 | "dev": true, 58 | "requires": { 59 | "@types/node": "*" 60 | } 61 | }, 62 | "aws4fetch": { 63 | "version": "1.0.9", 64 | "resolved": "https://registry.npmjs.org/aws4fetch/-/aws4fetch-1.0.9.tgz", 65 | "integrity": "sha512-+cO/dm/d75vDYQ8GoH/YfZt2LpRS3W/7Ua8TcW0xvB53RtkMQ5yQJKoL/VHPRConLUU5u1xrUKqAh4J/zoi6mQ==" 66 | }, 67 | "builtin-modules": { 68 | "version": "3.1.0", 69 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", 70 | "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", 71 | "dev": true 72 | }, 73 | "deep-freeze": { 74 | "version": "0.0.1", 75 | "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", 76 | "integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=", 77 | "dev": true 78 | }, 79 | "deepmerge": { 80 | "version": "4.2.2", 81 | "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", 82 | "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", 83 | "dev": true 84 | }, 85 | "fsevents": { 86 | "version": "2.1.3", 87 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", 88 | "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", 89 | "dev": true, 90 | "optional": true 91 | }, 92 | "is-module": { 93 | "version": "1.0.0", 94 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 95 | "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", 96 | "dev": true 97 | }, 98 | "path-parse": { 99 | "version": "1.0.6", 100 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 101 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 102 | "dev": true 103 | }, 104 | "picomatch": { 105 | "version": "2.2.2", 106 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", 107 | "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", 108 | "dev": true 109 | }, 110 | "resolve": { 111 | "version": "1.17.0", 112 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", 113 | "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", 114 | "dev": true, 115 | "requires": { 116 | "path-parse": "^1.0.6" 117 | } 118 | }, 119 | "rollup": { 120 | "version": "2.10.5", 121 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.10.5.tgz", 122 | "integrity": "sha512-05WRM/tjmPYwhOBvm/G9Qwa/HnAqn0TK0XxLDLQzoM4XdSmKjPBvhBl+U+Q/C6VJsucljyTQjGkZD503mjbPQg==", 123 | "dev": true, 124 | "requires": { 125 | "fsevents": "~2.1.2" 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /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 | // https://developers.cloudflare.com/workers/reference/apis/environment-variables/#secrets 18 | const aws = new AwsClient({ accessKeyId: MY_ACCESS_KEY, secretAccessKey: MY_SECRET_KEY }) 19 | 20 | // https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html 21 | const LAMBDA_FN_API = 'https://lambda.us-east-1.amazonaws.com/2015-03-31/functions' 22 | 23 | async function invokeMyLambda(event) { 24 | const res = await aws.fetch(`${LAMBDA_FN_API}/my-lambda/invocations`, { body: JSON.stringify(event) }) 25 | 26 | // `res` is a standard Response object: https://developer.mozilla.org/en-US/docs/Web/API/Response 27 | return res.json() 28 | } 29 | 30 | invokeMyLambda({my: 'event'}).then(json => console.log(json)) 31 | ``` 32 | 33 | You can see a more detailed example, a Cloudflare Worker script you can use as 34 | a replacement for [API Gateway](https://aws.amazon.com/api-gateway/), in the [`example`](./example) directory. 35 | 36 | # API 37 | 38 | `aws4fetch` exports two classes: `AwsClient` and `AwsV4Signer` 39 | 40 | ## `new AwsClient(options)` 41 | 42 | You can use the same instance of `AwsClient` for all your service calls as the service and region will be determined 43 | at fetch time – or you can create separate instances if you have different needs, eg no retrying for some service. 44 | 45 | ```js 46 | import { AwsClient } from 'aws4fetch' 47 | 48 | const aws = new AwsClient({ 49 | accessKeyId, // required, akin to AWS_ACCESS_KEY_ID 50 | secretAccessKey, // required, akin to AWS_SECRET_ACCESS_KEY 51 | sessionToken, // akin to AWS_SESSION_TOKEN if using temp credentials 52 | service, // AWS service, by default parsed at fetch time 53 | region, // AWS region, by default parsed at fetch time 54 | cache, // credential cache, defaults to `new Map()` 55 | retries, // number of retries before giving up, defaults to 10, set to 0 for no retrying 56 | initRetryMs, // defaults to 50 – timeout doubles each retry 57 | }) 58 | ``` 59 | 60 | ### `Promise aws.fetch(input[, init])` 61 | 62 | Has the same signature as the [global fetch function](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Syntax) 63 | 64 | ```js 65 | import { AwsClient } from 'aws4fetch' 66 | 67 | const aws = new AwsClient(opts) 68 | 69 | async function doFetch() { 70 | 71 | const response = await aws.fetch(url, { 72 | 73 | method, // if not supplied, will default to 'POST' if there's a body, otherwise 'GET' 74 | headers, // standard JS object literal, or Headers instance 75 | body, // optional, String or ArrayBuffer/ArrayBufferView – ie, remember to stringify your JSON 76 | 77 | // and any other standard fetch options, eg keepalive, etc 78 | 79 | // optional, largely if you want to override options in the AwsClient instance 80 | aws: { 81 | signQuery, // set to true to sign the query string instead of the Authorization header 82 | accessKeyId, // same as in AwsClient constructor above 83 | secretAccessKey, // same as in AwsClient constructor above 84 | sessionToken, // same as in AwsClient constructor above 85 | service, // same as in AwsClient constructor above 86 | region, // same as in AwsClient constructor above 87 | cache, // same as in AwsClient constructor above 88 | datetime, // defaults to now, to override use the form '20150830T123600Z' 89 | appendSessionToken, // set to true to add X-Amz-Security-Token after signing, defaults to true for iot 90 | allHeaders, // set to true to force all headers to be signed instead of the defaults 91 | singleEncode, // set to true to only encode %2F once (usually only needed for testing) 92 | }, 93 | }) 94 | 95 | console.log(await response.json()) 96 | } 97 | ``` 98 | 99 | NB: Due to the way bodies are handled in [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) 100 | instances, it's faster to invoke the function as above – using a URL as the `input` 101 | argument and passing the `body` in the `init` argument – instead of the form of 102 | invocation that uses a `Request` object directly as `input`. 103 | 104 | If you don't know which URL to call for the AWS service you want, the full list 105 | of AWS endpoints can be found here: 106 | https://docs.aws.amazon.com/general/latest/gr/rande.html 107 | 108 | And the APIs are documented here: https://docs.aws.amazon.com/ (the REST APIs 109 | are usually documented under "API Reference" for each service) 110 | 111 | ### `Promise aws.sign(input[, init])` 112 | 113 | Returns a Promise that resolves to an 114 | [AWS4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) 115 | signed [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) – 116 | has the same signature as `fetch`. Use this to create a `Request` you can send 117 | using `fetch()` yourself. 118 | 119 | ```js 120 | import { AwsClient } from 'aws4fetch' 121 | 122 | const aws = new AwsClient(opts) 123 | 124 | async function doFetch() { 125 | 126 | const request = await aws.sign(url, { 127 | 128 | method, // if not supplied, will default to 'POST' if there's a body, otherwise 'GET' 129 | headers, // standard JS object literal, or Headers instance 130 | body, // optional, String or ArrayBuffer/ArrayBufferView – ie, remember to stringify your JSON 131 | 132 | // and any other standard fetch options, eg keepalive, etc 133 | 134 | // optional, largely if you want to override options in the AwsClient instance 135 | aws: { 136 | signQuery, // set to true to sign the query string instead of the Authorization header 137 | accessKeyId, // same as in AwsClient constructor above 138 | secretAccessKey, // same as in AwsClient constructor above 139 | sessionToken, // same as in AwsClient constructor above 140 | service, // same as in AwsClient constructor above 141 | region, // same as in AwsClient constructor above 142 | cache, // same as in AwsClient constructor above 143 | datetime, // defaults to now, to override use the form '20150830T123600Z' 144 | appendSessionToken, // set to true to add X-Amz-Security-Token after signing, defaults to true for iot 145 | allHeaders, // set to true to force all headers to be signed instead of the defaults 146 | singleEncode, // set to true to only encode %2F once (usually only needed for testing) 147 | }, 148 | }) 149 | 150 | const response = await fetch(request) 151 | 152 | console.log(await response.json()) 153 | } 154 | ``` 155 | 156 | ## `new AwsV4Signer(options)` 157 | 158 | The underlying signing class for a request – use this if you just want to deal 159 | with the raw AWS4 signed method/url/headers/body. 160 | 161 | ```js 162 | import { AwsV4Signer } from 'aws4fetch' 163 | 164 | const signer = new AwsV4Signer({ 165 | url, // required, the AWS endpoint to sign 166 | accessKeyId, // required, akin to AWS_ACCESS_KEY_ID 167 | secretAccessKey, // required, akin to AWS_SECRET_ACCESS_KEY 168 | sessionToken, // akin to AWS_SESSION_TOKEN if using temp credentials 169 | method, // if not supplied, will default to 'POST' if there's a body, otherwise 'GET' 170 | headers, // standard JS object literal, or Headers instance 171 | body, // optional, String or ArrayBuffer/ArrayBufferView – ie, remember to stringify your JSON 172 | signQuery, // set to true to sign the query string instead of the Authorization header 173 | service, // AWS service, by default parsed at fetch time 174 | region, // AWS region, by default parsed at fetch time 175 | cache, // credential cache, defaults to `new Map()` 176 | datetime, // defaults to now, to override use the form '20150830T123600Z' 177 | appendSessionToken, // set to true to add X-Amz-Security-Token after signing, defaults to true for iot 178 | allHeaders, // set to true to force all headers to be signed instead of the defaults 179 | singleEncode, // set to true to only encode %2F once (usually only needed for testing) 180 | }) 181 | ``` 182 | 183 | ### `Promise<{ method, url, headers, body }> signer.sign()` 184 | 185 | Actually perform the signing of the request and return a Promise that resolves 186 | to an object containing the signed method, url, headers and body. 187 | 188 | `method` will be a `String`, `url` will be an instance of [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL), 189 | `headers` will be an instance of [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) and 190 | `body` will unchanged from the argument you supply to the constructor. 191 | 192 | ```js 193 | import { AwsV4Signer } from 'aws4fetch' 194 | 195 | const signer = new AwsV4Signer(opts) 196 | 197 | async function sign() { 198 | const { method, url, headers, body } = await signer.sign() 199 | 200 | console.log(method, url, [...headers], body) 201 | } 202 | ``` 203 | 204 | ### `Promise signer.authHeader()` 205 | 206 | Returns a Promise that resolves to the signed string to use in the 207 | [`Authorization` header](https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html#sigv4-add-signature-auth-header) 208 | 209 | Used by the `sign()` method – you shouldn't need to access this directly unless you're constructing your own requests. 210 | 211 | ### `Promise signer.signature()` 212 | 213 | Returns a Promise that resolves to the 214 | [hex signature](https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html#sigv4-calculate-signature) 215 | 216 | Used by the `sign()` method – you shouldn't need to access this directly unless you're constructing your own requests. 217 | 218 | # Installation 219 | 220 | With [npm](http://npmjs.org/) do: 221 | 222 | ``` 223 | npm install aws4fetch 224 | ``` 225 | 226 | Or you can also reference different formats straight from unpkg.com: 227 | 228 | [ES Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import): 229 | 230 | https://unpkg.com/aws4fetch@1.0.5/dist/aws4fetch.esm.js 231 | 232 | [UMD](https://github.com/umdjs/umd): 233 | 234 | https://unpkg.com/aws4fetch@1.0.5/dist/aws4fetch.umd.js 235 | 236 | [CommonJS](https://requirejs.org/docs/commonjs.html): 237 | 238 | https://unpkg.com/aws4fetch@1.0.5/dist/aws4fetch.cjs.js 239 | -------------------------------------------------------------------------------- /dist/aws4fetch.esm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * @copyright Michael Hart 2018 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 = [ 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 }, init, this, init && init.aws)); 52 | const signed = Object.assign({}, init, await signer.sign()); 53 | delete signed.aws; 54 | return new Request(signed.url.toString(), signed) 55 | } 56 | async fetch(input, init) { 57 | for (let i = 0; i <= this.retries; i++) { 58 | const fetched = fetch(await this.sign(input, init)); 59 | if (i === this.retries) { 60 | return fetched 61 | } 62 | const res = await fetched; 63 | if (res.status < 500 && res.status !== 429) { 64 | return res 65 | } 66 | await new Promise(resolve => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i))); 67 | } 68 | throw new Error('An unknown error occurred, ensure retries is not negative') 69 | } 70 | } 71 | class AwsV4Signer { 72 | constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) { 73 | if (url == null) throw new TypeError('url is a required option') 74 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 75 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 76 | this.method = method || (body ? 'POST' : 'GET'); 77 | this.url = new URL(url); 78 | this.headers = new Headers(headers || {}); 79 | this.body = body; 80 | this.accessKeyId = accessKeyId; 81 | this.secretAccessKey = secretAccessKey; 82 | this.sessionToken = sessionToken; 83 | let guessedService, guessedRegion; 84 | if (!service || !region) { 85 | [guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers); 86 | } 87 | this.service = service || guessedService || ''; 88 | this.region = region || guessedRegion || 'us-east-1'; 89 | this.cache = cache || new Map(); 90 | this.datetime = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, ''); 91 | this.signQuery = signQuery; 92 | this.appendSessionToken = appendSessionToken || this.service === 'iotdevicegateway'; 93 | this.headers.delete('Host'); 94 | const params = this.signQuery ? this.url.searchParams : this.headers; 95 | if (this.service === 's3' && !this.headers.has('X-Amz-Content-Sha256')) { 96 | this.headers.set('X-Amz-Content-Sha256', 'UNSIGNED-PAYLOAD'); 97 | } 98 | params.set('X-Amz-Date', this.datetime); 99 | if (this.sessionToken && !this.appendSessionToken) { 100 | params.set('X-Amz-Security-Token', this.sessionToken); 101 | } 102 | this.signableHeaders = ['host', ...this.headers.keys()] 103 | .filter(header => allHeaders || !UNSIGNABLE_HEADERS.includes(header)) 104 | .sort(); 105 | this.signedHeaders = this.signableHeaders.join(';'); 106 | this.canonicalHeaders = this.signableHeaders 107 | .map(header => header + ':' + (header === 'host' ? this.url.host : (this.headers.get(header) || '').replace(/\s+/g, ' '))) 108 | .join('\n'); 109 | this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, 'aws4_request'].join('/'); 110 | if (this.signQuery) { 111 | if (this.service === 's3' && !params.has('X-Amz-Expires')) { 112 | params.set('X-Amz-Expires', '86400'); 113 | } 114 | params.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256'); 115 | params.set('X-Amz-Credential', this.accessKeyId + '/' + this.credentialString); 116 | params.set('X-Amz-SignedHeaders', this.signedHeaders); 117 | } 118 | if (this.service === 's3') { 119 | try { 120 | this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, ' ')); 121 | } catch (e) { 122 | this.encodedPath = this.url.pathname; 123 | } 124 | } else { 125 | this.encodedPath = this.url.pathname.replace(/\/+/g, '/'); 126 | } 127 | if (!singleEncode) { 128 | this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, '/'); 129 | } 130 | this.encodedPath = encodeRfc3986(this.encodedPath); 131 | const seenKeys = new Set(); 132 | this.encodedSearch = [...this.url.searchParams] 133 | .filter(([k]) => { 134 | if (!k) return false 135 | if (this.service === 's3') { 136 | if (seenKeys.has(k)) return false 137 | seenKeys.add(k); 138 | } 139 | return true 140 | }) 141 | .map(pair => pair.map(p => encodeRfc3986(encodeURIComponent(p)))) 142 | .sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0) 143 | .map(pair => pair.join('=')) 144 | .join('&'); 145 | } 146 | async sign() { 147 | if (this.signQuery) { 148 | this.url.searchParams.set('X-Amz-Signature', await this.signature()); 149 | if (this.sessionToken && this.appendSessionToken) { 150 | this.url.searchParams.set('X-Amz-Security-Token', this.sessionToken); 151 | } 152 | } else { 153 | this.headers.set('Authorization', await this.authHeader()); 154 | } 155 | return { 156 | method: this.method, 157 | url: this.url, 158 | headers: this.headers, 159 | body: this.body, 160 | } 161 | } 162 | async authHeader() { 163 | return [ 164 | 'AWS4-HMAC-SHA256 Credential=' + this.accessKeyId + '/' + this.credentialString, 165 | 'SignedHeaders=' + this.signedHeaders, 166 | 'Signature=' + (await this.signature()), 167 | ].join(', ') 168 | } 169 | async signature() { 170 | const date = this.datetime.slice(0, 8); 171 | const cacheKey = [this.secretAccessKey, date, this.region, this.service].join(); 172 | let kCredentials = this.cache.get(cacheKey); 173 | if (!kCredentials) { 174 | const kDate = await hmac('AWS4' + this.secretAccessKey, date); 175 | const kRegion = await hmac(kDate, this.region); 176 | const kService = await hmac(kRegion, this.service); 177 | kCredentials = await hmac(kService, 'aws4_request'); 178 | this.cache.set(cacheKey, kCredentials); 179 | } 180 | return buf2hex(await hmac(kCredentials, await this.stringToSign())) 181 | } 182 | async stringToSign() { 183 | return [ 184 | 'AWS4-HMAC-SHA256', 185 | this.datetime, 186 | this.credentialString, 187 | buf2hex(await hash(await this.canonicalString())), 188 | ].join('\n') 189 | } 190 | async canonicalString() { 191 | return [ 192 | this.method.toUpperCase(), 193 | this.encodedPath, 194 | this.encodedSearch, 195 | this.canonicalHeaders + '\n', 196 | this.signedHeaders, 197 | await this.hexBodyHash(), 198 | ].join('\n') 199 | } 200 | async hexBodyHash() { 201 | let hashHeader = this.headers.get('X-Amz-Content-Sha256'); 202 | if (hashHeader == null) { 203 | if (this.body && typeof this.body !== 'string' && !('byteLength' in this.body)) { 204 | throw new Error('body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header') 205 | } 206 | hashHeader = buf2hex(await hash(this.body || '')); 207 | } 208 | return hashHeader 209 | } 210 | } 211 | async function hmac(key, string) { 212 | const cryptoKey = await crypto.subtle.importKey( 213 | 'raw', 214 | typeof key === 'string' ? encoder.encode(key) : key, 215 | { name: 'HMAC', hash: { name: 'SHA-256' } }, 216 | false, 217 | ['sign'], 218 | ); 219 | return crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(string)) 220 | } 221 | async function hash(content) { 222 | return crypto.subtle.digest('SHA-256', typeof content === 'string' ? encoder.encode(content) : content) 223 | } 224 | function buf2hex(buffer) { 225 | return Array.prototype.map.call(new Uint8Array(buffer), x => ('0' + x.toString(16)).slice(-2)).join('') 226 | } 227 | function encodeRfc3986(urlEncodedStr) { 228 | return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()) 229 | } 230 | function guessServiceRegion(url, headers) { 231 | const { hostname, pathname } = url; 232 | const match = hostname.replace('dualstack.', '').match(/([^.]+)\.(?:([^.]*)\.)?amazonaws\.com(?:\.cn)?$/); 233 | let [service, region] = (match || ['', '']).slice(1, 3); 234 | if (region === 'us-gov') { 235 | region = 'us-gov-west-1'; 236 | } else if (region === 's3' || region === 's3-accelerate') { 237 | region = 'us-east-1'; 238 | service = 's3'; 239 | } else if (service === 'iot') { 240 | if (hostname.startsWith('iot.')) { 241 | service = 'execute-api'; 242 | } else if (hostname.startsWith('data.jobs.iot.')) { 243 | service = 'iot-jobs-data'; 244 | } else { 245 | service = pathname === '/mqtt' ? 'iotdevicegateway' : 'iotdata'; 246 | } 247 | } else if (service === 'autoscaling') { 248 | const targetPrefix = (headers.get('X-Amz-Target') || '').split('.')[0]; 249 | if (targetPrefix === 'AnyScaleFrontendService') { 250 | service = 'application-autoscaling'; 251 | } else if (targetPrefix === 'AnyScaleScalingPlannerFrontendService') { 252 | service = 'autoscaling-plans'; 253 | } 254 | } else if (region == null && service.startsWith('s3-')) { 255 | region = service.slice(3).replace(/^fips-|^external-1/, ''); 256 | service = 's3'; 257 | } else if (service.endsWith('-fips')) { 258 | service = service.slice(0, -5); 259 | } else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) { 260 | [service, region] = [region, service]; 261 | } 262 | return [HOST_SERVICES[service] || service, region] 263 | } 264 | 265 | export { AwsClient, AwsV4Signer }; 266 | -------------------------------------------------------------------------------- /dist/aws4fetch.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | /** 6 | * @license MIT 7 | * @copyright Michael Hart 2018 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 = [ 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 }, init, this, init && init.aws)); 56 | const signed = Object.assign({}, init, await signer.sign()); 57 | delete signed.aws; 58 | return new Request(signed.url.toString(), signed) 59 | } 60 | async fetch(input, init) { 61 | for (let i = 0; i <= this.retries; i++) { 62 | const fetched = fetch(await this.sign(input, init)); 63 | if (i === this.retries) { 64 | return fetched 65 | } 66 | const res = await fetched; 67 | if (res.status < 500 && res.status !== 429) { 68 | return res 69 | } 70 | await new Promise(resolve => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i))); 71 | } 72 | throw new Error('An unknown error occurred, ensure retries is not negative') 73 | } 74 | } 75 | class AwsV4Signer { 76 | constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) { 77 | if (url == null) throw new TypeError('url is a required option') 78 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 79 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 80 | this.method = method || (body ? 'POST' : 'GET'); 81 | this.url = new URL(url); 82 | this.headers = new Headers(headers || {}); 83 | this.body = body; 84 | this.accessKeyId = accessKeyId; 85 | this.secretAccessKey = secretAccessKey; 86 | this.sessionToken = sessionToken; 87 | let guessedService, guessedRegion; 88 | if (!service || !region) { 89 | [guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers); 90 | } 91 | this.service = service || guessedService || ''; 92 | this.region = region || guessedRegion || 'us-east-1'; 93 | this.cache = cache || new Map(); 94 | this.datetime = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, ''); 95 | this.signQuery = signQuery; 96 | this.appendSessionToken = appendSessionToken || this.service === 'iotdevicegateway'; 97 | this.headers.delete('Host'); 98 | const params = this.signQuery ? this.url.searchParams : this.headers; 99 | if (this.service === 's3' && !this.headers.has('X-Amz-Content-Sha256')) { 100 | this.headers.set('X-Amz-Content-Sha256', 'UNSIGNED-PAYLOAD'); 101 | } 102 | params.set('X-Amz-Date', this.datetime); 103 | if (this.sessionToken && !this.appendSessionToken) { 104 | params.set('X-Amz-Security-Token', this.sessionToken); 105 | } 106 | this.signableHeaders = ['host', ...this.headers.keys()] 107 | .filter(header => allHeaders || !UNSIGNABLE_HEADERS.includes(header)) 108 | .sort(); 109 | this.signedHeaders = this.signableHeaders.join(';'); 110 | this.canonicalHeaders = this.signableHeaders 111 | .map(header => header + ':' + (header === 'host' ? this.url.host : (this.headers.get(header) || '').replace(/\s+/g, ' '))) 112 | .join('\n'); 113 | this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, 'aws4_request'].join('/'); 114 | if (this.signQuery) { 115 | if (this.service === 's3' && !params.has('X-Amz-Expires')) { 116 | params.set('X-Amz-Expires', '86400'); 117 | } 118 | params.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256'); 119 | params.set('X-Amz-Credential', this.accessKeyId + '/' + this.credentialString); 120 | params.set('X-Amz-SignedHeaders', this.signedHeaders); 121 | } 122 | if (this.service === 's3') { 123 | try { 124 | this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, ' ')); 125 | } catch (e) { 126 | this.encodedPath = this.url.pathname; 127 | } 128 | } else { 129 | this.encodedPath = this.url.pathname.replace(/\/+/g, '/'); 130 | } 131 | if (!singleEncode) { 132 | this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, '/'); 133 | } 134 | this.encodedPath = encodeRfc3986(this.encodedPath); 135 | const seenKeys = new Set(); 136 | this.encodedSearch = [...this.url.searchParams] 137 | .filter(([k]) => { 138 | if (!k) return false 139 | if (this.service === 's3') { 140 | if (seenKeys.has(k)) return false 141 | seenKeys.add(k); 142 | } 143 | return true 144 | }) 145 | .map(pair => pair.map(p => encodeRfc3986(encodeURIComponent(p)))) 146 | .sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0) 147 | .map(pair => pair.join('=')) 148 | .join('&'); 149 | } 150 | async sign() { 151 | if (this.signQuery) { 152 | this.url.searchParams.set('X-Amz-Signature', await this.signature()); 153 | if (this.sessionToken && this.appendSessionToken) { 154 | this.url.searchParams.set('X-Amz-Security-Token', this.sessionToken); 155 | } 156 | } else { 157 | this.headers.set('Authorization', await this.authHeader()); 158 | } 159 | return { 160 | method: this.method, 161 | url: this.url, 162 | headers: this.headers, 163 | body: this.body, 164 | } 165 | } 166 | async authHeader() { 167 | return [ 168 | 'AWS4-HMAC-SHA256 Credential=' + this.accessKeyId + '/' + this.credentialString, 169 | 'SignedHeaders=' + this.signedHeaders, 170 | 'Signature=' + (await this.signature()), 171 | ].join(', ') 172 | } 173 | async signature() { 174 | const date = this.datetime.slice(0, 8); 175 | const cacheKey = [this.secretAccessKey, date, this.region, this.service].join(); 176 | let kCredentials = this.cache.get(cacheKey); 177 | if (!kCredentials) { 178 | const kDate = await hmac('AWS4' + this.secretAccessKey, date); 179 | const kRegion = await hmac(kDate, this.region); 180 | const kService = await hmac(kRegion, this.service); 181 | kCredentials = await hmac(kService, 'aws4_request'); 182 | this.cache.set(cacheKey, kCredentials); 183 | } 184 | return buf2hex(await hmac(kCredentials, await this.stringToSign())) 185 | } 186 | async stringToSign() { 187 | return [ 188 | 'AWS4-HMAC-SHA256', 189 | this.datetime, 190 | this.credentialString, 191 | buf2hex(await hash(await this.canonicalString())), 192 | ].join('\n') 193 | } 194 | async canonicalString() { 195 | return [ 196 | this.method.toUpperCase(), 197 | this.encodedPath, 198 | this.encodedSearch, 199 | this.canonicalHeaders + '\n', 200 | this.signedHeaders, 201 | await this.hexBodyHash(), 202 | ].join('\n') 203 | } 204 | async hexBodyHash() { 205 | let hashHeader = this.headers.get('X-Amz-Content-Sha256'); 206 | if (hashHeader == null) { 207 | if (this.body && typeof this.body !== 'string' && !('byteLength' in this.body)) { 208 | throw new Error('body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header') 209 | } 210 | hashHeader = buf2hex(await hash(this.body || '')); 211 | } 212 | return hashHeader 213 | } 214 | } 215 | async function hmac(key, string) { 216 | const cryptoKey = await crypto.subtle.importKey( 217 | 'raw', 218 | typeof key === 'string' ? encoder.encode(key) : key, 219 | { name: 'HMAC', hash: { name: 'SHA-256' } }, 220 | false, 221 | ['sign'], 222 | ); 223 | return crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(string)) 224 | } 225 | async function hash(content) { 226 | return crypto.subtle.digest('SHA-256', typeof content === 'string' ? encoder.encode(content) : content) 227 | } 228 | function buf2hex(buffer) { 229 | return Array.prototype.map.call(new Uint8Array(buffer), x => ('0' + x.toString(16)).slice(-2)).join('') 230 | } 231 | function encodeRfc3986(urlEncodedStr) { 232 | return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()) 233 | } 234 | function guessServiceRegion(url, headers) { 235 | const { hostname, pathname } = url; 236 | const match = hostname.replace('dualstack.', '').match(/([^.]+)\.(?:([^.]*)\.)?amazonaws\.com(?:\.cn)?$/); 237 | let [service, region] = (match || ['', '']).slice(1, 3); 238 | if (region === 'us-gov') { 239 | region = 'us-gov-west-1'; 240 | } else if (region === 's3' || region === 's3-accelerate') { 241 | region = 'us-east-1'; 242 | service = 's3'; 243 | } else if (service === 'iot') { 244 | if (hostname.startsWith('iot.')) { 245 | service = 'execute-api'; 246 | } else if (hostname.startsWith('data.jobs.iot.')) { 247 | service = 'iot-jobs-data'; 248 | } else { 249 | service = pathname === '/mqtt' ? 'iotdevicegateway' : 'iotdata'; 250 | } 251 | } else if (service === 'autoscaling') { 252 | const targetPrefix = (headers.get('X-Amz-Target') || '').split('.')[0]; 253 | if (targetPrefix === 'AnyScaleFrontendService') { 254 | service = 'application-autoscaling'; 255 | } else if (targetPrefix === 'AnyScaleScalingPlannerFrontendService') { 256 | service = 'autoscaling-plans'; 257 | } 258 | } else if (region == null && service.startsWith('s3-')) { 259 | region = service.slice(3).replace(/^fips-|^external-1/, ''); 260 | service = 's3'; 261 | } else if (service.endsWith('-fips')) { 262 | service = service.slice(0, -5); 263 | } else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) { 264 | [service, region] = [region, service]; 265 | } 266 | return [HOST_SERVICES[service] || service, region] 267 | } 268 | 269 | exports.AwsClient = AwsClient; 270 | exports.AwsV4Signer = AwsV4Signer; 271 | -------------------------------------------------------------------------------- /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 2018 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 = [ 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 }, init, this, init && init.aws)); 58 | const signed = Object.assign({}, init, await signer.sign()); 59 | delete signed.aws; 60 | return new Request(signed.url.toString(), signed) 61 | } 62 | async fetch(input, init) { 63 | for (let i = 0; i <= this.retries; i++) { 64 | const fetched = fetch(await this.sign(input, init)); 65 | if (i === this.retries) { 66 | return fetched 67 | } 68 | const res = await fetched; 69 | if (res.status < 500 && res.status !== 429) { 70 | return res 71 | } 72 | await new Promise(resolve => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i))); 73 | } 74 | throw new Error('An unknown error occurred, ensure retries is not negative') 75 | } 76 | } 77 | class AwsV4Signer { 78 | constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) { 79 | if (url == null) throw new TypeError('url is a required option') 80 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 81 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 82 | this.method = method || (body ? 'POST' : 'GET'); 83 | this.url = new URL(url); 84 | this.headers = new Headers(headers || {}); 85 | this.body = body; 86 | this.accessKeyId = accessKeyId; 87 | this.secretAccessKey = secretAccessKey; 88 | this.sessionToken = sessionToken; 89 | let guessedService, guessedRegion; 90 | if (!service || !region) { 91 | [guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers); 92 | } 93 | this.service = service || guessedService || ''; 94 | this.region = region || guessedRegion || 'us-east-1'; 95 | this.cache = cache || new Map(); 96 | this.datetime = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, ''); 97 | this.signQuery = signQuery; 98 | this.appendSessionToken = appendSessionToken || this.service === 'iotdevicegateway'; 99 | this.headers.delete('Host'); 100 | const params = this.signQuery ? this.url.searchParams : this.headers; 101 | if (this.service === 's3' && !this.headers.has('X-Amz-Content-Sha256')) { 102 | this.headers.set('X-Amz-Content-Sha256', 'UNSIGNED-PAYLOAD'); 103 | } 104 | params.set('X-Amz-Date', this.datetime); 105 | if (this.sessionToken && !this.appendSessionToken) { 106 | params.set('X-Amz-Security-Token', this.sessionToken); 107 | } 108 | this.signableHeaders = ['host', ...this.headers.keys()] 109 | .filter(header => allHeaders || !UNSIGNABLE_HEADERS.includes(header)) 110 | .sort(); 111 | this.signedHeaders = this.signableHeaders.join(';'); 112 | this.canonicalHeaders = this.signableHeaders 113 | .map(header => header + ':' + (header === 'host' ? this.url.host : (this.headers.get(header) || '').replace(/\s+/g, ' '))) 114 | .join('\n'); 115 | this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, 'aws4_request'].join('/'); 116 | if (this.signQuery) { 117 | if (this.service === 's3' && !params.has('X-Amz-Expires')) { 118 | params.set('X-Amz-Expires', '86400'); 119 | } 120 | params.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256'); 121 | params.set('X-Amz-Credential', this.accessKeyId + '/' + this.credentialString); 122 | params.set('X-Amz-SignedHeaders', this.signedHeaders); 123 | } 124 | if (this.service === 's3') { 125 | try { 126 | this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, ' ')); 127 | } catch (e) { 128 | this.encodedPath = this.url.pathname; 129 | } 130 | } else { 131 | this.encodedPath = this.url.pathname.replace(/\/+/g, '/'); 132 | } 133 | if (!singleEncode) { 134 | this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, '/'); 135 | } 136 | this.encodedPath = encodeRfc3986(this.encodedPath); 137 | const seenKeys = new Set(); 138 | this.encodedSearch = [...this.url.searchParams] 139 | .filter(([k]) => { 140 | if (!k) return false 141 | if (this.service === 's3') { 142 | if (seenKeys.has(k)) return false 143 | seenKeys.add(k); 144 | } 145 | return true 146 | }) 147 | .map(pair => pair.map(p => encodeRfc3986(encodeURIComponent(p)))) 148 | .sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0) 149 | .map(pair => pair.join('=')) 150 | .join('&'); 151 | } 152 | async sign() { 153 | if (this.signQuery) { 154 | this.url.searchParams.set('X-Amz-Signature', await this.signature()); 155 | if (this.sessionToken && this.appendSessionToken) { 156 | this.url.searchParams.set('X-Amz-Security-Token', this.sessionToken); 157 | } 158 | } else { 159 | this.headers.set('Authorization', await this.authHeader()); 160 | } 161 | return { 162 | method: this.method, 163 | url: this.url, 164 | headers: this.headers, 165 | body: this.body, 166 | } 167 | } 168 | async authHeader() { 169 | return [ 170 | 'AWS4-HMAC-SHA256 Credential=' + this.accessKeyId + '/' + this.credentialString, 171 | 'SignedHeaders=' + this.signedHeaders, 172 | 'Signature=' + (await this.signature()), 173 | ].join(', ') 174 | } 175 | async signature() { 176 | const date = this.datetime.slice(0, 8); 177 | const cacheKey = [this.secretAccessKey, date, this.region, this.service].join(); 178 | let kCredentials = this.cache.get(cacheKey); 179 | if (!kCredentials) { 180 | const kDate = await hmac('AWS4' + this.secretAccessKey, date); 181 | const kRegion = await hmac(kDate, this.region); 182 | const kService = await hmac(kRegion, this.service); 183 | kCredentials = await hmac(kService, 'aws4_request'); 184 | this.cache.set(cacheKey, kCredentials); 185 | } 186 | return buf2hex(await hmac(kCredentials, await this.stringToSign())) 187 | } 188 | async stringToSign() { 189 | return [ 190 | 'AWS4-HMAC-SHA256', 191 | this.datetime, 192 | this.credentialString, 193 | buf2hex(await hash(await this.canonicalString())), 194 | ].join('\n') 195 | } 196 | async canonicalString() { 197 | return [ 198 | this.method.toUpperCase(), 199 | this.encodedPath, 200 | this.encodedSearch, 201 | this.canonicalHeaders + '\n', 202 | this.signedHeaders, 203 | await this.hexBodyHash(), 204 | ].join('\n') 205 | } 206 | async hexBodyHash() { 207 | let hashHeader = this.headers.get('X-Amz-Content-Sha256'); 208 | if (hashHeader == null) { 209 | if (this.body && typeof this.body !== 'string' && !('byteLength' in this.body)) { 210 | throw new Error('body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header') 211 | } 212 | hashHeader = buf2hex(await hash(this.body || '')); 213 | } 214 | return hashHeader 215 | } 216 | } 217 | async function hmac(key, string) { 218 | const cryptoKey = await crypto.subtle.importKey( 219 | 'raw', 220 | typeof key === 'string' ? encoder.encode(key) : key, 221 | { name: 'HMAC', hash: { name: 'SHA-256' } }, 222 | false, 223 | ['sign'], 224 | ); 225 | return crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(string)) 226 | } 227 | async function hash(content) { 228 | return crypto.subtle.digest('SHA-256', typeof content === 'string' ? encoder.encode(content) : content) 229 | } 230 | function buf2hex(buffer) { 231 | return Array.prototype.map.call(new Uint8Array(buffer), x => ('0' + x.toString(16)).slice(-2)).join('') 232 | } 233 | function encodeRfc3986(urlEncodedStr) { 234 | return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()) 235 | } 236 | function guessServiceRegion(url, headers) { 237 | const { hostname, pathname } = url; 238 | const match = hostname.replace('dualstack.', '').match(/([^.]+)\.(?:([^.]*)\.)?amazonaws\.com(?:\.cn)?$/); 239 | let [service, region] = (match || ['', '']).slice(1, 3); 240 | if (region === 'us-gov') { 241 | region = 'us-gov-west-1'; 242 | } else if (region === 's3' || region === 's3-accelerate') { 243 | region = 'us-east-1'; 244 | service = 's3'; 245 | } else if (service === 'iot') { 246 | if (hostname.startsWith('iot.')) { 247 | service = 'execute-api'; 248 | } else if (hostname.startsWith('data.jobs.iot.')) { 249 | service = 'iot-jobs-data'; 250 | } else { 251 | service = pathname === '/mqtt' ? 'iotdevicegateway' : 'iotdata'; 252 | } 253 | } else if (service === 'autoscaling') { 254 | const targetPrefix = (headers.get('X-Amz-Target') || '').split('.')[0]; 255 | if (targetPrefix === 'AnyScaleFrontendService') { 256 | service = 'application-autoscaling'; 257 | } else if (targetPrefix === 'AnyScaleScalingPlannerFrontendService') { 258 | service = 'autoscaling-plans'; 259 | } 260 | } else if (region == null && service.startsWith('s3-')) { 261 | region = service.slice(3).replace(/^fips-|^external-1/, ''); 262 | service = 's3'; 263 | } else if (service.endsWith('-fips')) { 264 | service = service.slice(0, -5); 265 | } else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) { 266 | [service, region] = [region, service]; 267 | } 268 | return [HOST_SERVICES[service] || service, region] 269 | } 270 | 271 | exports.AwsClient = AwsClient; 272 | exports.AwsV4Signer = AwsV4Signer; 273 | 274 | Object.defineProperty(exports, '__esModule', { value: true }); 275 | 276 | }))); 277 | -------------------------------------------------------------------------------- /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 | 65 | paths.forEach(p => tests.push({ url: `https://s3.amazonaws.com/test${p}` })) 66 | paths.forEach(p => tests.push({ url: `https://s3.amazonaws.com/test${p}`, signQuery: true })) 67 | paths.forEach(p => tests.push({ url: `https://runtime.sagemaker.us-east-1.amazonaws.com/test${p}` })) 68 | paths.forEach(p => tests.push({ url: `https://runtime.sagemaker.us-east-1.amazonaws.com/test${p}`, signQuery: true })) 69 | 70 | tests.forEach(test => { 71 | test.accessKeyId = process.env.AWS_ACCESS_KEY_ID 72 | test.secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY 73 | test.sessionToken = process.env.AWS_SESSION_TOKEN 74 | }) 75 | 76 | let okTests = [{ 77 | url: 'https://s3.us-east-1.amazonaws.com/', 78 | }, { 79 | url: 'https://sqs.us-east-1.amazonaws.com/?Action=ListQueues', 80 | }, { 81 | url: 'https://iam.amazonaws.com/?Action=ListGroups&Version=2010-05-08', 82 | }, { 83 | url: 'https://ec2.us-east-1.amazonaws.com/?Action=DescribeRegions&Version=2014-06-15', 84 | }, { 85 | url: 'https://sns.us-east-1.amazonaws.com/?Action=ListTopics&Version=2010-03-31', 86 | }, { 87 | url: 'https://sts.us-east-1.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15', 88 | }, { 89 | url: 'https://cloudsearch.us-east-1.amazonaws.com/?Action=ListDomainNames&Version=2013-01-01', 90 | }, { 91 | url: 'https://email.us-east-1.amazonaws.com/?Action=ListIdentities&Version=2010-12-01', 92 | }, { 93 | url: 'https://autoscaling.us-east-1.amazonaws.com/?Action=DescribeAutoScalingInstances&Version=2011-01-01', 94 | }, { 95 | url: 'https://elasticloadbalancing.us-east-1.amazonaws.com/?Action=DescribeLoadBalancers&Version=2012-06-01', 96 | }, { 97 | url: 'https://cloudformation.us-east-1.amazonaws.com/?Action=ListStacks&Version=2010-05-15', 98 | }, { 99 | url: 'https://elasticbeanstalk.us-east-1.amazonaws.com/?Action=ListAvailableSolutionStacks&Version=2010-12-01', 100 | }, { 101 | url: 'https://rds.us-east-1.amazonaws.com/?Action=DescribeDBInstances&Version=2012-09-17', 102 | }, { 103 | url: 'https://monitoring.us-east-1.amazonaws.com/?Action=ListMetrics&Version=2010-08-01', 104 | }, { 105 | url: 'https://redshift.us-east-1.amazonaws.com/?Action=DescribeClusters&Version=2012-12-01', 106 | }, { 107 | url: 'https://cloudfront.amazonaws.com/2014-05-31/distribution', 108 | }, { 109 | url: 'https://elasticache.us-east-1.amazonaws.com/?Action=DescribeCacheClusters&Version=2014-07-15', 110 | }, { 111 | url: 'https://elasticmapreduce.us-east-1.amazonaws.com/?Action=ListClusters&Version=2009-03-31', 112 | }, { 113 | url: 'https://route53.amazonaws.com/2013-04-01/hostedzone', 114 | }, { 115 | url: 'https://cognito-sync.us-east-1.amazonaws.com/identitypools', 116 | }, { 117 | url: 'https://elastictranscoder.us-east-1.amazonaws.com/2012-09-25/pipelines', 118 | }, { 119 | url: 'https://lambda.us-east-1.amazonaws.com/2014-11-13/functions/', 120 | }, { 121 | url: 'https://ecs.us-east-1.amazonaws.com/?Action=ListClusters&Version=2014-11-13', 122 | }, { 123 | url: 'https://glacier.us-east-1.amazonaws.com/-/vaults', 124 | headers: { 125 | 'X-Amz-Glacier-Version': '2012-06-01', 126 | 'Accept-Encoding': 'gzip, deflate, br', 127 | }, 128 | }, { 129 | url: 'https://dynamodb.us-east-1.amazonaws.com/', 130 | headers: { 131 | 'Content-Type': 'application/x-amz-json-1.0', 132 | 'X-Amz-Target': 'DynamoDB_20120810.ListTables', 133 | 'Accept-Encoding': 'gzip, deflate, br', 134 | }, 135 | body: '{}', 136 | }, { 137 | url: 'https://appstream2.us-east-1.amazonaws.com/', 138 | headers: { 139 | 'Content-Type': 'application/x-amz-json-1.1', 140 | 'X-Amz-Target': 'PhotonAdminProxyService.DescribeFleets', 141 | 'Accept-Encoding': 'gzip, deflate, br', 142 | }, 143 | body: '{}', 144 | }, { 145 | url: 'https://storagegateway.us-east-1.amazonaws.com/', 146 | headers: { 147 | 'Content-Type': 'application/x-amz-json-1.1', 148 | 'X-Amz-Target': 'StorageGateway_20120630.ListGateways', 149 | 'Accept-Encoding': 'gzip, deflate, br', 150 | }, 151 | body: '{}', 152 | }, { 153 | url: 'https://datapipeline.us-east-1.amazonaws.com/', 154 | headers: { 155 | 'Content-Type': 'application/x-amz-json-1.1', 156 | 'X-Amz-Target': 'DataPipeline.ListPipelines', 157 | 'Accept-Encoding': 'gzip, deflate, br', 158 | }, 159 | body: '{}', 160 | }, { 161 | url: 'https://opsworks.us-east-1.amazonaws.com/', 162 | headers: { 163 | 'Content-Type': 'application/x-amz-json-1.1', 164 | 'X-Amz-Target': 'OpsWorks_20130218.DescribeStacks', 165 | 'Accept-Encoding': 'gzip, deflate, br', 166 | }, 167 | body: '{}', 168 | }, { 169 | url: 'https://route53domains.us-east-1.amazonaws.com/', 170 | headers: { 171 | 'Content-Type': 'application/x-amz-json-1.1', 172 | 'X-Amz-Target': 'Route53Domains_v20140515.ListDomains', 173 | 'Accept-Encoding': 'gzip, deflate, br', 174 | }, 175 | body: '{}', 176 | }, { 177 | url: 'https://kinesis.us-east-1.amazonaws.com/', 178 | headers: { 179 | 'Content-Type': 'application/x-amz-json-1.1', 180 | 'X-Amz-Target': 'Kinesis_20131202.ListStreams', 181 | 'Accept-Encoding': 'gzip, deflate, br', 182 | }, 183 | body: '{}', 184 | }, { 185 | url: 'https://cloudtrail.us-east-1.amazonaws.com/', 186 | headers: { 187 | 'Content-Type': 'application/x-amz-json-1.1', 188 | 'X-Amz-Target': 'CloudTrail_20131101.DescribeTrails', 189 | 'Accept-Encoding': 'gzip, deflate, br', 190 | }, 191 | body: '{}', 192 | }, { 193 | url: 'https://logs.us-east-1.amazonaws.com/', 194 | headers: { 195 | 'Content-Type': 'application/x-amz-json-1.1', 196 | 'X-Amz-Target': 'Logs_20140328.DescribeLogGroups', 197 | 'Accept-Encoding': 'gzip, deflate, br', 198 | }, 199 | body: '{}', 200 | }, { 201 | url: 'https://codedeploy.us-east-1.amazonaws.com/', 202 | headers: { 203 | 'Content-Type': 'application/x-amz-json-1.1', 204 | 'X-Amz-Target': 'CodeDeploy_20141006.ListApplications', 205 | 'Accept-Encoding': 'gzip, deflate, br', 206 | }, 207 | body: '{}', 208 | }, { 209 | url: 'https://directconnect.us-east-1.amazonaws.com/', 210 | headers: { 211 | 'Content-Type': 'application/x-amz-json-1.1', 212 | 'X-Amz-Target': 'OvertureService.DescribeConnections', 213 | 'Accept-Encoding': 'gzip, deflate, br', 214 | }, 215 | body: '{}', 216 | }, { 217 | url: 'https://kms.us-east-1.amazonaws.com/', 218 | headers: { 219 | 'Content-Type': 'application/x-amz-json-1.1', 220 | 'X-Amz-Target': 'TrentService.ListKeys', 221 | 'Accept-Encoding': 'gzip, deflate, br', 222 | }, 223 | body: '{}', 224 | }, { 225 | url: 'https://config.us-east-1.amazonaws.com/', 226 | headers: { 227 | 'Content-Type': 'application/x-amz-json-1.1', 228 | 'X-Amz-Target': 'StarlingDoveService.DescribeDeliveryChannels', 229 | 'Accept-Encoding': 'gzip, deflate, br', 230 | }, 231 | body: '{}', 232 | }, { 233 | url: 'https://cloudhsmv2.us-east-1.amazonaws.com/', 234 | headers: { 235 | 'Content-Type': 'application/x-amz-json-1.1', 236 | 'X-Amz-Target': 'BaldrApiService.DescribeClusters', 237 | 'Accept-Encoding': 'gzip, deflate, br', 238 | }, 239 | body: '{}', 240 | }, { 241 | url: 'https://swf.us-east-1.amazonaws.com/', 242 | headers: { 243 | 'Content-Type': 'application/x-amz-json-1.0', 244 | 'X-Amz-Target': 'SimpleWorkflowService.ListDomains', 245 | 'Accept-Encoding': 'gzip, deflate, br', 246 | }, 247 | body: '{"registrationStatus":"REGISTERED"}', 248 | }, { 249 | url: 'https://cognito-identity.us-east-1.amazonaws.com/', 250 | headers: { 251 | 'Content-Type': 'application/x-amz-json-1.1', 252 | 'X-Amz-Target': 'AWSCognitoIdentityService.ListIdentityPools', 253 | 'Accept-Encoding': 'gzip, deflate, br', 254 | }, 255 | body: '{"MaxResults": 1}', 256 | }] 257 | 258 | okTests = okTests.concat(okTests.map(test => Object.assign({ signQuery: true }, test))) 259 | 260 | okTests.forEach(test => { 261 | test.accessKeyId = process.env.AWS_ACCESS_KEY_ID 262 | test.secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY 263 | test.sessionToken = process.env.AWS_SESSION_TOKEN 264 | }) 265 | 266 | const browser = await puppeteer.launch() 267 | 268 | try { 269 | const signed = await getSignedTests(tests, browser) 270 | const responses = await Promise.all(signed.map(request)) 271 | 272 | responses.map((r, i) => { 273 | if (/InvalidSignatureException|SignatureDoesNotMatch/.test(r.body)) { 274 | return { 275 | path: signed[i].path, 276 | canonicalString: signed[i].canonicalString, 277 | body: r.body.replace(/&/g, '&'), 278 | } 279 | } 280 | }).filter(Boolean).forEach(({ path, canonicalString, body }) => { 281 | console.log(path) 282 | console.log(canonicalString) 283 | console.log(body) 284 | }) 285 | } catch (e) { 286 | console.error(e) 287 | } 288 | 289 | try { 290 | const signed = await getSignedTests(okTests, browser) 291 | const responses = await Promise.all(signed.map(request)) 292 | 293 | responses.map((r, i) => { 294 | if (r.statusCode !== 200) { 295 | return { 296 | path: signed[i].path, 297 | canonicalString: signed[i].canonicalString, 298 | body: r.body.replace(/&/g, '&'), 299 | } 300 | } 301 | }).filter(Boolean).forEach(({ path, canonicalString, body }) => { 302 | console.log(path) 303 | console.log(canonicalString) 304 | console.log(body) 305 | }) 306 | } catch (e) { 307 | console.error(e) 308 | } 309 | 310 | await browser.close() 311 | 312 | console.log('Tests complete') 313 | })() 314 | 315 | async function getSignedTests(tests, browser) { 316 | const rollupFile = path.join(os.tmpdir(), 'aws4fetch.integration.test.js') 317 | fs.writeFileSync(rollupFile, ` 318 | import { AwsV4Signer } from '${__dirname}/../src/main' 319 | Promise.all(${JSON.stringify(tests)}.map(async(options) => { 320 | let signer = new AwsV4Signer(options) 321 | let canonicalString = await signer.canonicalString() 322 | let { method, url, headers, body } = await signer.sign() 323 | return { 324 | canonicalString, 325 | method, 326 | host: url.host, 327 | path: url.pathname + url.search, 328 | headers: [...headers].reduce((obj, [key, val]) => { obj[key] = val; return obj }, {}), 329 | body, 330 | } 331 | })) 332 | `) 333 | const bundle = await rollup.rollup({ input: rollupFile }) 334 | const { output: [{ code }] } = await bundle.generate({ format: 'es' }) 335 | const page = await browser.newPage() 336 | await page.goto('file:///dev/null') 337 | return page.evaluate(code) 338 | } 339 | 340 | const RETRY_ERRS = ['EADDRINFO', 'ETIMEDOUT', 'ECONNRESET', 'ESOCKETTIMEDOUT', 'ENOTFOUND', 'EMFILE'] 341 | 342 | async function request(options) { 343 | options.retries = options.retries || 0 344 | return new Promise((resolve, reject) => { 345 | const onError = err => { 346 | if (RETRY_ERRS.includes(err.code) && options.retries < 5) { 347 | options.retries++ 348 | return request(options).then(resolve).catch(reject) 349 | } 350 | reject(err) 351 | } 352 | https.request(options, res => { 353 | const bufs = [] 354 | res.on('error', onError) 355 | res.on('data', bufs.push.bind(bufs)) 356 | res.on('end', () => { 357 | resolve({ 358 | statusCode: res.statusCode, 359 | headers: res.headers, 360 | body: Buffer.concat(bufs).toString('utf8'), 361 | }) 362 | }) 363 | }).on('error', onError).end(options.body) 364 | }) 365 | } 366 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @license MIT 5 | * @copyright Michael Hart 2018 6 | */ 7 | 8 | const encoder = new TextEncoder() 9 | 10 | /** @type {Object.} */ 11 | const HOST_SERVICES = { 12 | appstream2: 'appstream', 13 | cloudhsmv2: 'cloudhsm', 14 | email: 'ses', 15 | marketplace: 'aws-marketplace', 16 | mobile: 'AWSMobileHubService', 17 | pinpoint: 'mobiletargeting', 18 | queue: 'sqs', 19 | 'git-codecommit': 'codecommit', 20 | 'mturk-requester-sandbox': 'mturk-requester', 21 | 'personalize-runtime': 'personalize', 22 | } 23 | 24 | // https://github.com/aws/aws-sdk-js/blob/cc29728c1c4178969ebabe3bbe6b6f3159436394/lib/signers/v4.js#L190-L198 25 | const UNSIGNABLE_HEADERS = [ 26 | 'authorization', 27 | 'content-type', 28 | 'content-length', 29 | 'user-agent', 30 | 'presigned-expires', 31 | 'expect', 32 | 'x-amzn-trace-id', 33 | 'range', 34 | 'connection', 35 | ] 36 | 37 | export class AwsClient { 38 | /** 39 | * @param {{ 40 | * accessKeyId: string 41 | * secretAccessKey: string 42 | * sessionToken?: string 43 | * service?: string 44 | * region?: string 45 | * cache?: Map 46 | * retries?: number 47 | * initRetryMs?: number 48 | * }} options 49 | */ 50 | constructor({ accessKeyId, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }) { 51 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 52 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 53 | this.accessKeyId = accessKeyId 54 | this.secretAccessKey = secretAccessKey 55 | this.sessionToken = sessionToken 56 | this.service = service 57 | this.region = region 58 | this.cache = cache || new Map() 59 | this.retries = retries != null ? retries : 10 // Up to 25.6 secs 60 | this.initRetryMs = initRetryMs || 50 61 | } 62 | 63 | /** 64 | * @typedef {RequestInit & { 65 | * aws?: { 66 | * accessKeyId?: string 67 | * secretAccessKey?: string 68 | * sessionToken?: string 69 | * service?: string 70 | * region?: string 71 | * cache?: Map 72 | * datetime?: string 73 | * signQuery?: boolean 74 | * appendSessionToken?: boolean 75 | * allHeaders?: boolean 76 | * singleEncode?: boolean 77 | * } 78 | * }} AwsRequestInit 79 | * 80 | * @param {RequestInfo} input 81 | * @param {?AwsRequestInit} [init] 82 | * @returns {Promise} 83 | */ 84 | async sign(input, init) { 85 | if (input instanceof Request) { 86 | const { method, url, headers, body } = input 87 | init = Object.assign({ method, url, headers }, init) 88 | if (init.body == null && headers.has('Content-Type')) { 89 | init.body = body != null && headers.has('X-Amz-Content-Sha256') ? body : await input.clone().arrayBuffer() 90 | } 91 | input = url 92 | } 93 | const signer = new AwsV4Signer(Object.assign({ url: input }, init, this, init && init.aws)) 94 | const signed = Object.assign({}, init, await signer.sign()) 95 | delete signed.aws 96 | return new Request(signed.url.toString(), signed) 97 | } 98 | 99 | /** 100 | * @param {RequestInfo} input 101 | * @param {?AwsRequestInit} [init] 102 | * @returns {Promise} 103 | */ 104 | async fetch(input, init) { 105 | for (let i = 0; i <= this.retries; i++) { 106 | const fetched = fetch(await this.sign(input, init)) 107 | if (i === this.retries) { 108 | return fetched // No need to await if we're returning anyway 109 | } 110 | const res = await fetched 111 | if (res.status < 500 && res.status !== 429) { 112 | return res 113 | } 114 | await new Promise(resolve => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i))) 115 | } 116 | throw new Error('An unknown error occurred, ensure retries is not negative') 117 | } 118 | } 119 | 120 | export class AwsV4Signer { 121 | /** 122 | * @param {{ 123 | * method?: string 124 | * url: string 125 | * headers?: HeadersInit 126 | * body?: BodyInit | null 127 | * accessKeyId: string 128 | * secretAccessKey: string 129 | * sessionToken?: string 130 | * service?: string 131 | * region?: string 132 | * cache?: Map 133 | * datetime?: string 134 | * signQuery?: boolean 135 | * appendSessionToken?: boolean 136 | * allHeaders?: boolean 137 | * singleEncode?: boolean 138 | * }} options 139 | */ 140 | constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) { 141 | if (url == null) throw new TypeError('url is a required option') 142 | if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') 143 | if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') 144 | 145 | this.method = method || (body ? 'POST' : 'GET') 146 | this.url = new URL(url) 147 | this.headers = new Headers(headers || {}) 148 | this.body = body 149 | 150 | this.accessKeyId = accessKeyId 151 | this.secretAccessKey = secretAccessKey 152 | this.sessionToken = sessionToken 153 | 154 | let guessedService, guessedRegion 155 | if (!service || !region) { 156 | ;[guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers) 157 | } 158 | /** @type {string} */ 159 | this.service = service || guessedService || '' 160 | this.region = region || guessedRegion || 'us-east-1' 161 | 162 | this.cache = cache || new Map() 163 | this.datetime = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, '') 164 | this.signQuery = signQuery 165 | this.appendSessionToken = appendSessionToken || this.service === 'iotdevicegateway' 166 | 167 | this.headers.delete('Host') // Can't be set in insecure env anyway 168 | 169 | const params = this.signQuery ? this.url.searchParams : this.headers 170 | if (this.service === 's3' && !this.headers.has('X-Amz-Content-Sha256')) { 171 | this.headers.set('X-Amz-Content-Sha256', 'UNSIGNED-PAYLOAD') 172 | } 173 | 174 | params.set('X-Amz-Date', this.datetime) 175 | if (this.sessionToken && !this.appendSessionToken) { 176 | params.set('X-Amz-Security-Token', this.sessionToken) 177 | } 178 | 179 | // headers are always lowercase in keys() 180 | this.signableHeaders = ['host', ...this.headers.keys()] 181 | .filter(header => allHeaders || !UNSIGNABLE_HEADERS.includes(header)) 182 | .sort() 183 | 184 | this.signedHeaders = this.signableHeaders.join(';') 185 | 186 | // headers are always trimmed: 187 | // https://fetch.spec.whatwg.org/#concept-header-value-normalize 188 | this.canonicalHeaders = this.signableHeaders 189 | .map(header => header + ':' + (header === 'host' ? this.url.host : (this.headers.get(header) || '').replace(/\s+/g, ' '))) 190 | .join('\n') 191 | 192 | this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, 'aws4_request'].join('/') 193 | 194 | if (this.signQuery) { 195 | if (this.service === 's3' && !params.has('X-Amz-Expires')) { 196 | params.set('X-Amz-Expires', '86400') // 24 hours 197 | } 198 | params.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256') 199 | params.set('X-Amz-Credential', this.accessKeyId + '/' + this.credentialString) 200 | params.set('X-Amz-SignedHeaders', this.signedHeaders) 201 | } 202 | 203 | if (this.service === 's3') { 204 | try { 205 | /** @type {string} */ 206 | this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, ' ')) 207 | } catch (e) { 208 | this.encodedPath = this.url.pathname 209 | } 210 | } else { 211 | this.encodedPath = this.url.pathname.replace(/\/+/g, '/') 212 | } 213 | if (!singleEncode) { 214 | this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, '/') 215 | } 216 | this.encodedPath = encodeRfc3986(this.encodedPath) 217 | 218 | const seenKeys = new Set() 219 | this.encodedSearch = [...this.url.searchParams] 220 | .filter(([k]) => { 221 | if (!k) return false // no empty keys 222 | if (this.service === 's3') { 223 | if (seenKeys.has(k)) return false // first val only for S3 224 | seenKeys.add(k) 225 | } 226 | return true 227 | }) 228 | .map(pair => pair.map(p => encodeRfc3986(encodeURIComponent(p)))) 229 | .sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0) 230 | .map(pair => pair.join('=')) 231 | .join('&') 232 | } 233 | 234 | /** 235 | * @returns {Promise<{ 236 | * method: string 237 | * url: URL 238 | * headers: Headers 239 | * body?: BodyInit | null 240 | * }>} 241 | */ 242 | async sign() { 243 | if (this.signQuery) { 244 | this.url.searchParams.set('X-Amz-Signature', await this.signature()) 245 | if (this.sessionToken && this.appendSessionToken) { 246 | this.url.searchParams.set('X-Amz-Security-Token', this.sessionToken) 247 | } 248 | } else { 249 | this.headers.set('Authorization', await this.authHeader()) 250 | } 251 | 252 | return { 253 | method: this.method, 254 | url: this.url, 255 | headers: this.headers, 256 | body: this.body, 257 | } 258 | } 259 | 260 | /** 261 | * @returns {Promise} 262 | */ 263 | async authHeader() { 264 | return [ 265 | 'AWS4-HMAC-SHA256 Credential=' + this.accessKeyId + '/' + this.credentialString, 266 | 'SignedHeaders=' + this.signedHeaders, 267 | 'Signature=' + (await this.signature()), 268 | ].join(', ') 269 | } 270 | 271 | /** 272 | * @returns {Promise} 273 | */ 274 | async signature() { 275 | const date = this.datetime.slice(0, 8) 276 | const cacheKey = [this.secretAccessKey, date, this.region, this.service].join() 277 | let kCredentials = this.cache.get(cacheKey) 278 | if (!kCredentials) { 279 | const kDate = await hmac('AWS4' + this.secretAccessKey, date) 280 | const kRegion = await hmac(kDate, this.region) 281 | const kService = await hmac(kRegion, this.service) 282 | kCredentials = await hmac(kService, 'aws4_request') 283 | this.cache.set(cacheKey, kCredentials) 284 | } 285 | return buf2hex(await hmac(kCredentials, await this.stringToSign())) 286 | } 287 | 288 | /** 289 | * @returns {Promise} 290 | */ 291 | async stringToSign() { 292 | return [ 293 | 'AWS4-HMAC-SHA256', 294 | this.datetime, 295 | this.credentialString, 296 | buf2hex(await hash(await this.canonicalString())), 297 | ].join('\n') 298 | } 299 | 300 | /** 301 | * @returns {Promise} 302 | */ 303 | async canonicalString() { 304 | return [ 305 | this.method.toUpperCase(), 306 | this.encodedPath, 307 | this.encodedSearch, 308 | this.canonicalHeaders + '\n', 309 | this.signedHeaders, 310 | await this.hexBodyHash(), 311 | ].join('\n') 312 | } 313 | 314 | /** 315 | * @returns {Promise} 316 | */ 317 | async hexBodyHash() { 318 | let hashHeader = this.headers.get('X-Amz-Content-Sha256') 319 | if (hashHeader == null) { 320 | if (this.body && typeof this.body !== 'string' && !('byteLength' in this.body)) { 321 | throw new Error('body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header') 322 | } 323 | hashHeader = buf2hex(await hash(this.body || '')) 324 | } 325 | return hashHeader 326 | } 327 | } 328 | 329 | /** 330 | * @param {string | ArrayBufferView | ArrayBuffer} key 331 | * @param {string} string 332 | * @returns {Promise} 333 | */ 334 | async function hmac(key, string) { 335 | // @ts-ignore // https://github.com/microsoft/TypeScript/issues/38715 336 | const cryptoKey = await crypto.subtle.importKey( 337 | 'raw', 338 | typeof key === 'string' ? encoder.encode(key) : key, 339 | { name: 'HMAC', hash: { name: 'SHA-256' } }, 340 | false, 341 | ['sign'], 342 | ) 343 | return crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(string)) 344 | } 345 | 346 | /** 347 | * @param {string | ArrayBufferView | ArrayBuffer} content 348 | * @returns {Promise} 349 | */ 350 | async function hash(content) { 351 | // @ts-ignore // https://github.com/microsoft/TypeScript/issues/38715 352 | return crypto.subtle.digest('SHA-256', typeof content === 'string' ? encoder.encode(content) : content) 353 | } 354 | 355 | /** 356 | * @param {ArrayBuffer | ArrayLike | SharedArrayBuffer} buffer 357 | * @returns {string} 358 | */ 359 | function buf2hex(buffer) { 360 | return Array.prototype.map.call(new Uint8Array(buffer), x => ('0' + x.toString(16)).slice(-2)).join('') 361 | } 362 | 363 | /** 364 | * @param {string} urlEncodedStr 365 | * @returns {string} 366 | */ 367 | function encodeRfc3986(urlEncodedStr) { 368 | return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()) 369 | } 370 | 371 | /** 372 | * @param {URL} url 373 | * @param {Headers} headers 374 | * @returns {string[]} [service, region] 375 | */ 376 | function guessServiceRegion(url, headers) { 377 | const { hostname, pathname } = url 378 | const match = hostname.replace('dualstack.', '').match(/([^.]+)\.(?:([^.]*)\.)?amazonaws\.com(?:\.cn)?$/) 379 | let [service, region] = (match || ['', '']).slice(1, 3) 380 | 381 | if (region === 'us-gov') { 382 | region = 'us-gov-west-1' 383 | } else if (region === 's3' || region === 's3-accelerate') { 384 | region = 'us-east-1' 385 | service = 's3' 386 | } else if (service === 'iot') { 387 | if (hostname.startsWith('iot.')) { 388 | service = 'execute-api' 389 | } else if (hostname.startsWith('data.jobs.iot.')) { 390 | service = 'iot-jobs-data' 391 | } else { 392 | service = pathname === '/mqtt' ? 'iotdevicegateway' : 'iotdata' 393 | } 394 | } else if (service === 'autoscaling') { 395 | const targetPrefix = (headers.get('X-Amz-Target') || '').split('.')[0] 396 | if (targetPrefix === 'AnyScaleFrontendService') { 397 | service = 'application-autoscaling' 398 | } else if (targetPrefix === 'AnyScaleScalingPlannerFrontendService') { 399 | service = 'autoscaling-plans' 400 | } 401 | } else if (region == null && service.startsWith('s3-')) { 402 | region = service.slice(3).replace(/^fips-|^external-1/, '') 403 | service = 's3' 404 | } else if (service.endsWith('-fips')) { 405 | service = service.slice(0, -5) 406 | } else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) { 407 | ;[service, region] = [region, service] 408 | } 409 | 410 | return [HOST_SERVICES[service] || service, region] 411 | } 412 | --------------------------------------------------------------------------------