Skip to content
Browse files

switching over to the AWS version 4 signature. closes #24

  • Loading branch information...
1 parent 66aab60 commit f0c59049e1cd77247e3507810fe7d52fb830a594 Ryan Fitzgerald committed Jul 20, 2012
View
25 README.md
@@ -38,6 +38,31 @@ If you would prefer to manage your own client, potentially with different auth p
accessKeyId: "AWSAccessKey", secretAccessKey: "SecretAccessKey"
});
```
+
+### Region support
+dynode supports all available DynamoDB regions. By default dynode will connect to the us-east-1 region.
+To connect to a different region pass in the region option when creating a client, for example:
+
+``` js
+ // connect to the US West (Northern California) Region
+ var client = new (dynode.Client)({
+ region: "us-west-1"
+ accessKeyId: "AWSAccessKey",
+ secretAccessKey: "SecretAccessKey"
+ });
+```
+
+The default client can also connect to any region
+
+``` js
+ // connect to the Asia Pacific (Tokyo) Region
+ dynode.auth({
+ region: "ap-northeast-1"
+ accessKeyId: "AWSAccessKey",
+ secretAccessKey: "SecretAccessKey"
+ });
+```
+
## Callback Signature
Callbacks return (error, [results], meta) where results are the returned data and meta is the extra information returned by DynamoDB
View
101 lib/dynode/aws-signer.js
@@ -0,0 +1,101 @@
+// Generates AWS V4 signature
+// see http://docs.amazonwebservices.com/general/latest/gr/signature-version-4.html
+var crypto = require('crypto'),
+ util = require('utile');
+
+var Signer = module.exports;
+
+Signer.algorithm = "AWS4-HMAC-SHA256";
+
+Signer.signature = function(credentials, request, date, region) {
+ var secret = credentials.secretAccessKey;
+ var signedDate = hmac("AWS4" + secret, today(date));
+ var signedRegion = hmac(signedDate, region);
+ var signedService = hmac(signedRegion, "dynamodb");
+ var signedCredentials = hmac(signedService, 'aws4_request');
+
+ return hmac(signedCredentials, Signer.stringToSign(request, date, region) );
+};
+
+Signer.authorization = function(credentials, request, date, region) {
+ return [
+ Signer.algorithm + " Credential=" + credentials.accessKeyId + "/" + Signer._credentialScope(date, region),
+ "SignedHeaders=" + Signer._signedHeaders(request.headers),
+ "Signature=" + hex(Signer.signature(credentials, request, date, region))
+ ].join(', ');
+};
+
+Signer.canonicalRequest = function(request) {
+ return [
+ request.method.toUpperCase(),
+ request.uri,
+ request.query,
+ Signer._canonicalHeaders(request.headers) + "\n",
+ Signer._signedHeaders(request.headers),
+ Signer._digest(request.body || '')
+ ].join("\n");
+};
+
+Signer.stringToSign = function(request, date, region) {
+ return [
+ Signer.algorithm,
+ Signer._requestDate(date),
+ Signer._credentialScope(date, region),
+ Signer._digest(Signer.canonicalRequest(request) )
+ ].join("\n");
+};
+
+Signer._canonicalHeaders = function(headers) {
+ var toSign = headersToSign(headers);
+
+ return toSign.map(function(key) {
+ return util.format("%s:%s", key.trim().toLowerCase(), headers[key].trim());
+ }).sort().join('\n');
+};
+
+Signer._signedHeaders = function(headers) {
+ return headersToSign(headers).map(toLower).sort().join(";");
+};
+
+Signer._digest = function(str) {
+ return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
+};
+
+Signer._requestDate = function(date) {
+ return date.getUTCFullYear() +
+ pad(date.getUTCMonth()+1) +
+ pad(date.getUTCDate())+'T' +
+ pad(date.getUTCHours()) +
+ pad(date.getUTCMinutes()) +
+ pad(date.getUTCSeconds())+'Z';
+};
+
+Signer._credentialScope = function(date, region) {
+ return [today(date), region, "dynamodb/aws4_request"].join("/");
+};
+
+function hex(str) {
+ return new Buffer(str, "binary").toString("hex");
+}
+
+function today(date) {
+ return date.getUTCFullYear() + pad(date.getUTCMonth()+1) + pad(date.getUTCDate());
+}
+
+function hmac(key, value) {
+ return crypto.createHmac('sha256', key).update(value).digest("binary");
+}
+
+function pad(n) {
+ return n<10 ? '0'+ n : n;
+}
+
+var notAuthorization = function(str){return str != 'authorization';};
+
+function headersToSign(headers) {
+ return Object.keys(headers).filter(notAuthorization);
+}
+
+function toLower(str) {
+ return str.trim().toLowerCase();
+}
View
30 lib/dynode/client.js
@@ -149,7 +149,7 @@ Client.prototype.scan = function(tableName, options, cb) {
var items = {};
if(resp.Items) {
- var items = _.map(resp.Items, Types.parse);
+ items = _.map(resp.Items, Types.parse);
delete resp.Items;
}
@@ -174,10 +174,10 @@ Client.prototype.batchGetItem = function(options, cb) {
var meta = {UnprocessedKeys: resp.UnprocessedKeys, ConsumedCapacityUnits: {}};
for (var table in resp.Responses) {
meta.ConsumedCapacityUnits[table] = resp.Responses[table].ConsumedCapacityUnits;
- responses[table] = resp.Responses[table].Items.map(Types.parse)
+ responses[table] = resp.Responses[table].Items.map(Types.parse);
}
return cb(null, responses, meta);
- });
+ });
};
Client.prototype.batchWriteItem = function(options, cb) {
@@ -235,16 +235,18 @@ Client.prototype.truncate = function(tableName, options, cb) {
if(throughput < chunkSize) chunkSize = throughput;
+ var toDeleteStatment = function(item){
+ var key = {hash: item[hashKey]};
+ if(rangeKey) key.range = item[rangeKey];
+
+ return {del : key};
+ };
+
for (i=0,j=items.length; i<j; i+=chunkSize) {
var chunks = items.slice(i,i+chunkSize);
var writes = {};
- writes[tableName] = chunks.map(function(item){
- var key = {hash: item[hashKey]};
- if(rangeKey) key.range = item[rangeKey];
-
- return {del : key};
- });
+ writes[tableName] = chunks.map(toDeleteStatment);
batchWrites.push(async.apply(self.batchWriteItem.bind(self), writes));
}
@@ -259,7 +261,7 @@ Client.prototype._request = function(action, options, cb) {
options = this._prefixTableName(action, options);
- var operation = retry.operation({retries: 10, factor: 2, minTimeout: 50});
+ var operation = retry.operation({retries: 10, factor: 2, minTimeout: 50, randomize: true});
operation.attempt(function(currentAttempt) {
self.request.send(action, options, function(err, resp) {
@@ -275,7 +277,7 @@ Client.prototype._prefixTableName = function(action, options) {
if(!this.config.tableNamePrefix) return options;
- if(options.TableName) {
+ if(options.TableName) {
options.TableName = self.config.tableNamePrefix + options.TableName;
} else if(action === 'BatchGetItem' || action === 'BatchWriteItem') {
var items = _.reduce(options.RequestItems, function(memo, attrs, table) {
@@ -287,7 +289,7 @@ Client.prototype._prefixTableName = function(action, options) {
options.RequestItems = items;
} else if(action === 'ListTables' && options.ExclusiveStartTableName) {
options.ExclusiveStartTableName = self.config.tableNamePrefix + options.ExclusiveStartTableName;
- };
+ }
return options;
};
@@ -342,7 +344,7 @@ Client.prototype._parseKeySchema = function(options) {
var rangeKeyName = Object.keys(options.range)[0];
var rangeKeyType = options.range[rangeKeyName].name[0].toUpperCase();
keySchema["RangeKeyElement"] = {"AttributeName":rangeKeyName,"AttributeType":rangeKeyType};
- };
+ }
return keySchema;
-}
+};
View
39 lib/dynode/request-signer.js
@@ -1,39 +0,0 @@
-var crypto = require('crypto'),
- util = require('utile');
-
-var Signer = exports;
-
-var isAmzHeader = function(str){return str.match(/x-amz-|host/)};
-
-Signer.headersToSign = function(headers) {
- return Object.keys(headers).filter(isAmzHeader);
-};
-
-Signer.sign = function(headers, body, credentials) {
- var headersToSign = Signer.headersToSign(headers);
-
- var canonicalHeaders = headersToSign.map(function(key) {
- return util.format("%s:%s\n", key.trim().toLowerCase(), headers[key].trim());
- }).sort().join('');
-
- var toSign = {
- method : "POST",
- uri : "/",
- query : "",
- headers : canonicalHeaders,
- body : JSON.stringify(body)
- };
-
- var strToSign = [toSign.method, toSign.uri, toSign.query, toSign.headers, toSign.body].join("\n");
-
- var signature = Signer.generateSignature(strToSign, credentials.secretAccessKey);
-
- var authorization = "AWS3 AWSAccessKeyId="+credentials.accessKeyId+",Algorithm=HmacSHA256,SignedHeaders="+headersToSign.join(';')+",Signature="+signature;
-
- return util.mixin({}, headers, {"x-amzn-authorization": authorization});
-};
-
-Signer.generateSignature = function(strToSign, key) {
- var digest = crypto.createHash('sha256').update(strToSign, 'utf8').digest('binary');
- return crypto.createHmac('sha256', key).update(new Buffer(digest, 'binary')).digest("base64");
-};
View
92 lib/dynode/request.js
@@ -1,62 +1,70 @@
var http = require("http"),
_ = require("underscore"),
- signer = require('./request-signer'),
+ Signer = require('./aws-signer'),
AmazonError = require('./amazon-error'),
- URL = require('url'),
- STS = require('./sts').STS;
+ URL = require('url');
var Request = exports.Request = function Request(config) {
- this.sts = new STS(config);
+ this.credentials = {accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey};
this.config = _.defaults(config, {
prefix : "DynamoDB_20111205.",
- host : "dynamodb.us-east-1.amazonaws.com"
+ region: "us-east-1"
});
+ this.config.host = "dynamodb." + this.config.region + ".amazonaws.com";
+
if(this.config.https) {
http = require("https");
}
};
Request.prototype.send = function(action, messageBody, cb) {
- var self = this;
-
- this.sts.getSessionToken(function(err, credentials) {
- if(err) return cb(err);
-
- var headers = {
- "host" : self.config.host,
- "x-amz-date" : new Date().toGMTString(),
- "x-amz-security-token" : credentials.sessionToken,
- "x-amz-target" : self.config.prefix + action,
- "content-type" : "application/x-amz-json-1.0"
- };
-
- var opts = {
- method : "POST",
- headers : signer.sign(headers, messageBody, credentials),
- host: self.config.host
- };
-
- var request = http.request(opts, function(res) {
- var data = "";
-
- res.on("data", function(chunk){ data += chunk });
- res.on("end", function() {
- var response = JSON.parse(data);
-
- if (res.statusCode != 200) {
- return cb(new AmazonError({type : response["__type"], message: response["message"], statusCode: res.statusCode, action: action}));
- }
-
- return cb(null, response);
- });
- });
+ var self = this,
+ date = new Date();
+
+ var headers = {
+ "host" : self.config.host,
+ "x-amz-date" : Signer._requestDate(date),
+ "x-amz-target" : self.config.prefix + action,
+ "content-type" : "application/x-amz-json-1.0"
+ };
+
+ var req = {
+ method : "POST",
+ uri : "/",
+ query : "",
+ headers : headers,
+ body : JSON.stringify(messageBody)
+ };
+
+ headers.authorization = Signer.authorization(self.credentials, req, date, self.config.region);
- request.on("error", cb);
+ var opts = {
+ method : req.method,
+ path : req.uri,
+ headers : headers,
+ host: self.config.host
+ };
- request.write(JSON.stringify(messageBody));
- request.end();
+ var request = http.request(opts, function(res) {
+ var data = "";
+
+ res.on("data", function(chunk){ data += chunk; });
+ res.on("end", function() {
+ var response = JSON.parse(data);
+
+ if (res.statusCode != 200) {
+ return cb(new AmazonError({type : response["__type"], message: response["message"], statusCode: res.statusCode, action: action}));
+ }
+
+ return cb(null, response);
+ });
});
-}
+ request.on("error", cb);
+
+ request.write(req.body);
+ request.end();
+
+};
View
98 lib/dynode/sts.js
@@ -1,98 +0,0 @@
-var https = require("https"),
- querystring = require('querystring'),
- crypto = require('crypto'),
- URL = require('url'),
- querystring = require('querystring'),
- AmazonError = require('./amazon-error'),
- xml2js = require('xml2js');
-
-var defaults = {
- host : "sts.amazonaws.com",
- version : "2011-06-15",
- duration : 43200, // 12 hours
- algorithm : "HmacSHA256",
- signatureVersion : 2
-};
-
-var STS = exports.STS = function STS(config) {
- if ( !config.accessKeyId || ! config.secretAccessKey) {
- throw new Error('You must set the AWS credentials: accessKeyId + secretAccessKey');
- }
-
- this.config = config;
- this.parser = new xml2js.Parser();
-
- var credentials = null;
-
- Object.defineProperty(this, "credentials", {
-
- get : function () {
- if(credentials && credentials.expiration > new Date()) {
- return credentials;
- } else {
- return null;
- }
- },
-
- set : function(resp) {
- credentials = {
- sessionToken : resp.SessionToken,
- secretAccessKey : resp.SecretAccessKey,
- expiration : new Date(resp.Expiration),
- accessKeyId : resp.AccessKeyId
- };
- }
- });
-
-};
-
-STS.prototype.getSessionToken = function(cb) {
- if(this.credentials) return cb(null, this.credentials);
-
- var self = this;
-
- var opts = {
- host : defaults.host,
- path : "/?" + querystring.stringify(this._buildParams())
- };
-
- var request = https.get(opts, function(res) {
- var data = "";
-
- res.on("data", function(chunk){ data += chunk });
- res.on("end", function() {
- self.parser.parseString(data, function (err, result) {
- if(err) return cb(err);
-
- if (res.statusCode != 200) {
- return cb(new AmazonError({type : result.Error.Code, message: result.Error.Message, statusCode: res.statusCode}));
- }
-
- self.credentials = result.GetSessionTokenResult.Credentials;
- return cb(null, self.credentials);
- });
- });
- });
-
- request.on("error", cb);
-};
-
-STS.prototype._buildParams = function() {
- var params = {
- AWSAccessKeyId : this.config.accessKeyId,
- Action : "GetSessionToken",
- DurationSeconds : defaults.duration,
- SignatureMethod : defaults.algorithm,
- SignatureVersion : defaults.signatureVersion,
- Timestamp : (new Date).toISOString(),
- Version : defaults.version
- };
-
- var toSign = ['GET', defaults.host, "/", querystring.stringify(params)].join("\n");
-
- var signature = crypto.createHmac('sha256', this.config.secretAccessKey).update(toSign).digest("base64");
-
- params.Signature = signature;
-
- return params;
-};
View
7 package.json
@@ -15,14 +15,13 @@
],
"keywords": ["dynamoDB", "database", "aws", "nosql", "amazon"],
"dependencies": {
- "utile": "0.0.10",
+ "utile": "0.1.x",
"underscore": "1.3.x",
- "xml2js": "0.1.x",
"retry" : "0.6.x"
},
"devDependencies": {
- "mocha" : "1.0.x",
- "chai": "0.5.x"
+ "mocha" : "1.3.x",
+ "chai": "1.1.x"
},
"scripts": {
"test" : "make test"
View
1 test/integration/dynode-test.js
@@ -14,6 +14,7 @@ describe('Dynode Integration Tests', function() {
it('should list all tables', function(done) {
dynode.listTables({}, function(err, tables) {
+ should.not.exist(err);
tables.should.have.property("TableNames");
done();
});
View
54 test/integration/sts-test.js
@@ -1,54 +0,0 @@
-var STS = require("../../lib/dynode/sts").STS,
- should = require('chai').should();
-
-describe('STS Client Integration Tests', function() {
- var sts;
-
- describe("with valid access keys", function() {
-
- it('should get session token', function(done) {
- sts = new STS({accessKeyId : process.env.AWS_ACCEESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY});
-
- sts.getSessionToken(function(err, credentials) {
-
- should.exist(credentials.sessionToken);
- should.exist(credentials.secretAccessKey);
- should.exist(credentials.accessKeyId);
- should.exist(credentials.expiration);
- credentials.expiration.should.be.an.instanceof(Date);
-
- done();
- });
- });
-
- });
-
- describe("with invalid access keys", function() {
-
- it('should return Invalid Token Error', function(done) {
- sts = new STS({accessKeyId : "asdfasdfasdf", secretAccessKey: "asdf"});
-
- sts.getSessionToken(function(err, credentials) {
- err.type.should.equal("InvalidClientTokenId");
- should.not.exist(credentials);
-
- done();
- });
-
- });
-
- it('should throw exception when access key isnt given', function() {
- var func = function(){ new STS({secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY});};
-
- should.throw(func);
- });
-
- it('should throw exception when secret access key isnt given', function() {
- var func = function(){ new STS({accessKeyId: process.env.AWS_ACCEESS_KEY_ID});};
-
- should.throw(func);
- });
-
- });
-
-});
View
102 test/unit/aws-signer-test.js
@@ -0,0 +1,102 @@
+var Signer = require("../../lib/dynode/aws-signer"),
+ util = require('utile'),
+ should = require('chai').should();
+
+describe("AWS version 4 signer", function() {
+ // these are the test credentials provided by the amazon test suite
+ // see http://docs.amazonwebservices.com/general/latest/gr/signature-v4-test-suite.html#signature-v4-test-suite-derived-creds
+ var credentials = {accessKeyId: "fakeKeyId", secretAccessKey: "secret"},
+ region = "us-east-1",
+ body = {
+ "TableName":"my-table",
+ "Keys":[{"HashKeyElement":{"S":"Bill & Ted's Excellent Adventure"},"RangeKeyElement":{"S":1989}}]
+ };
+
+ var headers = {
+ "host": "dynamodb.us-east-1.amazonaws.com",
+ "x-amz-date": "Mon, 16 Jan 2012 17:50:52 GMT",
+ "x-amz-target": "DynamoDB_20111205.GetItem",
+ "content-type" : "application/x-amz-json-1.0"
+ };
+
+ var request = {
+ method : "POST",
+ uri : "/",
+ query : "",
+ headers : headers,
+ body : JSON.stringify(body)
+ };
+
+ it("should generate canonical request", function() {
+ var expected = [
+ "POST",
+ "/",
+ "",
+ "content-type:application/x-amz-json-1.0",
+ "host:dynamodb.us-east-1.amazonaws.com",
+ "x-amz-date:Mon, 16 Jan 2012 17:50:52 GMT",
+ "x-amz-target:DynamoDB_20111205.GetItem" + "\n",
+ "content-type;host;x-amz-date;x-amz-target",
+ "2e7f349e500e10dd9f3b194656850082459dd7f8f2c0025c2306246b3a1b3edf"].join("\n");
+
+ Signer.canonicalRequest(request).should.eql(expected);
+ });
+
+ it("should generate canonical headers", function() {
+ var expected = [
+ "content-type:application/x-amz-json-1.0",
+ "host:dynamodb.us-east-1.amazonaws.com",
+ "x-amz-date:Mon, 16 Jan 2012 17:50:52 GMT",
+ "x-amz-target:DynamoDB_20111205.GetItem"
+ ].join("\n");
+
+ Signer._canonicalHeaders(request.headers).should.eql(expected);
+ });
+
+ it("should generate signed headers", function() {
+ var expected = ["content-type","host","x-amz-date","x-amz-target"].join(";");
+
+ Signer._signedHeaders(request.headers).should.eql(expected);
+ });
+
+ it("should digest the body", function() {
+ Signer._digest("Action=ListGroupsForUser&UserName=Test&Version=2010-05-08")
+ .should.eql("14a1b0cf5748461c63d3a5fee5e42ed623422b7b4fa62a58a57258f1a195cff8");
+ });
+
+ it("should generate request date in proper format", function() {
+ var d = new Date("2012-02-28T02:22:10.000Z");
+ Signer._requestDate(d).should.eql("20120228T022210Z");
+ });
+
+ it("should generate credential scope", function() {
+ var d = new Date("2012-02-28T02:22:10.000Z");
+ Signer._credentialScope(d, "us-east-1").should.eql("20120228/us-east-1/dynamodb/aws4_request");
+ });
+
+ it("should generate string to sign", function(){
+ var expected = [
+ "AWS4-HMAC-SHA256",
+ "20120228T022210Z",
+ "20120228/us-east-1/dynamodb/aws4_request",
+ "e59538c538586c305a276c1be9428832888d8c27cb277513563a82c7d01db86b"].join("\n");
+
+ var d = new Date("2012-02-28T02:22:10.000Z");
+ Signer.stringToSign(request, d, region).should.eql(expected);
+ });
+
+ it("should generate full signature", function() {
+ var d = new Date("2012-02-28T02:22:10.000Z");
+ var signature = Signer.signature(credentials, request, d, region);
+
+ new Buffer(signature, "binary").toString("hex").should.eql("45ff518b58d5a6f5efeea7711341ebace5b23dcfa2d27bd13fdcdaf84973959b");
+ });
+
+ it("should authorization string", function() {
+ var d = new Date("2012-02-28T02:22:10.000Z");
+ var auth = Signer.authorization(credentials, request, d, region);
+ console.log(auth);
+ // signature.should.eql("d211c0edd7705306e8840240f38e5dc120a178ed1d6840f51e990ffe28f6124b");
+ });
+
+});

0 comments on commit f0c5904

Please sign in to comment.
Something went wrong with that request. Please try again.