├── .bithoundrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── README.md ├── dist └── aws-request.js ├── package.json ├── src ├── calculateSignature.js ├── getCanonicalDate.js ├── getCanonicalQueryString.js ├── getCanonicalRequestHash.js └── request.js ├── test ├── index.test.js └── test-helper.js └── vendor └── crypto.js /.bithoundrc: -------------------------------------------------------------------------------- 1 | { 2 | "test": [ 3 | "test/index.test.js" 4 | ], 5 | "ignore": [ 6 | "vendor/**", 7 | "dist/**", 8 | "test/test-helper.js" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | dist/* 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "rules": { 4 | "max-len": ["error", 100], 5 | "valid-jsdoc": "warn", 6 | "semi": ["error", "always"], 7 | "arrow-parens": 0, 8 | "indent": ["error", 2, { "SwitchCase": 1 }], 9 | "no-shadow": ["error", { 10 | "builtinGlobals": true, 11 | "hoist": "all", 12 | "allow": [ 13 | "cb", "done", "resolve", "reject" 14 | ] 15 | }], 16 | "no-use-before-define": ["error", { "functions": true, "classes": true }], 17 | "no-inner-declarations": [2, "both"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Loacal temp files 2 | _temp* 3 | _old 4 | 5 | # IDE 6 | .idea 7 | .c9revisions 8 | *.sublime* 9 | 10 | # Env 11 | .env 12 | 13 | # Dependency directories 14 | node_modules 15 | typings 16 | 17 | # Local dependency directory 18 | !src/node_modules 19 | 20 | # Optional npm cache directory 21 | .npm 22 | 23 | # Logs 24 | logs 25 | *.log 26 | npm-debug.log* 27 | 28 | # Test code coverage 29 | coverage 30 | lib-cov 31 | .nyc_output 32 | 33 | #Other 34 | *.DS_Store 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | script: 5 | - npm run test 6 | after_success: 7 | - npm run coverage-report 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${file}" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [ 3 | 100 4 | ], 5 | "editor.tabSize": 2, 6 | "editor.renderIndentGuides": true, 7 | "files.trimTrailingWhitespace": true, 8 | "files.insertFinalNewline": true 9 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apps-script-aws-request 2 | 3 | [![version](https://img.shields.io/badge/version-0.1.1-blue.svg?style=flat-square)]() 4 | [![Travis](https://img.shields.io/travis/wmakeev/apps-script-aws-request.svg?maxAge=1800&style=flat-square)](https://travis-ci.org/wmakeev/apps-script-aws-request) 5 | [![Coveralls](https://img.shields.io/coveralls/wmakeev/apps-script-aws-request.svg?maxAge=1800&style=flat-square)](https://coveralls.io/github/wmakeev/apps-script-aws-request) 6 | [![bitHound DevDependencies](https://img.shields.io/bithound/devDependencies/github/wmakeev/apps-script-aws-request.svg?maxAge=1800&style=flat-square)](https://www.bithound.io/github/wmakeev/apps-script-aws-request/master/dependencies/npm) 7 | 8 | > An interface to authenticate AWS api requests from within google apps scripts. 9 | 10 | Originaly forked from [smithy545/aws-apps-scripts](https://github.com/smithy545/aws-apps-scripts) 11 | 12 | ## Setup 13 | 14 | ### Use public library 15 | 16 | **Script ID:** ```1UmToPBwCgl8rjKHqqkgthduXt36tvv4CF985DEE0WAtx9UXFgA-QJ97n``` 17 | 18 | 1. Create a new project in google scripts. 19 | 20 | 2. Go `Resources → Libraries`. 21 | 22 | 3. Copy paste Script ID to `Add a library` field and press `Add`. 23 | 24 | 4. Select version and specify identifier as `AWS`. 25 | 26 | ### Create you own library 27 | 28 | 1. Create a new project in google scripts. 29 | 30 | 2. Copy paste `dist/aws-request.js` in your project file and save it. 31 | 32 | 3. Go `File → Manage versions` and press `Save new version`. 33 | 34 | 4. You can press `Share` and make it public. 35 | 36 | 5. Copy you library Script ID from `File → Project properties → Script ID` 37 | 38 | 6. Use new library with you Script ID in other projects [Use public library](#use-public-library). 39 | 40 | ### Use in same file with other scripts 41 | 42 | 1. Create a new project in google scripts. 43 | 44 | 2. Create new file in you project by template 45 | 46 | ```js 47 | var AWS = (function () { 48 | 49 | // Copy paste `dist/aws-request.js` content here 50 | 51 | return { request: request } 52 | })() 53 | ``` 54 | 55 | ## Usage 56 | 57 | Example with lambda function invocation 58 | 59 | ```js 60 | var path = '/2015-03-31/functions/hello/invocations'; 61 | var event = { 62 | name: 'Google' 63 | }; 64 | 65 | var result = AWS.request({ 66 | accessKey: AWS_ACCESS_KEY, 67 | secretKey: AWS_SECRET_KEY, 68 | service: 'lambda', 69 | region: 'eu-west-1', 70 | method: 'POST', 71 | path: path, 72 | payload: event 73 | }); 74 | 75 | Logger.log(result.getResponseCode()); // ➝ 200 76 | Logger.log(result.getContentText()); // ➝ { "message": "Hello Google!" } 77 | ``` 78 | 79 | ## API 80 | 81 | ### `AWS.request(params: Object): HTTPResponse` 82 | 83 | Where **`params`**: 84 | 85 | Field name | Type | Default value | Description | 86 | -----------|-------|---------------|-------------| 87 | `accessKey` | `string` | `N/A` (required) | AWS access key | 88 | `secretKey` | `string` | `N/A` (required) | AWS secret key | 89 | `service` | `string` | `N/A` (required) | AWS service (e.g. `ec2`, `iam`, `codecommit`) | 90 | `region` | `string` | `us-east-1` | AWS region | 91 | `path` | `string` | `/` | Path to api function (without query) | 92 | `query` | `object` | `{}` | Query string parameters (e.g. `{ Action: "ListUsers" }`) | 93 | `method` | `string` | `GET` | Http method (e.g. `GET`, `POST`) | 94 | `headers` | `object` | `{}` | Http request headers `Host`, and `X-Amz-Date` are premade for you | 95 | `payload` | `string` or `object` | `""` | Payload to send (object payload will be stringified)| 96 | 97 | Returns [HTTPResponse](https://developers.google.com/apps-script/reference/url-fetch/http-response) 98 | 99 | ## License 100 | 101 | ISC © Vitaliy V. Makeev 102 | -------------------------------------------------------------------------------- /dist/aws-request.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | var CryptoJs_ = (function () { 4 | var window = {}; 5 | var Crypto = undefined; 6 | /** 7 | * Crypto-JS v2.5.3 8 | * http://code.google.com/p/crypto-js/ 9 | * (c) 2009-2012 by Jeff Mott. All rights reserved. 10 | * http://code.google.com/p/crypto-js/wiki/License 11 | */ 12 | // start sha256/CryptoJS 13 | (typeof Crypto=="undefined"||!Crypto.util)&&function(){var d=window.Crypto={},k=d.util={rotl:function(b,a){return b<>>32-a},rotr:function(b,a){return b<<32-a|b>>>a},endian:function(b){if(b.constructor==Number)return k.rotl(b,8)&16711935|k.rotl(b,24)&4278255360;for(var a=0;a0;b--)a.push(Math.floor(Math.random()*256));return a},bytesToWords:function(b){for(var a=[],c=0,e=0;c>>5]|=(b[c]&255)<< 14 | 24-e%32;return a},wordsToBytes:function(b){for(var a=[],c=0;c>>5]>>>24-c%32&255);return a},bytesToHex:function(b){for(var a=[],c=0;c>>4).toString(16)),a.push((b[c]&15).toString(16));return a.join("")},hexToBytes:function(b){for(var a=[],c=0;c>>6*(3-p)&63)):a.push("=");return a.join("")},base64ToBytes:function(b){if(typeof atob=="function")return g.stringToBytes(atob(b));for(var b=b.replace(/[^A-Z0-9+\/]/ig,""),a=[],c=0,e=0;c>> 16 | 6-e*2);return a}},d=d.charenc={};d.UTF8={stringToBytes:function(b){return g.stringToBytes(unescape(encodeURIComponent(b)))},bytesToString:function(b){return decodeURIComponent(escape(g.bytesToString(b)))}};var g=d.Binary={stringToBytes:function(b){for(var a=[],c=0;c>5]|=128<<24-f%32;e[(f+64>>9<<4)+15]=f;for(t=0;t>>7)^(l<<14|l>>>18)^l>>>3)+(d[h-7]>>>0)+((j<<15|j>>>17)^(j<<13|j>>>19)^j>>>10)+(d[h-16]>>>0));j=f&g^f&m^g&m;var u=(f<<30|f>>>2)^(f<<19|f>>>13)^(f<<10|f>>>22);l=(s>>>0)+((i<<26|i>>>6)^(i<<21|i>>>11)^(i<<7|i>>>25))+ 21 | (i&n^~i&o)+c[h]+(d[h]>>>0);j=u+j;s=o;o=n;n=i;i=r+l>>>0;r=m;m=g;g=f;f=l+j>>>0}a[0]+=f;a[1]+=g;a[2]+=m;a[3]+=r;a[4]+=i;a[5]+=n;a[6]+=o;a[7]+=s}return a};e._blocksize=16;e._digestsize=32})(); 22 | (function(){var d=Crypto,k=d.util,g=d.charenc,b=g.UTF8,a=g.Binary;d.HMAC=function(c,e,d,g){e.constructor==String&&(e=b.stringToBytes(e));d.constructor==String&&(d=b.stringToBytes(d));d.length>c._blocksize*4&&(d=c(d,{asBytes:!0}));for(var f=d.slice(0),d=d.slice(0),q=0;q b[0]) ? 1 : -1; 130 | }) 131 | .forEach(function (header) { 132 | canonicalHeaders.push(header[0] + ':' + header[1] + '\n'); 133 | signedHeaders.push(header[0]); 134 | }); 135 | 136 | var canonicalHeadersStr = canonicalHeaders.join(''); 137 | var signedHeadersStr = signedHeaders.join(';'); 138 | var normalizedPath = path 139 | .split(/\//g) 140 | .map(function (part) { 141 | return encodeURIComponent(part); 142 | }) 143 | .join('/'); 144 | 145 | var canonicalRequest = [ 146 | method, 147 | normalizedPath, 148 | queryString, 149 | canonicalHeadersStr, 150 | signedHeadersStr, 151 | CryptoJs_.SHA256(payloadString) 152 | ].join('\n'); 153 | 154 | return { 155 | hashedCanonicalRequest: CryptoJs_.SHA256(canonicalRequest), 156 | signedHeaders: signedHeadersStr 157 | }; 158 | } 159 | /* global getCanonicalFullDate_, getCanonicalShortDate_, getCanonicalQueryString_, 160 | calculateSignature_, getCanonicalRequestHash_, UrlFetchApp */ 161 | 162 | /* exported request */ 163 | 164 | /** 165 | * AWS hash algorithm name 166 | */ 167 | var HASH_ALGORITHM_ = 'AWS4-HMAC-SHA256'; 168 | 169 | var AWS_SINGLE_ENDPOINT_ = { 170 | 'cloudfront': 'cloudfront.amazonaws.com', 171 | 'health': 'health.us-east-1.amazonaws.com', 172 | 'iam': 'iam.amazonaws.com', 173 | 'importexport': 'importexport.amazonaws.com', 174 | 'shield': 'shield.us-east-1.amazonaws.com', 175 | 'waf': 'waf.amazonaws.com' 176 | }; 177 | 178 | var getCurrentDate_ = function () { 179 | return new Date(); 180 | }; 181 | 182 | /** 183 | * Authenticates and sends the given parameters for an AWS api request. 184 | * @param {{ 185 | accessKey: string, 186 | secretKey: string, 187 | service: string, 188 | region: string, 189 | path: string, 190 | query: object, 191 | method: string, 192 | headers: object, 193 | payload: (string|object), 194 | }} params **The AWS service request parameters** 195 | - `accessKey` - AWS access key 196 | - `secretKey` - AWS secret key key 197 | - `service` - the aws service to connect to (e.g. 'ec2', 'iam', 'codecommit') 198 | - `region` - (optional) the aws region your command will go to. Defaults to 'us-east-1'. 199 | - `path` - (optional) the path to api function (without query). Defaults to '/'. 200 | - `query` - (optional) the query string parameters 201 | - `method` - (optional) the http method (e.g. 'GET', 'POST'). Defaults to GET. 202 | - `headers` - (optional) the headers to attach to the request. Host and X-Amz-Date are premade 203 | for you. 204 | - `payload` - (optional) the payload to send. Defaults to ''. 205 | * @returns {HTTPResponse} HTTPResponse object 206 | */ 207 | function request(params) { 208 | var temp; 209 | 210 | if (params.accessKey == null) { 211 | throw new Error('Access key undefined'); 212 | } else if (params.secretKey == null) { 213 | throw new Error('Secret key undefined'); 214 | } else if (params.service == null) { 215 | throw new Error('Service undefined'); 216 | } 217 | 218 | var accessKey = params.accessKey; 219 | var secretKey = params.secretKey; 220 | var service = params.service.toLowerCase(); 221 | var region = params.region ? params.region.toLowerCase() : 'us-east-1'; 222 | var path = params.path || '/'; 223 | var query = params.query || {}; 224 | var method = (params.method || 'GET').toUpperCase(); 225 | var headers = params.headers || {}; 226 | var payload = params.payload || ''; 227 | 228 | var host = AWS_SINGLE_ENDPOINT_[service] 229 | ? AWS_SINGLE_ENDPOINT_[service] 230 | : service + '.' + region + '.amazonaws.com'; 231 | 232 | if (path.substring(0, 1) !== '/') { 233 | path = '/' + path; 234 | } 235 | 236 | var payloadString = ''; 237 | if (typeof payload !== 'string') { 238 | payloadString = JSON.stringify(payload); 239 | } 240 | 241 | var curDate = getCurrentDate_(); 242 | var dateStringFull = getCanonicalFullDate_(curDate); // 20150830T123600Z 243 | var dateStringShort = getCanonicalShortDate_(curDate); // 20150830 244 | var queryString = getCanonicalQueryString_(query); 245 | 246 | headers['Host'] = host; 247 | headers['X-Amz-Date'] = dateStringFull; 248 | 249 | // Task 1: Create a Canonical Request for Signature 250 | // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html 251 | 252 | temp = getCanonicalRequestHash_(method, path, headers, queryString, payloadString); 253 | var hashedCanonicalRequest = temp.hashedCanonicalRequest; 254 | var signedHeaders = temp.signedHeaders; 255 | 256 | // Task 2: Create a String to Sign for Signature 257 | // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html 258 | 259 | var credentialScope = dateStringShort + '/' + region + '/' + service + '/aws4_request'; 260 | 261 | var stringToSign = [ 262 | HASH_ALGORITHM_, 263 | dateStringFull, 264 | credentialScope, 265 | hashedCanonicalRequest 266 | ].join('\n'); 267 | 268 | // Task 3: Calculate the Signature 269 | // http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html 270 | 271 | var signature = calculateSignature_(secretKey, dateStringShort, region, service, stringToSign); 272 | 273 | // Task 4: Add the Signing Information to the Request 274 | // http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html 275 | 276 | headers['Authorization'] = [ 277 | HASH_ALGORITHM_ + ' Credential=' + accessKey + '/' + credentialScope, 278 | 'SignedHeaders=' + signedHeaders, 279 | 'Signature=' + signature 280 | ].join(', '); 281 | 282 | // Sending request 283 | 284 | delete headers['Host']; // fetch will add Host header 285 | var fetchOptions = { 286 | method: method, 287 | headers: headers, 288 | muteHttpExceptions: true, 289 | payload: payloadString 290 | }; 291 | 292 | var uri = 'https://' + host + path + (queryString ? '?' + queryString : ''); 293 | 294 | return UrlFetchApp.fetch(uri, fetchOptions); 295 | } 296 | 297 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apps-script-aws-request", 3 | "private": true, 4 | "version": "0.1.1", 5 | "description": "An interface to authenticate AWS api requests from within google scripts", 6 | "scripts": { 7 | "clean-dist": "rm -rf dist || : && mkdir dist", 8 | "clean-test": "rm -rf _temp-test || : && mkdir _temp-test", 9 | "clean": "npm run clean-dist && npm run clean-test", 10 | "lint": "eslint src && eslint test", 11 | "concat-dist": "npm run clean-dist && cat vendor/crypto.js src/*.js > dist/aws-request.js", 12 | "concat-test": "npm run clean-test && cat dist/aws-request.js test/test-helper.js > _temp-test/aws-request.js", 13 | "build": "npm run concat-dist && npm run concat-test", 14 | "test": "npm run lint && npm run build && nyc --silent tape test/*.test.js | tap-spec && nyc report --reporter=text --reporter=lcov", 15 | "coverage-report": "nyc report --reporter=text-lcov | coveralls" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/wmakeev/apps-script-aws-request.git" 20 | }, 21 | "keywords": [ 22 | "aws", 23 | "google", 24 | "google-apps-script", 25 | "auth" 26 | ], 27 | "author": { 28 | "name": "Philip Smith", 29 | "email": "pas147@case.edu" 30 | }, 31 | "maintainers": [ 32 | { 33 | "name": "Vitaliy V. Makeev", 34 | "email": "w.makeev@gmail.com" 35 | } 36 | ], 37 | "license": "ISC", 38 | "bugs": { 39 | "url": "https://github.com/wmakeev/apps-script-aws-request/issues" 40 | }, 41 | "homepage": "https://github.com/wmakeev/apps-script-aws-request", 42 | "devDependencies": { 43 | "coveralls": "^2.13.0", 44 | "eslint": "^3.19.0", 45 | "nyc": "^10.2.0", 46 | "sinon": "^2.1.0", 47 | "tap-spec": "^4.1.1", 48 | "tape": "^4.6.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/calculateSignature.js: -------------------------------------------------------------------------------- 1 | /* global CryptoJs_ */ 2 | /* exported calculateSignature_ */ 3 | 4 | /** 5 | * Returns signature for request 6 | * http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html 7 | * @param {string} key Your secret access key 8 | * @param {string} dateStamp Date in YYYYMMDD format 9 | * @param {string} regionName AWS region (e.g. 'us-east-1') 10 | * @param {string} serviceName AWS service name (e.g. 'ec2', 'iam', 'codecommit') 11 | * @param {string} stringToSign String to sign 12 | * @returns {string} Signed string 13 | */ 14 | function calculateSignature_ (key, dateStamp, regionName, serviceName, stringToSign) { 15 | var HMAC = CryptoJs_.HMAC; 16 | var SHA256 = CryptoJs_.SHA256; 17 | 18 | var kDate = HMAC(SHA256, dateStamp, 'AWS4' + key, { asBytes: true }); 19 | var kRegion = HMAC(SHA256, regionName, kDate, { asBytes: true }); 20 | var kService = HMAC(SHA256, serviceName, kRegion, { asBytes: true }); 21 | var kSigning = HMAC(SHA256, 'aws4_request', kService, { asBytes: true }); 22 | 23 | return HMAC(SHA256, stringToSign, kSigning, { asBytes: false }); 24 | } 25 | -------------------------------------------------------------------------------- /src/getCanonicalDate.js: -------------------------------------------------------------------------------- 1 | /* exported getCanonicalFullDate_, getCanonicalShortDate_ */ 2 | 3 | /** 4 | * Extract number values from ISO date string (https://regex101.com/r/5Ysvdf/1) 5 | */ 6 | var DATE_VALUES_REGEX_ = new RegExp(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/); 7 | 8 | /** 9 | * Return date and time string 10 | * @param {Date} date Date 11 | * @returns {string} Date formated to YYYYMMDDTHHmmssZ 12 | */ 13 | function getCanonicalFullDate_(date) { 14 | var match = DATE_VALUES_REGEX_.exec(date.toISOString()); 15 | return match[1] + match[2] + match[3] + 'T' + match[4] + match[5] + match[6] + 'Z'; 16 | } 17 | 18 | /** 19 | * Return date string 20 | * @param {Date} date Date 21 | * @returns {string} Date formated to YYYYMMDD 22 | */ 23 | function getCanonicalShortDate_(date) { 24 | var match = DATE_VALUES_REGEX_.exec(date.toISOString()); 25 | return match[1] + match[2] + match[3]; 26 | } 27 | -------------------------------------------------------------------------------- /src/getCanonicalQueryString.js: -------------------------------------------------------------------------------- 1 | /* exported getCanonicalQueryString_ */ 2 | 3 | /** 4 | * Returns canonical query string 5 | * @param {object} query Query key-value object 6 | * @returns {string} Canonical query string 7 | */ 8 | function getCanonicalQueryString_(query) { 9 | var key; 10 | var queryParts = []; 11 | 12 | for (key in query) { 13 | if (!query.hasOwnProperty(key)) continue; 14 | queryParts.push([key, query[key]]); 15 | } 16 | 17 | return queryParts 18 | .map(function (part) { 19 | var value = part[1] != null ? part[1] : ''; 20 | return encodeURIComponent(part[0].trim()) + '=' + encodeURIComponent(value); 21 | }) 22 | .sort(function (a, b) { 23 | if (a === b) return 0; 24 | return (a > b) ? 1 : -1; 25 | }) 26 | .join('&'); 27 | } 28 | -------------------------------------------------------------------------------- /src/getCanonicalRequestHash.js: -------------------------------------------------------------------------------- 1 | /* global CryptoJs_ */ 2 | /* exported getCanonicalRequestHash_ */ 3 | 4 | /** 5 | * Returns hashed canonical request 6 | * @param {string} method HTTP method 7 | * @param {string} path Request path 8 | * @param {object} headers Request headers 9 | * @param {string} queryString Request query string 10 | * @param {string} payloadString Request payload as string 11 | * @returns {{ 12 | hashedCanonicalRequest: string, 13 | signedHeaders: string 14 | }} hashed canonical request and signed headers 15 | */ 16 | function getCanonicalRequestHash_(method, path, headers, queryString, payloadString) { 17 | var canonicalHeaders = []; 18 | var signedHeaders = []; 19 | Object.keys(headers) 20 | .map(function (key) { return [key, headers[key]]; }) 21 | .map(function (header) { 22 | return [header[0].trim().toLowerCase(), header[1].trim().replace(/\s{2,}/g, ' ')]; 23 | }) 24 | .sort(function (a, b) { 25 | if (a[0] === b[0]) return 0; 26 | else return (a[0] > b[0]) ? 1 : -1; 27 | }) 28 | .forEach(function (header) { 29 | canonicalHeaders.push(header[0] + ':' + header[1] + '\n'); 30 | signedHeaders.push(header[0]); 31 | }); 32 | 33 | var canonicalHeadersStr = canonicalHeaders.join(''); 34 | var signedHeadersStr = signedHeaders.join(';'); 35 | var normalizedPath = path 36 | .split(/\//g) 37 | .map(function (part) { 38 | return encodeURIComponent(part); 39 | }) 40 | .join('/'); 41 | 42 | var canonicalRequest = [ 43 | method, 44 | normalizedPath, 45 | queryString, 46 | canonicalHeadersStr, 47 | signedHeadersStr, 48 | CryptoJs_.SHA256(payloadString) 49 | ].join('\n'); 50 | 51 | return { 52 | hashedCanonicalRequest: CryptoJs_.SHA256(canonicalRequest), 53 | signedHeaders: signedHeadersStr 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | /* global getCanonicalFullDate_, getCanonicalShortDate_, getCanonicalQueryString_, 2 | calculateSignature_, getCanonicalRequestHash_, UrlFetchApp */ 3 | 4 | /* exported request */ 5 | 6 | /** 7 | * AWS hash algorithm name 8 | */ 9 | var HASH_ALGORITHM_ = 'AWS4-HMAC-SHA256'; 10 | 11 | var AWS_SINGLE_ENDPOINT_ = { 12 | 'cloudfront': 'cloudfront.amazonaws.com', 13 | 'health': 'health.us-east-1.amazonaws.com', 14 | 'iam': 'iam.amazonaws.com', 15 | 'importexport': 'importexport.amazonaws.com', 16 | 'shield': 'shield.us-east-1.amazonaws.com', 17 | 'waf': 'waf.amazonaws.com' 18 | }; 19 | 20 | var getCurrentDate_ = function () { 21 | return new Date(); 22 | }; 23 | 24 | /** 25 | * Authenticates and sends the given parameters for an AWS api request. 26 | * @param {{ 27 | accessKey: string, 28 | secretKey: string, 29 | service: string, 30 | region: string, 31 | path: string, 32 | query: object, 33 | method: string, 34 | headers: object, 35 | payload: (string|object), 36 | }} params **The AWS service request parameters** 37 | - `accessKey` - AWS access key 38 | - `secretKey` - AWS secret key key 39 | - `service` - the aws service to connect to (e.g. 'ec2', 'iam', 'codecommit') 40 | - `region` - (optional) the aws region your command will go to. Defaults to 'us-east-1'. 41 | - `path` - (optional) the path to api function (without query). Defaults to '/'. 42 | - `query` - (optional) the query string parameters 43 | - `method` - (optional) the http method (e.g. 'GET', 'POST'). Defaults to GET. 44 | - `headers` - (optional) the headers to attach to the request. Host and X-Amz-Date are premade 45 | for you. 46 | - `payload` - (optional) the payload to send. Defaults to ''. 47 | * @returns {HTTPResponse} HTTPResponse object 48 | */ 49 | function request(params) { 50 | var temp; 51 | 52 | if (params.accessKey == null) { 53 | throw new Error('Access key undefined'); 54 | } else if (params.secretKey == null) { 55 | throw new Error('Secret key undefined'); 56 | } else if (params.service == null) { 57 | throw new Error('Service undefined'); 58 | } 59 | 60 | var accessKey = params.accessKey; 61 | var secretKey = params.secretKey; 62 | var service = params.service.toLowerCase(); 63 | var region = params.region ? params.region.toLowerCase() : 'us-east-1'; 64 | var path = params.path || '/'; 65 | var query = params.query || {}; 66 | var method = (params.method || 'GET').toUpperCase(); 67 | var headers = params.headers || {}; 68 | var payload = params.payload || ''; 69 | 70 | var host = AWS_SINGLE_ENDPOINT_[service] 71 | ? AWS_SINGLE_ENDPOINT_[service] 72 | : service + '.' + region + '.amazonaws.com'; 73 | 74 | if (path.substring(0, 1) !== '/') { 75 | path = '/' + path; 76 | } 77 | 78 | var payloadString = ''; 79 | if (typeof payload !== 'string') { 80 | payloadString = JSON.stringify(payload); 81 | } 82 | 83 | var curDate = getCurrentDate_(); 84 | var dateStringFull = getCanonicalFullDate_(curDate); // 20150830T123600Z 85 | var dateStringShort = getCanonicalShortDate_(curDate); // 20150830 86 | var queryString = getCanonicalQueryString_(query); 87 | 88 | headers['Host'] = host; 89 | headers['X-Amz-Date'] = dateStringFull; 90 | 91 | // Task 1: Create a Canonical Request for Signature 92 | // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html 93 | 94 | temp = getCanonicalRequestHash_(method, path, headers, queryString, payloadString); 95 | var hashedCanonicalRequest = temp.hashedCanonicalRequest; 96 | var signedHeaders = temp.signedHeaders; 97 | 98 | // Task 2: Create a String to Sign for Signature 99 | // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html 100 | 101 | var credentialScope = dateStringShort + '/' + region + '/' + service + '/aws4_request'; 102 | 103 | var stringToSign = [ 104 | HASH_ALGORITHM_, 105 | dateStringFull, 106 | credentialScope, 107 | hashedCanonicalRequest 108 | ].join('\n'); 109 | 110 | // Task 3: Calculate the Signature 111 | // http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html 112 | 113 | var signature = calculateSignature_(secretKey, dateStringShort, region, service, stringToSign); 114 | 115 | // Task 4: Add the Signing Information to the Request 116 | // http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html 117 | 118 | headers['Authorization'] = [ 119 | HASH_ALGORITHM_ + ' Credential=' + accessKey + '/' + credentialScope, 120 | 'SignedHeaders=' + signedHeaders, 121 | 'Signature=' + signature 122 | ].join(', '); 123 | 124 | // Sending request 125 | 126 | delete headers['Host']; // fetch will add Host header 127 | var fetchOptions = { 128 | method: method, 129 | headers: headers, 130 | muteHttpExceptions: true, 131 | payload: payloadString 132 | }; 133 | 134 | var uri = 'https://' + host + path + (queryString ? '?' + queryString : ''); 135 | 136 | return UrlFetchApp.fetch(uri, fetchOptions); 137 | } 138 | 139 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | /* global require */ 2 | /* eslint no-global-assign: 0 */ 3 | 4 | var test = require('tape'); 5 | var lib = require('../_temp-test/aws-request'); 6 | 7 | test('CryptoJs', function (t) { 8 | var CryptoJs = lib.CryptoJs_; 9 | t.ok(CryptoJs.HMAC, 'should have HMAC method'); 10 | t.ok(CryptoJs.SHA256, 'should have SHA256 method'); 11 | 12 | var result; 13 | 14 | result = CryptoJs.HMAC(CryptoJs.SHA256, 'foo-value', 'foo-key'); 15 | t.equal(result, '6b2de18301bda95140a9fea76a89cf8c48bed77751c588ea8e5019faeb0d50ee', 16 | 'should calculate HmacSHA256'); 17 | 18 | result = CryptoJs.SHA256('foo-value', 'foo-key'); 19 | t.equal(result, 'c77164b59bd03483ccf7dd119a04707cb47ca6c49884b3be9ae85eb39c0c735b', 20 | 'should calculate SHA256'); 21 | 22 | t.end(); 23 | }); 24 | 25 | test('getCanonicalDate functions', function (t) { 26 | var getCanonicalFullDate = lib.getCanonicalFullDate_; 27 | var getCanonicalShortDate = lib.getCanonicalShortDate_; 28 | 29 | t.ok(lib.DATE_VALUES_REGEX_, 'should have DATE_VALUES_REGEX_ constant'); 30 | t.equal(typeof getCanonicalFullDate, 'function', 'should have getCanonicalFullDate_ function'); 31 | t.equal(typeof getCanonicalShortDate, 'function', 'should have getCanonicalShortDate_ function'); 32 | 33 | var date = new Date('2017-03-15T07:03:15.356Z'); 34 | 35 | t.equal(getCanonicalFullDate(date), '20170315T070315Z', 'should return full date'); 36 | t.equal(getCanonicalShortDate(date), '20170315', 'should return short date'); 37 | 38 | t.end(); 39 | }); 40 | 41 | test('getCanonicalQueryString function', function (t) { 42 | var getCanonicalQueryString = lib.getCanonicalQueryString_; 43 | 44 | t.equal(typeof getCanonicalQueryString, 'function', 45 | 'should be function'); 46 | 47 | var result; 48 | 49 | result = getCanonicalQueryString({ 50 | 'foo1': 'value1', 51 | ' baz ': ' bar "baz" qux=1 ', 52 | 'bar': 10, 53 | ' Foo 2': void 0 54 | }); 55 | 56 | t.equal(result, 57 | 'Foo%202=&bar=10&baz=%20bar%20%22baz%22%20qux%3D1%20&foo1=value1', 58 | 'should return canonical query string'); 59 | 60 | t.end(); 61 | }); 62 | 63 | test('calculateSignature function', function (t) { 64 | var calculateSignature = lib.calculateSignature_; 65 | 66 | t.equal(typeof calculateSignature, 'function', 'should have calculateSignature_ function'); 67 | t.equal( 68 | calculateSignature('aws_key', '20170105', 'eu-west-1', 'lambda', 'stringToSign'), 69 | '339189553fd488688adb3b972b57974633c966b379d74cc736ca6e23b7295508'); 70 | 71 | t.end(); 72 | }); 73 | 74 | test('getCanonicalRequestHash function', function (t) { 75 | var getCanonicalRequestHash = lib.getCanonicalRequestHash_; 76 | 77 | t.equal(typeof getCanonicalRequestHash, 'function', 78 | 'should have getCanonicalRequestHash_ function'); 79 | 80 | var result = getCanonicalRequestHash( 81 | 'GET', 82 | '/', 83 | { 84 | 'Host': 'iam.amazonaws.com', 85 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 86 | 'X-Amz-Date': '20150830T123600Z' 87 | }, 88 | 'Action=ListUsers&Version=2010-05-08', 89 | ''); 90 | 91 | t.deepEqual(result, { 92 | hashedCanonicalRequest: 'f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59', 93 | signedHeaders: 'content-type;host;x-amz-date' 94 | }); 95 | 96 | t.end(); 97 | }); 98 | 99 | test('request', function (t) { 100 | var request = lib.request; 101 | 102 | t.ok(lib.HASH_ALGORITHM_, 'should have HASH_ALGORITHM_ constant'); 103 | t.equal(typeof request, 'function', 'should have request function'); 104 | 105 | t.end(); 106 | }); 107 | 108 | test('request #1', function (t) { 109 | var request = lib.request; 110 | var UrlFetchApp = lib.UrlFetchApp; 111 | 112 | // setup env 113 | UrlFetchApp.fetch.reset(); 114 | lib.testCurrentDate = '2015-08-30T12:36:00Z'; 115 | 116 | var params = { 117 | accessKey: 'AKIDEXAMPLE', 118 | secretKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', 119 | service: 'iam', 120 | query: { 121 | Action: 'ListUsers', 122 | Version: '2010-05-08' 123 | }, 124 | headers: { 125 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' 126 | } 127 | }; 128 | 129 | var result = request(params); 130 | t.equal(result, 'ok'); 131 | 132 | t.ok(UrlFetchApp.fetch.calledOnce, 'should called once'); 133 | 134 | t.equal(UrlFetchApp.fetch.firstCall.args[0], 135 | 'https://iam.amazonaws.com/?Action=ListUsers&Version=2010-05-08', 136 | 'should request uri'); 137 | 138 | t.deepEqual(UrlFetchApp.fetch.firstCall.args[1], { 139 | headers: { 140 | 'Authorization': 'AWS4-HMAC-SHA256 ' + 141 | 'Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, ' + 142 | 'SignedHeaders=content-type;host;x-amz-date, ' + 143 | 'Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7', 144 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 145 | 'X-Amz-Date': '20150830T123600Z' 146 | }, 147 | method: 'GET', 148 | muteHttpExceptions: true, 149 | payload: '' 150 | }, 'should request with params'); 151 | 152 | t.end(); 153 | }); 154 | 155 | test('request #2', function (t) { 156 | var request = lib.request; 157 | var UrlFetchApp = lib.UrlFetchApp; 158 | 159 | // setup env 160 | UrlFetchApp.fetch.reset(); 161 | lib.testCurrentDate = '2017-04-12T17:57:24Z'; // 162 | 163 | var name = 'arn:aws:lambda:eu-west-1:913984845600:function:vensi_warehouse'; 164 | var event = { 165 | type: 'productSlots/append', 166 | payload: { 167 | code: '1555', 168 | slots: ['M1010', 'M1011'] 169 | } 170 | }; 171 | 172 | var params = { 173 | accessKey: 'AKIDEXAMPLE', 174 | secretKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY', 175 | service: 'lambda', 176 | region: 'eu-west-1', 177 | path: '/2015-03-31/functions/' + encodeURI(name) + '/invocations', 178 | method: 'POST', 179 | headers: { 180 | 'Content-Type': 'application/json; charset=utf-8' 181 | }, 182 | payload: event 183 | }; 184 | 185 | var result = request(params); 186 | t.equal(result, 'ok'); 187 | 188 | t.ok(UrlFetchApp.fetch.calledOnce, 'should called once'); 189 | 190 | t.equal(UrlFetchApp.fetch.firstCall.args[0], 191 | 'https://lambda.eu-west-1.amazonaws.com/2015-03-31/functions' + 192 | '/arn:aws:lambda:eu-west-1:913984845600:function:vensi_warehouse/invocations', 193 | 'should request uri'); 194 | 195 | t.deepEqual(UrlFetchApp.fetch.firstCall.args[1], { 196 | headers: { 197 | 'Authorization': 'AWS4-HMAC-SHA256 ' + 198 | 'Credential=AKIDEXAMPLE/20170412/eu-west-1/lambda/aws4_request, ' + 199 | 'SignedHeaders=content-type;host;x-amz-date, ' + 200 | 'Signature=54923843a90f47be0fd1331ba8c814bfb7fa5f082927395955cb8e29549c574a', 201 | 'Content-Type': 'application/json; charset=utf-8', 'X-Amz-Date': '20170412T175724Z' 202 | }, 203 | method: 'POST', 204 | muteHttpExceptions: true, 205 | payload: '{"type":"productSlots/append","payload":{"code":"1555","slots":["M1010","M1011"]}}' 206 | }, 'should request with params'); 207 | 208 | t.end(); 209 | }); 210 | -------------------------------------------------------------------------------- /test/test-helper.js: -------------------------------------------------------------------------------- 1 | /* global require, exports, CryptoJs_, DATE_VALUES_REGEX_, HASH_ALGORITHM_, calculateSignature_, 2 | getCanonicalFullDate_, getCanonicalShortDate_, getCanonicalQueryString_, 3 | getCanonicalRequestHash_, getCurrentDate_, request */ 4 | 5 | var sinon = require('sinon'); 6 | 7 | var UrlFetchApp = { 8 | fetch: sinon.spy(function () { return 'ok'; }) 9 | }; 10 | 11 | /* eslint no-global-assign: 0 */ 12 | getCurrentDate_ = function () { 13 | return new Date(exports.testCurrentDate); // 20170412T165045Z 14 | }; 15 | 16 | exports.CryptoJs_ = CryptoJs_; 17 | exports.DATE_VALUES_REGEX_ = DATE_VALUES_REGEX_; 18 | exports.HASH_ALGORITHM_ = HASH_ALGORITHM_; 19 | exports.calculateSignature_ = calculateSignature_; 20 | exports.getCanonicalFullDate_ = getCanonicalFullDate_; 21 | exports.getCanonicalShortDate_ = getCanonicalShortDate_; 22 | exports.calculateSignature_ = calculateSignature_; 23 | exports.getCanonicalQueryString_ = getCanonicalQueryString_; 24 | exports.getCanonicalRequestHash_ = getCanonicalRequestHash_; 25 | exports.getCurrentDate_ = getCurrentDate_; 26 | exports.request = request; 27 | exports.UrlFetchApp = UrlFetchApp; 28 | 29 | 30 | -------------------------------------------------------------------------------- /vendor/crypto.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | var CryptoJs_ = (function () { 4 | var window = {}; 5 | var Crypto = undefined; 6 | /** 7 | * Crypto-JS v2.5.3 8 | * http://code.google.com/p/crypto-js/ 9 | * (c) 2009-2012 by Jeff Mott. All rights reserved. 10 | * http://code.google.com/p/crypto-js/wiki/License 11 | */ 12 | // start sha256/CryptoJS 13 | (typeof Crypto=="undefined"||!Crypto.util)&&function(){var d=window.Crypto={},k=d.util={rotl:function(b,a){return b<>>32-a},rotr:function(b,a){return b<<32-a|b>>>a},endian:function(b){if(b.constructor==Number)return k.rotl(b,8)&16711935|k.rotl(b,24)&4278255360;for(var a=0;a0;b--)a.push(Math.floor(Math.random()*256));return a},bytesToWords:function(b){for(var a=[],c=0,e=0;c>>5]|=(b[c]&255)<< 14 | 24-e%32;return a},wordsToBytes:function(b){for(var a=[],c=0;c>>5]>>>24-c%32&255);return a},bytesToHex:function(b){for(var a=[],c=0;c>>4).toString(16)),a.push((b[c]&15).toString(16));return a.join("")},hexToBytes:function(b){for(var a=[],c=0;c>>6*(3-p)&63)):a.push("=");return a.join("")},base64ToBytes:function(b){if(typeof atob=="function")return g.stringToBytes(atob(b));for(var b=b.replace(/[^A-Z0-9+\/]/ig,""),a=[],c=0,e=0;c>> 16 | 6-e*2);return a}},d=d.charenc={};d.UTF8={stringToBytes:function(b){return g.stringToBytes(unescape(encodeURIComponent(b)))},bytesToString:function(b){return decodeURIComponent(escape(g.bytesToString(b)))}};var g=d.Binary={stringToBytes:function(b){for(var a=[],c=0;c>5]|=128<<24-f%32;e[(f+64>>9<<4)+15]=f;for(t=0;t>>7)^(l<<14|l>>>18)^l>>>3)+(d[h-7]>>>0)+((j<<15|j>>>17)^(j<<13|j>>>19)^j>>>10)+(d[h-16]>>>0));j=f&g^f&m^g&m;var u=(f<<30|f>>>2)^(f<<19|f>>>13)^(f<<10|f>>>22);l=(s>>>0)+((i<<26|i>>>6)^(i<<21|i>>>11)^(i<<7|i>>>25))+ 21 | (i&n^~i&o)+c[h]+(d[h]>>>0);j=u+j;s=o;o=n;n=i;i=r+l>>>0;r=m;m=g;g=f;f=l+j>>>0}a[0]+=f;a[1]+=g;a[2]+=m;a[3]+=r;a[4]+=i;a[5]+=n;a[6]+=o;a[7]+=s}return a};e._blocksize=16;e._digestsize=32})(); 22 | (function(){var d=Crypto,k=d.util,g=d.charenc,b=g.UTF8,a=g.Binary;d.HMAC=function(c,e,d,g){e.constructor==String&&(e=b.stringToBytes(e));d.constructor==String&&(d=b.stringToBytes(d));d.length>c._blocksize*4&&(d=c(d,{asBytes:!0}));for(var f=d.slice(0),d=d.slice(0),q=0;q