Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

merge conflict

  • Loading branch information...
commit 91ef645a0c28d0891ec23d193ac995d6fc1165b7 2 parents 23cb21a + 89520ff
@ckarlof ckarlof authored
View
10 bin/api
@@ -28,7 +28,7 @@ function credentialsFunc(id, callback) {
var credentials = {
id: id,
key: new Buffer(key, 'base64'),
- algorithm: 'hmac-sha-256',
+ algorithm: 'sha256',
user: id
};
@@ -41,7 +41,7 @@ options.auth.getCredentialsFunc = credentialsFunc;
// modify default error format
options.format = {
error: function (result) {
- Hapi.Log.event(['error'], result.message);
+ console.error('Error: ', result);
return {
code: result.code,
payload: { success: false, errorCode: result.code, errorMessage: result.message },
@@ -54,8 +54,10 @@ options.format = {
var bindTo = config.process.api;
var server = new Hapi.Server(bindTo.host, bindTo.port, options);
-// don't require authorization for docs (may change after hapi 0.9.2)
-server._routes['get'][0].config.auth = config.hapi.docs.auth;
+if (config.hapi.docs) {
+ server.addRoute({ method: 'GET', path: '/api/docs', handler: { docs: true }, config: { auth: { mode: 'none' } } });
+}
+
console.log("api starting up");
View
2  bin/router
@@ -35,7 +35,7 @@ app.use(function(req, res, next) {
// redirect requests to the api processes
app.use(function(req, res, next) {
if (/^\/api/.test(req.url)) {
- forward(api_url + (req.url === '/api/docs' ? '/docs' : req.url),
+ forward(api_url + req.url,
req, res,
function(err) {
if (err) {
View
31 client/client.js
@@ -50,6 +50,13 @@ function request(args, cb) {
},
processData: false,
error: function(data, res, status) {
+ if (typeof data === 'string') {
+ try {
+ data = JSON.parse(data);
+ } catch (e) {
+ return cb && cb('Invalid JSON response: ' + e);
+ }
+ }
if (cb) cb({ error: data, status: status });
},
complete: function () {
@@ -132,9 +139,11 @@ GombotClient.prototype = {
self.keys = r;
self.user = args.email;
- args.data = JSON.stringify({email: args.email, pass: GombotCrypto.hexToBase64(r.authKey), newsletter: args.newsletter});
- // send request with authKey as the password
- request(args, cb);
+ self.createEncryptedPayload(args.payload, function (err, ciphertext) {
+ args.data = JSON.stringify({email: args.email, pass: GombotCrypto.hexToBase64(r.authKey), payload: ciphertext, newsletter: args.newsletter});
+ // send request with authKey as the password
+ request(args, cb);
+ });
});
},
// check auth status
@@ -202,16 +211,22 @@ GombotClient.prototype = {
getPayload: function(args, cb) {
args = mergeArgs(args, this);
args.method = 'get';
- args.path = this.path + '/v1/payload';
+ args.path = this.path + '/v1/payload' + '?updated=' + (args.updated || 0);
var keys = this.keys;
var self = this;
authRequest(args, function (err, data) {
if (err) return cb(err);
- self.decryptPayload(data.payload, function (err, plaintext) {
- if (err) return cb(err);
- cb(null, {success: data.success, payload: JSON.parse(plaintext), updated: data.updated, ciphertext: data.payload });
- });
+ if (data.sync) {
+ GombotCrypto.decrypt(keys, data.payload, function (err, plaintext) {
+ if (err) return cb(err);
+ cb(null, {success: data.success, payload: JSON.parse(plaintext), updated: data.updated, ciphertext: data.payload });
+ });
+ } else {
+ // both will be null
+ data.ciphertext = data.payload;
+ cb(null, data);
+ }
});
},
getTimestamp: function(args, cb) {
View
27 client/crypto.js
@@ -214,25 +214,40 @@ var GombotCrypto = (function() {
url.port = (url.scheme === 'https' ? '443' : '80');
}
+ var hash = null;
+ if (args.payload) {
+ var bitArray = sjcl.hash.sha256.hash(args.payload);
+ hash = sjcl.codec.hex.fromBits(bitArray);
+ }
+
setTimeout(function() {
if (typeof Hawk === 'undefined') {
var hmac = new sjcl.misc.hmac(keyBits);
- var body =
+ var body = 'hawk.1.header\n' +
// string representation of seconds since epoch
args.date.toString() + "\n" +
+ // random nonce
+ args.nonce + '\n' +
// normalized method
args.method + '\n' +
// path
- url.path + '\n' +
+ url.path + (url.query ? '?' + url.query : '') + '\n' +
// hostname
url.host + '\n' +
// port
url.port + '\n' +
- // random nonce
- args.nonce + '\n';
+ // hash of body
+ (hash || '') + '\n' +
+ // extra app data
+ (args.ext || '') + '\n';
var mac = sjcl.codec.base64.fromBits(hmac.mac(body));
- var header = 'Hawk id="' + args.email + '", ts="' + args.date + (args.nonce ? '", ext="' + args.nonce : '') + '", mac="' + mac + '"';
+ var header = 'Hawk id="' + args.email +
+ '", ts="' + args.date +
+ '", nonce="' + args.nonce +
+ (hash ? '", hash="' + hash : '') +
+ (args.ext ? '", ext="' + args.ext : '') +
+ '", mac="' + mac + '"';
var headers = {
Authorization: header
};
@@ -240,7 +255,7 @@ var GombotCrypto = (function() {
var credentials = {
id: args.email,
key: new Buffer(args.keys.authKey, 'hex'),
- algorithm: 'hmac-sha-256'
+ algorithm: 'sha256'
};
var headers = {
View
6 etc/config.js
@@ -21,11 +21,7 @@ var config = module.exports = {
},
hapi: {
name: "Gombot API Server",
- docs: {
- auth: {
- mode: 'none'
- }
- },
+ docs: true,
auth: {
scheme: 'hawk',
hostHeaderName: process.env.HOST_HEADER || 'X-Forwarded-Host'
View
20 lib/api/v1/account.js
@@ -3,6 +3,7 @@ var db = require('../../db.js');
var B = Hapi.Types.Boolean;
var S = Hapi.Types.String;
+var N = Hapi.Types.Number;
module.exports = {
method: 'POST',
@@ -11,12 +12,19 @@ module.exports = {
auth: {
mode: 'none'
},
- description: 'Stage a new account',
+ description: 'Stage a new account with an initial payload',
validate: {
schema: {
email: S().email().required(),
pass: S().required(),
- newsletter: B()
+ newsletter: B(),
+ payload: S().required()
+ }
+ },
+ response: {
+ schema: {
+ success: B().required(),
+ updated: N().required()
}
}
}
@@ -25,8 +33,12 @@ module.exports = {
function handler(request) {
db.stageAccount(request.payload, function(err) {
if (err) return request.reply(Hapi.Error.badRequest("Could not create account: " + err));
- request.reply({
- success: true
+ db.storePayload(request.payload.email, request.payload.payload, function(err, updated) {
+ if (err) return request.reply(Hapi.Error.badRequest("Could not seed initial data: " + err));
+ request.reply({
+ success: true,
+ updated: updated
+ });
});
});
}
View
13 lib/api/v1/context.js
@@ -2,7 +2,8 @@ var crypto = require('crypto'),
Hapi = require('hapi');
var N = Hapi.Types.Number,
- S = Hapi.Types.String;
+ S = Hapi.Types.String,
+ B = Hapi.Types.Boolean;
module.exports = {
method: 'GET',
@@ -13,8 +14,11 @@ module.exports = {
},
description: 'Get "context" for subsequent operations',
response: {
- server_time: N(),
- entropy: S()
+ schema: {
+ success: B(),
+ server_time: N(),
+ entropy: S()
+ }
}
}
};
@@ -24,7 +28,8 @@ function handler(request) {
if (err) return request.reply(Hapi.Error.internal("error attaining entropy"));
request.reply({
server_time: Math.round((new Date()).getTime() / 1000),
- entropy: bytes.toString('base64')
+ entropy: bytes.toString('base64'),
+ success: true
});
});
};
View
30 lib/api/v1/payload.js
@@ -28,11 +28,19 @@ module.exports = [
mode: 'hawk'
},
config: {
- description: 'Retreive user credentials',
+ description: 'Returns user credentials if the client\'s are outdated.',
+ validate: {
+ query: {
+ updated: N().required()
+ }
+ },
response: {
- success: B(),
- payload: S().required(),
- updated: N().required()
+ schema: {
+ success: B().required(),
+ payload: S().required().allow(null),
+ updated: N().required(),
+ sync: B().required()
+ }
}
}
}
@@ -41,23 +49,27 @@ module.exports = [
function put(request) {
var id = request.session.id;
var payload = request.payload.payload;
- console.log('storing payload', payload);
- db.storePayload(id, payload, function(err) {
+
+ db.storePayload(id, payload, function(err, timestamp) {
if (err) return request.reply(Hapi.Error.internal("Could not store payload: " + err));
request.reply({
- success: true
+ success: true,
+ updated: timestamp
});
});
}
function get(request) {
var id = request.session.id;
+ var ts = request.query.updated;
db.getPayload(id, function(err, doc) {
if (err) return request.reply(Hapi.Error.internal("Could not retreive payload: " + err));
+ var sync = doc.timestamp > ts;
request.reply({
success: true,
- payload: doc.payload,
- updated: doc.timestamp
+ payload: (sync ? doc.payload : null),
+ updated: doc.timestamp,
+ sync: sync
});
});
}
View
6 lib/api/v1/payload/timestamp.js
@@ -14,8 +14,10 @@ module.exports = {
config: {
description: 'Retreive last update time of user credentials',
response: {
- success: B(),
- updated: N().required()
+ schema: {
+ success: B(),
+ updated: N().required()
+ }
}
}
};
View
4 lib/api/v1/status.js
@@ -11,7 +11,9 @@ module.exports = {
config: {
description: 'Check authorization status',
response: {
- success: B()
+ schema: {
+ success: B()
+ }
}
}
};
View
5 lib/db/couchbase.js
@@ -58,10 +58,11 @@ module.exports = {
return this;
},
storePayload: function(id, payload, cb) {
- db.set(id + '-payload', {payload: payload, timestamp: +new Date},
+ var time = +new Date;
+ db.set(id + '-payload', {payload: payload, timestamp: time},
function (err, meta) {
if (err) cb(err);
- else cb(null);
+ else cb(null, time);
});
return this;
},
View
5 lib/db/json.js
@@ -44,9 +44,10 @@ module.exports = {
return this;
},
storePayload: function(id, payload, cb) {
+ var timestamp = +new Date;
setTimeout(function() {
- db[id + '-payload'] = {payload: payload, timestamp: +new Date};
- cb(null);
+ db[id + '-payload'] = { payload: payload, timestamp: timestamp };
+ cb(null, timestamp);
}, 0);
return this;
},
View
6 package.json
@@ -4,14 +4,14 @@
"version": "0.0.1",
"private": true,
"dependencies": {
- "hapi": "0.9.2",
+ "hapi": "0.11.3",
"walkdir": "0.0.5",
"express": "3.0.2",
"nunjucks": "0.1.5",
"irc": "0.3.3",
- "awsbox": "0.3.3",
+ "awsbox": "0.3.5",
"temp": "0.4.0",
- "hawk": "0.0.6"
+ "hawk": "0.6.0"
},
"optionalDependencies": {
"couchbase": "0.0.7"
View
17 test/api.v1.account.js
@@ -13,7 +13,9 @@ describe('the servers', function() {
should.exist(r);
servers = r;
client = new Client('http://' + servers.host + ':' + servers.port + '/api');
- done();
+ client.context({}, function (err) {
+ done();
+ });
});
});
});
@@ -23,11 +25,22 @@ describe('/api/v1/account', function() {
client.account({
email: 'foo@account.com',
pass: 'bar',
- newsletter: false
+ newsletter: false,
+ payload: 'foo'
}, function(err, r) {
should.not.exist(err);
should.exist(r);
(r.success).should.be.true;
+ should.exist(r.updated);
+ done();
+ });
+ });
+ it ('should get initial payload', function(done) {
+ client.getPayload({ updated: 0 }, function(err, r) {
+ should.not.exist(err);
+ should.exist(r);
+ should.exist(r.updated);
+ (r.payload).should.equal('foo');
done();
});
});
View
1  test/api.v1.context.js
@@ -26,6 +26,7 @@ describe("/api/v1/context", function() {
should.exist(r.session_context);
(r.server_time).should.be.a('number');
(r.entropy).should.be.a('string');
+ (r.success).should.be.a('boolean');
done();
});
});
View
68 test/api.v1.payload.js
@@ -9,6 +9,8 @@ var client;
var test_user = 'foo@payload.com';
var test_pass = 'bar';
+var updated;
+
describe('the servers', function() {
it('should start up', function(done) {
runner(function(err, r) {
@@ -16,12 +18,15 @@ describe('the servers', function() {
should.exist(r);
servers = r;
client = new Client('http://' + servers.host + ':' + servers.port + '/api');
- done();
+ client.context({}, function (err) {
+ done();
+ });
});
});
});
function createAccount(email, pass, cb) {
+ console.error('creating account');
client.account({
email: email,
pass: pass
@@ -32,35 +37,56 @@ function createAccount(email, pass, cb) {
}
describe("/api/v1/payload", function() {
- it ('should store payload', function(done) {
- createAccount(test_user, test_pass, function() {
- try {
- client.storePayload({
- payload: 'foo'
- }, function(err, r) {
- should.not.exist(err);
- should.exist(r);
- done();
- });
- } catch (e) {
- done(e);
- }
- });
+ it ('should create account', function(done) {
+ createAccount(test_user, test_pass, done);
});
- it ('should get payload', function(done) {
+ it ('should fail if missing payload', function(done) {
+ try {
+ client.storeEncryptedPayload({}, function(err, r) {
+ should.exist(err);
+ should.not.exist(r);
+ should.exist(err.error);
+ (err.error.errorCode).should.equal(400);
+ return done();
+ });
+ } catch (e) {
+ return done(e);
+ }
+ });
+ it ('should store payload', function(done) {
try {
- client.getPayload({}, function(err, r) {
- console.error('??????', r);
+ client.storePayload({
+ payload: 'foo'
+ }, function(err, r) {
should.not.exist(err);
should.exist(r);
should.exist(r.updated);
- (r.payload).should.equal('foo');
- done();
+ return done();
});
} catch (e) {
- done(e);
+ return done(e);
}
});
+ it ('should get payload', function(done) {
+ client.getPayload({}, function(err, r) {
+ should.not.exist(err);
+ should.exist(r);
+ should.exist(r.updated);
+ (r.payload).should.equal('foo');
+ updated = r.updated;
+ done();
+ });
+ });
+ it ('should not get payload if in sync', function(done) {
+ client.getPayload({ updated: updated }, function(err, r) {
+ should.not.exist(err);
+ should.exist(r);
+ should.exist(r.sync);
+ (r.sync).should.equal(false);
+ should.not.exist(r.payload);
+ done();
+ });
+ });
});
describe('the servers', function() {
View
2  test/crypto.sign.js
@@ -80,7 +80,7 @@ describe('GumbotCrypto.sign', function() {
should.not.exist(err);
should.exist(rez);
(rez.Authorization).should.be.a('string');
- (rez.Authorization).should.equal('Hawk id="bar", ts="1352177818", ext="one time only please", mac="6b+dMvKSWdAD0hFHR/Ik3MzkIS6mPdzRc31DGNM8dbI="');
+ (rez.Authorization).should.equal('Hawk id="bar", ts="1352177818", nonce="one time only please", hash="2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881", mac="hXcx942Qj1tazZiKlw8VZ+fa0jUl4wFavD6dBPNLQvc="');
done();
});
});
Please sign in to comment.
Something went wrong with that request. Please try again.