From 303326ded5ef0207cb76f2e817148e46b5095b74 Mon Sep 17 00:00:00 2001 From: Ben Adida <ben@adida.net> Date: Wed, 6 Jul 2011 15:50:49 -0700 Subject: [PATCH] refactored browserid/wsapi to use proper express routes --- README.md | 10 + browserid/app.js | 19 +- browserid/lib/wsapi.js | 455 +++++++++--------- .../tests/registration-status-wsapi-test.js | 3 + 4 files changed, 247 insertions(+), 240 deletions(-) diff --git a/README.md b/README.md index 78616d91b..1c24f1e61 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,13 @@ All of the servers here are based on node.js, and some number of 3rd party node 1. install required software 2. run the top level *run.js* script: `node ./run.js` 3. visit the demo application ('rp') in your web browser (url output on the console at runtime)â + +## Testing + +We should start using this: + + https://github.com/LearnBoost/tobi + +for integration testing + +and straight Vows for unit testing \ No newline at end of file diff --git a/browserid/app.js b/browserid/app.js index e3fc71968..4152448b0 100644 --- a/browserid/app.js +++ b/browserid/app.js @@ -33,17 +33,10 @@ function router(app) { app.get('/sign_in', internal_redirector('/dialog/index.html')); app.get('/register_iframe', internal_redirector('/dialog/register_iframe.html')); - app.all('/wsapi/:method', function(req, resp, next) { - try { - wsapi[req.params.method](req, resp); - } catch (e) { - var errMsg = "oops, error executing wsapi method: " + method + " (" + e.toString() +")"; - console.log(errMsg); - httputils.fourOhFour(response, errMsg); - } - }); + // register all the WSAPI handlers + wsapi.setup(app); - app.get('/users/acct\::identity.xml', function(req, resp, next) { + app.get('/users/:identity.xml', function(req, resp, next) { webfinger.renderUserPage(req.params.identity, function (resultDocument) { if (resultDocument === undefined) { httputils.fourOhFour(resp, "I don't know anything about: " + req.params.identity + "\n"); @@ -80,9 +73,6 @@ exports.setup = function(server) { } }); - // add the methods - router(server); - // a tweak to get the content type of host-meta correct server.use(function(req, resp, next) { if (req.url === '/.well-known/host-meta') { @@ -90,4 +80,7 @@ exports.setup = function(server) { } next(); }); + + // add the actual URL handlers other than static + router(server); } diff --git a/browserid/lib/wsapi.js b/browserid/lib/wsapi.js index f76acfdf3..9d6b1f257 100644 --- a/browserid/lib/wsapi.js +++ b/browserid/lib/wsapi.js @@ -1,5 +1,7 @@ // a module which implements the authorities web server api. -// every export is a function which is a WSAPI method handler +// it used to be that we stuffed every function in exports. +// now we're using proper express function registration to deal +// with HTTP methods and the like, apply middleware, etc. const db = require('./db.js'), url = require('url'), @@ -33,245 +35,244 @@ function checkAuthed(req, resp) { return true; } -/* checks to see if an email address is known to the server - * takes 'email' as a GET argument */ -exports.have_email = function(req, resp) { - // get inputs from get data! - var email = url.parse(req.url, true).query['email']; - db.emailKnown(email, function(known) { - httputils.jsonResponse(resp, known); - }); -}; - -/* First half of account creation. Stages a user account for creation. - * this involves creating a secret url that must be delivered to the - * user via their claimed email address. Upon timeout expiry OR clickthrough - * the staged user account transitions to a valid user account */ -exports.stage_user = function(req, resp) { - var urlobj = url.parse(req.url, true); - var getArgs = urlobj.query; - - if (!checkParams(getArgs, resp, [ "email", "pass", "pubkey", "site" ])) { - return; - } - - // bcrypt the password - getArgs.hash = bcrypt.encrypt_sync(getArgs.pass, bcrypt.gen_salt_sync(4)) - - try { - // upon success, stage_user returns a secret (that'll get baked into a url - // and given to the user), on failure it throws - var secret = db.stageUser(getArgs); - - // store the email being registered in the session data - if (!req.session) req.session = {}; - - // store inside the session the details of this pending verification - req.session.pendingVerification = { - email: getArgs.email, - hash: getArgs.hash // we must store both email and password to handle the case where - // a user re-creates an account - specifically, registration status - // must ensure the new credentials work to properly verify that - // the user has clicked throught the email link. note, this salted, bcrypted - // representation of a user's password will get thrust into an encrypted cookie - // served over an encrypted (SSL) session. guten, yah. - }; - - httputils.jsonResponse(resp, true); - - // let's now kick out a verification email! - email.sendVerificationEmail(getArgs.email, getArgs.site, secret); - - } catch(e) { - // we should differentiate tween' 400 and 500 here. - httputils.badRequest(resp, e.toString()); - } -}; - -exports.registration_status = function(req, resp) { - if (!req.session || - (!(typeof req.session.pendingVerification === 'object') && - !(typeof req.session.pendingAddition === 'string'))) - { - httputils.badRequest( - resp, - "api abuse: registration_status called without a pending email addition/verification"); - return; - } +function setup(app) { + /* checks to see if an email address is known to the server + * takes 'email' as a GET argument */ + app.get('/wsapi/have_email', function(req, resp) { + // get inputs from get data! + var email = url.parse(req.url, true).query['email']; + db.emailKnown(email, function(known) { + httputils.jsonResponse(resp, known); + }); + }); + + /* First half of account creation. Stages a user account for creation. + * this involves creating a secret url that must be delivered to the + * user via their claimed email address. Upon timeout expiry OR clickthrough + * the staged user account transitions to a valid user account */ + app.get('/wsapi/stage_user', function(req, resp) { + var urlobj = url.parse(req.url, true); + var getArgs = urlobj.query; + + if (!checkParams(getArgs, resp, [ "email", "pass", "pubkey", "site" ])) { + return; + } - // Is the current session trying to add an email, or register a new one? - if (req.session.pendingAddition) { - // this is a pending email addition, it requires authentication - if (!checkAuthed(req, resp)) return; + // bcrypt the password + getArgs.hash = bcrypt.encrypt_sync(getArgs.pass, bcrypt.gen_salt_sync(4)); + + try { + // upon success, stage_user returns a secret (that'll get baked into a url + // and given to the user), on failure it throws + var secret = db.stageUser(getArgs); + + // store the email being registered in the session data + if (!req.session) req.session = {}; + + // store inside the session the details of this pending verification + req.session.pendingVerification = { + email: getArgs.email, + hash: getArgs.hash // we must store both email and password to handle the case where + // a user re-creates an account - specifically, registration status + // must ensure the new credentials work to properly verify that + // the user has clicked throught the email link. note, this salted, bcrypted + // representation of a user's password will get thrust into an encrypted cookie + // served over an encrypted (SSL) session. guten, yah. + }; + + httputils.jsonResponse(resp, true); + + // let's now kick out a verification email! + email.sendVerificationEmail(getArgs.email, getArgs.site, secret); + + } catch(e) { + // we should differentiate tween' 400 and 500 here. + httputils.badRequest(resp, e.toString()); + } + }); - // check if the currently authenticated user has the email stored under pendingAddition - // in their acct. - db.emailsBelongToSameAccount( - req.session.pendingAddition, req.session.authenticatedUser, - function(registered) - { - if (registered) { - delete req.session.pendingAddition; - httputils.jsonResponse(resp, "complete"); - } else { - httputils.jsonResponse(resp, "pending"); + app.get('/wsapi/registration_status', function(req, resp) { + if (!req.session || + (!(typeof req.session.pendingVerification === 'object') && + !(typeof req.session.pendingAddition === 'string'))) + { + httputils.badRequest(resp, "api abuse: registration_status called without a pending email addition/verification"); + return; } - }); - } - else - { - // this is a pending registration, let's check if the creds stored on the - // session are good yet. - - var v = req.session.pendingVerification; - db.checkAuthHash(v.email, v.hash, function(authed) { - if (authed) { - delete req.session.pendingVerification; - req.session.authenticatedUser = v.email; - httputils.jsonResponse(resp, "complete"); + + // Is the current session trying to add an email, or register a new one? + if (req.session.pendingAddition) { + // this is a pending email addition, it requires authentication + if (!checkAuthed(req, resp)) return; + + // check if the currently authenticated user has the email stored under pendingAddition + // in their acct. + db.emailsBelongToSameAccount(req.session.pendingAddition, + req.session.authenticatedUser, + function(registered) { + if (registered) { + delete req.session.pendingAddition; + httputils.jsonResponse(resp, "complete"); + } else { + httputils.jsonResponse(resp, "pending"); + } + }); } else { - httputils.jsonResponse(resp, "pending"); + // this is a pending registration, let's check if the creds stored on the + // session are good yet. + + var v = req.session.pendingVerification; + db.checkAuthHash(v.email, v.hash, function(authed) { + if (authed) { + delete req.session.pendingVerification; + req.session.authenticatedUser = v.email; + httputils.jsonResponse(resp, "complete"); + } else { + httputils.jsonResponse(resp, "pending"); + } + }); + } + }); + + + app.get('/wsapi/authenticate_user', function(req, resp) { + var urlobj = url.parse(req.url, true); + var getArgs = urlobj.query; + + if (!checkParams(getArgs, resp, [ "email", "pass" ])) return; + + db.checkAuth(getArgs.email, getArgs.pass, function(rv) { + if (rv) { + if (!req.session) req.session = {}; + req.session.authenticatedUser = getArgs.email; + } + httputils.jsonResponse(resp, rv); + }); + }); + + // FIXME: need CSRF protection + app.get('/wsapi/add_email', function (req, resp) { + var urlobj = url.parse(req.url, true); + var getArgs = urlobj.query; + + if (!checkParams(getArgs, resp, [ "email", "pubkey", "site" ])) return; + + if (!checkAuthed(req, resp)) return; + + try { + // upon success, stage_user returns a secret (that'll get baked into a url + // and given to the user), on failure it throws + var secret = db.stageEmail(req.session.authenticatedUser, getArgs.email, getArgs.pubkey); + + // store the email being added in session data + req.session.pendingAddition = getArgs.email; + + httputils.jsonResponse(resp, true); + + // let's now kick out a verification email! + email.sendVerificationEmail(getArgs.email, getArgs.site, secret); + } catch(e) { + // we should differentiate tween' 400 and 500 here. + httputils.badRequest(resp, e.toString()); } }); - } -}; - -exports.authenticate_user = function(req, resp) { - var urlobj = url.parse(req.url, true); - var getArgs = urlobj.query; - - if (!checkParams(getArgs, resp, [ "email", "pass" ])) return; - - db.checkAuth(getArgs.email, getArgs.pass, function(rv) { - if (rv) { - if (!req.session) req.session = {}; - req.session.authenticatedUser = getArgs.email; - } - httputils.jsonResponse(resp, rv); - }); -}; - -// need CSRF protection - -exports.add_email = function (req, resp) { - var urlobj = url.parse(req.url, true); - var getArgs = urlobj.query; - - if (!checkParams(getArgs, resp, [ "email", "pubkey", "site" ])) return; - - if (!checkAuthed(req, resp)) return; - - try { - // upon success, stage_user returns a secret (that'll get baked into a url - // and given to the user), on failure it throws - var secret = db.stageEmail(req.session.authenticatedUser, getArgs.email, getArgs.pubkey); - - // store the email being added in session data - req.session.pendingAddition = getArgs.email; - - httputils.jsonResponse(resp, true); - - // let's now kick out a verification email! - email.sendVerificationEmail(getArgs.email, getArgs.site, secret); - } catch(e) { - // we should differentiate tween' 400 and 500 here. - httputils.badRequest(resp, e.toString()); - } -}; - -exports.remove_email = function(req, resp) { - // this should really be POST, but for now I'm having trouble seeing - // how to get POST args properly, so it's a GET (Ben). - // hmmm, I really want express or some other web framework! - var urlobj = url.parse(req.url, true); - var getArgs = urlobj.query; - - if (!checkParams(getArgs, resp, [ "email"])) return; - if (!checkAuthed(req, resp)) return; - - db.removeEmail(req.session.authenticatedUser, getArgs.email, function(error) { - if (error) { - console.log("error removing email " + getArgs.email); - httputils.badRequest(resp, error.toString()); - } else { - httputils.jsonResponse(resp, true); - }}); -}; - -exports.account_cancel = function(req, resp) { - // this should really be POST - if (!checkAuthed(req, resp)) return; - - db.cancelAccount(req.session.authenticatedUser, function(error) { - if (error) { - console.log("error cancelling account : " + error.toString()); - httputils.badRequest(resp, error.toString()); - } else { - httputils.jsonResponse(resp, true); - }}); -}; - -exports.set_key = function (req, resp) { - var urlobj = url.parse(req.url, true); - var getArgs = urlobj.query; - if (!checkParams(getArgs, resp, [ "email", "pubkey" ])) return; - if (!checkAuthed(req, resp)) return; - db.addKeyToEmail(req.session.authenticatedUser, getArgs.email, getArgs.pubkey, function (rv) { - httputils.jsonResponse(resp, rv); - }); -}; -exports.am_authed = function(req,resp) { - // if they're authenticated for an email address that we don't know about, - // then we should purge the stored cookie - if (!isAuthed(req)) { - httputils.jsonResponse(resp, false); - } else { - db.emailKnown(req.session.authenticatedUser, function (known) { - if (!known) req.session = {} - httputils.jsonResponse(resp, known); + app.get('/wsapi/remove_email', function(req, resp) { + // this should really be POST, but for now I'm having trouble seeing + // how to get POST args properly, so it's a GET (Ben). + // hmmm, I really want express or some other web framework! + var urlobj = url.parse(req.url, true); + var getArgs = urlobj.query; + + if (!checkParams(getArgs, resp, [ "email"])) return; + if (!checkAuthed(req, resp)) return; + + db.removeEmail(req.session.authenticatedUser, getArgs.email, function(error) { + if (error) { + console.log("error removing email " + getArgs.email); + httputils.badRequest(resp, error.toString()); + } else { + httputils.jsonResponse(resp, true); + }}); }); - } -}; -exports.logout = function(req,resp) { - req.session = {}; - httputils.jsonResponse(resp, "ok"); -}; + app.get('/wsapi/account_cancel', function(req, resp) { + // this should really be POST + if (!checkAuthed(req, resp)) return; + + db.cancelAccount(req.session.authenticatedUser, function(error) { + if (error) { + console.log("error cancelling account : " + error.toString()); + httputils.badRequest(resp, error.toString()); + } else { + httputils.jsonResponse(resp, true); + }}); + }); -exports.sync_emails = function(req,resp) { - if (!checkAuthed(req, resp)) return; + app.get('/wsapi/set_key', function (req, resp) { + var urlobj = url.parse(req.url, true); + var getArgs = urlobj.query; + if (!checkParams(getArgs, resp, [ "email", "pubkey" ])) return; + if (!checkAuthed(req, resp)) return; + db.addKeyToEmail(req.session.authenticatedUser, getArgs.email, getArgs.pubkey, function (rv) { + httputils.jsonResponse(resp, rv); + }); + }); - var requestBody = ""; - req.on('data', function(str) { - requestBody += str; - }); - req.on('end', function() { - try { - var emails = JSON.parse(requestBody); - } catch(e) { - httputils.badRequest(resp, "malformed payload: " + e); - } - db.getSyncResponse(req.session.authenticatedUser, emails, function(err, syncResponse) { - if (err) httputils.serverError(resp, err); - else httputils.jsonResponse(resp, syncResponse); + app.get('/wsapi/am_authed', function(req,resp) { + // if they're authenticated for an email address that we don't know about, + // then we should purge the stored cookie + if (!isAuthed(req)) { + httputils.jsonResponse(resp, false); + } else { + db.emailKnown(req.session.authenticatedUser, function (known) { + if (!known) req.session = {} + httputils.jsonResponse(resp, known); + }); + } }); - }); -}; -exports.prove_email_ownership = function(req, resp) { - var urlobj = url.parse(req.url, true); - var getArgs = urlobj.query; + app.get('/wsapi/logout', function(req,resp) { + req.session = {}; + httputils.jsonResponse(resp, "ok"); + }); - // validate inputs - if (!checkParams(getArgs, resp, [ "token" ])) return; + app.post('/wsapi/sync_emails', function(req,resp) { + if (!checkAuthed(req, resp)) return; + + var requestBody = ""; + req.on('data', function(str) { + requestBody += str; + }); + req.on('end', function() { + try { + var emails = JSON.parse(requestBody); + } catch(e) { + httputils.badRequest(resp, "malformed payload: " + e); + } + db.getSyncResponse(req.session.authenticatedUser, emails, function(err, syncResponse) { + if (err) httputils.serverError(resp, err); + else httputils.jsonResponse(resp, syncResponse); + }); + }); + }); - db.gotVerificationSecret(getArgs.token, function(e) { - if (e) { - console.log("error completing the verification: " + e); - httputils.jsonResponse(resp, false); - } else { - httputils.jsonResponse(resp, true); - } - }); + app.get('/wsapi/prove_email_ownership', function(req, resp) { + var urlobj = url.parse(req.url, true); + var getArgs = urlobj.query; + + // validate inputs + if (!checkParams(getArgs, resp, [ "token" ])) return; + + db.gotVerificationSecret(getArgs.token, function(e) { + if (e) { + console.log("error completing the verification: " + e); + httputils.jsonResponse(resp, false); + } else { + httputils.jsonResponse(resp, true); + } + }); + }); } + +exports.setup = setup; diff --git a/browserid/tests/registration-status-wsapi-test.js b/browserid/tests/registration-status-wsapi-test.js index 7837e90d2..df8d28ba6 100755 --- a/browserid/tests/registration-status-wsapi-test.js +++ b/browserid/tests/registration-status-wsapi-test.js @@ -8,6 +8,9 @@ const assert = require('assert'), var suite = vows.describe('registration-status-wsapi'); +// FIXME: these tests are probably going to fail after Ben +// revamps wsapi to be more express-like. + // ever time a new token is sent out, let's update the global // var 'token' var token = undefined; -- GitLab