diff --git a/ChangeLog b/ChangeLog index 22c4f387b4f17efa2c59fdaf515225af0c1abcfb..4c5f55bd2a68eb6efda0cf930999861c85d78ff7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,8 @@ -train-2012.05.25 (in progress): +train-2012.06.08 (in progress): + * Support non-english passwords: issue #1631 + * remove obsolete code - 'code_update' handler: issue #1645 + +train-2012.05.25: * many KPI improvements: #1597, #1613 * code cleanup: #1599, #1602 * verification links sent before deployment, should still work after - transitional code required by issue #1000: #1592 @@ -8,6 +12,8 @@ train-2012.05.25 (in progress): * when user types in wrong password while verifying secondary address (on different browser), show clear tooltip style error: #1557 * don't make a user type their password when not neccesary (adding secondary address to acct with only primary addresses): #1555 * perform rigorous checking of inputs to dialog from RP. (PR #1627, bug #747859) + * support new parameter names in .get & .request APIs: #1643 + * perform rigorous checking of arguments returned from primary IdPs: bug #758449 train-2012.05.14: * Password is now requested in dialog for new user signup: #1000, #290 diff --git a/bin/browserid b/bin/browserid index b0109170d498315d4e450bd8661dbb7ee47f90bc..3cb11b874f1a4dc9d8bac954d1627965fba2eff2 100755 --- a/bin/browserid +++ b/bin/browserid @@ -126,17 +126,7 @@ wsapi.setup({ // #9 - handle views for dynamicish content views.setup(app); -function doShutdown(readyForShutdownCB) { - require('../lib/bcrypt.js').shutdown(); - db.close(readyForShutdownCB) -} - -// #11 - calls to /code_update from localhost will restart the daemon, -// this feature is not externally accessible and is only used by -// the update logic -shutdown.installUpdateHandler(app, doShutdown); - -// #12 if the BROWSERID_FAKE_VERIFICATION env var is defined, we'll include +// #10 if the BROWSERID_FAKE_VERIFICATION env var is defined, we'll include // fake_verification.js. This is used during testing only and should // never be included in a production deployment if (process.env['BROWSERID_FAKE_VERIFICATION']) { @@ -161,7 +151,10 @@ db.open(config.get('database'), function (error) { } // shut down express gracefully on SIGINT - shutdown.handleTerminationSignals(app, doShutdown); + shutdown.handleTerminationSignals(app, function(readyForShutdownCB) { + require('../lib/bcrypt.js').shutdown(); + db.close(readyForShutdownCB) + }); var bindTo = config.get('bind_to'); app.listen(bindTo.port, bindTo.host, function() { diff --git a/bin/dbwriter b/bin/dbwriter index 456a78837d9406c83193fdf54fa2a4b9c8b42e6b..c629863e2241f62b8d94e08342cd66697caffe70 100755 --- a/bin/dbwriter +++ b/bin/dbwriter @@ -96,11 +96,6 @@ function doShutdown(readyForShutdownCB) { db.close(readyForShutdownCB) } -// calls to /code_update from localhost will restart the daemon, -// this feature is not externally accessible and is only used by -// the update logic -shutdown.installUpdateHandler(app, doShutdown); - // open the databse db.open(config.get('database'), function (error) { if (error) { diff --git a/bin/keysigner b/bin/keysigner index 4ed646c9a897b5cf6fb48880dbf5b1617a4271fb..714a4fed322de04c1971f966c1f94755ec1c4784 100755 --- a/bin/keysigner +++ b/bin/keysigner @@ -101,9 +101,6 @@ app.post('/wsapi/cert_key', validate(["email", "pubkey", "ephemeral"]), function }); }); -// shutdown when code_update is invoked -shutdown.installUpdateHandler(app); - // shutdown nicely on signals shutdown.handleTerminationSignals(app, function() { cc.exit(); diff --git a/bin/verifier b/bin/verifier index 4499256930558eed528919976ddd7a576ada1498..8363e217d27da5a53f41c4a767a6779560d9a8c8 100755 --- a/bin/verifier +++ b/bin/verifier @@ -126,9 +126,6 @@ app.post('/verify', function(req, resp, next) { }); }); -// shutdown when /code_update is invoked -shutdown.installUpdateHandler(app); - // shutdown nicely on signals shutdown.handleTerminationSignals(app, function() { cc.exit(); diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index c04de734595374018a42235e3f6eab198c553951..13c3b5fc26a8fa5c93a79d1779ec3854bd95ab32 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -2,6 +2,10 @@ - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +**NOTE:** this document is outdated and should be updated, it's left here +because there is *some* still some potentially useful information. +Reader beware. + # How to deploy BrowserID This describes how to take the code here, put it on a server, and build @@ -142,9 +146,8 @@ if [ "x$GL_REPO" == 'xbrowserid' ] ; then echo "generating production resources" cd $NEWCODE/browserid && ./compress.sh && cd - - # stop the servers - curl -o --url http://localhost:62700/code_update > /dev/null 2>&1 - curl -o --url http://localhost:62800/code_update > /dev/null 2>&1 + # XXX: stop the servers! you should deliver SIGINT to each + # process # now move code into place, and keep a backup of the last code # that was in production in .old @@ -275,11 +278,6 @@ server { listen 80 default; server_name browserid.org; - # disallow external server restart. - location = /code_update { - internal; - } - # pass /verify invocations to the verifier location /verify { proxy_pass http://127.0.0.1:62800; diff --git a/lib/http_forward.js b/lib/http_forward.js index f4b76f5519261cff60d4ad9554fd12d5cffec1cf..f04001e1d6282c7ef88de5030f3ac36824657223 100644 --- a/lib/http_forward.js +++ b/lib/http_forward.js @@ -111,7 +111,7 @@ exports.forward = function(dest, req, res, cb) { var data; if (req.headers['content-type'].indexOf('application/json') === 0) data = JSON.stringify(req.body); else data = querystring.stringify(req.body); - preq.setHeader('content-length', data.length); + preq.setHeader('content-length', Buffer.byteLength(data)); preq.write(data); preq.end(); } else { diff --git a/lib/logging.js b/lib/logging.js index d868df2f772c92da03767466d3abd83d5818029a..058d161869a7e3113ef506304a6f8b81e16e8c0e 100644 --- a/lib/logging.js +++ b/lib/logging.js @@ -40,7 +40,7 @@ exports.logger = new (winston.Logger)({ timestamp: function () { return new Date().toISOString() }, filename: filename, colorize: true, - handleExceptions: !!process.env['LOG_TO_CONSOLE'] + handleExceptions: true })] }); @@ -51,6 +51,6 @@ exports.enableConsoleLogging = function() { }); }; - - if (process.env['LOG_TO_CONSOLE']) exports.enableConsoleLogging(); + +exports.logger.exitOnError = false; diff --git a/lib/shutdown.js b/lib/shutdown.js index a4a9a6bc08b5b9bef8b2c695470370b2b355ddca..7fa3c2fdea4a826205b61d3f087e292016627121 100644 --- a/lib/shutdown.js +++ b/lib/shutdown.js @@ -2,10 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* code_update is a tiny abstraction of a handler that can be - * used to shutdown gracefully upon signals, and can be used - * to install a 'code_update' hook into a running express - * server. +/* shutdown.js is an abstraction for installing graceful shutdown + * handlers into processes so that they gracefully shutdown upon + * signals. */ const logger = require("./logging.js").logger; @@ -78,17 +77,3 @@ exports.handleTerminationSignals = function(app, callback) { process.on('SIGINT', endIt('INT')).on('SIGTERM', endIt('TERM')).on('SIGQUIT', endIt('QUIT')); }; - -const CODE_UPDATE_URL = '/code_update'; - -exports.installUpdateHandler = function(app, callback) { - var terminate = connectionListener(app); - app.get(CODE_UPDATE_URL, function(req, resp, next) { - // don't allow an imprecise match (like one with a trailing slash) to shut the server down. - // bug #699171 - if (req.url !== CODE_UPDATE_URL) return next(); - - logger.warn("code updated. closing " + app.connections + " connections and shutting down."); - terminate(callback); - }); -}; diff --git a/lib/statsd.js b/lib/statsd.js index 0e231f3c2383ea92234d4594eee9211548585b86..f7278dca5628b18f7f7329cb04043078822ef653 100644 --- a/lib/statsd.js +++ b/lib/statsd.js @@ -29,3 +29,7 @@ if (statsd_config && statsd_config.enabled) { statsd = new StatsD(options["host"], options["port"]); } + +process.on('uncaughtException', function(err) { + if (statsd) statsd.increment(PREFIX + 'uncaught_exception'); +}); diff --git a/lib/wsapi_client.js b/lib/wsapi_client.js index 256fcf71c6e395fad3a18dae23fadbb5cef2c0fc..c6ae5bd4208c15fb8a2f94fa219e7c6cb3bd626f 100644 --- a/lib/wsapi_client.js +++ b/lib/wsapi_client.js @@ -129,7 +129,7 @@ exports.post = function(cfg, path, context, postArgs, cb) { if (typeof postArgs === 'object') { postArgs['csrf'] = csrf; body = JSON.stringify(postArgs); - headers['Content-Length'] = body.length; + headers['Content-Length'] = Buffer.byteLength(body); } var req = meth.request({ diff --git a/resources/static/dialog/controllers/dialog.js b/resources/static/dialog/controllers/dialog.js index 1a3372d72a40f43d41d7b643c642e5a263948856..8e5b8f1cd22e84a05175a9b7b1393c7c69e4a45f 100644 --- a/resources/static/dialog/controllers/dialog.js +++ b/resources/static/dialog/controllers/dialog.js @@ -167,6 +167,24 @@ BrowserID.Modules.Dialog = (function() { params.tosURL = fixupURL(origin_url, paramsFromRP.termsOfService); params.privacyURL = fixupURL(origin_url, paramsFromRP.privacyPolicy); } + + if (hash.indexOf("#CREATE_EMAIL=") === 0) { + var email = hash.replace(/#CREATE_EMAIL=/, ""); + if (!bid.verifyEmail(email)) + throw "invalid #CREATE_EMAIL= (" + email + ")"; + params.type = "primary"; + params.email = email; + params.add = false; + } + else if (hash.indexOf("#ADD_EMAIL=") === 0) { + var email = hash.replace(/#ADD_EMAIL=/, ""); + if (!bid.verifyEmail(email)) + throw "invalid #ADD_EMAIL= (" + email + ")"; + params.type = "primary"; + params.email = email; + params.add = true; + } + } catch(e) { // note: renderError accepts HTML and cheerfully injects it into a // frame with a powerful origin. So convert 'e' first. @@ -184,19 +202,6 @@ BrowserID.Modules.Dialog = (function() { // XXX Perhaps put this into the state machine. self.bind(win, "unload", onWindowUnload); - if(hash.indexOf("#CREATE_EMAIL=") === 0) { - var email = hash.replace(/#CREATE_EMAIL=/, ""); - params.type = "primary"; - params.email = email; - params.add = false; - } - else if(hash.indexOf("#ADD_EMAIL=") === 0) { - var email = hash.replace(/#ADD_EMAIL=/, ""); - params.type = "primary"; - params.email = email; - params.add = true; - } - self.publish("start", params); } diff --git a/resources/static/lib/jschannel.js b/resources/static/lib/jschannel.js index ad70c3344f106368cf763af3b45f160008ee36ee..c2a5ee41093b37086c9742b8615ff3b8a5756d5c 100644 --- a/resources/static/lib/jschannel.js +++ b/resources/static/lib/jschannel.js @@ -1,4 +1,4 @@ -/** +/* * js_channel is a very lightweight abstraction on top of * postMessage which defines message formats and semantics * to support interactions more rich than just message passing diff --git a/resources/views/signup.ejs b/resources/views/signup.ejs index 6e786017f8f383c8af4415c7b49de23fb741a312..e700ed1d8af6ded303ab6c69509ca656863d85c5 100644 --- a/resources/views/signup.ejs +++ b/resources/views/signup.ejs @@ -53,7 +53,7 @@ <li class="password_entry"> <label class="serif" for="password">Password</label> - <input class="sans" id="password" placeholder="Password" type="password" tabindex="2" maxlength="80"> + <input class="sans" id="password" placeholder="Password" type="password" maxlength="80"> <div id="password_required" class="tooltip" for="password"> Password is required. @@ -70,7 +70,7 @@ <li class="password_entry"> <label class="serif" for="vpassword">Verify Password</label> - <input class="sans" id="vpassword" placeholder="Repeat Password" type="password" tabindex="2" maxlength="80"> + <input class="sans" id="vpassword" placeholder="Repeat Password" type="password" maxlength="80"> <div id="password_required" class="tooltip" for="vpassword"> Verification password is required. @@ -85,7 +85,7 @@ <div class="submit cf forminputs"> <div class="remember cf"> - <a class="action" href="/signin">Existing account? Sign in.</a> + <a class="action" href="/signin" tabindex="2">Existing account? Sign in.</a> </div> <button>Verify Email</button> </div> @@ -99,7 +99,7 @@ </p> <p> - <button id="authWithPrimary">Verify</button> + <button id="authWithPrimary" tabindex="1">Verify</button> </p> </li> </ul> diff --git a/scripts/browserid.spec b/scripts/browserid.spec index 36a711c95e824dfe187a0362939f13123223bbb8..c1b5c7126022afaff6d6aa54d8aab8bcea40fa2f 100644 --- a/scripts/browserid.spec +++ b/scripts/browserid.spec @@ -1,7 +1,7 @@ %define _rootdir /opt/browserid Name: browserid-server -Version: 0.2012.05.25 +Version: 0.2012.06.08 Release: 1%{?dist}_%{svnrev} Summary: BrowserID server Packager: Pete Fritchman <petef@mozilla.com> diff --git a/scripts/compress-worker.js b/scripts/compress-worker.js index 4c9616bffe4a048a05e3cbcd4288ef65f60fb2ad..277a3f7b410d71eaf886c23e0b9967c86d53bae3 100644 --- a/scripts/compress-worker.js +++ b/scripts/compress-worker.js @@ -29,15 +29,35 @@ function compressResource(staticPath, name, files, cb) { }); } + function extract_copyright(code) { + var tok = jsp.tokenizer(code), toks, ret = ""; + toks = tok().comments_before; + + if (toks.length >= 1) { + var c = toks[0]; + // copyrights that we'll include MUST be before code body and have + // the form: /** */ + if (c.value.substr(0, 1) === '*' && c.type === 'comment2') { + ret += "/*" + c.value + "*/"; + } + } + + return ret; + }; + function compress() { try { var final_code; if (/\.js$/.test(name)) { + // extract copyright + var copyright = extract_copyright(orig_code) || ""; + if (copyright.length) copyright += "\n\n"; + // compress javascript var ast = jsp.parse(orig_code); // parse code and get the initial AST ast = pro.ast_mangle(ast); // get a new AST with mangled names ast = pro.ast_squeeze(ast); // get an AST with compression optimizations - final_code = pro.split_lines(pro.gen_code(ast), 32 * 1024); // compressed code here + final_code = copyright + pro.split_lines(pro.gen_code(ast), 32 * 1024); // compressed code here } else if (/\.css$/.test(name)) { // compress css var cach_code = cachify_embedded(orig_code); @@ -89,7 +109,9 @@ function compressResource(staticPath, name, files, cb) { } function cachify_embedded (css_src) { - return css_src.replace(/url\s*\(['"](.*)\s*['"]\s*\)/g, function (str, url) { + // RegExp is set up to handle multiple url's per declaration, which is + // possible for things like background-images. + return css_src.replace(/url\s*\(['"]([^\)'"]+)\s*['"]\s*\)/g, function (str, url) { // This will throw an error if url doesn't exist. This is good as we will // catch typos during build. logger.info("For " + str + " making " + url + " into " + cachify.cachify(url)); @@ -107,4 +129,4 @@ process.on('message', function(m) { info: info }); }); -}); \ No newline at end of file +}); diff --git a/tests/simple-stage-user-utf8-password.js b/tests/simple-stage-user-utf8-password.js new file mode 100644 index 0000000000000000000000000000000000000000..cdd07a8dafbd20b7d17f081a8ecd3211c645892d --- /dev/null +++ b/tests/simple-stage-user-utf8-password.js @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +require('./lib/test_env.js'); + +const +assert = require('assert'), +vows = require('vows'), +start_stop = require('./lib/start-stop.js'), +wsapi = require('./lib/wsapi.js'); + +var suite = vows.describe('simple-stage-user-utf8-password'); + +// disable vows (often flakey?) async error behavior +suite.options.error = false; + +start_stop.addStartupBatches(suite); + +const +TEST_DOMAIN = 'example.domain', +TEST_ORIGIN = 'http://127.0.0.1:10002', +TEST_SITE = 'http://example.com:652'; + +// This test simply stages a secondary user. It does so for two users, +// one with a password that is only ascii, and the other with non-ascii +// characters in the password (GH-1631). + +const test_users = + [{ + email: 'testuser1@' + TEST_DOMAIN, + password: 'fakepass', + }, + { + email: 'testuser2@' + TEST_DOMAIN, + password: 'поддельный пароль', // Russian 'fake password' (34 bytes UTF-8) + }]; + +function makeBatch(site, user) { + var batch = { + "staging an account": { + topic: wsapi.post('/wsapi/stage_user', { + site: site, + email: user.email, + pass: user.password, + }), + "is 200 OK": function(err, r) { + assert.strictEqual(r.code, 200); + }, + "and a token": { + topic: function() { + start_stop.waitForToken(this.callback); + }, + "is obtained": function (t) { + assert.strictEqual(typeof t, 'string'); + }, + "and the token can be used": { + topic: function(token) { + wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this); + }, + "to verify email ownership": function(err, r) { + assert.equal(r.code, 200); + assert.strictEqual(JSON.parse(r.body).success, true); + token = undefined; + } + } + } + } + }; + return batch; +} + +suite.addBatch(makeBatch(TEST_SITE, test_users[0])); +suite.addBatch(makeBatch(TEST_SITE, test_users[1])); + +start_stop.addShutdownBatches(suite); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module);