@incdevco/framework
Version:
node.js lambda framework
976 lines (577 loc) • 22.5 kB
JavaScript
/* global angular AWS CryptoJS */
angular.module('aws', [])
.constant('AWS_SHA_256', 'AWS4-HMAC-SHA256')
.constant('AWS4', 'AWS4')
.constant('AWS4_REQUEST', 'aws4_request')
.factory('AWSHelper', [
function () {
return {
getCognitoIdentityCredentials: function (config) {
return new AWS.CognitoIdentityCredentials(config);
},
getCognitoSyncManager: function () {
return new AWS.CognitoSyncManager();
}
};
}
])
.factory('ApiGatewayRequestInterceptor', [
'AwsSignature',
'$injector',
'$log',
function (signature, $injector, $log) {
function Interceptor(config, authenticate) {
var $rootScope, self = this;
if (!config.apiKey) {
throw new Error('No apiKey Provided');
}
this.apiKey = config.apiKey;
this._authenticate = authenticate;
this.credentials = null;
this.endpoint = config.endpoint;
this.maxAttempts = config.maxAttempts || 2;
this.region = config.region;
$rootScope = $injector.get('$rootScope');
$rootScope.$on('aws.credentials', function (event, credentials) {
self.setCredentials(credentials);
$log.debug('Interceptor', 'aws.credentials', self.credentials);
});
$rootScope.$on('deauthenticated', function () {
self.setCredentials(null);
$log.debug('Interceptor', 'deauthenticated', self.credentials);
});
}
Interceptor.prototype.authenticate = function () {
return this._authenticate();
};
Interceptor.prototype.generate = function () {
var self = this;
return {
request: function (config) {
$log.debug('ApiGatewayRequestInterceptor');
if (self.match(config)) {
return self.sign(config);
} else {
return config;
}
},
responseError: function (response) {
$log.debug('ApiGatewayRequestInterceptor', 'responseError', response);
if (response.config
&& self.match(response.config)
&& self.shouldRetry(response)) {
$log.debug('ApiGatewayRequestInterceptor', 'handling', response);
return self.authenticate()
.then(function () {
self.prepareForRetry(response.config);
return $injector.get('$http')(response.config);
});
} else {
$log.debug('ApiGatewayRequestInterceptor', 'not handling', response);
return $injector.get('$q').reject(response);
}
}
};
};
Interceptor.prototype.match = function (config) {
//console.log('ApiGatewayRequestInterceptor', config.url);
//console.log('ApiGatewayRequestInterceptor', this.endpoint);
//console.log('ApiGatewayRequestInterceptor', (config.url.indexOf(this.endpoint) === 0));
return (config.url.indexOf(this.endpoint) === 0);
};
Interceptor.prototype.prepareForRetry = function (config) {
delete config.headers['Authorization'];
delete config.headers['x-amz-security-token'];
};
Interceptor.prototype.setCredentials = function (credentials) {
this.credentials = credentials;
};
Interceptor.prototype.sign = function (config) {
var credentials = this.credentials;
var self = this;
var params = {
apiKey: this.apiKey,
endpoint: this.endpoint,
region: this.region,
serviceName: 'execute-api'
};
$log.debug('sign', credentials);
if (credentials && credentials.secretKey) {
return signature.sign(config, credentials, params);
} else if (config.auth) {
return this.authenticate()
.then(function () {
return signature.sign(config, self.credentials, params);
});
} else {
return config;
}
};
Interceptor.prototype.shouldRetry = function (response) {
$log.debug('shouldRetry', response);
if (response.status === 403
|| response.status < 0) {
response.config.attempts =
parseInt(response.config.attempts) || 0;
response.config.attempts++;
if (response.config.attempts < this.maxAttempts) {
return true;
}
}
return false;
};
return Interceptor;
}
])
.factory('ApiGatewayRequestSignatureInterceptor', [
'AwsSignature',
'$injector',
'$log',
function (signature, $injector, $log) {
function Interceptor(config) {
if (!config.apiKey) {
throw new Error('No apiKey Provided');
}
this.apiKey = config.apiKey;
this.credentials = null;
this.endpoint = config.endpoint;
this.maxAttempts = config.maxAttempts || 2;
this.region = config.region;
}
Interceptor.prototype.clearCredentials = function () {
this.credentials = null;
};
Interceptor.prototype.generate = function () {
var self = this;
return {
request: function (config) {
$log.debug('ApiGatewayRequestInterceptor');
if (self.match(config)
&& self.shouldSign(config)) {
return self.sign(config);
} else {
return config;
}
}
};
};
Interceptor.prototype.match = function (config) {
//console.log('ApiGatewayRequestInterceptor', config.url);
//console.log('ApiGatewayRequestInterceptor', this.endpoint);
//console.log('ApiGatewayRequestInterceptor', (config.url.indexOf(this.endpoint) === 0));
return (config.url.indexOf(this.endpoint) === 0);
};
Interceptor.prototype.setCredentials = function (credentials) {
if (!credentials.accessKey) {
throw new Error('accessKey is required to setCredentials');
}
if (!credentials.secretKey) {
throw new Error('secretKey is required to setCredentials');
}
if (!credentials.sessionToken) {
throw new Error('sessionToken is required to setCredentials');
}
this.credentials = credentials;
};
Interceptor.prototype.shouldSign = function (config) {
if (config.auth || config.sign) {
return true;
}
return false;
};
Interceptor.prototype.sign = function (config) {
var params = {
apiKey: this.apiKey,
endpoint: this.endpoint,
region: this.region,
serviceName: 'execute-api'
};
$log.debug('sign', this.credentials);
if (this.credentials) {
return signature.sign(config, this.credentials, params);
} else {
var exception = new Error('no credentials to sign with');
exception.code = 'NoCredentials';
exception.requestConfig = config;
throw exception;
}
};
return Interceptor;
}
])
.factory('ApiGatewaySendRequestGenerator', [
'AwsSignature',
'$injector',
'$log',
function (signature, $injector, $log) {
var defaultHeaders = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
return function (config) {
var apiEndpoint = config.apiEndpoint;
var apiKey = config.apiKey;
var apiRegion = config.apiRegion;
var headers = config.headers;
return function (input, credentials) {
var request = {
auth: input.auth || false,
headers: angular.copy(defaultHeaders),
method: input.method || 'GET',
sign: (input.sign !== undefined) ?
input.sign : true,
url: apiEndpoint + input.url
};
angular.extend(request.headers, headers, input.headers);
if (input.params) {
request.params = angular.copy(input.params);
} else {
request.params = {};
}
if (request.method === 'GET') {
request.data = '';
} else {
if (input.data) {
if (typeof input.data !== 'string') {
input.data = angular.toJson(input.data);
}
request.data = input.data;
}
}
if (credentials) {
signature.sign(request, credentials, {
apiKey: apiKey,
endpoint: apiEndpoint,
region: apiRegion,
serviceName: 'execute-api'
});
}
return $injector.get('$http')(request);
};
};
}
])
.factory('AwsSignature', [
'AWS_SHA_256',
'AWS4',
'AWS4_REQUEST',
'$log',
function (AWS_SHA_256,
AWS4,
AWS4_REQUEST,
$log) {
var parser = document.createElement('a');
return {
buildAuthorizationHeader: function (accessKey, credentialScope, headers, signature) {
if (!accessKey) {
throw new Error('No accessKey Provided');
}
return AWS_SHA_256 + ' Credential='
+ accessKey + '/' + credentialScope
+ ', SignedHeaders=' + this.buildCanonicalSignedHeaders(headers)
+ ', Signature=' + signature;
},
buildCanonicalHeaders: function (headers) {
var canonical = '', sortedKeys;
sortedKeys = Object.keys(headers);
sortedKeys.sort();
sortedKeys.forEach(function (key) {
canonical += key.toLowerCase()
+ ':' + headers[key] + '\n';
});
return canonical;
},
buildCanonicalQueryString: function (query) {
var canonical = '', sortedQuery;
if (!query || Object.keys(query).length < 1) {
return '';
}
sortedQuery = Object.keys(query);
sortedQuery.sort();
sortedQuery.forEach(function (key, index) {
if (index != 0) {
canonical += '&';
}
canonical += key + '=' + encodeURIComponent(query[key]);
});
return canonical;
},
buildCanonicalRequest: function (request) {
var canonical = request.method + '\n';
canonical += this.buildCanonicalUri(request.url) + '\n';
canonical += this.buildCanonicalQueryString(request.params) + '\n';
canonical += this.buildCanonicalHeaders(request.headers) + '\n';
canonical += this.buildCanonicalSignedHeaders(request.headers) + '\n';
canonical += this.hexEncode(this.hash(request.data));
return canonical;
},
buildCanonicalSignedHeaders: function (headers) {
var canonical = '', keys, sortedHeaders = [];
keys = Object.keys(headers);
keys.forEach(function (key) {
sortedHeaders.push(key.toLowerCase());
});
//$log.debug('sortedHeaders', sortedHeaders);
sortedHeaders.sort();
//$log.debug('sortedHeaders', sortedHeaders);
sortedHeaders.forEach(function (key) {
canonical += key + ';';
});
return canonical.replace(/;$/, '');
},
buildCanonicalUri: function (url) {
return encodeURI(url);
},
buildCredentialScope: function (datetime, region, serviceName) {
return datetime.substr(0, 8)
+ '/' + region
+ '/' + serviceName
+ '/' + AWS4_REQUEST;
},
buildStringToSign: function (datetime, credentialScope, hashedCanonicalRequest) {
return AWS_SHA_256 + '\n'
+ datetime + '\n'
+ credentialScope + '\n'
+ hashedCanonicalRequest;
},
calculateSignature: function (key, stringToSign) {
return this.hexEncode(this.hmac(key, stringToSign));
},
calculateSigningKey: function (datetime, secretKey, region, serviceName) {
var key = this.hmac(AWS4 + secretKey, datetime.substr(0, 8));
key = this.hmac(key, region);
key = this.hmac(key, serviceName);
key = this.hmac(key, AWS4_REQUEST);
return key;
},
createDateTime: function () {
var datetime = new Date().toISOString()
.replace(/\.\d{3}Z$/, 'Z')
.replace(/[:\-]|\.\d{3}/g, '');
return datetime;
},
hash: function (value) {
return CryptoJS.SHA256(value);
},
hashCanonicalRequest: function (request) {
return this.hexEncode(this.hash(request));
},
hexEncode: function (value) {
return value.toString(CryptoJS.enc.Hex);
},
hmac: function (secret, value) {
return CryptoJS.HmacSHA256(value, secret, {
asBytes: true
});
},
sign: function (request, credentials, config) {
var datetime = this.createDateTime();
var originalUrl = request.url;
if (!credentials || !credentials.secretKey) {
return request;
}
parser.href = config.endpoint;
request.url = request.url.replace(config.endpoint, '');
request.headers['Host'] = parser.hostname;
request.headers['x-amz-date'] = datetime;
request.headers['x-api-key'] = config.apiKey;
if (request.method === 'GET') {
delete request.headers['Content-Type'];
}
var canonicalRequest = this.buildCanonicalRequest(request);
//console.warn('canonicalRequest', canonicalRequest);
var hashedCanonicalRequest = this.hashCanonicalRequest(canonicalRequest);
//console.warn('hashedCanonicalRequest', hashedCanonicalRequest);
var credentialScope = this.buildCredentialScope(datetime, config.region, config.serviceName);
//console.warn('credentialScope', credentialScope);
var stringToSign = this.buildStringToSign(datetime, credentialScope, hashedCanonicalRequest);
//console.warn('stringToSign', stringToSign);
var signingKey = this.calculateSigningKey(datetime, credentials.secretKey, config.region, config.serviceName);
//console.warn('signingKey', signingKey);
var signature = this.calculateSignature(signingKey, stringToSign);
//console.warn('signature', signature);
request.headers['Authorization'] = this.buildAuthorizationHeader(credentials.accessKey, credentialScope, request.headers, signature);
//console.warn('request.headers[\'Authorization\']', request.headers['Authorization']);
request.headers['Content-Type'] = 'application/json';
request.headers['x-amz-security-token'] = credentials.sessionToken;
//console.warn('request.headers[\'x-amz-security-token\']', request.headers['x-amz-security-token']);
delete request.headers['Host'];
request.url = originalUrl;
var querystring = this.buildCanonicalQueryString(request.params);
if (querystring != '') {
request.url += '?' + querystring;
}
delete request.params;
return request;
}
};
}
])
.factory('CognitoIdentity', [
'AWSHelper',
'$log',
'$q',
'$rootScope',
function (AWSHelper, $log, $q, $rootScope) {
/**
* {
* IdentityId: IdentityId,
* IdentityPoolId: IdentityPoolId,
* Logins: Logins,
* RoleSessionName: RoleSessionName
* }
*/
var fn = function (config, region) {
var configString = JSON.stringify(config, null, 2);
var deferred = $q.defer();
AWS.config.region = region;
AWS.config.credentials = AWSHelper.getCognitoIdentityCredentials(config);
AWS.config.credentials.get(function (exception) {
if (exception) {
$log.error('AWS.config.credentials.get', exception);
return deferred.reject(exception);
} else {
var credentials = {
accessKey: AWS.config.credentials.accessKeyId,
expireTime: AWS.config.credentials.expireTime,
secretKey: AWS.config.credentials.secretAccessKey,
sessionToken: AWS.config.credentials.sessionToken
};
var identity = {
expireTime: AWS.config.credentials.expireTime,
identityId: AWS.config.credentials.identityId,
identityPoolId: config.IdentityPoolId,
logins: config.logins
};
$log.debug('broadcasting aws.credentials', configString);
//$rootScope.$broadcast('aws.credentials', credentials);
//$rootScope.$broadcast('cognito.identity', identity);
return deferred.resolve({
credentials: credentials,
expireTime: AWS.config.credentials.expireTime,
identity: identity
});
}
});
return deferred.promise;
};
fn.clearCachedId = function () {
AWS.config.credentials.clearCachedId();
};
return fn;
}
])
.factory('CognitoSync', [
'AWSHelper',
'$log',
'$q',
'$rootScope',
function (AWSHelper, $log, $q, $rootScope) {
var manager, ready = $q.defer();
$rootScope.$on('aws.credentials', function () {
console.log('CognitoSync', 'aws.credentials');
manager = AWSHelper.getCognitoSyncManager();
});
function getManager() {
if (!manager) {
manager = AWSHelper.getCognitoSyncManager();
}
return manager;
}
function Dataset(name, dataset) {
this.name = name;
this._dataset = dataset;
}
Dataset.prototype.get = function (key) {
var deferred = $q.defer();
this._dataset.get(key, function (exception, value) {
if (exception) {
$log.error(exception);
deferred.reject(exception);
} else {
deferred.resolve(value);
}
});
return deferred.promise;
};
Dataset.prototype.set = function (key, value) {
var deferred = $q.defer();
this._dataset.put(key, value, function (exception) {
if (exception) {
$log.error(exception);
deferred.reject(exception);
} else {
deferred.resolve(value);
}
});
return deferred.promise;
};
Dataset.prototype.syncResolveLocal = function () {
var deferred = $q.defer();
this._dataset.synchronize({
onSuccess: function (dataset, newRecords) {
deferred.resolve(newRecords);
},
onFailure: function (error) {
$log.error('onFailure', error);
deferred.reject(error);
},
onConflict: function (dataset, conflicts, callback) {
var resolved = [];
conflicts.forEach(function (conflict) {
resolved.push(conflict.resolveWithLocalRecord());
});
dataset.resolve(resolved, function () {
deferred.resolve();
return callback(true);
});
},
onDatasetDeleted: function (dataset, name, callback) {
deferred.reject(new Error('Dataset Deleted'));
return callback(true);
}
});
return deferred.promise;
};
Dataset.prototype.syncResolveRemote = function () {
var deferred = $q.defer();
this._dataset.synchronize({
onSuccess: function (dataset, newRecords) {
deferred.resolve(newRecords);
},
onFailure: function (error) {
$log.error('onFailure', error);
deferred.reject(error);
},
onConflict: function (dataset, conflicts, callback) {
var resolved = [];
conflicts.forEach(function (conflict) {
resolved.push(conflict.resolveWithRemoteRecord());
});
dataset.resolve(resolved, function () {
deferred.resolve();
return callback(true);
});
},
onDatasetDeleted: function (dataset, name, callback) {
deferred.reject(new Error('Dataset Deleted'));
return callback(true);
}
});
return deferred.promise;
};
return function (datasetName) {
var deferred = $q.defer();
console.log('CognitoSync', datasetName);
getManager()
.openOrCreateDataset(datasetName, function (exception, dataset) {
if (exception) {
return deferred.reject(exception);
} else {
return deferred.resolve(new Dataset(datasetName, dataset));
}
});
return deferred.promise;
};
}
]);