diff --git a/bin/api b/bin/api index b62f155..baba2db 100755 --- a/bin/api +++ b/bin/api @@ -11,22 +11,38 @@ function fatal(msg) { process.exit(1); } -var bindTo = config.process.api; +function credentialsFunc(id, callback) { + db.getAuthKey(id, function (err, key){ + if (err) return callback(err); + + var credentials = { + id: id, + key: key, + algorithm: 'hmac-sha-256', + user: id + }; + + return callback(null, credentials); + }); +} + +var apiOptions = config.hapi; +apiOptions.auth.getCredentialsFunc = credentialsFunc; + // Create a server with a host and port -var server = new Hapi.Server(bindTo.host, bindTo.port, config.hapi); +var bindTo = config.process.api; +var server = new Hapi.Server(bindTo.host, bindTo.port, apiOptions); console.log("api starting up"); -server.on('bound', function(host, port) { - console.log("running on http://" + host + ":" + port); -}); - // now load up api handlers apiLoader(server, function(err) { if (err) fatal(err); db.connect(config.db, function() { // Start the server - server.start(); + server.start(function() { + console.log("running on http://" + server.settings.host + ":" + server.settings.port); + }); }); }); diff --git a/client/client.js b/client/client.js index 7330ae7..8768a41 100644 --- a/client/client.js +++ b/client/client.js @@ -1,12 +1,19 @@ if (typeof GombotCrypto === 'undefined') { var GombotCrypto = require('./crypto.js'); } +if (typeof URLParse === 'undefined') { + var URLParse = require('./urlparse.js'); +} -;(function() { +(function() { -GombotClient = function(host, port) { - this.host = host; - this.port = port; +GombotClient = function(path) { + var url = URLParse(path); + + this.scheme = url.scheme; + this.host = url.host; + this.port = url.port; + this.path = url.path || ''; }; var xhr = typeof jQuery !== 'undefined' ? jQuery.ajax : require('xhrequest'); @@ -21,8 +28,11 @@ function request(args, cb) { var req = { url: url, method: method, + type: method, data: args.data, - headers: {}, + //dataType: 'json', + //accepts: {json: 'application/json'}, + headers: args.headers || {}, success: function(data, res, status) { try { var body = JSON.parse(data); @@ -32,16 +42,24 @@ function request(args, cb) { body.session_context = {}; cb(null, body); }, + processData: false, error: function(data, res, status) { cb('Error: ' + data + '\nStatus: ' + status); } }; if (method == 'PUT' || method == 'POST') { - req.headers['Content-Type'] = 'application/json'; + req.contentType = req.headers['Content-Type'] = 'application/json'; } xhr(url, req); } +function mergeArgs(args, def) { + args.scheme = def.scheme; + args.host = args.host || def.host; + args.port = args.port || def.port; + return args; +} + GombotClient.prototype = { // get "session context" from the server context: function(args, cb) { @@ -49,28 +67,55 @@ GombotClient.prototype = { cb = args; args = {}; } - args.host = args.host || this.host; - args.port = args.port || this.port; + args = mergeArgs(args, this); args.method = 'get'; - args.path = '/v1/context'; + args.path = this.path + '/v1/context'; request(args, cb); }, account: function(args, cb) { - args.host = args.host || this.host; - args.port = args.port || this.port; - args.method = 'put'; - args.path = '/v1/account'; + var self = this; + args = mergeArgs(args, this); + args.method = 'post'; + args.path = this.path + '/v1/account'; // compute the authKey - var keys = GombotCrypto.derive({ + var headers = GombotCrypto.derive({ email: args.email, - password: args.password + password: args.pass }, function(err, r) { + self.authKey = r.authKey; args.data = JSON.stringify({email: args.email, pass: r.authKey}); // send request with authKey as the password request(args, cb); }); + }, + status: function(args, cb) { + args = mergeArgs(args, this); + args.method = 'get'; + args.path = this.path + '/v1/status'; + + var url = args.scheme ? args.scheme : 'http'; + url += '://' + args.host; + if (args.port) url += ':' + args.port; + url += args.path; + + // compute the authKey + GombotCrypto.sign({ + email: args.email, + key: args.key, + url: url, + host: args.host, + port: args.port, + method: args.method, + nonce: args.nonce, + date: args.date + }, function(err, r) { + if (err) return cb(err); + args.headers = r; + // send request with authKey as the password + request(args, cb); + }); } }; diff --git a/client/crypto.js b/client/crypto.js index 1891f9f..586fab0 100644 --- a/client/crypto.js +++ b/client/crypto.js @@ -7,6 +7,10 @@ if (typeof URLParse === 'undefined') { var URLParse = require('./urlparse.js'); } +if (typeof Hawk === 'undefined') { + var Hawk = require('hawk'); +} + var GombotCrypto = (function() { // the number of rounds used in PBKDF2 to generate a stretched derived // key from a user password. @@ -115,48 +119,56 @@ var GombotCrypto = (function() { if (typeof cb !== 'function') throw new Error("missing required callback argument"); - // how about if the key is poorly formated? - var keyBits = sjcl.codec.base64.toBits(args.key); - // normalize method args.method = args.method.toUpperCase(); - args.url = URLParse(args.url); - // add a port if default is in use - if (!args.url.port) { - args.url.port = (args.url.scheme === 'https' ? '443' : '80'); - } + // how about if the key is poorly formated? + //var keyBits = sjcl.codec.base64.toBits(args.key); + + var url = URLParse(args.url); + //// add a port if default is in use + //if (!args.url.port) { + //args.url.port = (args.url.scheme === 'https' ? '443' : '80'); + //} setTimeout(function() { // see https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 // for normalization procedure - var hmac = new sjcl.misc.hmac(keyBits); - var body = - // string representation of seconds since epoch - args.date.toString() + "\n" + - // random nonce - args.nonce + '\n' + - // normalized method - args.method + '\n' + - // path - args.path + '\n' + - // hostname - args.host + '\n' + - // port - args.port + '\n' + - // payload - args.payload + '\n'; + //var hmac = new sjcl.misc.hmac(keyBits); + //var body = + //// string representation of seconds since epoch + //args.date.toString() + "\n" + + //// random nonce + //args.nonce + '\n' + + //// normalized method + //args.method + '\n' + + //// path + //args.path + '\n' + + //// hostname + //args.host + '\n' + + //// port + //args.port + '\n' + + //// payload + //args.payload + '\n'; - var mac = sjcl.codec.base64.fromBits(hmac.mac(body)); + //var mac = sjcl.codec.base64.fromBits(hmac.mac(body)); - // now formulate the authorization header. - var val = - 'MAC id="' + args.email + '", ' + - 'ts="' + args.date + '", ' + - 'nonce="' + args.nonce + '", ' + - 'mac="' + mac + '"'; + //// now formulate the authorization header. + //var val = + //'MAC id="' + args.email + '", ' + + //'ts="' + args.date + '", ' + + //'nonce="' + args.nonce + '", ' + + //'mac="' + mac + '"'; - var headers = { "Authorization": val }; + var credentials = { + id: args.email, + key: args.key, + algorithm: 'hmac-sha-256' + }; + + var headers = { + Authorization: Hawk.getAuthorizationHeader(credentials, args.method, args.url, url.host, url.port, args.nonce, args.date) + }; // and pass a bag of calculated authorization headers (only one) // back to the client diff --git a/etc/config.js b/etc/config.js index 80b1c3a..0b0949a 100644 --- a/etc/config.js +++ b/etc/config.js @@ -21,7 +21,10 @@ var config = module.exports = { }, hapi: { name: "Gombot API Server", - docs: true + docs: true, + auth: { + scheme: 'hawk' + } }, db: { hosts: [ 'localhost:8091' ], diff --git a/lib/api/v1/account.js b/lib/api/v1/account.js index b0e904f..f692289 100644 --- a/lib/api/v1/account.js +++ b/lib/api/v1/account.js @@ -6,10 +6,12 @@ var B = Hapi.Types.Boolean, S = Hapi.Types.String; module.exports = { - method: 'PUT', + method: 'POST', handler: handler, config: { - auth: false, + auth: { + mode: 'none' + }, description: 'Stage a new account', schema: { email: B(), @@ -22,7 +24,6 @@ module.exports = { }; function handler(request) { - console.log('$$$$$$$$$$'); db.stageAccount(request.payload, function(err) { if (err) request.reply(Hapi.Error.internal("error staging account")); request.reply({ diff --git a/lib/api/v1/context.js b/lib/api/v1/context.js index aae8fdf..2619298 100644 --- a/lib/api/v1/context.js +++ b/lib/api/v1/context.js @@ -8,7 +8,9 @@ module.exports = { method: 'GET', handler: handler, config: { - auth: false, + auth: { + mode: 'none' + }, description: 'Get "context" for subsequent operations', response: { server_time: N(), diff --git a/lib/api/v1/status.js b/lib/api/v1/status.js new file mode 100644 index 0000000..82411d2 --- /dev/null +++ b/lib/api/v1/status.js @@ -0,0 +1,23 @@ +var Hapi = require('hapi'); + +var B = Hapi.Types.Boolean; + +module.exports = { + method: 'GET', + handler: handler, + auth: { + mode: 'hawk' + }, + config: { + description: 'Check authorization status', + response: { + success: B() + } + } +}; + +function handler(request) { + request.reply({ + success: true + }); +} diff --git a/lib/db/json.js b/lib/db/json.js index 6af972f..9ff7218 100644 --- a/lib/db/json.js +++ b/lib/db/json.js @@ -7,17 +7,25 @@ module.exports = { db = {}; cb(null); }, 0); + return this; }, stageAccount: function(data, cb) { var account = { pass: data.pass, email: data.email, - staged: true + staged: false + //staged: true }; setTimeout(function() { db[data.email] = account; cb(null); }, 0); return this; + }, + getAuthKey: function(email, cb) { + setTimeout(function() { + cb(null, db[email].pass); + }, 0); + return this; } }; diff --git a/lib/views.js b/lib/views.js index 0638df0..c61a441 100644 --- a/lib/views.js +++ b/lib/views.js @@ -1,5 +1,6 @@ const fs = require('fs'); const path = require('path'); +const db = require('./db'); // generated from public key during packaging var appid = 'gbmmgmjoeplelogofbnjpmkmpodpfaif'; @@ -19,7 +20,7 @@ function setup(app) { }); app.post('/join_alpha', function(req, res) { - console.log('got new alpha user email:', req.param('email')); + console.log('got new alpha user email:', req.params.email); res.redirect('/download'); }); diff --git a/package.json b/package.json index e5ecf22..3e613da 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,14 @@ "version": "0.0.1", "private": true, "dependencies": { - "hapi": "git://github.com/lloyd/hapi#7e6cf0", + "hapi": "0.9.2", "walkdir": "0.0.5", "express": "3.0.2", "nunjucks": "0.1.5", "irc": "0.3.3", "awsbox": "0.3.3", - "temp": "0.4.0" + "temp": "0.4.0", + "hawk": "0.0.6" }, "optionalDependencies": { "couchbase": "0.0.4" diff --git a/test/api.v1.account.js b/test/api.v1.account.js index 4660e2a..3de8356 100644 --- a/test/api.v1.account.js +++ b/test/api.v1.account.js @@ -12,7 +12,7 @@ describe('the servers', function() { should.not.exist(err); should.exist(r); servers = r; - client = new Client(servers.host, servers.port); + client = new Client('http://' + servers.host + ':' + servers.port); done(); }); }); @@ -22,7 +22,7 @@ describe('/api/v1/account', function() { it('staging should return success', function(done) { client.account({ email: 'foo', - password: 'bar' + pass: 'bar' }, function(err, r) { should.not.exist(err); should.exist(r); diff --git a/test/api.v1.context.js b/test/api.v1.context.js index 36d1839..cbfab53 100644 --- a/test/api.v1.context.js +++ b/test/api.v1.context.js @@ -12,7 +12,7 @@ describe('the servers', function() { should.not.exist(err); should.exist(r); servers = r; - client = new Client(servers.host, servers.port); + client = new Client('http://' + servers.host + ':' + servers.port); done(); }); }); diff --git a/test/api.v1.status.js b/test/api.v1.status.js new file mode 100644 index 0000000..1ca88db --- /dev/null +++ b/test/api.v1.status.js @@ -0,0 +1,62 @@ +const +should = require('should'), +runner = require('./lib/runner.js'), +Client = require('../client/client.js'); + +var servers; +var client; + +var test_user = 'foo'; +var test_pass = 'bar'; + +describe('the servers', function() { + it('should start up', function(done) { + runner(function(err, r) { + should.not.exist(err); + should.exist(r); + servers = r; + client = new Client('http://' + servers.host + ':' + servers.port); + done(); + }); + }); +}); + +function createAccount(email, pass, cb) { + client.account({ + email: email, + pass: pass + }, function(err, r) { + if (err) cb(err); + cb(null, client.key); + }); +} + +describe("/api/v1/status", function() { + it ('should pass authorization', function(done) { + createAccount(test_user, test_pass, function() { + try { + client.status({ + email: test_user, + key: client.authKey, + nonce: 'oh hai', + date: new Date() + }, function(err, r) { + should.not.exist(err); + should.exist(r); + done(); + }); + } catch (e) { + done(e); + } + }); + }); +}); + +describe('the servers', function() { + it('should stop', function(done) { + servers.stop(function(err) { + should.not.exist(err); + done(); + }); + }); +}); diff --git a/test/crypto.sign.js b/test/crypto.sign.js index c0b5598..3ef7c71 100644 --- a/test/crypto.sign.js +++ b/test/crypto.sign.js @@ -77,7 +77,8 @@ describe('GumbotCrypto.sign', function() { should.not.exist(err); should.exist(rez); (rez.Authorization).should.be.a('string'); - (rez.Authorization).should.equal('MAC id="bar", ts="1352177818", nonce="one time only please", mac="Zt21WXS7nkwIUdocxbzMBsXKv+0NREsxQ7aBHA9MS4w="'); + //(rez.Authorization).should.equal('MAC id="bar", ts="1352177818", nonce="one time only please", mac="Zt21WXS7nkwIUdocxbzMBsXKv+0NREsxQ7aBHA9MS4w="'); + (rez.Authorization).should.equal('Hawk id="bar", ts="1352177818", ext="one time only please", mac="2JSJGewL+/9eoCKgf51mEbhI4cZuEVqNEeZkC3SfXp4="'); done(); }); });