Skip to content

Instantly share code, notes, and snippets.

@ozten
Created June 12, 2012 22:37
Show Gist options
  • Save ozten/2920556 to your computer and use it in GitHub Desktop.
Save ozten/2920556 to your computer and use it in GitHub Desktop.
bigten diff
diff --git a/ChangeLog b/ChangeLog
index e30219c..74cc136 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,22 +1,7 @@
-train-2012.06.22: (in progress)
- *
-
-train-2012.06.08:
- * rebrand from 'browserid' to 'persona': (including regressions #1711 #1706 #1716 #1719)
- * new "router" process added - subsumes responsibility of old "webhead" process, handles all requets forwarding: #1657
- * Support non-english passwords: #1631
+train-2012.06.08 (in progress):
+ * Support non-english passwords: issue #1631
+ * remove obsolete code - 'code_update' handler: issue #1645
* allow sessions to persist for 4 weeks after a user confirms ownership of a device (was effectively 1 week): #1632
- * general code cleanup, removal of obsolete code, accidental globals cleaned up: #1645 #1681 #1699
- * fix tab order on signup page: #1635
- * minified include.js again has link to unminified source: #1624
- * improve handling of uncaught exceptions (new statsd counter): #1558
- * fix cachify stuff to allow multiple background images per css dec: #1652
- * 50x error pages added to repo
- * authenticated sessions are now 4 weeks: #1632, #1572
- * improvements to KPI system: #1614 #1619 #1660 #1698
- * UI improvements: #1684
- * new secret debug menu added to dialog
- * "silent assertions" (via observer api) now check cert expiry and don't issue invalid assertions
train-2012.05.25:
* many KPI improvements: #1597, #1613
diff --git a/bin/browserid b/bin/browserid
index 8806efb..3cb11b8 100755
--- a/bin/browserid
+++ b/bin/browserid
@@ -60,6 +60,10 @@ app.use(i18n.abide({
disable_locale_check: config.get('disable_locale_check')
}));
+// limit all content bodies to 10kb, at which point we'll forcefully
+// close down the connection.
+app.use(express.limit("10kb"));
+
var statsd_config = config.get('statsd');
if (statsd_config && statsd_config.enabled) {
logger_statsd = require("connect-logger-statsd");
diff --git a/config/local.json b/config/local.json
index 6d346f5..13524e8 100644
--- a/config/local.json
+++ b/config/local.json
@@ -11,6 +11,7 @@
},
"express_log_format": "dev_bid",
"email_to_console": true,
- "env": "local",
- "kpi_backend_sample_rate": 1.0
+ "kpi_backend_sample_rate": 1.0,
+ "bigtent_domains": ["gmail.com", "yahoo.com", "hotmail.com"],
+ "env": "local"
}
diff --git a/docs/AWS_DEPLOYMENT.md b/docs/AWS_DEPLOYMENT.md
index 5d31edb..f91c5d7 100644
--- a/docs/AWS_DEPLOYMENT.md
+++ b/docs/AWS_DEPLOYMENT.md
@@ -17,16 +17,7 @@ In order to use these deploy scripts, you need the following:
1. have built and locally run browserid
2. an ssh key in `~/.ssh/id_rsa.pub`
3. an AWS account that is "signed up" for EC2
- 4. (optionally) a secrets bundle that you get from lloyd (for DNS, SSL, and mail setup)
-
-For the secrets bundle, you'll need gpg to unpack it, and will do
-the following:
-
- $ cd
- $ curl -s http://people.mozilla.org/~lhilaiel/persona_goodies.tgz.gpg | gpg -d | tar xvzf -
-
-You'll be asked for the decryption password from GPG. Get that from
-lloyd.
+ 4. the "DNS secret" that you get from lloyd
Once you have these things, you'll need to relay them to deployment
scripts via your environment. you might put something like this
@@ -36,8 +27,8 @@ in your `.bashrc`:
export AWS_ID=<your id>
# This is your Secret Access Key from your AWS Security Credentials
export AWS_SECRET=<your secret>
- # install super magic secrets into your environment
- . $HOME/.persona_secrets/env.sh
+ # This is a magic credential you get from lloyd
+ export BROWSERID_DEPLOY_DNS_KEY=98...33
## Verify the credentials
@@ -55,42 +46,38 @@ you can use a different name that is short but meaningful to what you're
going to deploy. Once chosen, invoke `deploy.js` like this:
$ scripts/deploy.js deploy some_name_i_chose
- awsbox cmd: node_modules/.bin/awsbox create -n some_name_i_chose -p /Users/lth/.persona_secrets/cert.pem -s /Users/lth/.persona_secrets/key.pem -d -u https://some_name_i_chose.personatest.org -x /Users/lth/.persona_secrets/smtp.json
- reading .awsbox.json
- attempting to set up VM "some_name_i_chose"
- ... Checking for DNS availability of some_name_i_chose.personatest.org
+ attempting to set up some_name_i_chose.hacksign.in
... VM launched, waiting for startup (should take about 20s)
- ... Adding DNS Record for some_name_i_chose.personatest.org
- ... Instance ready, setting human readable name in aws
+ ... Instance ready, setting up DNS
+ ... DNS set up, setting human readable name in aws
... name set, waiting for ssh access and configuring
- ... adding additional configuration values
- ... public url will be: https://some_name_i_chose.personatest.org
+ ... nope. not yet. retrying.
+ ... nope. not yet. retrying.
+ ... nope. not yet. retrying.
... nope. not yet. retrying.
... nope. not yet. retrying.
... victory! server is accessible and configured
... and your git remote is all set up
- ... finally, installing custom packages: mysql-server
- ... copying up SSL cert
Yay! You have your very own deployment. Here's the basics:
- 1. deploy your code: git push some_name_i_chose HEAD:master
- 2. visit your server on the web: https://some_name_i_chose.personatest.org
- 3. ssh in with sudo: ssh ec2-user@some_name_i_chose.personatest.org
- 4. ssh as the deployment user: ssh app@some_name_i_chose.personatest.org
+ 1. deploy your code: git push some_name_i_chose <mybranch>:master
+ 2. visit your server on the web: https://some_name_i_chose.hacksign.in
+ 3. test via a website: http://some_name_i_chose.myfavoritebeer.org
+ 4. ssh in with sudo: ssh ec2-user@some_name_i_chose.hacksign.in
+ 5. ssh as the deployment user: ssh app@some_name_i_chose.hacksign.in
- Here are your server's details: {
- "instanceId": "i-f0b35e89",
- "imageId": "ami-ac8524c5",
+ enjoy! Here's your server details {
+ "instanceId": "i-8f4beeea",
+ "imageId": "ami-6900d100",
"instanceState": {
"code": "16",
"name": "running"
},
- "dnsName": "ec2-23-21-24-182.compute-1.amazonaws.com",
- "keyName": "awsbox deploy key (4736caec113ccb53aa62bb165c58c17d)",
+ "dnsName": "ec2-184-73-84-132.compute-1.amazonaws.com",
+ "keyName": "browserid deploy key (4736caec113ccb53aa62bb165c58c17d)",
"instanceType": "t1.micro",
- "ipAddress": "23.21.24.182",
- "name": "i-f0b35e89"
+ "ipAddress": "184.73.84.132"
}
The output contains instructions for use. Note that every occurance of
@@ -138,11 +125,8 @@ These things cost money by the hour, not a lot, but money. So when you want to
decommission a VM and release your hold on the DNS name, simply:
$ scripts/deploy.js destroy some_name_i_chose
- awsbox cmd: node_modules/.bin/awsbox destroy some_name_i_chose
- trying to destroy VM for some_name_i_chose: done
- trying to remove git remote: done
- trying to remove DNS: some_name_i_chose.personatest.org
- deleting some_name_i_chose.personatest.org: done
+ trying to destroy VM for some_name_i_chose.hacksign.in: done
+ trying to remove DNS for some_name_i_chose.hacksign.in: done
## Overview of what's deployed to VMs
@@ -155,20 +139,18 @@ There are several things that are pre-configured for your pleasure:
on the server, that you can push to.
3. `post-update` hook: when you push to the `master` branch of the server's
git repository, this code restarts your services to pick up the changes.
- 4. SSL support and 503 support - you'll get SSL for free and will see
+ 4. nginx with SSL and 503 support - you'll get SSL for free and will see
a reasonable error message when your servers aren't running.
5. a mysql database with a browserid user without any password.
### User accounts
-VMs have three pre-configured users, all of which you have passphraseless SSH
+VMs have two pre-configured users, both which you have passphraseless SSH
access to:
* `ec2-user` is an account with full sudo access.
* `app` is an account that has no sudo, receives and builds code via git
pushes, and runs the application servers.
- * `proxy` is the account the the HTTP reverse proxy that front-ends your server
- runs as.
Feel free to start a new server, and ssh in as `app` to explore all of the
configuration. An attempt has been made to isolate as much configuration
diff --git a/example/rp/i/logo.png b/example/rp/i/logo.png
deleted file mode 100644
index 4af65cb..0000000
Binary files a/example/rp/i/logo.png and /dev/null differ
diff --git a/example/rp/index.html b/example/rp/index.html
index fa60c2b..8487ef7 100644
--- a/example/rp/index.html
+++ b/example/rp/index.html
@@ -8,7 +8,7 @@
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0; maximum-scale=1.0; width=device-width;">
<title>
-Persona Relying Party
+BrowserID Relying Party
</title>
<style type="text/css">
@@ -48,31 +48,23 @@ pre {
</head>
<body>
<div class="title">
- Persona Test Relying Party
+ BrowserID Test Relying Party
</div>
<div class="intro">
This is a RP for testing, it allows you to drive the <tt>navigator.id.get()</tt> call manually
- to locally test Persona.
+ to locally test BrowserID.
</div>
<div class="specify">
<p><b>What flavor of assertion would you like?</b></p>
<ul>
<li>
- <input type="checkbox" id="privacyPolicy">
- <label for="privacyPolicy">Supply a privacy policy</label>
+ <input type="checkbox" id="privacy">
+ <label for="privacy">Supply a privacy policy</label>
</li><li>
- <input type="checkbox" id="termsOfService">
- <label for="termsOfService">Supply a ToS</label>
- </li><li>
- <input type="checkbox" id="siteName">
- <label for="siteName">Supply Site Name</label><br />
- </li>
- </li><li>
- <input type="checkbox" id="siteLogo">
- <label for="siteLogo">Supply Site Logo</label><br />
- </li>
+ <input type="checkbox" id="tos">
+ <label for="tos">Supply a ToS</label>
</li><li>
<input type="text" id="requiredEmail" width="80">
<label for="requiredEmail">Require a specific email</label><br />
@@ -189,10 +181,8 @@ $(document).ready(function() {
$(".specify button.assertion").attr('disabled', 'true');
navigator.id.request({
- privacyPolicy: $('#privacyPolicy').attr('checked') ? "/privacy.html" : undefined,
- termsOfService: $('#termsOfService').attr('checked') ? "/TOS.html" : undefined,
- siteName: $('#siteName').attr('checked') ? "Persona Test Relying Party" : undefined,
- siteLogo: $('#siteLogo').attr('checked') ? "/i/logo.png" : undefined,
+ privacyURL: $('#privacy').attr('checked') ? "/privacy.html" : undefined,
+ tosURL: $('#tos').attr('checked') ? "/TOS.html" : undefined,
requiredEmail: requiredEmail,
oncancel: function() {
loggit("oncancel");
diff --git a/lib/browserid/fake_verification.js b/lib/browserid/fake_verification.js
index b9e3a21..03f33f1 100644
--- a/lib/browserid/fake_verification.js
+++ b/lib/browserid/fake_verification.js
@@ -11,7 +11,7 @@
const
configuration = require('../configuration.js'),
url = require('url'),
-db = require('../db.js'),
+db = require('../db.js');
logger = require('../logging.js').logger,
wsapi = require('../wsapi');
@@ -22,7 +22,7 @@ exports.addVerificationWSAPI = function(app) {
app.get('/wsapi/fake_verification', function(req, res) {
var email = url.parse(req.url, true).query['email'];
db.verificationSecretForEmail(email, function(err, secret) {
- if (err) return wsapi.databaseDown(res, err);
+ if (err) return wsapi.databaseDown(resp, err);
if (secret) res.write(secret);
else res.writeHead(400, {"Content-Type": "text/plain"});
res.end();
diff --git a/lib/browserid/views.js b/lib/browserid/views.js
index 035cb9e..1fc696f 100644
--- a/lib/browserid/views.js
+++ b/lib/browserid/views.js
@@ -173,12 +173,12 @@ exports.setup = function(app) {
} else {
// this is stage or production, explicitly disable all resources under /test
app.get(/^\/test/, function(req, res) {
- httputils.notFound(res, "Cannot " + req.method + " " + req.url);
+ httputils.notFound("Cannot " + req.method + " " + req.url);
});
}
// REDIRECTS
- const REDIRECTS = {
+ REDIRECTS = {
"/manage": "/",
"/users": "/",
"/users/": "/",
diff --git a/lib/configuration.js b/lib/configuration.js
index 0e0d3c2..b7e3d32 100644
--- a/lib/configuration.js
+++ b/lib/configuration.js
@@ -91,7 +91,6 @@ var conf = module.exports = convict({
format: 'string?',
env: 'MYSQL_PASSWORD'
},
- host: 'string?',
create_schema: 'boolean = true',
may_write: 'boolean = true',
name: {
@@ -115,8 +114,7 @@ var conf = module.exports = convict({
smtp: {
host: 'string?',
user: 'string?',
- pass: 'string?',
- port: 'integer = 25'
+ pass: 'string?'
},
statsd: {
enabled: {
@@ -200,6 +198,15 @@ var conf = module.exports = convict({
format: 'string?',
env: 'DBWRITER_URL'
},
+ bigtent_url: {
+ doc: "Base URL to a BigTent server for proxyidp support",
+ format: 'string = "https://dev.bigtent.mozilla.org"',
+ env: 'BIGTENT_URL'
+ },
+ bigtent_domains: {
+ doc: "List of email domains which should be considered proxyidp candidtes.",
+ format: 'array { string }* = []'
+ },
browserid_url: {
format: 'string?',
env: 'BROWSERID_URL'
diff --git a/lib/db.js b/lib/db.js
index 1f08f6e..8fe22be 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -117,6 +117,13 @@ exports.onReady = function(f) {
};
});
+// Note: createUserWithPrimaryEmail is used for the following email types:
+// * primary
+// * proxyidp
+// This is because proxyidps act very much like Primary IdPs and an a Primary
+// may opt into being a Primary at any point without any data migration in our
+// database.
+
exports.addTestUser = function() {
// would we like to check the environment here?
checkReady();
diff --git a/lib/db/json.js b/lib/db/json.js
index 376a2a7..497bdf4 100644
--- a/lib/db/json.js
+++ b/lib/db/json.js
@@ -158,7 +158,7 @@ exports.userOwnsEmail = function(uid, email, cb) {
function addEmailToAccount(userID, email, type, cb) {
// validate 'type' isn't bogus
- if ([ 'secondary', 'primary' ].indexOf(type) === -1) {
+ if ([ 'secondary', 'primary', 'proxyidp' ].indexOf(type) === -1) {
return process.nextTick(function() {
cb("invalid type");
});
diff --git a/lib/db/mysql.js b/lib/db/mysql.js
index f9d37b7..8c2d805 100644
--- a/lib/db/mysql.js
+++ b/lib/db/mysql.js
@@ -29,8 +29,6 @@
* +------------------------+
*/
-/*global dne:true */
-
const
mysql = require('./mysql_wrapper.js'),
secrets = require('../secrets.js'),
@@ -458,7 +456,7 @@ exports.updatePassword = function(uid, hash, cb) {
[ hash, uid ],
function (err, rows) {
if (!err && (!rows || rows.affectedRows !== 1)) {
- err = "no record with id " + uid;
+ err = "no record with email " + email;
}
cb(err);
});
diff --git a/lib/email.js b/lib/email.js
index ee44447..4bb0c50 100644
--- a/lib/email.js
+++ b/lib/email.js
@@ -13,10 +13,7 @@ logger = require('./logging.js').logger;
/* if smtp parameters are configured, use them */
try { var smtp_params = config.get('smtp'); } catch(e) {};
if (smtp_params && smtp_params.host) {
- emailer.SMTP = {
- host: smtp_params.host,
- port: smtp_params.port
- };
+ emailer.SMTP = { host: smtp_params.host };
logger.info("delivering email via SMTP host: " + emailer.SMTP.host);
if (smtp_params.user) {
emailer.SMTP.use_authentication = true;
diff --git a/lib/i18n.js b/lib/i18n.js
index 7b2d214..45944b2 100644
--- a/lib/i18n.js
+++ b/lib/i18n.js
@@ -115,7 +115,7 @@ exports.abide = function (options) {
if (mo_cache[locale].mo_exists) {
if (mo_cache[locale].gt === null) {
mo_cache[locale].gt = new Gettext();
- var mo_path = mo_file_path(locale);
+ mo_path = mo_file_path(locale);
mo_cache[locale].gt.addTextdomain(locale,
fs.readFileSync(mo_path));
mo_cache[locale].gt.textdomain(locale);
@@ -153,7 +153,7 @@ function qualityCmp(a, b) {
* lang: 'pl', quality: 0.7
* }
*/
-var parseAcceptLanguage = exports.parseAcceptLanguage = function (header) {
+exports.parseAcceptLanguage = parseAcceptLanguage = function (header) {
// pl,fr-FR;q=0.3,en-US;q=0.1
if (! header || ! header.split) {
return [];
@@ -163,7 +163,7 @@ var parseAcceptLanguage = exports.parseAcceptLanguage = function (header) {
var parts = raw_lang.split(';');
var q = 1;
if (parts.length > 1 && parts[1].indexOf('q=') == 0) {
- var qval = parseFloat(parts[1].split('=')[1]);
+ qval = parseFloat(parts[1].split('=')[1]);
if (isNaN(qval) === false) {
q = qval;
}
@@ -179,7 +179,7 @@ var parseAcceptLanguage = exports.parseAcceptLanguage = function (header) {
// supported languages, returns the best match or a default language.
//
// languages must be a sorted list, the first match is returned.
-var bestLanguage = exports.bestLanguage = function(languages, supported_languages, defaultLanguage) {
+exports.bestLanguage = bestLanguage = function(languages, supported_languages, defaultLanguage) {
var lower = supported_languages.map(function (l) { return l.toLowerCase(); });
for(var i=0; i < languages.length; i++) {
var lq = languages[i];
@@ -199,7 +199,7 @@ var bestLanguage = exports.bestLanguage = function(languages, supported_language
* language: en-US
* locale: en_US
*/
-var localeFrom = exports.localeFrom = function (language) {
+exports.localeFrom = localeFrom = function (language) {
if (! language || ! language.split) {
return "";
}
@@ -248,7 +248,7 @@ exports.languageFrom = function (locale) {
* Positional Example:
* format("%s %s", ["Hello", "World"]);
*/
-var format = exports.format = function (fmt, obj, named) {
+exports.format = format = function (fmt, obj, named) {
if (! fmt) return "";
if (named) {
return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
diff --git a/lib/load_gen/common.js b/lib/load_gen/common.js
index b02f17a..59758e2 100644
--- a/lib/load_gen/common.js
+++ b/lib/load_gen/common.js
@@ -97,7 +97,7 @@ exports.genAssertionAndVerify = function(cfg, user, ctx, email, audience, cb) {
}, function (err, r) {
try {
if (err) throw err;
- if (r.code !== 200) throw "non-200 status: " + r.code;
+ if (r.code !== 200) throw "non-200 status: " + resp.code;
if (!JSON.parse(r.body).status === 'okay') throw "verification failed with: " + r.reason;
cb(undefined);
} catch(e) {
diff --git a/lib/proxyidp.js b/lib/proxyidp.js
new file mode 100644
index 0000000..f764e2c
--- /dev/null
+++ b/lib/proxyidp.js
@@ -0,0 +1,17 @@
+/* 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/. */
+
+const config = require('./configuration');
+
+var proxied = config.get('bigtent_domains');
+
+exports.isProxyIdP = function (email) {
+ var pieces = email.split('@');
+ if (pieces.length == 2) {
+ if (proxied.indexOf(pieces[1].toLowerCase()) >= 0) {
+ return true;
+ }
+ }
+ return false;
+};
diff --git a/lib/static_resources.js b/lib/static_resources.js
index d86bfd7..ad17eac 100644
--- a/lib/static_resources.js
+++ b/lib/static_resources.js
@@ -104,21 +104,17 @@ var dialog_js = und.flatten([
'/dialog/controllers/generate_assertion.js',
'/dialog/controllers/is_this_your_computer.js',
'/dialog/controllers/set_password.js',
- '/dialog/controllers/rp_info.js',
+
'/dialog/start.js'
]]);
exports.resources = {
'/production/dialog.css': [
- '/fonts/fonts_common.css',
- '/fonts/fonts_dialog.css',
'/css/common.css',
'/dialog/css/popup.css',
'/dialog/css/m.css'
],
'/production/browserid.css': [
- '/fonts/fonts_common.css',
- '/fonts/fonts_mainsite.css',
'/css/common.css',
'/css/style.css',
'/css/m.css'
@@ -182,7 +178,7 @@ exports.all = function(langs) {
/**
* Get all resource urls for a specified resource based on the locale
*/
-var getResources = exports.getResources = function(path, locale) {
+exports.getResources = getResources = function(path, locale) {
var res = [];
if (exports.resources[path]) {
exports.resources[path].forEach(function(r) {
diff --git a/lib/verifier/certassertion.js b/lib/verifier/certassertion.js
index a8342f3..ae0ee22 100644
--- a/lib/verifier/certassertion.js
+++ b/lib/verifier/certassertion.js
@@ -11,6 +11,7 @@ config = require("../configuration.js"),
logger = require("../logging.js").logger,
secrets = require('../secrets.js'),
primary = require('../primary.js'),
+proxyidp = require('../proxyidp'),
urlparse = require('urlparse');
require("jwcrypto/lib/algs/ds");
@@ -134,7 +135,9 @@ function verify(assertion, audience, successCB, errorCB) {
// that the email's domain delegated authority to the issuer
var domainFromEmail = principal.email.replace(/^.*@/, '');
- if (ultimateIssuer != HOSTNAME && ultimateIssuer !== domainFromEmail)
+ if (ultimateIssuer != HOSTNAME &&
+ ultimateIssuer !== domainFromEmail &&
+ ! proxyidp.isProxyIdP(principal.email))
{
primary.delegatesAuthority(domainFromEmail, ultimateIssuer, function (delegated) {
if (delegated) {
diff --git a/lib/wsapi/add_email_with_assertion.js b/lib/wsapi/add_email_with_assertion.js
index e8649ce..7b208d3 100644
--- a/lib/wsapi/add_email_with_assertion.js
+++ b/lib/wsapi/add_email_with_assertion.js
@@ -23,6 +23,7 @@ exports.i18n = false;
// authenticated.
exports.process = function(req, res) {
// first let's verify that the assertion is valid
+ logger.info('AOK PROXYIPD add_email_with_assertion assertion=' + req.body.assertion);
primary.verifyAssertion(req.body.assertion, function(err, email) {
if (err) {
return res.json({
diff --git a/lib/wsapi/address_info.js b/lib/wsapi/address_info.js
index 1694382..a2938b4 100644
--- a/lib/wsapi/address_info.js
+++ b/lib/wsapi/address_info.js
@@ -3,12 +3,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const
+config = require('../configuration.js'),
db = require('../db.js'),
+httputils = require('../httputils.js'),
primary = require('../primary.js'),
+proxyidp = require('../proxyidp.js'),
wsapi = require('../wsapi.js'),
-httputils = require('../httputils.js'),
url = require('url'),
-logger = require('../logging.js').logger;
+util = require('util');
// return information about an email address.
// type: is this an address with 'primary' or 'secondary' support?
@@ -35,6 +37,7 @@ exports.process = function(req, res) {
}
// Saftey value for production branch only
var done = false;
+ var bt_done = false;
primary.checkSupport(m[1], function(err, urls, publicKey, delegates) {
if (done) {
return;
@@ -48,6 +51,22 @@ exports.process = function(req, res) {
if (urls) {
urls.type = 'primary';
res.json(urls);
+ } else if (proxyidp.isProxyIdP(email)) {
+ // TODO DRY violation: BigTent /.well-known/browserid has these urls too
+ var bigtent = url.parse(config.get('bigtent_url')).hostname;
+
+ primary.checkSupport(bigtent, function(err, urls, publicKey, delegates) {
+ if (bt_done) {
+ return;
+ }
+ bt_done = true;
+ if (err || ! urls) {
+ logger.warn('error checking BigTent for IdP details: ' + err);
+ return httputils.serverError(res, "BigTent unavailable");
+ }
+ urls.type = 'primary';
+ res.json(urls);
+ });
} else {
db.emailKnown(email, function(err, known) {
if (err) {
diff --git a/lib/wsapi/auth_with_assertion.js b/lib/wsapi/auth_with_assertion.js
index 37127d6..db49ec8 100644
--- a/lib/wsapi/auth_with_assertion.js
+++ b/lib/wsapi/auth_with_assertion.js
@@ -38,7 +38,8 @@ exports.process = function(req, res) {
if (err) return wsapi.databaseDown(res, err);
// if this is a known primary email, authenticate the user and we're done!
- if (type === 'primary') {
+ if (type === 'primary' ||
+ type === 'proxyidp') {
return db.emailToUID(email, function(err, uid) {
if (err) return wsapi.databaseDown(res, err);
if (!uid) return res.json({ success: false, reason: "internal error" });
@@ -92,7 +93,6 @@ exports.process = function(req, res) {
return res.json({ success: false, reason: "internal error creating account" });
}
- logger.info("successfully created primary acct for " + email + " (" + r.userid + ")");
wsapi.authenticateSession(req.session, r.userid, 'assertion',
req.body.ephemeral ? config.get('ephemeral_session_duration_ms')
: config.get('authentication_duration_ms'));
diff --git a/lib/wsapi/complete_email_addition.js b/lib/wsapi/complete_email_addition.js
index 53ddb36..abd0124 100644
--- a/lib/wsapi/complete_email_addition.js
+++ b/lib/wsapi/complete_email_addition.js
@@ -23,12 +23,39 @@ exports.process = function(req, res) {
// 1. you must already be authenticated as the user who initiated the verification
// 2. you must provide the password of the initiator.
+ // TRANSITIONAL CODE COMMENT
+ // for issue 1000 we moved initial password selection to the browserid dialog (from
+ // the verification page). Rolling out this change causes some temporal pain.
+ // Outstannding verification links sent before the change was deployed will have
+ // email addition requests that require passwords without passwords in the stage table.
+ // When the verification page is loaded for
+ // these links, we prompt the user for a password. That password is sent up with
+ // the request. this code and comment should all be purged after the new code
+ // has been in production for 2 weeks.
+
+ var transitionalPassword = null;
+
+ // END TRANSITIONAL CODE COMMENT
+
+
db.authForVerificationSecret(req.body.token, function(err, initiator_hash, initiator_uid) {
if (err) {
logger.info("unknown verification secret: " + err);
return wsapi.databaseDown(res, err);
}
+ // TRANSITIONAL CODE
+ if (!initiator_hash) {
+ if (!req.body.pass) return httputils.authRequired(res, "password required");
+ var err = wsapi.checkPassword(req.body.pass);
+ if (err) {
+ logger.warn("invalid password received: " + err);
+ return httputils.badRequest(res, err);
+ }
+ transitionalPassword = req.body.pass;
+ postAuthentication();
+ } else
+ // END TRANSITIONAL CODE
if (req.session.userid === initiator_uid) {
postAuthentication();
} else if (typeof req.body.pass === 'string') {
@@ -54,6 +81,23 @@ exports.process = function(req, res) {
} else {
wsapi.authenticateSession(req.session, uid, 'password');
res.json({ success: true });
+
+ // TRANSITIONAL CODE
+ if (transitionalPassword) {
+ wsapi.bcryptPassword(transitionalPassword, function(err, hash) {
+ if (err) {
+ logger.warn("couldn't bcrypt pass for old verification link: " + err);
+ return;
+ }
+
+ db.updatePassword(uid, hash, function(err) {
+ if (err) {
+ logger.warn("couldn't bcrypt pass for old verification link: " + err);
+ }
+ });
+ });
+ }
+ // END TRANSITIONAL CODE
}
});
};
diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js
index 7a65ec4..5b87c3e 100644
--- a/lib/wsapi/complete_user_creation.js
+++ b/lib/wsapi/complete_user_creation.js
@@ -7,8 +7,7 @@ db = require('../db.js'),
wsapi = require('../wsapi.js'),
httputils = require('../httputils'),
logger = require('../logging.js').logger,
-bcrypt = require('../bcrypt'),
-config = require('../configuration');
+bcrypt = require('../bcrypt');
exports.method = 'post';
exports.writes_db = true;
@@ -29,6 +28,16 @@ exports.process = function(req, res) {
// and then control a browserid account that they can use to prove they own
// the email address of the attacked.
+ // TRANSITIONAL CODE COMMENT
+ // for issue 1000 we moved initial password selection to the browserid dialog (from
+ // the verification page). Rolling out this change causes some temporal pain.
+ // Outstannding verification links sent before the change was deployed will have
+ // new user requests without passwords. When the verification page is loaded for
+ // these links, we prompt the user for a password. That password is sent up with
+ // the request. this code and comment should all be purged after the new code
+ // has been in production for 2 weeks.
+ // END TRANSITIONAL CODE COMMENT
+
// is this the same browser?
if (typeof req.session.pendingCreation === 'string' &&
req.body.token === req.session.pendingCreation) {
@@ -37,6 +46,13 @@ exports.process = function(req, res) {
// is a password provided?
else if (typeof req.body.pass === 'string') {
return db.authForVerificationSecret(req.body.token, function(err, hash) {
+ // TRANSITIONAL CODE
+ // if hash is null, no password was provided during verification and
+ // this is an old-style verification. We accept the password and will
+ // update it after the verification is complete.
+ if (err == 'no password for user' || !hash) return postAuthentication();
+ // END TRANSITIONAL CODE
+
if (err) {
logger.warn("couldn't get password for verification secret: " + err);
return wsapi.databaseDown(res, err);
@@ -58,33 +74,67 @@ exports.process = function(req, res) {
}
function postAuthentication() {
+ // the time the email verification is performed, we'll clear the pendingCreation
+ // data on the session.
+ delete req.session.pendingCreation;
+
db.haveVerificationSecret(req.body.token, function(err, known) {
if (err) return wsapi.databaseDown(res, err);
- if (!known) {
- // clear the pendingCreation token from the session if we find no such
- // token in the database
- delete req.session.pendingCreation;
- return res.json({ success: false} );
- }
+ if (!known) return res.json({ success: false} );
- db.gotVerificationSecret(req.body.token, function(err, email, uid) {
- if (err) {
- logger.warn("couldn't complete email verification: " + err);
- wsapi.databaseDown(res, err);
- } else {
- // clear the pendingCreation token from the session once we
- // successfully complete user creation
- delete req.session.pendingCreation;
+ // TRANSITIONAL CODE
+ // user is authorized (1 or 2 above) OR user has no password set, in which
+ // case for a short time we'll accept the password provided with the verification
+ // link, and set it as theirs.
+ var transitionalPassword = null;
- // At this point, the user is either on the same browser with a token from
- // their email address, OR they've provided their account password. It's
- // safe to grant them an authenticated session.
- wsapi.authenticateSession(req.session, uid, 'password',
- config.get('ephemeral_session_duration_ms'));
- res.json({ success: true });
+ db.authForVerificationSecret(req.body.token, function(err, hash) {
+ if (err == 'no password for user' || !hash) {
+ if (!req.body.pass) return httputils.authRequired(res, "password required");
+ err = wsapi.checkPassword(req.body.pass);
+ if (err) {
+ logger.warn("invalid password received: " + err);
+ return httputils.badRequest(res, err);
+ }
+ transitionalPassword = req.body.pass;
}
+ completeCreation();
});
+ // END TRANSITIONAL CODE
+
+ function completeCreation() {
+ db.gotVerificationSecret(req.body.token, function(err, email, uid) {
+ if (err) {
+ logger.warn("couldn't complete email verification: " + err);
+ wsapi.databaseDown(res, err);
+ } else {
+ // FIXME: not sure if we want to do this (ba)
+ // at this point the user has set a password associated with an email address
+ // that they've verified. We create an authenticated session.
+ wsapi.authenticateSession(req.session, uid, 'password',
+ config.get('ephemeral_session_duration_ms'));
+ res.json({ success: true });
+
+ // TRANSITIONAL CODE
+ if (transitionalPassword) {
+ wsapi.bcryptPassword(transitionalPassword, function(err, hash) {
+ if (err) {
+ logger.warn("couldn't bcrypt pass for old verification link: " + err);
+ return;
+ }
+
+ db.updatePassword(uid, hash, function(err) {
+ if (err) {
+ logger.warn("couldn't bcrypt pass for old verification link: " + err);
+ }
+ });
+ });
+ }
+ // END TRANSITIONAL CODE
+ }
+ });
+ }
});
}
};
diff --git a/lib/wsapi/stage_email.js b/lib/wsapi/stage_email.js
index 9ad39e9..60129dd 100644
--- a/lib/wsapi/stage_email.js
+++ b/lib/wsapi/stage_email.js
@@ -8,8 +8,7 @@ wsapi = require('../wsapi.js'),
httputils = require('../httputils'),
logger = require('../logging.js').logger,
email = require('../email.js'),
-sanitize = require('../sanitize'),
-config = require('../configuration');
+sanitize = require('../sanitize');
/* First half of account creation. Stages a user account for creation.
* this involves creating a secret url that must be delivered to the
diff --git a/lib/wsapi/stage_user.js b/lib/wsapi/stage_user.js
index 8498194..ff1dd24 100644
--- a/lib/wsapi/stage_user.js
+++ b/lib/wsapi/stage_user.js
@@ -8,8 +8,7 @@ wsapi = require('../wsapi.js'),
httputils = require('../httputils'),
logger = require('../logging.js').logger,
email = require('../email.js'),
-sanitize = require('../sanitize'),
-config = require('../configuration');
+sanitize = require('../sanitize');
/* First half of account creation. Stages a user account for creation.
* this involves creating a secret url that must be delivered to the
@@ -60,7 +59,7 @@ exports.process = function(req, res) {
if (err) {
if (err.indexOf('exceeded') != -1) {
logger.warn("max load hit, failing on auth request with 503: " + err);
- return httputils.serviceUnavailable(res, "server is too busy");
+ return httputils.serviceUnavailable("server is too busy");
}
logger.error("can't bcrypt: " + err);
return res.json({ success: false });
diff --git a/lib/wsapi_client.js b/lib/wsapi_client.js
index 81dbce7..c6ae5bd 100644
--- a/lib/wsapi_client.js
+++ b/lib/wsapi_client.js
@@ -43,7 +43,7 @@ exports.clearCookies = function(ctx) {
};
exports.getCookie = function(ctx, which) {
- if (typeof which === 'string') which = new RegExp('/^' + which + '$/');
+ if (typeof which === 'string') which = new Regex('/^' + which + '$/');
var cookieNames = Object.keys(ctx.cookieJar);
for (var i = 0; i < cookieNames.length; i++) {
if (which.test(cookieNames[i])) return ctx.cookieJar[cookieNames[i]];
@@ -114,7 +114,6 @@ exports.post = function(cfg, path, context, postArgs, cb) {
// parse the server URL (cfg.browserid)
var uObj;
var meth;
- var body;
try {
uObj = url.parse(cfg.browserid);
meth = uObj.protocol === 'http:' ? http : https;
diff --git a/package.json b/package.json
index 8157d2e..c9412e2 100644
--- a/package.json
+++ b/package.json
@@ -35,8 +35,9 @@
"winston": "0.5.6"
},
"devDependencies": {
+ "xml2js": "0.1.13",
"vows": "0.5.13",
- "awsbox": "0.2.10",
+ "aws-lib": "0.0.5",
"irc": "0.3.3"
},
"scripts": {
diff --git a/resources/static/css/common.css b/resources/static/css/common.css
index c12bdae..c380446 100644
--- a/resources/static/css/common.css
+++ b/resources/static/css/common.css
@@ -291,6 +291,7 @@ a.secondary[disabled], .submit_disabled a.secondary, .submit_disabled a.secondar
}
.headline-main, h1, h2, h3, h4 {
+ font-family: 'Open Sans', "Lucida Sans", "Lucida Grande", "Lucida Sans Unicode", Verdana, sans-serif;
font-weight: normal;
text-shadow: 0px 1px 0px rgba(255,255,255,0.75);
}
diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js
index 80435e5..e01cb57 100644
--- a/resources/static/dialog/controllers/actions.js
+++ b/resources/static/dialog/controllers/actions.js
@@ -58,10 +58,6 @@ BrowserID.Modules.Actions = (function() {
if(data.ready) _.defer(data.ready);
},
- doRPInfo: function(info) {
- startService("rp_info", info);
- },
-
doCancel: function() {
if(onsuccess) onsuccess(null);
},
diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js
index ac413aa..2069a57 100644
--- a/resources/static/dialog/controllers/authenticate.js
+++ b/resources/static/dialog/controllers/authenticate.js
@@ -40,7 +40,6 @@ BrowserID.Modules.Authenticate = (function() {
function checkEmail(info) {
var email = getEmail(),
self = this;
-
if (!email) return;
if(info && info.type) {
@@ -54,11 +53,9 @@ BrowserID.Modules.Authenticate = (function() {
function onAddressInfo(info) {
addressInfo = info;
-
- if(info.type === "primary") {
+ if(info.IdPEnabled) {
self.close("primary_user", info, info);
- }
- else if(info.known) {
+ } else if(info.known) {
enterPasswordState.call(self);
} else {
createSecondaryUser.call(self);
@@ -120,8 +117,6 @@ BrowserID.Modules.Authenticate = (function() {
function enterPasswordState(callback) {
var self=this;
- dom.setInner("#password", "");
-
self.publish("enter_password", addressInfo);
self.submit = authenticate;
showHint("returning", function() {
@@ -149,7 +144,6 @@ BrowserID.Modules.Authenticate = (function() {
var Module = bid.Modules.PageModule.extend({
start: function(options) {
options = options || {};
-
addressInfo = null;
lastEmail = options.email || "";
diff --git a/resources/static/dialog/controllers/dialog.js b/resources/static/dialog/controllers/dialog.js
index 8d9725d..8e5b8f1 100644
--- a/resources/static/dialog/controllers/dialog.js
+++ b/resources/static/dialog/controllers/dialog.js
@@ -75,6 +75,7 @@ BrowserID.Modules.Dialog = (function() {
function setOrigin(origin) {
user.setOrigin(origin);
+ dom.setInner("#sitename", user.getHostname());
}
function onWindowUnload() {
@@ -92,12 +93,6 @@ BrowserID.Modules.Dialog = (function() {
return encodeURI(u.validate().normalize().toString());
}
- function fixupAbsolutePath(origin_url, path) {
- if (/^\//.test(path)) return fixupURL(origin_url, path);
-
- throw "must be an absolute path: (" + path + ")";
- }
-
var Dialog = bid.Modules.PageModule.extend({
start: function(options) {
var self=this;
@@ -173,17 +168,6 @@ BrowserID.Modules.Dialog = (function() {
params.privacyURL = fixupURL(origin_url, paramsFromRP.privacyPolicy);
}
- if (paramsFromRP.siteLogo) {
- // Until we have our head around the dangers of data uris and images
- // that come from other domains, only allow absolute paths from the
- // origin.
- params.siteLogo = fixupAbsolutePath(origin_url, paramsFromRP.siteLogo);
- }
-
- if (paramsFromRP.siteName) {
- params.siteName = _.escape(paramsFromRP.siteName);
- }
-
if (hash.indexOf("#CREATE_EMAIL=") === 0) {
var email = hash.replace(/#CREATE_EMAIL=/, "");
if (!bid.verifyEmail(email))
diff --git a/resources/static/dialog/controllers/provision_primary_user.js b/resources/static/dialog/controllers/provision_primary_user.js
index 367195e..7f9469a 100644
--- a/resources/static/dialog/controllers/provision_primary_user.js
+++ b/resources/static/dialog/controllers/provision_primary_user.js
@@ -57,7 +57,7 @@ BrowserID.Modules.ProvisionPrimaryUser = (function() {
if(!(auth && prov)) {
user.addressInfo(email, function(status) {
- if(status.type === "primary") {
+ if(status.IdPEnabled) {
provisionPrimaryUser.call(self, email, status.auth, status.prov);
}
else {
diff --git a/resources/static/dialog/controllers/required_email.js b/resources/static/dialog/controllers/required_email.js
index 6e5f97e..982a727 100644
--- a/resources/static/dialog/controllers/required_email.js
+++ b/resources/static/dialog/controllers/required_email.js
@@ -140,7 +140,7 @@ BrowserID.Modules.RequiredEmail = (function() {
// 2) Authenticated user who does not control address.
// 3) Unauthenticated user.
user.addressInfo(email, function(info) {
- if(info.type === "primary") primaryInfo = info;
+ if(info.IdPEnabled) primaryInfo = info;
if (info.type === "primary" && info.authed) {
// this is a primary user who is authenticated with their IdP.
diff --git a/resources/static/dialog/controllers/rp_info.js b/resources/static/dialog/controllers/rp_info.js
deleted file mode 100644
index 3bc9b20..0000000
--- a/resources/static/dialog/controllers/rp_info.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
-/*global _: true, BrowserID: true, PageController: true */
-/* 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/. */
-
-
-/**
- * Purpose:
- * Display to the user RP related data such as hostname, name, and logo.
- */
-BrowserID.Modules.RPInfo = (function() {
- "use strict";
-
- var bid = BrowserID,
- renderer = bid.Renderer,
- sc;
-
- var Module = bid.Modules.PageModule.extend({
- start: function(options) {
- options = options || {};
-
- /**
- * Very important security info - it is assumed all parameters are
- * already properly escaped before being passed here. This is done
- * in dialog.js. Check it.
- *
- * hostname is set internally based on the RP URL,
- * so it will not be escaped. It is set initially in user.js at the very
- * bottom for the main site, and then in dialog.js->get for the dialog.
- */
- renderer.render("#rp_info", "rp_info", {
- hostname: options.hostname,
- siteName: options.siteName,
- siteLogo: options.siteLogo
- });
-
- sc.start.call(this, options);
- }
- });
-
- sc = Module.sc;
-
- return Module;
-
-}());
-
diff --git a/resources/static/dialog/controllers/verify_primary_user.js b/resources/static/dialog/controllers/verify_primary_user.js
index 9e38e6b..67da7d1 100644
--- a/resources/static/dialog/controllers/verify_primary_user.js
+++ b/resources/static/dialog/controllers/verify_primary_user.js
@@ -1,5 +1,6 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
-/*global _: true, BrowserID: true, PageController: true */
+/*jshint browser:true, jQuery: true, forin: false, laxbreak:true */
+/*global _: true, BrowserID: true */
+
/* 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/. */
@@ -7,56 +8,106 @@ BrowserID.Modules.VerifyPrimaryUser = (function() {
"use strict";
var bid = BrowserID,
+ user = bid.User,
+ errors = bid.Errors,
+ wait = bid.Wait,
sc,
win,
add,
email,
auth_url,
helpers = bid.Helpers,
- complete = helpers.complete;
-
- function verify(callback) {
- this.publish("primary_user_authenticating");
-
- // replace any hashes that may be there already.
- var returnTo = win.document.location.href.replace(/#.*$/, "");
-
- var type = add ? "ADD_EMAIL" : "CREATE_EMAIL";
- var url = helpers.toURL(auth_url, {
- email: email,
- return_to: returnTo + "#" + type + "=" +email
- });
+ complete = helpers.complete,
+ delayScreenTimeout,
+ isProxyIdP;
+
+ // yahoo resizes themselves
+ var RESIZE_TABLE = {
+ "gmail.com$": { w: 900, h: 600 },
+ "hotmail.com$": { w: 700, h: 488 }
+ };
+
+ function resizeWindow(email) {
+ for(var key in RESIZE_TABLE) {
+ var regExp = new RegExp(key);
+ if (regExp.test(email)) {
+ var dimensions = RESIZE_TABLE[key];
+ win.resizeTo(dimensions.w, dimensions.h);
+ return;
+ }
+ }
+ }
- win.document.location = url;
- complete(callback);
+ function verify(callback) {
+ var self = this,
+ // replace any hashes that may be there already.
+ returnTo = win.document.location.href.replace(/#.*$/, ""),
+ type = add ? "ADD_EMAIL" : "CREATE_EMAIL",
+ url = helpers.toURL(auth_url, {
+ email: email,
+ return_to: returnTo + "#" + type + "=" +email
+ });
+
+ // primary_user_authenticating must be published before the wait
+ // screen is rendered or else the wait screen is taken away when all the
+ // modules are stopped.
+ self.publish("primary_user_authenticating", { url: url });
+ self.renderWait("wait", wait.redirectToIdP);
+
+ // Use the setTimeout delay so the wait screen actually renders. If the
+ // document's location is redirected before the screen is displayed, the
+ // user never sees it and it looks pretty ugly.
+ setTimeout(function() {
+ // only resize the window if redirecting to a proxyIdP. All other IdPs
+ // should abide by our rules of 700x400 default.
+ if (isProxyIdP) resizeWindow(email);
+ win.document.location = url;
+
+ complete(callback);
+ }, delayScreenTimeout);
}
function cancel(callback) {
this.close("cancel_state");
- callback && callback();
+ complete(callback);
}
var Module = bid.Modules.PageModule.extend({
- start: function(data) {
+ start: function(options) {
var self=this;
- data = data || {};
-
- win = data.window || window;
- add = data.add;
- email = data.email;
- auth_url = data.auth_url;
-
- var templateData = helpers.extend({}, data, {
- requiredEmail: data.requiredEmail || false,
- privacy_url: data.privacyURL || null,
- tos_url: data.tosURL || null
- });
- self.renderDialog("verify_primary_user", templateData);
-
- self.click("#cancel", cancel);
-
- sc.start.call(self, data);
+ options = options || {};
+
+ win = options.window || window;
+ add = options.add;
+ email = options.email;
+ delayScreenTimeout = typeof options.delay_screen_timeout === "number" ? options.delay_screen_timeout : 500;
+
+ sc.start.call(self, options);
+
+ user.addressInfo(email, function(addressInfo) {
+ auth_url = addressInfo.auth;
+ isProxyIdP = addressInfo.type === "proxyidp";
+
+ // immediately call verify if the user is being shuffled off to a proxy
+ // idp. This skips the verification screen that normal IdP users see.
+ // Inconsistent - yet. Perhaps we will change this universally.
+ if (isProxyIdP) {
+ verify.call(self, options.ready);
+ }
+ else {
+ var templateData = helpers.extend({}, options, {
+ auth_url: addressInfo.auth || null,
+ requiredEmail: options.requiredEmail || false,
+ privacy_url: options.privacyURL || null,
+ tos_url: options.tosURL || null
+ });
+ self.renderDialog("verify_primary_user", templateData);
+
+ self.click("#cancel", cancel);
+ complete(options.ready);
+ }
+ }, self.getErrorDialog(errors.addressInfo));
},
submit: verify
diff --git a/resources/static/dialog/css/m.css b/resources/static/dialog/css/m.css
index 2a75780..6991dbe 100644
--- a/resources/static/dialog/css/m.css
+++ b/resources/static/dialog/css/m.css
@@ -43,7 +43,7 @@
}
#signIn {
- top: auto; /* this will be set in JS to be at the bottom of the header */
+ top: 45px; /* 45px is a magic number - the height of the favicon area */
right: 0;
width: auto;
padding: 0;
@@ -57,41 +57,13 @@
* being partially cut off by the site URL bar
*/
position: static;
- padding: 10px;
- border-bottom: 1px solid rgba(0,0,0,0.05);
- text-align: center;
- left: 0;
- }
-
- #favicon img {
- max-width: 32px;
- max-height: 32px;
- display: inline;
- margin: 0 10px 0 0;
- vertical-align: middle;
- }
-
- #favicon h2, #favicon h3 {
- margin: 0 5px;
- height: auto;
- line-height: 32px;
- font-size: 20px;
- display: inline;
- vertical-align: middle;
- }
-
- #favicon h3 {
- font-size: 16px;
}
- #favicon .vertical {
- height: auto;
- line-height: 32px;
- vertical-align: middle;
+ #favicon {
+ padding: 10px;
}
-
- #signIn .table, #signIn .container {
+ #signIn .table {
width: 100%;
}
@@ -107,8 +79,21 @@
padding: 0;
}
- #formWrap {
- background-color: transparent;
+ #favicon {
+ text-align: center;
+ }
+
+ #favicon img {
+ width: 32px;
+ height: auto;
+ display: inline;
+ margin: 0;
+ vertical-align: middle;
+ }
+
+ #favicon .vertical {
+ height: auto;
+ line-height: 20px;
}
.form #formWrap, .waiting #wait, .delay #delay, #error #error {
diff --git a/resources/static/dialog/css/popup.css b/resources/static/dialog/css/popup.css
index 39d97a1..8a655b7 100644
--- a/resources/static/dialog/css/popup.css
+++ b/resources/static/dialog/css/popup.css
@@ -51,6 +51,9 @@ header {
* overflow the dialog box.
*/
*padding: 10px 0;
+ /*
+ font-weight: bold;
+ */
border-bottom: 1px solid #c7c6c1;
/*-ms-filter through zoom: 1 are fixes for IE6 and IE7 so they show the header
@@ -71,8 +74,6 @@ header {
background: url("/i/persona-logo-transparent.png") 0 0 no-repeat;
text-indent: -9999px;
display: inline-block;
- *display: block;
- zoom: 1;
}
footer {
@@ -152,10 +153,10 @@ section > .contents {
/* Fix for IE6 not displaying the unsupported dialog correctly. IE6 by
* default sets the height and width of the element to 0 meaning nothing
* shows up on the screen.
- * Note, height is a magic number that depend on the height of the
+ * Note, these are magic numbers that depend on the width and height of the
* dialog. The height also depends on the height of the header and footer.
*/
- _width: 100%;
+ _width: 682px;
_height: 250px;
}
@@ -270,8 +271,7 @@ section > .contents {
left: 400px;
top: 0;
bottom: 0;
- right: 20px; /* The same as the left padding of the left hand side */
- overflow: hidden;
+ right: 0;
z-index: 10;
}
@@ -283,31 +283,14 @@ section > .contents {
#favicon img {
display: block;
- margin: 0 auto;
- max-height: 128px;
- max-width: 128px;
-}
-
-#favicon h2, #favicon h3 {
- white-space: nowrap;
- text-overflow: ellipsis;
- line-height: 1.3; /* the 1.3em is to keep y, g, j, etc from having their bottoms chopped off */
- overflow: hidden;
-}
-
-#favicon h2 {
- margin: 10px 0 0 0;
-}
-
-#favicon h3 {
- font-size: 19px;
- margin-top: 0;
+ margin: 0 auto 10px;
}
#favicon .vertical {
display: table-cell;
text-align: center;
- max-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
div#required_email {
@@ -458,7 +441,3 @@ a.emphasize:active {
min-width: 4em;
}
-.unsupported, .cookies_disabled {
- text-align: center;
-}
-
diff --git a/resources/static/dialog/resources/helpers.js b/resources/static/dialog/resources/helpers.js
index 6422560..9f86736 100644
--- a/resources/static/dialog/resources/helpers.js
+++ b/resources/static/dialog/resources/helpers.js
@@ -105,7 +105,7 @@
}
else {
user.addressInfo(email, function(info) {
- if (info.type === "primary") {
+ if (info.IdPEnabled) {
var info = _.extend(info, { email: email, add: true });
self.publish("primary_user", info, info);
complete(callback, true);
diff --git a/resources/static/dialog/resources/screen_size_hacks.js b/resources/static/dialog/resources/screen_size_hacks.js
index 33685cb..503d848 100644
--- a/resources/static/dialog/resources/screen_size_hacks.js
+++ b/resources/static/dialog/resources/screen_size_hacks.js
@@ -8,15 +8,13 @@
*/
function onResize() {
var selectEmailEl = $("#selectEmail"),
- contentEl = $("#content"),
- signInEl = $("#signIn");
+ contentEl = $("#content");
selectEmailEl.css("position", "static");
if($(window).width() >= 640) {
// First, remove the mobile hacks
selectEmailEl.css("width", "");
contentEl.css("min-height", "");
- signInEl.css("top", "");
// This is a hack for desktop mode which centers the form vertically in
// the middle of its container. We have to do this hack because we use
@@ -107,11 +105,6 @@
contentEl.css("min-height", contentHeight + "px");
$("section,#signIn").css("position", "");
-
- favIconHeight = $("#favicon").outerHeight();
-
- // Force the top of the main content area to be below the favicon area.
- signInEl.css("top", favIconHeight + "px");
}
selectEmailEl.css("position", "");
diff --git a/resources/static/dialog/resources/state.js b/resources/static/dialog/resources/state.js
index c56eee0..d9756d1 100644
--- a/resources/static/dialog/resources/state.js
+++ b/resources/static/dialog/resources/state.js
@@ -35,7 +35,7 @@ BrowserID.State = (function() {
save = true;
}
- var func = controller[msg].bind(controller);
+ var func = self.controller[msg].bind(self.controller);
self.gotoState(save, func, options);
},
cancelState = self.popState.bind(self);
@@ -44,9 +44,7 @@ BrowserID.State = (function() {
self.hostname = info.hostname;
self.privacyURL = info.privacyURL;
self.tosURL = info.tosURL;
- requiredEmail = info.requiredEmail;
-
- startAction(false, "doRPInfo", info);
+ var requiredEmail = self.requiredEmail = info.requiredEmail;
if (info.email && info.type === "primary") {
primaryVerificationInfo = info;
@@ -71,7 +69,8 @@ BrowserID.State = (function() {
});
handleState("authentication_checked", function(msg, info) {
- var authenticated = info.authenticated;
+ var authenticated = info.authenticated,
+ requiredEmail = self.requiredEmail;
if (requiredEmail) {
self.email = requiredEmail;
@@ -112,15 +111,15 @@ BrowserID.State = (function() {
*/
info = _.extend({ email: self.newUserEmail || self.addEmailEmail || self.resetPasswordEmail }, info);
- if(self.newUserEmail) {
+ if (self.newUserEmail) {
self.newUserEmail = null;
startAction(false, "doStageUser", info);
}
- else if(self.addEmailEmail) {
+ else if (self.addEmailEmail) {
self.addEmailEmail = null;
startAction(false, "doStageEmail", info);
}
- else if(self.resetPasswordEmail) {
+ else if (self.resetPasswordEmail) {
self.resetPasswordEmail = null;
startAction(false, "doResetPassword", info);
}
@@ -128,7 +127,7 @@ BrowserID.State = (function() {
handleState("user_staged", function(msg, info) {
self.stagedEmail = info.email;
- info.required = !!requiredEmail;
+ info.required = !!self.requiredEmail;
startAction("doConfirmUser", info);
});
@@ -138,10 +137,9 @@ BrowserID.State = (function() {
});
handleState("primary_user", function(msg, info) {
- addPrimaryUser = !!info.add;
- email = info.email;
-
- var idInfo = storage.getEmail(email);
+ self.addPrimaryUser = !!info.add;
+ var email = self.email = info.email,
+ idInfo = storage.getEmail(email);
if (idInfo && idInfo.cert) {
redirectToState("primary_user_ready", info);
}
@@ -154,25 +152,28 @@ BrowserID.State = (function() {
});
handleState("primary_user_provisioned", function(msg, info) {
- info.add = !!addPrimaryUser;
// The user is is authenticated with their IdP. Two possibilities exist
// for the email - 1) create a new account or 2) add address to the
// existing account. If the user is authenticated with BrowserID, #2
// will happen. If not, #1.
+ info = info || {};
+ info.add = !!self.addPrimaryUser;
startAction("doPrimaryUserProvisioned", info);
});
handleState("primary_user_unauthenticated", function(msg, info) {
- info = helpers.extend(info, {
- add: !!addPrimaryUser,
- email: email,
+ var requiredEmail = self.requiredEmail;
+
+ info = helpers.extend(info || {}, {
+ add: !!self.addPrimaryUser,
+ email: self.email,
requiredEmail: !!requiredEmail,
privacyURL: self.privacyURL,
tosURL: self.tosURL
});
- if (primaryVerificationInfo) {
- primaryVerificationInfo = null;
+ if (self.primaryVerificationInfo) {
+ self.primaryVerificationInfo = null;
if (requiredEmail) {
startAction("doCannotVerifyRequiredPrimary", info);
}
@@ -188,6 +189,7 @@ BrowserID.State = (function() {
}
else {
startAction("doVerifyPrimaryUser", info);
+ complete(info.complete);
}
});
@@ -355,7 +357,7 @@ BrowserID.State = (function() {
handleState("stage_email", function(msg, info) {
user.passwordNeededToAddSecondaryEmail(function(passwordNeeded) {
- if(passwordNeeded) {
+ if (passwordNeeded) {
self.addEmailEmail = info.email;
// cancel is disabled if the user is doing the initial password set
// for a requiredEmail.
@@ -372,7 +374,7 @@ BrowserID.State = (function() {
handleState("email_staged", function(msg, info) {
self.stagedEmail = info.email;
- info.required = !!requiredEmail;
+ info.required = !!self.requiredEmail;
startAction("doConfirmEmail", info);
});
@@ -388,18 +390,17 @@ BrowserID.State = (function() {
var State = BrowserID.StateMachine.extend({
start: function(options) {
+ var self=this;
+
options = options || {};
- controller = options.controller;
- if (!controller) {
+ self.controller = options.controller;
+ if (!self.controller) {
throw "start: controller must be specified";
}
- addPrimaryUser = false;
- email = requiredEmail = null;
-
- State.sc.start.call(this, options);
- startStateMachine.call(this);
+ State.sc.start.call(self, options);
+ startStateMachine.call(self);
}
});
diff --git a/resources/static/dialog/start.js b/resources/static/dialog/start.js
index 70c574b..543136b 100644
--- a/resources/static/dialog/start.js
+++ b/resources/static/dialog/start.js
@@ -42,7 +42,6 @@
moduleManager.register("xhr_delay", modules.XHRDelay);
moduleManager.register("xhr_disable_form", modules.XHRDisableForm);
moduleManager.register("set_password", modules.SetPassword);
- moduleManager.register("rp_info", modules.RPInfo);
moduleManager.start("xhr_delay");
moduleManager.start("xhr_disable_form");
diff --git a/resources/static/dialog/views/rp_info.ejs b/resources/static/dialog/views/rp_info.ejs
deleted file mode 100644
index 5928a18..0000000
--- a/resources/static/dialog/views/rp_info.ejs
+++ /dev/null
@@ -1,21 +0,0 @@
-<% /* 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/. */ %>
-
-<% if(siteLogo) { %>
- <img id="rp_logo" src="<%= siteLogo %>" />
-<% } %>
-
-
-<% if(siteName) { %>
- <h2 id="rp_name"><%= siteName %></h2>
-<% } %>
-
-<% if(hostname) { %>
- <% if(siteName) { %>
- <h3 id="rp_hostname"><%= hostname %></h3>
- <% } else { %>
- <h2 id="rp_hostname"><%= hostname %></h2>
- <% } %>
-<% } %>
-
diff --git a/resources/static/dialog/views/test_template_with_input.ejs b/resources/static/dialog/views/test_template_with_input.ejs
index ff302b1..86d4ffa 100644
--- a/resources/static/dialog/views/test_template_with_input.ejs
+++ b/resources/static/dialog/views/test_template_with_input.ejs
@@ -2,7 +2,5 @@
- 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/. -->
-<form>
- <input id="templateInput" type="text" value="" />
-</form>
+<input id="templateInput" type="text" value="" />
diff --git a/resources/static/fonts/OpenSans-Bold.eot b/resources/static/fonts/OpenSans-Bold.eot
deleted file mode 100644
index 7c74b9c..0000000
Binary files a/resources/static/fonts/OpenSans-Bold.eot and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-Bold.ttf b/resources/static/fonts/OpenSans-Bold.ttf
deleted file mode 100644
index fd79d43..0000000
Binary files a/resources/static/fonts/OpenSans-Bold.ttf and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-Bold.woff b/resources/static/fonts/OpenSans-Bold.woff
deleted file mode 100644
index a8b5bc2..0000000
Binary files a/resources/static/fonts/OpenSans-Bold.woff and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-BoldItalic.eot b/resources/static/fonts/OpenSans-BoldItalic.eot
deleted file mode 100644
index e1c6979..0000000
Binary files a/resources/static/fonts/OpenSans-BoldItalic.eot and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-BoldItalic.ttf b/resources/static/fonts/OpenSans-BoldItalic.ttf
deleted file mode 100644
index 9bc8009..0000000
Binary files a/resources/static/fonts/OpenSans-BoldItalic.ttf and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-BoldItalic.woff b/resources/static/fonts/OpenSans-BoldItalic.woff
deleted file mode 100644
index 6a18338..0000000
Binary files a/resources/static/fonts/OpenSans-BoldItalic.woff and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-Italic.eot b/resources/static/fonts/OpenSans-Italic.eot
deleted file mode 100644
index 3fa5327..0000000
Binary files a/resources/static/fonts/OpenSans-Italic.eot and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-Italic.ttf b/resources/static/fonts/OpenSans-Italic.ttf
deleted file mode 100644
index c90da48..0000000
Binary files a/resources/static/fonts/OpenSans-Italic.ttf and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-Italic.woff b/resources/static/fonts/OpenSans-Italic.woff
deleted file mode 100644
index 039a294..0000000
Binary files a/resources/static/fonts/OpenSans-Italic.woff and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-Light.eot b/resources/static/fonts/OpenSans-Light.eot
deleted file mode 100644
index 3c203d8..0000000
Binary files a/resources/static/fonts/OpenSans-Light.eot and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-Light.ttf b/resources/static/fonts/OpenSans-Light.ttf
deleted file mode 100644
index 0d38189..0000000
Binary files a/resources/static/fonts/OpenSans-Light.ttf and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-Light.woff b/resources/static/fonts/OpenSans-Light.woff
deleted file mode 100644
index 8025ba6..0000000
Binary files a/resources/static/fonts/OpenSans-Light.woff and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-LightItalic.eot b/resources/static/fonts/OpenSans-LightItalic.eot
deleted file mode 100644
index 6005799..0000000
Binary files a/resources/static/fonts/OpenSans-LightItalic.eot and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-LightItalic.ttf b/resources/static/fonts/OpenSans-LightItalic.ttf
deleted file mode 100644
index 68299c4..0000000
Binary files a/resources/static/fonts/OpenSans-LightItalic.ttf and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-LightItalic.woff b/resources/static/fonts/OpenSans-LightItalic.woff
deleted file mode 100644
index c1e29dc..0000000
Binary files a/resources/static/fonts/OpenSans-LightItalic.woff and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-Regular.eot b/resources/static/fonts/OpenSans-Regular.eot
deleted file mode 100644
index 091cd51..0000000
Binary files a/resources/static/fonts/OpenSans-Regular.eot and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-Regular.ttf b/resources/static/fonts/OpenSans-Regular.ttf
deleted file mode 100644
index db43334..0000000
Binary files a/resources/static/fonts/OpenSans-Regular.ttf and /dev/null differ
diff --git a/resources/static/fonts/OpenSans-Regular.woff b/resources/static/fonts/OpenSans-Regular.woff
deleted file mode 100644
index 58e6cb3..0000000
Binary files a/resources/static/fonts/OpenSans-Regular.woff and /dev/null differ
diff --git a/resources/static/fonts/fonts_common.css b/resources/static/fonts/fonts_common.css
deleted file mode 100644
index a88cb32..0000000
--- a/resources/static/fonts/fonts_common.css
+++ /dev/null
@@ -1,33 +0,0 @@
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 400;
- src: url('/fonts/OpenSans-Regular.eot');
- src: local('Open Sans'),
- local('OpenSans'),
- url('/fonts/OpenSans-Regular.eot') format('embedded-opentype'),
- url('/fonts/OpenSans-Regular.woff') format('woff'),
- url('/fonts/OpenSans-Regular.ttf') format('truetype');
-}
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 300;
- src: url('/fonts/OpenSans-Light.eot');
- src: local('Open Sans Light'),
- local('OpenSans-Light'),
- url('/fonts/OpenSans-Light.eot') format('embedded-opentype'),
- url('/fonts/OpenSans-Light.woff') format('woff'),
- url('/fonts/OpenSans-Light.ttf') format('truetype');
-}
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 700;
- src: url('/fonts/OpenSans-Bold.eot');
- src: local('Open Sans Bold'),
- local('OpenSans-Bold'),
- url('/fonts/OpenSans-Bold.eot') format('embedded-opentype'),
- url('/fonts/OpenSans-Bold.woff') format('woff'),
- url('/fonts/OpenSans-Bold.ttf') format('truetype');
-}
diff --git a/resources/static/fonts/fonts_dialog.css b/resources/static/fonts/fonts_dialog.css
deleted file mode 100644
index e69de29..0000000
diff --git a/resources/static/fonts/fonts_mainsite.css b/resources/static/fonts/fonts_mainsite.css
deleted file mode 100644
index a425735..0000000
--- a/resources/static/fonts/fonts_mainsite.css
+++ /dev/null
@@ -1,12 +0,0 @@
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 400;
- src: url('/fonts/OpenSans-Italic.eot');
- src: local('Open Sans Italic'),
- local('OpenSans-Italic'),
- url('/fonts/OpenSans-Italic.eot') format('embedded-opentype'),
- url('/fonts/OpenSans-Italic.woff') format('woff'),
- url('/fonts/OpenSans-Italic.ttf') format('truetype');
-}
-
diff --git a/resources/static/include_js/include.js b/resources/static/include_js/include.js
index 42c8bf9..6da3e97 100644
--- a/resources/static/include_js/include.js
+++ b/resources/static/include_js/include.js
@@ -1112,15 +1112,11 @@
navigator.id = {
request: function(options) {
- if (this != navigator.id)
- throw new Error("all navigator.id calls must be made on the navigator.id object");
options = options || {};
checkCompat(false);
return internalRequest(options);
},
watch: function(options) {
- if (this != navigator.id)
- throw new Error("all navigator.id calls must be made on the navigator.id object");
checkCompat(false);
internalWatch(options);
},
@@ -1128,8 +1124,6 @@
// The callback parameter is DEPRECATED, instead you should use the
// the .onlogout observer of the .watch() api.
logout: function(callback) {
- if (this != navigator.id)
- throw new Error("all navigator.id calls must be made on the navigator.id object");
// allocate iframe if it is not allocated
_open_hidden_iframe();
// send logout message if the commChan exists
diff --git a/resources/static/pages/page_helpers.js b/resources/static/pages/page_helpers.js
index 8510ff2..966cf9b 100644
--- a/resources/static/pages/page_helpers.js
+++ b/resources/static/pages/page_helpers.js
@@ -145,6 +145,7 @@ BrowserID.PageHelpers = (function() {
url: "https://browserid.org/authenticate_with_primary",
// This is the relay that will be used when the IdP redirects to sign_in_complete
relay_url: "https://browserid.org/relay",
+ // Caution: If you change this width or height, also update mozilla/browserid-bigtent
window_features: "width=700,height=375",
params: url
}, function(error, result) {
diff --git a/resources/static/pages/signup.js b/resources/static/pages/signup.js
index 3fef51b..dcb59a7 100644
--- a/resources/static/pages/signup.js
+++ b/resources/static/pages/signup.js
@@ -108,7 +108,7 @@ BrowserID.signUp = (function() {
}
else {
user.addressInfo(email, function(info) {
- if(info.type === "primary") {
+ if(info.IdPEnabled) {
createPrimaryUser.call(self, info, oncomplete);
}
else {
diff --git a/resources/static/pages/start.js b/resources/static/pages/start.js
index f6bb1b2..7f735a5 100644
--- a/resources/static/pages/start.js
+++ b/resources/static/pages/start.js
@@ -59,35 +59,20 @@ $(function() {
return divHeight === 110;
}
- function elementHeightWithMargins(element) {
- element = $(element);
- var height = element.outerHeight()
- + parseInt(element.css("margin-top"), 10)
- + parseInt(element.css("margin-bottom"), 10);
- return height;
- }
-
-
xhr.init({ time_until_delay: 10 * 1000 });
network.init();
$(".display_always,.display_auth,.display_nonauth").hide();
-
$(window).bind('resize', function() {
- var height = $(window).height()
- // To find the height of the content, subtract the height of the
- // header and footer INCLUDING any top and bottom margins they
- // have. If the margins are not included, the center content
- // will be too tall and a scroll bar appears.
- - elementHeightWithMargins("header")
- - elementHeightWithMargins("footer");
-
+ var height = $(window).height() - $("header").outerHeight() - $("footer").outerHeight();
$("#vAlign").css({ "height": height });
+
// On the manage page, the content element sometimes does not take up the
// full height of the screen, leaving the footer to float somewhere in the
// middle. To compensate, force the min-height of the content so that the
// footer remains at the bottom of the screen.
+
var paddingTop = 0, paddingBottom = 0;
if(paddingAddedToMinHeight()) {
diff --git a/resources/static/shared/modules/page_module.js b/resources/static/shared/modules/page_module.js
index 54b5708..36aa78c 100644
--- a/resources/static/shared/modules/page_module.js
+++ b/resources/static/shared/modules/page_module.js
@@ -15,16 +15,6 @@ BrowserID.Modules.PageModule = (function() {
cancelEvent = helpers.cancelEvent,
mediator = bid.Mediator;
- function onKeypress(event) {
- if (event.which === 13) {
- // IE8 does not trigger the submit event when hitting enter. Submit the
- // form if the key press was an enter and prevent the default action so
- // the form is not submitted twice.
- event.preventDefault();
- this.submit();
- }
- }
-
function onSubmit() {
if (!dom.hasClass("body", "submit_disabled") && this.validate()) {
this.submit();
@@ -69,7 +59,6 @@ BrowserID.Modules.PageModule = (function() {
self.options = options || {};
self.bind("form", "submit", cancelEvent(onSubmit));
- self.bind("input", "keypress", onKeypress);
},
stop: function() {
diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js
index 752ce42..7430957 100644
--- a/resources/static/shared/network.js
+++ b/resources/static/shared/network.js
@@ -94,10 +94,6 @@ BrowserID.Network = (function() {
// Any time the context info changes, we want to know about it.
mediator.subscribe('context_info', onContextChange);
- // BEGIN TEST API
- this.cookiesEnabledOverride = config && config.cookiesEnabledOverride;
- // END TEST API
-
clearContext();
},
@@ -513,15 +509,15 @@ BrowserID.Network = (function() {
/**
* Get information about an email address. Who vouches for it?
- * (is it a primary or a secondary)
+ * (is it a primary, proxyidp or a secondary)
* @method addressInfo
* @param {string} email - Email address to check.
* @param {function} [onComplete] - Called with an object on success,
* containing these properties:
- * type: <secondary|primary>
+ * type: <secondary|primary|proxyidp>
* known: boolean, present - present if type is secondary
- * auth: string - url to send users for auth - present if type is primary
- * prov: string - url to embed for silent provisioning - present if type is secondary
+ * auth: string - url to send users for auth
+ * prov: string - url to embed for silent provisioning
* @param {function} [onFailure] - Called on XHR failure.
*/
addressInfo: function(email, onComplete, onFailure) {
@@ -681,12 +677,6 @@ BrowserID.Network = (function() {
enabled = false;
}
- // BEGIN TESTING API
- if (typeof Network.cookiesEnabledOverride === "boolean") {
- enabled = Network.cookiesEnabledOverride;
- }
- // END TESTING API
-
complete(onComplete, enabled);
}, onFailure);
},
diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js
index 0695c83..e892ad4 100644
--- a/resources/static/shared/user.js
+++ b/resources/static/shared/user.js
@@ -679,7 +679,6 @@ BrowserID.User = (function() {
if(cookiesEnabled) {
network.checkAuth(function(authenticated) {
setAuthenticationStatus(authenticated);
- if (!authenticated) authenticated = false;
complete(onComplete, authenticated);
}, onFailure);
}
@@ -693,13 +692,12 @@ BrowserID.User = (function() {
* Check whether the current user is authenticated. If authenticated, sync
* identities.
* @method checkAuthenticationAndSync
- * @param {function} [onComplete] - Called on sync completion with one
- * boolean parameter, authenticated. authenticated will be true if user
- * is authenticated, false otw.
+ * @param {function} [onComplete] - Called on sync completion.
* @param {function} [onFailure] - Called on error.
*/
checkAuthenticationAndSync: function(onComplete, onFailure) {
- User.checkAuthentication(function(authenticated) {
+ network.checkAuth(function(authenticated) {
+ setAuthenticationStatus(authenticated);
if (authenticated) {
User.syncEmails(function() {
onComplete && onComplete(authenticated);
@@ -750,20 +748,19 @@ BrowserID.User = (function() {
/**
* Get information about an email address. Who vouches for it?
- * (is it a primary or a secondary)
+ * (is it a primary, proxyidp or a secondary)
* @method addressInfo
* @param {string} email - Email address to check.
* @param {function} [onComplete] - Called with an object on success,
* containing these properties:
- * type: <secondary|primary>
+ * type: <secondary|primary|proxyidp>
* known: boolean, present if type is secondary. True if email
* address is registered with BrowserID.
- * authed: boolean, present if type is primary - whether the user
+ * authed: boolean, present if type is primary or proxyidp - whether the user
* is authenticated to the IdP as this user.
* auth: string - url to send users for auth - present if type is
- * primary.
- * prov: string - url to embed for silent provisioning - present
- * if type is secondary.
+ * primary or proxyidp.
+ * prov: string - url to embed for silent provisioning
* @param {function} [onFailure] - Called on XHR failure.
*/
addressInfo: function(email, onComplete, onFailure) {
@@ -780,7 +777,12 @@ BrowserID.User = (function() {
else {
network.addressInfo(email, function(info) {
info.email = email;
- if(info.type === "primary") {
+ // Note - even though addressInfo may return an email of type
+ // primary, proxyidp or secondary, emails are only stored in
+ // localStorage as primary or secondary. proxyidp is stored as primary.
+ if(info.type === "primary" || info.type === "proxyidp") {
+ info.IdPEnabled = true;
+
User.isUserAuthenticatedToPrimary(email, info, function(authed) {
info.authed = authed;
complete(info);
@@ -1055,7 +1057,7 @@ BrowserID.User = (function() {
* Get an assertion for the current domain if the user is signed into it
* @method getPersistentSigninAssertion
* @param {function} onComplete - called on completion. Called with an
- * an email and assertion if successful, null otw.
+ * assertion if successful, null otw.
* @param {function} onFailure - called on XHR failure.
*/
getSilentAssertion: function(siteSpecifiedEmail, onComplete, onFailure) {
@@ -1067,7 +1069,7 @@ BrowserID.User = (function() {
// so if we rely on localstorage only and check authentication status
// only when we know a network request will be required, we very well
// might have fewer race conditions and do fewer network requests.
- User.checkAuthenticationAndSync(function(authenticated) {
+ User.checkAuthentication(function(authenticated) {
if (authenticated) {
var loggedInEmail = storage.getLoggedIn(origin);
if (loggedInEmail !== siteSpecifiedEmail) {
diff --git a/resources/static/shared/wait-messages.js b/resources/static/shared/wait-messages.js
index 00a64aa..63ec7aa 100644
--- a/resources/static/shared/wait-messages.js
+++ b/resources/static/shared/wait-messages.js
@@ -15,6 +15,11 @@ BrowserID.Wait = (function(){
message: gettext("Please wait a few seconds while we sign you into the site.")
},
+ redirectToIdP: {
+ title: gettext("You are being redirected to your email provider"),
+ message: ""
+ },
+
slowXHR: {
title: gettext("We are sorry, this request is taking a LOOONG time."),
message: gettext("This message will go away when the request completes (hopefully soon). If you wait too long, close this window and try again."),
diff --git a/resources/static/test/cases/controllers/actions.js b/resources/static/test/cases/controllers/actions.js
index 8d90d69..4118557 100644
--- a/resources/static/test/cases/controllers/actions.js
+++ b/resources/static/test/cases/controllers/actions.js
@@ -8,6 +8,7 @@
var bid = BrowserID,
user = bid.User,
+ xhr = bid.Mocks.xhr,
controller,
el,
testHelpers = bid.TestHelpers,
@@ -53,8 +54,7 @@
});
asyncTest("doVerifyPrimaryUser - start the verify_primary_user service", function() {
- testActionStartsModule("doVerifyPrimaryUser", {},
- "verify_primary_user");
+ testActionStartsModule("doVerifyPrimaryUser", {}, "verify_primary_user");
});
asyncTest("doCannotVerifyRequiredPrimary - show the error screen", function() {
diff --git a/resources/static/test/cases/controllers/authenticate.js b/resources/static/test/cases/controllers/authenticate.js
index 6827fce..b22e5c5 100644
--- a/resources/static/test/cases/controllers/authenticate.js
+++ b/resources/static/test/cases/controllers/authenticate.js
@@ -117,42 +117,30 @@
controller.checkEmail();
});
- asyncTest("clear password if user changes email address", function() {
- xhr.useResult("known_secondary");
- $("#email").val("registered@testuser.com");
-
- var enterPasswordCount = 0;
- mediator.subscribe("enter_password", function() {
- // The first time the password is shown, change the email address. The
- // second time the password is shown, make sure the password was cleared.
-
- if(enterPasswordCount == 0) {
- // simulate the user changing the email address. This should clear the
- // password.
- $("#password").val("password");
- $("#email").val("testuser@testuser.com");
- $("#email").keyup();
- controller.checkEmail();
- }
- else {
- equal($("#password").val(), "", "password field was cleared");
- start();
- }
+ asyncTest("checkEmail with email that has normal IdP support, expect 'primary_user' message", function() {
+ $("#email").val("unregistered@testuser.com");
+ xhr.useResult("primary");
- enterPasswordCount++;
+ register("primary_user", function(msg, info) {
+ equal(info.email, "unregistered@testuser.com", "email correctly passed");
+ equal(info.auth, "https://auth_url", "IdP authentication URL passed");
+ equal(info.prov, "https://prov_url", "IdP provisioning URL passed");
+ equal(info.type, "primary", "correct IdP type set");
+ start();
});
controller.checkEmail();
});
- asyncTest("checkEmail with email that has IdP support - 'primary_user' message", function() {
+ asyncTest("checkEmail with email that has proxy IdP support, expect 'primary_user' message", function() {
$("#email").val("unregistered@testuser.com");
- xhr.useResult("primary");
+ xhr.useResult("proxyidp");
register("primary_user", function(msg, info) {
equal(info.email, "unregistered@testuser.com", "email correctly passed");
equal(info.auth, "https://auth_url", "IdP authentication URL passed");
equal(info.prov, "https://prov_url", "IdP provisioning URL passed");
+ equal(info.type, "proxyidp", "correct IdP type set");
start();
});
diff --git a/resources/static/test/cases/controllers/dialog.js b/resources/static/test/cases/controllers/dialog.js
index ed8aba8..4190cd9 100644
--- a/resources/static/test/cases/controllers/dialog.js
+++ b/resources/static/test/cases/controllers/dialog.js
@@ -469,123 +469,5 @@
});
});
- asyncTest("get with relative siteLogo - not allowed", function() {
- createController({
- ready: function() {
- mediator.subscribe("start", function(msg, info) {
- ok(false, "start should not have been called");
- });
-
- var retval = controller.get(HTTP_TEST_DOMAIN, {
- siteLogo: "logo.png",
- });
-
- equal(retval, "must be an absolute path: (logo.png)", "expected error");
- testErrorVisible();
- start();
- }
- });
- });
-
- asyncTest("get with javascript: siteLogo - not allowed", function() {
- createController({
- ready: function() {
- mediator.subscribe("start", function(msg, info) {
- ok(false, "start should not have been called");
- });
-
- var retval = controller.get(HTTP_TEST_DOMAIN, {
- siteLogo: "javascript:alert('xss')",
- });
-
- equal(retval, "must be an absolute path: (javascript:alert('xss'))", "expected error");
- testErrorVisible();
- start();
- }
- });
- });
-
- asyncTest("get with data-uri: siteLogo - not allowed", function() {
- createController({
- ready: function() {
- mediator.subscribe("start", function(msg, info) {
- ok(false, "start should not have been called");
- });
-
- var retval = controller.get(HTTP_TEST_DOMAIN, {
- siteLogo: "data:image/png,FAKEDATA",
- });
-
- equal(retval, "must be an absolute path: (data:image/png,FAKEDATA)", "expected error");
- testErrorVisible();
- start();
- }
- });
- });
-
- asyncTest("get with http: siteLogo - not allowed", function() {
- createController({
- ready: function() {
- mediator.subscribe("start", function(msg, info) {
- ok(false, "start should not have been called");
- });
-
- var retval = controller.get(HTTP_TEST_DOMAIN, {
- siteLogo: HTTP_TEST_DOMAIN + "://logo.png",
- });
-
- equal(retval, "must be an absolute path: (" + HTTP_TEST_DOMAIN + "://logo.png)", "expected error");
- testErrorVisible();
- start();
- }
- });
- });
-
- asyncTest("get with https: siteLogo - not allowed", function() {
- createController({
- ready: function() {
- mediator.subscribe("start", function(msg, info) {
- ok(false, "start should not have been called");
- });
-
- var retval = controller.get(HTTP_TEST_DOMAIN, {
- siteLogo: HTTPS_TEST_DOMAIN + "://logo.png",
- });
-
- equal(retval, "must be an absolute path: (" + HTTPS_TEST_DOMAIN + "://logo.png)", "expected error");
- testErrorVisible();
- start();
- }
- });
- });
-
- asyncTest("get with absolute path - allowed URL but it must be properly escaped", function() {
- createController({
- ready: function() {
- var startInfo;
- mediator.subscribe("start", function(msg, info) {
- startInfo = info;
- });
-
- var siteLogo = '/i/card.png" onerror="alert(\'xss\')" <script>alert(\'more xss\')</script>';
- var retval = controller.get(HTTP_TEST_DOMAIN, {
- siteLogo: siteLogo
- });
-
- start();
-
- testHelpers.testObjectValuesEqual(startInfo, {
- siteLogo: encodeURI(HTTP_TEST_DOMAIN + siteLogo)
- });
- equal(typeof retval, "undefined", "no error expected");
- testErrorNotVisible();
- start();
- }
- });
-
- });
-
-
-
}());
diff --git a/resources/static/test/cases/controllers/rp_info.js b/resources/static/test/cases/controllers/rp_info.js
deleted file mode 100644
index 0b89868..0000000
--- a/resources/static/test/cases/controllers/rp_info.js
+++ /dev/null
@@ -1,88 +0,0 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
-/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
-/* 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/. */
-(function() {
- "use strict";
-
- var controller,
- bid = BrowserID,
- user = bid.User,
- testHelpers = bid.TestHelpers,
- register = bid.TestHelpers.register,
- WindowMock = bid.Mocks.WindowMock,
- RP_HOSTNAME = "hostname.org",
- RP_NAME = "RP Name",
- RP_HTTPS_LOGO = "https://en.gravatar.com/userimage/6966791/c4feac761b8544cce13e0406f36230aa.jpg";
-
- module("controllers/rp_info", {
- setup: testHelpers.setup,
-
- teardown: function() {
- if (controller) {
- try {
- controller.destroy();
- controller = null;
- } catch(e) {
- // could already be destroyed from the close
- }
- }
- window.scriptRun = null;
- delete window.scriptRun;
- testHelpers.teardown();
- }
- });
-
-
- function createController(options) {
- options = _.extend({ hostname: RP_HOSTNAME }, options);
-
- controller = bid.Modules.RPInfo.create();
- controller.start(options || {});
- }
-
- test("neither siteName nor logo specified - show rp_hostname only", function() {
- createController();
- equal($("#rp_hostname").html(), RP_HOSTNAME, "rp_hostname filled in");
- ok(!$("#rp_name").html(), "rp_name empty");
- ok(!$("#rp_logo").attr("src"), "rp logo not shown");
- });
-
- test("siteName only specified - show specified siteName and rp_hostname", function() {
- createController({
- siteName: RP_NAME,
- });
-
- equal($("#rp_hostname").html(), RP_HOSTNAME, "rp_hostname filled in");
- equal($("#rp_name").html(), RP_NAME, "rp_name filled in");
- ok(!$("#rp_logo").attr("src"), "rp logo not shown");
- });
-
- test("siteLogos are allowed", function() {
- var docMock = new WindowMock().document;
- docMock.location.protocol = "http:";
-
- createController({
- document: docMock,
- siteLogo: RP_HTTPS_LOGO
- });
-
- equal($("#rp_logo").attr("src"), RP_HTTPS_LOGO, "rp logo shown");
- equal($("#rp_hostname").html(), RP_HOSTNAME, "rp_hostname filled in");
- ok(!$("#rp_name").html(), "rp_name empty");
- });
-
- test("both siteName and siteLogo specified - show siteName, siteLogo and rp_hostname", function() {
- createController({
- siteName: RP_NAME,
- siteLogo: RP_HTTPS_LOGO
- });
-
- equal($("#rp_hostname").html(), RP_HOSTNAME, "rp_hostname filled in");
- equal($("#rp_name").html(), RP_NAME, "rp_name filled in");
- equal($("#rp_logo").attr("src"), RP_HTTPS_LOGO, "rp logo shown");
- });
-
-}());
-
diff --git a/resources/static/test/cases/controllers/verify_primary_user.js b/resources/static/test/cases/controllers/verify_primary_user.js
index 5203792..d5a0791 100644
Binary files a/resources/static/test/cases/controllers/verify_primary_user.js and b/resources/static/test/cases/controllers/verify_primary_user.js differ
diff --git a/resources/static/test/cases/include.js b/resources/static/test/cases/include.js
index 0cadc3e..00bd855 100644
--- a/resources/static/test/cases/include.js
+++ b/resources/static/test/cases/include.js
@@ -24,24 +24,5 @@
});
});
- test("DOM calls fails when unbound from navigator.id", function() {
- _.each([
- "watch",
- "request",
- "logout"
- ], function(item, index) {
- var the_func = navigator.id[item];
-
- var fails = false;
- try {
- the_func();
- } catch (x) {
- fails = true;
- }
-
- ok(fails);
- });
- });
-
}());
diff --git a/resources/static/test/cases/resources/state.js b/resources/static/test/cases/resources/state.js
index 1addb96..80fe489 100644
--- a/resources/static/test/cases/resources/state.js
+++ b/resources/static/test/cases/resources/state.js
@@ -34,6 +34,15 @@
}
}
+ function testActionCalled(action, requiredData) {
+ ok(actions.called[action], action + " called");
+
+ for(var key in requiredData) {
+ equal(actions.info[action][key], requiredData[key], key + " value set correctly");
+ }
+
+ }
+
function createMachine() {
machine = bid.State.create();
actions = new ActionsMock();
@@ -179,9 +188,11 @@
mediator.publish("primary_user", { email: TEST_EMAIL });
});
- test("primary_user with unprovisioned primary user - call doProvisionPrimaryUser", function() {
+ test("primary_user with unprovisioned user - call doProvisionPrimaryUser", function() {
mediator.publish("primary_user", { email: TEST_EMAIL });
- ok(actions.called.doProvisionPrimaryUser, "doPrimaryUserProvisioned called");
+ testActionCalled("doProvisionPrimaryUser", {
+ email: TEST_EMAIL
+ });
});
test("primary_user_provisioned - call doEmailChosen", function() {
@@ -189,10 +200,13 @@
ok(actions.called.doPrimaryUserProvisioned, "doPrimaryUserProvisioned called");
});
- test("primary_user_unauthenticated before verification - call doVerifyPrimaryUser", function() {
+ asyncTest("primary_user_unauthenticated before IdP authentication - call doVerifyPrimaryUser", function() {
mediator.publish("start");
- mediator.publish("primary_user_unauthenticated");
- ok(actions.called.doVerifyPrimaryUser, "doVerifyPrimaryUser called");
+ mediator.publish("primary_user", { email: TEST_EMAIL });
+ mediator.publish("primary_user_unauthenticated", { complete: function() {
+ testActionCalled("doVerifyPrimaryUser", { email: TEST_EMAIL });
+ start();
+ }});
});
test("primary_user_unauthenticated after required email - call doCannotVerifyRequiredPrimary", function() {
diff --git a/resources/static/test/cases/shared/modules/page_module.js b/resources/static/test/cases/shared/modules/page_module.js
index 0544d20..7baac24 100644
--- a/resources/static/test/cases/shared/modules/page_module.js
+++ b/resources/static/test/cases/shared/modules/page_module.js
@@ -198,35 +198,6 @@
$("body").removeClass("submit_disabled");
controller.onSubmit();
equal(submitCalled, true, "submit permitted to complete");
- });
-
- test("form is submitted once 'enter keypress' event", function() {
- createController();
- controller.renderDialog("test_template_with_input", {
- title: "Test title",
- message: "Test message"
- });
-
- controller.start();
-
-
- var submitCalled = 0;
- controller.submit = function() {
- submitCalled++;
- };
-
- // synthesize the entire series of key* events so we replicate the behavior
- // of keyboard interaction.
- var e = jQuery.Event("keydown", { keyCode: 13, which: 13 });
- $("#templateInput").trigger(e);
-
- var e = jQuery.Event("keyup", { keyCode: 13, which: 13 });
- $("#templateInput").trigger(e);
-
- var e = jQuery.Event("keypress", { keyCode: 13, which: 13 });
- $("#templateInput").trigger(e);
-
- equal(submitCalled, 1, "submit called a single time");
- });
+ })
}());
diff --git a/resources/static/test/cases/shared/network.js b/resources/static/test/cases/shared/network.js
index 4ce7680..27a219e 100644
--- a/resources/static/test/cases/shared/network.js
+++ b/resources/static/test/cases/shared/network.js
@@ -20,6 +20,7 @@
module("shared/network", {
setup: function() {
testHelpers.setup();
+ network.init();
},
teardown: function() {
testHelpers.teardown();
@@ -563,6 +564,19 @@
equal(data.type, "primary", "type is primary");
ok("auth" in data, "auth field exists");
ok("prov" in data, "prov field exists");
+ equal(data.type, "primary", "type correctly set to primary");
+ start();
+ }, testHelpers.unexpectedXHRFailure);
+ });
+
+ asyncTest("addressInfo with proxyidp email", function() {
+ transport.useResult("proxyidp");
+
+ network.addressInfo(TEST_EMAIL, function onComplete(data) {
+ equal(data.type, "proxyidp", "type is proxyidp");
+ ok("auth" in data, "auth field exists");
+ ok("prov" in data, "prov field exists");
+ equal(data.type, "proxyidp", "type correctly set to proxyidp");
start();
}, testHelpers.unexpectedXHRFailure);
});
@@ -592,28 +606,12 @@
});
asyncTest("cookiesEnabled with cookies enabled - return true status", function() {
- network.init({ cookiesEnabledOverride: true });
network.cookiesEnabled(function(status) {
equal(status, true, "cookies are enabled, correct status");
start();
}, testHelpers.unexpectedXHRFailure);
});
- asyncTest("cookiesEnabled with cookies disabled - return true status", function() {
- network.init({ cookiesEnabledOverride: false });
- network.cookiesEnabled(function(status) {
- equal(status, false, "cookies are disabled, correct status");
- start();
- }, testHelpers.unexpectedXHRFailure);
- });
-
- asyncTest("cookiesEnabled with browser defined cookie status - wait and see", function() {
- network.cookiesEnabled(function(status) {
- equal(status, true, "hopefully cookies are enabled, correct status");
- start();
- }, testHelpers.unexpectedXHRFailure);
- });
-
asyncTest("cookiesEnabled with onComplete exception thrown - should not call onComplete a second time", function() {
// Since we are manually throwing an exception, it must be caught
// below.
diff --git a/resources/static/test/cases/shared/user.js b/resources/static/test/cases/shared/user.js
index a07a5fe..a287d9b 100644
--- a/resources/static/test/cases/shared/user.js
+++ b/resources/static/test/cases/shared/user.js
@@ -14,8 +14,6 @@ var jwcrypto = require("./lib/jwcrypto");
testHelpers = bid.TestHelpers,
testOrigin = testHelpers.testOrigin,
failureCheck = testHelpers.failureCheck,
- testUndefined = testHelpers.testUndefined,
- testNotUndefined = testHelpers.testNotUndefined,
provisioning = bid.Mocks.Provisioning,
TEST_EMAIL = "testuser@testuser.com";
@@ -514,40 +512,24 @@ var jwcrypto = require("./lib/jwcrypto");
asyncTest("checkAuthentication with valid authentication", function() {
- storage.addSecondaryEmail(TEST_EMAIL);
xhr.setContextInfo("auth_level", "primary");
-
lib.checkAuthentication(function(authenticated) {
equal(authenticated, "primary", "We are authenticated!");
- testNotUndefined(storage.getEmail(TEST_EMAIL), "localStorage is not cleared");
start();
});
});
- asyncTest("checkAuthentication with invalid authentication - localStorage cleared", function() {
- storage.addSecondaryEmail(TEST_EMAIL);
+ asyncTest("checkAuthentication with invalid authentication", function() {
xhr.setContextInfo("auth_level", undefined);
-
lib.checkAuthentication(function(authenticated) {
- equal(authenticated, false, "We are not authenticated!");
- testUndefined(storage.getEmail(TEST_EMAIL), "localStorage was cleared");
+ equal(authenticated, undefined, "We are not authenticated!");
start();
});
});
- asyncTest("checkAuthentication with cookies disabled - localStorage is not cleared, user can enable their cookies and try again", function() {
- storage.addSecondaryEmail(TEST_EMAIL);
- network.init({ cookiesEnabledOverride: false });
-
- lib.checkAuthentication(function(authenticated) {
- equal(authenticated, false, "We are not authenticated!");
- testNotUndefined(storage.getEmail(TEST_EMAIL), "localStorage is not cleared");
- start();
- });
- });
asyncTest("checkAuthentication with XHR failure", function() {
xhr.useResult("contextAjaxError");
@@ -566,24 +548,12 @@ var jwcrypto = require("./lib/jwcrypto");
});
- asyncTest("checkAuthenticationAndSync with invalid authentication - localStorage cleared", function() {
- storage.addSecondaryEmail(TEST_EMAIL);
- xhr.setContextInfo("auth_level", undefined);
-
- lib.checkAuthenticationAndSync(function onComplete(authenticated) {
- equal(authenticated, false, "We are not authenticated!");
- testUndefined(storage.getEmail(TEST_EMAIL), "localStorage was cleared");
- start();
- }, testHelpers.unexpectedXHRFailure);
- });
- asyncTest("checkAuthenticationAndSync with cookies disabled - localStorage not cleared, user can enable their cookies and try again", function() {
- storage.addSecondaryEmail(TEST_EMAIL);
- network.init({ cookiesEnabledOverride: false });
+ asyncTest("checkAuthenticationAndSync with invalid authentication", function() {
+ xhr.setContextInfo("auth_level", undefined);
lib.checkAuthenticationAndSync(function onComplete(authenticated) {
- equal(authenticated, false, "We are not authenticated!");
- testNotUndefined(storage.getEmail(TEST_EMAIL), "localStorage is not cleared");
+ equal(authenticated, undefined, "We are not authenticated!");
start();
}, testHelpers.unexpectedXHRFailure);
});
@@ -972,34 +942,6 @@ var jwcrypto = require("./lib/jwcrypto");
});
- asyncTest("getSilentAssertion with logged in user, emails match - user already logged in, call callback with email and null assertion", function() {
- var LOGGED_IN_EMAIL = TEST_EMAIL;
- xhr.setContextInfo("auth_level", "password");
-
- lib.syncEmailKeypair(LOGGED_IN_EMAIL, function() {
- storage.setLoggedIn(lib.getOrigin(), LOGGED_IN_EMAIL);
- lib.getSilentAssertion(LOGGED_IN_EMAIL, function(email, assertion) {
- equal(email, LOGGED_IN_EMAIL, "correct email");
- strictEqual(assertion, null, "correct assertion");
- start();
- }, testHelpers.unexpectedXHRFailure);
- }, testHelpers.unexpectedXHRFailure);
- });
-
- asyncTest("getSilentAssertion with logged in user, request email different from logged in email, valid cert for logged in email - logged in user needs assertion - call callback with email and assertion.", function() {
- xhr.setContextInfo("auth_level", "password");
- var LOGGED_IN_EMAIL = TEST_EMAIL;
- var REQUESTED_EMAIL = "requested@testuser.com";
-
- lib.syncEmailKeypair(LOGGED_IN_EMAIL, function() {
- storage.setLoggedIn(lib.getOrigin(), LOGGED_IN_EMAIL);
- lib.getSilentAssertion(REQUESTED_EMAIL, function(email, assertion) {
- equal(email, LOGGED_IN_EMAIL, "correct email");
- testAssertion(assertion, start);
- }, testHelpers.unexpectedXHRFailure);
- }, testHelpers.unexpectedXHRFailure);
- });
-
asyncTest("logoutUser", function(onSuccess) {
lib.authenticate(TEST_EMAIL, "testuser", function(authenticated) {
lib.syncEmails(function() {
@@ -1091,13 +1033,14 @@ var jwcrypto = require("./lib/jwcrypto");
);
});
- asyncTest("addressInfo with primary authenticated user", function() {
+ asyncTest("addressInfo with normal IdP authenticated user", function() {
xhr.useResult("primary");
provisioning.setStatus(provisioning.AUTHENTICATED);
lib.addressInfo(
"registered@testuser.com",
function(info) {
equal(info.type, "primary", "correct type");
+ equal(info.IdPEnabled, true, "a primary user is IdPEnabled");
equal(info.email, "registered@testuser.com", "correct email");
equal(info.authed, true, "user is authenticated with IdP");
start();
@@ -1121,6 +1064,37 @@ var jwcrypto = require("./lib/jwcrypto");
);
});
+ asyncTest("addressInfo with proxy IdP authenticated user", function() {
+ xhr.useResult("proxyidp");
+ provisioning.setStatus(provisioning.AUTHENTICATED);
+ lib.addressInfo(
+ "registered@testuser.com",
+ function(info) {
+ equal(info.type, "proxyidp", "correct type");
+ equal(info.IdPEnabled, true, "a primary user is IdPEnabled");
+ equal(info.email, "registered@testuser.com", "correct email");
+ equal(info.authed, true, "user is authenticated with IdP");
+ start();
+ },
+ testHelpers.unexpectedFailure
+ );
+ });
+
+ asyncTest("addressInfo with proxy IdP unauthenticated user", function() {
+ xhr.useResult("proxyidp");
+ provisioning.setStatus(provisioning.NOT_AUTHENTICATED);
+ lib.addressInfo(
+ "registered@testuser.com",
+ function(info) {
+ equal(info.type, "proxyidp", "correct type");
+ equal(info.email, "registered@testuser.com", "correct email");
+ equal(info.authed, false, "user is not authenticated with IdP");
+ start();
+ },
+ testHelpers.unexpectedFailure
+ );
+ });
+
asyncTest("hasSecondary returns false if the user has 0 secondary email address", function() {
lib.hasSecondary(function(hasSecondary) {
equal(hasSecondary, false, "hasSecondary is false");
diff --git a/resources/static/test/mocks/window.js b/resources/static/test/mocks/window.js
index 5105142..d000f3f 100644
--- a/resources/static/test/mocks/window.js
+++ b/resources/static/test/mocks/window.js
@@ -19,7 +19,14 @@ BrowserID.Mocks.WindowMock = (function() {
WindowMock.prototype = {
open: function(url, name, options) {
this.open_url = url;
- }
+ },
+
+ resizeTo: function(width, height) {
+ this.width = width;
+ this.height = height;
+ },
+ width: 0,
+ height: 0
};
return WindowMock;
diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js
index e2d5602..a206f99 100644
--- a/resources/static/test/mocks/xhr.js
+++ b/resources/static/test/mocks/xhr.js
@@ -15,7 +15,9 @@ BrowserID.Mocks.xhr = (function() {
code_version: "ABC123",
random_seed: "H+ZgKuhjVckv/H4i0Qvj/JGJEGDVOXSIS5RCOjY9/Bo=",
data_sample_rate: 1
- };
+ },
+ AUTH_URL = "https://auth_url",
+ PROV_URL = "https://prov_url";
// this cert is meaningless, but it has the right format
var random_cert = "eyJhbGciOiJSUzEyOCJ9.eyJpc3MiOiJpc3N1ZXIuY29tIiwiZXhwIjoxMzE2Njk1MzY3NzA3LCJwdWJsaWMta2V5Ijp7ImFsZ29yaXRobSI6IlJTIiwibiI6IjU2MDYzMDI4MDcwNDMyOTgyMzIyMDg3NDE4MTc2ODc2NzQ4MDcyMDM1NDgyODk4MzM0ODExMzY4NDA4NTI1NTk2MTk4MjUyNTE5MjY3MTA4MTMyNjA0MTk4MDA0NzkyODQ5MDc3ODY4OTUxOTA2MTcwODEyNTQwNzEzOTgyOTU0NjUzODEwNTM5OTQ5Mzg0NzEyNzczMzkwMjAwNzkxOTQ5NTY1OTAzNDM5NTIxNDI0OTA5NTc2ODMyNDE4ODkwODE5MjA0MzU0NzI5MjE3MjA3MzYwMTA1OTA2MDM5MDIzMjk5NTYxMzc0MDk4OTQyNzg5OTk2NzgwMTAyMDczMDcxNzYwODUyODQxMDY4OTg5ODYwNDAzNDMxNzM3NDgwMTgyNzI1ODUzODk5NzMzNzA2MDY5IiwiZSI6IjY1NTM3In0sInByaW5jaXBhbCI6eyJlbWFpbCI6InRlc3R1c2VyQHRlc3R1c2VyLmNvbSJ9fQ.aVIO470S_DkcaddQgFUXciGwq2F_MTdYOJtVnEYShni7I6mqBwK3fkdWShPEgLFWUSlVUtcy61FkDnq2G-6ikSx1fUZY7iBeSCOKYlh6Kj9v43JX-uhctRSB2pI17g09EUtvmb845EHUJuoowdBLmLa4DSTdZE-h4xUQ9MsY7Ik";
@@ -117,13 +119,20 @@ BrowserID.Mocks.xhr = (function() {
"get /wsapi/address_info?email=unregistered%40testuser.com valid": { type: "secondary", known: false },
"get /wsapi/address_info?email=unregistered%40testuser.com unknown_secondary": { type: "secondary", known: false },
"get /wsapi/address_info?email=registered%40testuser.com known_secondary": { type: "secondary", known: true },
- "get /wsapi/address_info?email=registered%40testuser.com primary": { type: "primary", auth: "https://auth_url", prov: "https://prov_url" },
- "get /wsapi/address_info?email=unregistered%40testuser.com primary": { type: "primary", auth: "https://auth_url", prov: "https://prov_url" },
+ "get /wsapi/address_info?email=registered%40testuser.com primary": { type: "primary", auth: AUTH_URL, prov: PROV_URL },
+ "get /wsapi/address_info?email=unregistered%40testuser.com primary": { type: "primary", auth: AUTH_URL, prov: PROV_URL },
+ "get /wsapi/address_info?email=registered%40testuser.com proxyidp": { type: "proxyidp", auth: AUTH_URL, prov: PROV_URL },
+ "get /wsapi/address_info?email=unregistered%40testuser.com proxyidp": { type: "proxyidp", auth: AUTH_URL, prov: PROV_URL },
"get /wsapi/address_info?email=testuser%40testuser.com unknown_secondary": { type: "secondary", known: false },
"get /wsapi/address_info?email=testuser%40testuser.com known_secondary": { type: "secondary", known: true },
"get /wsapi/address_info?email=registered%40testuser.com mustAuth": { type: "secondary", known: true },
- "get /wsapi/address_info?email=testuser%40testuser.com primary": { type: "primary", auth: "https://auth_url", prov: "https://prov_url" },
+ "get /wsapi/address_info?email=testuser%40testuser.com primary": { type: "primary", auth: AUTH_URL, prov: PROV_URL },
+ "get /wsapi/address_info?email=testuser%40testuser.com proxyidp": { type: "proxyidp", auth: AUTH_URL, prov: PROV_URL },
"get /wsapi/address_info?email=testuser%40testuser.com ajaxError": undefined,
+ "get /wsapi/address_info?email=testuser%40gmail.com primary": { type: "primary", auth: AUTH_URL, prov: PROV_URL },
+ "get /wsapi/address_info?email=testuser%40gmail.com proxyidp": { type: "proxyidp", auth: AUTH_URL, prov: PROV_URL },
+ "get /wsapi/address_info?email=testuser%40yahoo.com proxyidp": { type: "proxyidp", auth: AUTH_URL, prov: PROV_URL },
+ "get /wsapi/address_info?email=testuser%40hotmail.com proxyidp": { type: "proxyidp", auth: AUTH_URL, prov: PROV_URL },
"post /wsapi/add_email_with_assertion invalid": { success: false },
"post /wsapi/add_email_with_assertion valid": { success: true },
"post /wsapi/prolong_session valid": { success: true },
diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js
index c309dee..2c77f42 100644
--- a/resources/static/test/testHelpers/helpers.js
+++ b/resources/static/test/testHelpers/helpers.js
@@ -68,7 +68,7 @@ BrowserID.TestHelpers = (function() {
transport.setContextInfo("cookies_enabled", true);
transport.useResult("valid");
- network.init({ forceCookieStatus: undefined });
+ network.init();
clearStorage();
$("body").stop().show();
@@ -223,14 +223,6 @@ BrowserID.TestHelpers = (function() {
testHasClass: function(selector, className, msg) {
ok($(selector).hasClass(className),
selector + " has className " + className + " - " + msg);
- },
-
- testUndefined: function(toTest, msg) {
- equal(typeof toTest, "undefined", msg || "object is undefined");
- },
-
- testNotUndefined: function(toTest, msg) {
- notEqual(typeof toTest, "undefined", msg || "object is defined");
}
};
diff --git a/resources/views/dialog.ejs b/resources/views/dialog.ejs
index 9bf2250..1752efd 100644
--- a/resources/views/dialog.ejs
+++ b/resources/views/dialog.ejs
@@ -5,7 +5,8 @@
<form novalidate>
<div id="favicon">
<div class="table">
- <div class="vertical" id="rp_info">
+ <div class="vertical">
+ <strong id="sitename"></strong>
</div>
</div>
</div>
diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs
index 6ca9a9a..c98dd03 100644
--- a/resources/views/dialog_layout.ejs
+++ b/resources/views/dialog_layout.ejs
@@ -16,6 +16,7 @@
<!--[if lt IE 9]>
<%- cachify_css('/production/ie8_dialog.css') %>
<![endif]-->
+ <link href='https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,700,700italic' rel='stylesheet' type='text/css'>
<title><%= gettext('Mozilla Persona') %></title>
</head>
<body class="waiting">
diff --git a/resources/views/layout.ejs b/resources/views/layout.ejs
index 15f6cc7..8c8d01c 100644
--- a/resources/views/layout.ejs
+++ b/resources/views/layout.ejs
@@ -10,12 +10,13 @@
<!--[if lt IE 9]>
<script src="/lib/html5shim.js"></script>
<![endif]-->
+ <link href='https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,700,700italic' rel='stylesheet' type='text/css'>
<%- cachify_css('/production/browserid.css') %>
<!--[if lt IE 9]>
<%- cachify_css('/production/ie8_main.css') %>
<![endif]-->
<%- cachify_js(util.format('/production/%s/browserid.js', locale)) %>
- <title><%= format(gettext("Mozilla Persona: %s"), [title]) %></title>
+ <title><%= format(gettext("Mozilla Person: %s"), [title]) %></title>
</head>
<body class="loading">
<a href="#" id="showDevelopment">&nbsp;</a>
diff --git a/resources/views/signin.ejs b/resources/views/signin.ejs
index a0ed5a5..e9fa217 100644
--- a/resources/views/signin.ejs
+++ b/resources/views/signin.ejs
@@ -42,7 +42,7 @@
</div>
<div id="cannot_authenticate" class="tooltip" for="password">
- The account cannot be logged in with this username and password.
+ The account cannot be logged in with this username and password.')
</li>
</ul>
diff --git a/resources/views/test.ejs b/resources/views/test.ejs
index 8f6ea43..dd25eca 100644
--- a/resources/views/test.ejs
+++ b/resources/views/test.ejs
@@ -25,9 +25,6 @@
<a href="#" onclick="$('#contents').hide(); return false;">Close</a>
<h3>Test Contents, this will be updated and can be safely ignored</h3>
- <div id="rp_info">
- </div>
-
<div id="page_head">
</div>
@@ -134,7 +131,6 @@
<script src="/dialog/controllers/primary_user_provisioned.js"></script>
<script src="/dialog/controllers/is_this_your_computer.js"></script>
<script src="/dialog/controllers/set_password.js"></script>
- <script src="/dialog/controllers/rp_info.js"></script>
<script src="/pages/page_helpers.js"></script>
<script src="/pages/verify_secondary_address.js"></script>
@@ -195,7 +191,6 @@
<script src="cases/controllers/primary_user_provisioned.js"></script>
<script src="cases/controllers/is_this_your_computer.js"></script>
<script src="cases/controllers/set_password.js"></script>
- <script src="cases/controllers/rp_info.js"></script>
<!-- must go last or all other tests will fail. -->
<script src="cases/controllers/dialog.js"></script>
diff --git a/scripts/awsbox/post_create.sh b/scripts/awsbox/post_create.sh
deleted file mode 100755
index 33e1de7..0000000
--- a/scripts/awsbox/post_create.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env bash
-
-sudo /sbin/chkconfig mysqld on
-sudo /sbin/service mysqld start
-sudo mysql -u root < $(dirname "${BASH_SOURCE[0]}")/create_browserid_user.sql
-echo "CREATE USER 'browserid'@'localhost';" | mysql -u root
-echo "CREATE DATABASE browserid;" | mysql -u root
-echo "GRANT ALL ON browserid.* TO 'browserid'@'localhost';" | mysql -u root
-
diff --git a/scripts/awsbox/post_deploy.js b/scripts/awsbox/post_deploy.js
deleted file mode 100755
index af133aa..0000000
--- a/scripts/awsbox/post_deploy.js
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/bash
-
-if [ ! -f $HOME/var/root.cert ] ; then
- echo ">> generating keypair"
- scripts/generate_ephemeral_keys.sh
- mv var/root.{cert,secretkey} $HOME/var
-else
- echo ">> no keypair needed. you gots one"
-fi
-
-echo ">> updating strings"
-svn co -q http://svn.mozilla.org/projects/l10n-misc/trunk/browserid/locale
-./locale/compile-mo.sh locale/
-
-echo ">> generating production resources"
-scripts/compress
diff --git a/scripts/browserid.spec b/scripts/browserid.spec
index 25fa883..c1b5c71 100644
--- a/scripts/browserid.spec
+++ b/scripts/browserid.spec
@@ -1,7 +1,7 @@
%define _rootdir /opt/browserid
Name: browserid-server
-Version: 0.2012.06.22
+Version: 0.2012.06.08
Release: 1%{?dist}_%{svnrev}
Summary: BrowserID server
Packager: Pete Fritchman <petef@mozilla.com>
diff --git a/scripts/deploy.js b/scripts/deploy.js
index fdc90bf..aebb6c7 100755
--- a/scripts/deploy.js
+++ b/scripts/deploy.js
@@ -1,82 +1,155 @@
#!/usr/bin/env node
-var path = require('path'),
-child_process = require('child_process');
-
-/*
- * A thin wrapper around awsbox that expects certain env
- * vars and invokes awsbox for ya to deploy a VM.
- */
-
-if (!process.env['AWS_ID'] || ! process.env['AWS_SECRET']) {
- console.log("You haven't defined AWS_ID and AWS_SECRET in the environment");
- console.log("Get these values from the amazon web console and try again.");
- process.exit(1);
+const
+aws = require('./deploy/aws.js');
+path = require('path');
+vm = require('./deploy/vm.js'),
+key = require('./deploy/key.js'),
+ssh = require('./deploy/ssh.js'),
+git = require('./deploy/git.js'),
+dns = require('./deploy/dns.js');
+
+var verbs = {};
+
+function checkErr(err) {
+ if (err) {
+ process.stderr.write('fatal error: ' + err + "\n");
+ process.exit(1);
+ }
}
-if (!process.env['ZERIGO_DNS_KEY'] && process.env['PERSONA_DEPLOY_DNS_KEY']) {
- process.env['ZERIGO_DNS_KEY'] = process.env['PERSONA_DEPLOY_DNS_KEY'];
+function printInstructions(name, deets) {
+ console.log("Yay! You have your very own deployment. Here's the basics:\n");
+ console.log(" 1. deploy your code: git push " + name + " <mybranch>:master");
+ console.log(" 2. visit your server on the web: https://" + name + ".hacksign.in");
+ console.log(" 3. test via a website: http://" + name + ".myfavoritebeer.org");
+ console.log(" 4. ssh in with sudo: ssh ec2-user@" + name + ".hacksign.in");
+ console.log(" 5. ssh as the deployment user: ssh app@" + name + ".hacksign.in\n");
+ console.log("enjoy! Here's your server details", JSON.stringify(deets, null, 4));
}
-var cmd = path.join(__dirname, '..', 'node_modules', '.bin', 'awsbox');
-cmd = path.relative(process.env['PWD'], cmd);
-
-if (process.argv.length > 1 &&
- process.argv[2] === 'create' ||
- process.argv[2] === 'deploy')
-{
- var options = {};
-
- if (process.argv.length > 3) options.n = process.argv[3];
-
- if (process.env['PERSONA_SSL_PRIV'] || process.env['PERSONA_SSL_PUB']) {
- options.p = process.env['PERSONA_SSL_PUB'];
- options.s = process.env['PERSONA_SSL_PRIV'];
+function validateName(name) {
+ if (!/^[a-z][0-9a-z_\-]*$/.test(name)) {
+ throw "invalid name! must be a valid dns fragment ([z-a0-9\-_])";
}
+}
- if (process.env['ZERIGO_DNS_KEY']) {
- options.d = true;
-
- // when we have a DNS key, we can set a hostname!
- var scheme = (options.p ? 'https' : 'http') + '://';
+verbs['destroy'] = function(args) {
+ if (!args || args.length != 1) {
+ throw 'missing required argument: name of instance';
+ }
+ var name = args[0];
+ validateName(name);
+ var hostname = name + ".hacksign.in";
+
+ process.stdout.write("trying to destroy VM for " + hostname + ": ");
+ vm.destroy(name, function(err, deets) {
+ console.log(err ? ("failed: " + err) : "done");
+ process.stdout.write("trying to remove DNS for " + hostname + ": ");
+ dns.deleteRecord(hostname, function(err) {
+ console.log(err ? "failed: " + err : "done");
+ if (deets && deets.ipAddress) {
+ process.stdout.write("trying to remove git remote: ");
+ git.removeRemote(name, deets.ipAddress, function(err) {
+ console.log(err ? "failed: " + err : "done");
+ });
+ }
+ });
+ });
+}
- if (process.env['PERSONA_DEPLOYMENT_HOSTNAME']) {
- options.u = scheme + process.env['PERSONA_DEPLOYMENT_HOSTNAME'];
- } else if (options.n) {
- options.u = scheme + options.n + ".personatest.org";
- }
+verbs['test'] = function() {
+ // let's see if we can contact aws and zerigo
+ process.stdout.write("Checking DNS management access: ");
+ dns.inUse("somerandomname", function(err) {
+ console.log(err ? "NOT ok: " + err : "good");
+ process.stdout.write("Checking AWS access: ");
+ vm.list(function(err) {
+ console.log(err ? "NOT ok: " + err : "good");
+ });
+ });
+}
- } else {
- console.log('WARNING: No DNS key defined in the environment! ' +
- 'I cannot set up DNS for you. We\'ll do this by IP.');
+verbs['deploy'] = function(args) {
+ if (!args || args.length != 1) {
+ throw 'missing required argument: name of instance';
}
+ var name = args[0];
+ validateName(name);
+ var hostname = name + ".hacksign.in";
+ var longName = 'browserid deployment (' + name + ')';
+
+ console.log("attempting to set up " + name + ".hacksign.in");
+
+ dns.inUse(hostname, function(err, r) {
+ checkErr(err);
+ if (r) checkErr("sorry! that name '" + name + "' is already being used. so sad");
+
+ vm.startImage(function(err, r) {
+ checkErr(err);
+ console.log(" ... VM launched, waiting for startup (should take about 20s)");
+
+ vm.waitForInstance(r.instanceId, function(err, deets) {
+ checkErr(err);
+ console.log(" ... Instance ready, setting up DNS");
+ dns.updateRecord(name, "hacksign.in", deets.ipAddress, function(err) {
+ checkErr(err);
+ console.log(" ... DNS set up, setting human readable name in aws");
+
+ vm.setName(r.instanceId, longName, function(err) {
+ checkErr(err);
+ console.log(" ... name set, waiting for ssh access and configuring");
+ var config = { public_url: "https://" + name + ".hacksign.in"};
+
+ ssh.copyUpConfig(deets.ipAddress, config, function(err, r) {
+ checkErr(err);
+ console.log(" ... victory! server is accessible and configured");
+ git.addRemote(name, deets.ipAddress, function(err, r) {
+ if (err && /already exists/.test(err)) {
+ console.log("OOPS! you already have a git remote named 'test'!");
+ console.log("to create a new one: git remote add <name> " +
+ "app@" + deets.ipAddress + ":git");
+ } else {
+ checkErr(err);
+ }
+ console.log(" ... and your git remote is all set up");
+ console.log("");
+ printInstructions(name, deets);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+};
- // pass through/override with user provided vars
- for (var i = 3; i < process.argv.length; i++) {
- var k = process.argv[i];
- if (i + 1 < process.argv.length && k.length === 2 && k[0] === '-') {
- options[k[1]] = process.argv[++i];
+verbs['list'] = function(args) {
+ vm.list(function(err, r) {
+ checkErr(err);
+ console.log(JSON.stringify(r, null, 2));
+ });
+};
+
+var error = (process.argv.length <= 2);
+
+if (!error) {
+ var verb = process.argv[2];
+ if (!verbs[verb]) error = "no such command: " + verb;
+ else {
+ try {
+ verbs[verb](process.argv.slice(3));
+ } catch(e) {
+ error = "error running '" + verb + "' command: " + e;
}
}
+}
- if (process.env['PERSONA_EPHEMERAL_CONFIG']) {
- options.x = process.env['PERSONA_EPHEMERAL_CONFIG'];
- }
-
- cmd += " create --ssl=force";
+if (error) {
+ if (typeof error === 'string') process.stderr.write('fatal error: ' + error + "\n\n");
- Object.keys(options).forEach(function(opt) {
- cmd += " -" + opt;
- cmd += typeof options[opt] === 'string' ? " " + options[opt] : "";
- });
-} else {
- cmd += " " + process.argv.slice(2).join(' ');
+ process.stderr.write('A command line tool to deploy BrowserID onto Amazon\'s EC2\n');
+ process.stderr.write('Usage: ' + path.basename(__filename) +
+ ' <' + Object.keys(verbs).join('|') + "> [args]\n");
+ process.exit(1);
}
-
-console.log("awsbox cmd: " + cmd);
-var cp = child_process.exec(cmd, function(err) {
- if (err) process.exit(err.code);
- else process.exit(0);
-});
-cp.stdout.pipe(process.stdout);
-cp.stderr.pipe(process.stderr);
diff --git a/scripts/deploy/aws.js b/scripts/deploy/aws.js
new file mode 100644
index 0000000..6641989
--- /dev/null
+++ b/scripts/deploy/aws.js
@@ -0,0 +1,7 @@
+const
+awslib = require('aws-lib');
+
+module.exports = awslib.createEC2Client(process.env['AWS_ID'], process.env['AWS_SECRET'], {
+ version: '2011-12-15'
+});
+
diff --git a/scripts/deploy/dns.js b/scripts/deploy/dns.js
new file mode 100644
index 0000000..99a2f58
--- /dev/null
+++ b/scripts/deploy/dns.js
@@ -0,0 +1,83 @@
+const
+http = require('http'),
+xml2js = new (require('xml2js')).Parser(),
+jsel = require('JSONSelect');
+
+const envVar = 'BROWSERID_DEPLOY_DNS_KEY';
+if (!process.env[envVar]) {
+ throw "Missing api key! contact lloyd and set the key in your env: "
+ + envVar;
+}
+
+const api_key = process.env[envVar];
+
+function doRequest(method, path, body, cb) {
+ var req = http.request({
+ auth: 'lloyd@hilaiel.com:' + api_key,
+ host: 'ns.zerigo.com',
+ port: 80,
+ path: path,
+ method: method,
+ headers: {
+ 'Content-Type': 'application/xml',
+ 'Content-Length': body ? body.length : 0
+ }
+ }, function(r) {
+ if ((r.statusCode / 100).toFixed(0) != 2 &&
+ r.statusCode != 404) {
+ return cb("non 200 status: " + r.statusCode);
+ }
+ buf = "";
+ r.on('data', function(chunk) {
+ buf += chunk;
+ });
+ r.on('end', function() {
+ xml2js.parseString(buf, cb);
+ });
+ });
+ if (body) req.write(body);
+ req.end();
+};
+
+exports.updateRecord = function (hostname, zone, ip, cb) {
+ doRequest('GET', '/api/1.1/zones.xml', null, function(err, r) {
+ if (err) return cb(err);
+ var m = jsel.match('object:has(:root > .domain:val(?)) > .id .#',
+ [ zone ], r);
+ if (m.length != 1) return cb("couldn't extract domain id from zerigo");
+ var path = '/api/1.1/hosts.xml?zone_id=' + m[0];
+ var body = '<host><data>' + ip + '</data><host-type>A</host-type>';
+ body += '<hostname>' + hostname + '</hostname>'
+ body += '</host>';
+ doRequest('POST', path, body, function(err, r) {
+ cb(err);
+ });
+ });
+};
+
+exports.deleteRecord = function (hostname, cb) {
+ doRequest('GET', '/api/1.1/hosts.xml?fqdn=' + hostname, null, function(err, r) {
+ if (err) return cb(err);
+ var m = jsel.match('.host .id > .#', r);
+ if (!m.length) return cb("no such DNS record");
+ function deleteOne() {
+ if (!m.length) return cb(null);
+ var one = m.shift();
+ doRequest('DELETE', '/api/1.1/hosts/' + one + '.xml', null, function(err) {
+ if (err) return cb(err);
+ deleteOne();
+ });
+ }
+ deleteOne();
+ });
+};
+
+exports.inUse = function (hostname, cb) {
+ doRequest('GET', '/api/1.1/hosts.xml?fqdn=' + hostname, null, function(err, r) {
+ if (err) return cb(err);
+ var m = jsel.match('.host', r);
+ // we shouldn't have multiple! oops! let's return the first one
+ if (m.length) return cb(null, m[0]);
+ cb(null, null);
+ });
+}
diff --git a/scripts/deploy/git.js b/scripts/deploy/git.js
new file mode 100644
index 0000000..4fc20d1
--- /dev/null
+++ b/scripts/deploy/git.js
@@ -0,0 +1,120 @@
+const
+child_process = require('child_process');
+spawn = child_process.spawn,
+path = require('path');
+
+exports.addRemote = function(name, host, cb) {
+ var cmd = 'git remote add ' + name + ' app@'+ host + ':git';
+ child_process.exec(cmd, cb);
+};
+
+// remove a remote, but only if it is pointed to a specific
+// host. This will keep deploy from killing manuall remotes
+// that you've set up
+exports.removeRemote = function(name, host, cb) {
+ var desired = 'app@'+ host + ':git';
+ var cmd = 'git remote -v show | grep push';
+ child_process.exec(cmd, function(err, r) {
+ try {
+ var remotes = {};
+ r.split('\n').forEach(function(line) {
+ if (!line.length) return;
+ var line = line.split('\t');
+ if (!line.length == 2) return;
+ remotes[line[0]] = line[1].split(" ")[0];
+ });
+ if (remotes[name] && remotes[name] === desired) {
+ child_process.exec('git remote rm ' + name, cb);
+ } else {
+ throw "no such remote";
+ }
+ } catch(e) {
+ cb(e);
+ }
+ });
+};
+
+exports.currentSHA = function(dir, cb) {
+ if (typeof dir === 'function' && cb === undefined) {
+ cb = dir;
+ dir = path.join(__dirname, '..', '..');
+ }
+ console.log(dir);
+
+ var p = spawn('git', [ 'log', '--pretty=%h', '-1' ], {
+ env: { GIT_DIR: path.join(dir, ".git") }
+ });
+ var buf = "";
+ p.stdout.on('data', function(d) {
+ buf += d;
+ });
+ p.on('exit', function(code, signal) {
+ console.log(buf);
+ var gitsha = buf.toString().trim();
+ if (gitsha && gitsha.length === 7) {
+ return cb(null, gitsha);
+ }
+ cb("can't extract git sha from " + dir);
+ });
+};
+
+function splitAndEmit(chunk, cb) {
+ if (chunk) chunk = chunk.toString();
+ if (typeof chunk === 'string') {
+ chunk.split('\n').forEach(function (line) {
+ line = line.trim();
+ if (line.length) cb(line);
+ });
+ }
+}
+
+exports.push = function(dir, host, pr, cb) {
+ if (typeof host === 'function' && cb === undefined) {
+ cb = pr;
+ pr = host;
+ host = dir;
+ dir = path.join(__dirname, '..', '..');
+ }
+
+ var p = spawn('git', [ 'push', 'app@' + host + ":git", 'dev:master' ], {
+ env: {
+ GIT_DIR: path.join(dir, ".git"),
+ GIT_WORK_TREE: dir
+ }
+ });
+ p.stdout.on('data', function(c) { splitAndEmit(c, pr); });
+ p.stderr.on('data', function(c) { splitAndEmit(c, pr); });
+ p.on('exit', function(code, signal) {
+ return cb(code = 0);
+ });
+};
+
+exports.pull = function(dir, remote, branch, pr, cb) {
+ var p = spawn('git', [ 'pull', "-f", remote, branch + ":" + branch ], {
+ env: {
+ GIT_DIR: path.join(dir, ".git"),
+ GIT_WORK_TREE: dir,
+ PWD: dir
+ },
+ cwd: dir
+ });
+
+ p.stdout.on('data', function(c) { splitAndEmit(c, pr); });
+ p.stderr.on('data', function(c) { splitAndEmit(c, pr); });
+
+ p.on('exit', function(code, signal) {
+ return cb(code = 0);
+ });
+}
+
+exports.init = function(dir, cb) {
+ var p = spawn('git', [ 'init' ], {
+ env: {
+ GIT_DIR: path.join(dir, ".git"),
+ GIT_WORK_TREE: dir
+ }
+ });
+ p.on('exit', function(code, signal) {
+ return cb(code = 0);
+ });
+};
diff --git a/scripts/deploy/key.js b/scripts/deploy/key.js
new file mode 100644
index 0000000..d93da01
--- /dev/null
+++ b/scripts/deploy/key.js
@@ -0,0 +1,57 @@
+const
+aws = require('./aws.js'),
+path = require('path'),
+fs = require('fs'),
+child_process = require('child_process'),
+jsel = require('JSONSelect'),
+crypto = require('crypto');
+
+const keyPath = process.env['PUBKEY'] || path.join(process.env['HOME'], ".ssh", "id_rsa.pub");
+
+exports.read = function(cb) {
+ fs.readFile(keyPath, cb);
+};
+
+exports.fingerprint = function(cb) {
+ exports.read(function(err, buf) {
+ if (err) return cb(err);
+ var b = new Buffer(buf.toString().split(' ')[1], 'base64');
+ var md5sum = crypto.createHash('md5');
+ md5sum.update(b);
+ cb(null, md5sum.digest('hex'));
+ });
+/*
+ child_process.exec(
+ "ssh-keygen -lf " + keyPath,
+ function(err, r) {
+ if (!err) r = r.split(' ')[1];
+ cb(err, r);
+ });
+*/
+};
+
+exports.getName = function(cb) {
+ exports.fingerprint(function(err, fingerprint) {
+ if (err) return cb(err);
+
+ var keyName = "browserid deploy key (" + fingerprint + ")";
+
+ // is this fingerprint known?
+ aws.call('DescribeKeyPairs', {}, function(result) {
+ var found = jsel.match(":has(.keyName:val(?)) > .keyName", [ keyName ], result);
+ if (found.length) return cb(null, keyName);
+
+ // key isn't yet installed!
+ exports.read(function(err, key) {
+ aws.call('ImportKeyPair', {
+ KeyName: keyName,
+ PublicKeyMaterial: new Buffer(key).toString('base64')
+ }, function(result) {
+ if (!result) return cb('null result from ec2 on key addition');
+ if (result.Errors) return cb(result.Errors.Error.Message);
+ cb(null, keyName);
+ });
+ });
+ });
+ });
+};
diff --git a/scripts/deploy/sec.js b/scripts/deploy/sec.js
new file mode 100644
index 0000000..d821169
--- /dev/null
+++ b/scripts/deploy/sec.js
@@ -0,0 +1,59 @@
+const
+aws = require('./aws.js');
+jsel = require('JSONSelect'),
+key = require('./key.js');
+
+// every time you change the security group, change this version number
+// so new deployments will create a new group with the changes
+const SECURITY_GROUP_VERSION = 1;
+
+function createError(msg, r) {
+ var m = jsel.match('.Message', r);
+ if (m.length) msg += ": " + m[0];
+ return msg;
+}
+
+exports.getName = function(cb) {
+ var groupName = "browserid group v" + SECURITY_GROUP_VERSION;
+
+ // is this fingerprint known?
+ aws.call('DescribeSecurityGroups', {
+ GroupName: groupName
+ }, function(r) {
+ if (jsel.match('.Code:val("InvalidGroup.NotFound")', r).length) {
+ aws.call('CreateSecurityGroup', {
+ GroupName: groupName,
+ GroupDescription: 'A security group for browserid deployments'
+ }, function(r) {
+ if (!r || !r.return === 'true') {
+ return cb(createError('failed to create security group', r));
+ }
+ aws.call('AuthorizeSecurityGroupIngress', {
+ GroupName: groupName,
+ "IpPermissions.1.IpProtocol": 'tcp',
+ "IpPermissions.1.FromPort": 80,
+ "IpPermissions.1.ToPort": 80,
+ "IpPermissions.1.IpRanges.1.CidrIp": "0.0.0.0/0",
+ "IpPermissions.2.IpProtocol": 'tcp',
+ "IpPermissions.2.FromPort": 22,
+ "IpPermissions.2.ToPort": 22,
+ "IpPermissions.2.IpRanges.1.CidrIp": "0.0.0.0/0",
+ "IpPermissions.3.IpProtocol": 'tcp',
+ "IpPermissions.3.FromPort": 443,
+ "IpPermissions.3.ToPort": 443,
+ "IpPermissions.3.IpRanges.1.CidrIp" : "0.0.0.0/0"
+ }, function(r) {
+ if (!r || !r.return === 'true') {
+ return cb(createError('failed to create security group', r));
+ }
+ cb(null, groupName);
+ });
+ });
+ } else {
+ // already exists?
+ var m = jsel.match('.securityGroupInfo > .item > .groupName', r);
+ if (m.length && m[0] === groupName) return cb(null, groupName);
+ cb(createError('error creating group', r));
+ }
+ });
+};
diff --git a/scripts/deploy/ssh.js b/scripts/deploy/ssh.js
new file mode 100644
index 0000000..290abf1
--- /dev/null
+++ b/scripts/deploy/ssh.js
@@ -0,0 +1,43 @@
+const
+child_process = require('child_process'),
+temp = require('temp'),
+fs = require('fs');
+
+const MAX_TRIES = 20;
+
+exports.copyUpConfig = function(host, config, cb) {
+ var tries = 0;
+ temp.open({}, function(err, r) {
+ fs.writeFileSync(r.path, JSON.stringify(config, null, 4));
+ var cmd = 'scp -o "StrictHostKeyChecking no" ' + r.path + ' app@' + host + ":config.json";
+ function oneTry() {
+ child_process.exec(cmd, function(err, r) {
+ if (err) {
+ if (++tries > MAX_TRIES) return cb("can't connect via SSH. stupid amazon");
+ console.log(" ... nope. not yet. retrying.");
+ setTimeout(oneTry, 5000);
+ } else {
+ cb();
+ }
+ });
+ }
+ oneTry();
+ });
+};
+
+exports.copySSL = function(host, pub, priv, cb) {
+ var cmd = 'scp -o "StrictHostKeyChecking no" ' + pub + ' ec2-user@' + host + ":/etc/ssl/certs/hacksign.in.crt";
+ child_process.exec(cmd, function(err, r) {
+ if (err) return cb(err);
+ var cmd = 'scp -o "StrictHostKeyChecking no" ' + priv + ' ec2-user@' + host + ":/etc/ssl/certs/hacksign.in.key";
+ child_process.exec(cmd, function(err, r) {
+ var cmd = 'ssh -o "StrictHostKeyChecking no" ec2-user@' + host + " 'sudo /etc/init.d/nginx restart'";
+ child_process.exec(cmd, cb);
+ });
+ });
+};
+
+exports.addSSHPubKey = function(host, pubkey, cb) {
+ var cmd = 'ssh -o "StrictHostKeyChecking no" ec2-user@' + host + " 'echo \'" + pubkey + "\' >> .ssh/authorized_keys'";
+ child_process.exec(cmd, cb);
+};
diff --git a/scripts/deploy/vm.js b/scripts/deploy/vm.js
new file mode 100644
index 0000000..de38451
--- /dev/null
+++ b/scripts/deploy/vm.js
@@ -0,0 +1,121 @@
+const
+aws = require('./aws.js');
+jsel = require('JSONSelect'),
+key = require('./key.js'),
+sec = require('./sec.js');
+
+const BROWSERID_TEMPLATE_IMAGE_ID = 'ami-6ed07107';
+
+function extractInstanceDeets(horribleBlob) {
+ var instance = {};
+ ["instanceId", "imageId", "instanceState", "dnsName", "keyName", "instanceType",
+ "ipAddress"].forEach(function(key) {
+ if (horribleBlob[key]) instance[key] = horribleBlob[key];
+ });
+ var name = jsel.match('.tagSet :has(.key:val("Name")) > .value', horribleBlob);
+ if (name.length) {
+ instance.fullName = name[0];
+ // if this is a 'browserid deployment', we'll only display the hostname chosen by the
+ // user
+ var m = /^browserid deployment \((.*)\)$/.exec(instance.fullName);
+ instance.name = m ? m[1] : instance.fullName;
+ } else {
+ instance.name = instance.instanceId;
+ }
+ return instance;
+}
+
+exports.list = function(cb) {
+ aws.call('DescribeInstances', {}, function(result) {
+ var instances = {};
+ var i = 1;
+ jsel.forEach(
+ '.instancesSet > .item:has(.instanceState .name:val("running"))',
+ result, function(item) {
+ var deets = extractInstanceDeets(item);
+ instances[deets.name || 'unknown ' + i++] = deets;
+ });
+ cb(null, instances);
+ });
+};
+
+exports.destroy = function(name, cb) {
+ exports.list(function(err, r) {
+ if (err) return cb('failed to list vms: ' + err);
+ if (!r[name]) return cb('no such vm');
+
+ aws.call('TerminateInstances', {
+ InstanceId: r[name].instanceId
+ }, function(result) {
+ try { return cb(result.Errors.Error.Message); } catch(e) {};
+ cb(null, r[name]);
+ });
+ });
+};
+
+function returnSingleImageInfo(result, cb) {
+ if (!result) return cb('no results from ec2 api');
+ try { return cb(result.Errors.Error.Message); } catch(e) {};
+ try {
+ result = jsel.match('.instancesSet > .item', result)[0];
+ cb(null, extractInstanceDeets(result));
+ } catch(e) {
+ return cb("couldn't extract new instance details from ec2 response: " + e);
+ }
+}
+
+exports.startImage = function(cb) {
+ key.getName(function(err, keyName) {
+ if (err) return cb(err);
+ sec.getName(function(err, groupName) {
+ if (err) return cb(err);
+ aws.call('RunInstances', {
+ ImageId: BROWSERID_TEMPLATE_IMAGE_ID,
+ KeyName: keyName,
+ SecurityGroup: groupName,
+ InstanceType: 't1.micro',
+ MinCount: 1,
+ MaxCount: 1
+ }, function (result) {
+ returnSingleImageInfo(result, cb);
+ });
+ });
+ });
+};
+
+exports.waitForInstance = function(id, cb) {
+ aws.call('DescribeInstanceStatus', {
+ InstanceId: id
+ }, function(r) {
+ if (!r) return cb('no response from ec2');
+ // we're waiting and amazon might not have created the image yet! that's
+ // not an error, just an api timing quirk
+ var waiting = jsel.match('.Error .Code:val("InvalidInstanceID.NotFound")', r);
+ if (waiting.length) {
+ return setTimeout(function(){ exports.waitForInstance(id, cb); }, 1000);
+ }
+
+ if (!r.instanceStatusSet) return cb('malformed response from ec2' + JSON.stringify(r, null, 2));
+ if (Object.keys(r.instanceStatusSet).length) {
+ var deets = extractInstanceDeets(r.instanceStatusSet.item);
+ if (deets && deets.instanceState && deets.instanceState.name === 'running') {
+ return aws.call('DescribeInstances', { InstanceId: id }, function(result) {
+ returnSingleImageInfo(result, cb);
+ });
+ }
+ }
+ setTimeout(function(){ exports.waitForInstance(id, cb); }, 1000);
+ });
+};
+
+exports.setName = function(id, name, cb) {
+ aws.call('CreateTags', {
+ "ResourceId.0": id,
+ "Tag.0.Key": 'Name',
+ "Tag.0.Value": name
+ }, function(result) {
+ if (result && result.return === 'true') return cb(null);
+ try { return cb(result.Errors.Error.Message); } catch(e) {};
+ return cb('unknown error setting instance name');
+ });
+};
diff --git a/scripts/deploy_dev.js b/scripts/deploy_dev.js
new file mode 100755
index 0000000..21c2308
--- /dev/null
+++ b/scripts/deploy_dev.js
@@ -0,0 +1,132 @@
+#!/usr/bin/env node
+
+/*
+ * Deploy dev.diresworb.org, for fun and profit.
+ */
+
+const
+aws = require('./deploy/aws.js');
+path = require('path');
+vm = require('./deploy/vm.js'),
+key = require('./deploy/key.js'),
+ssh = require('./deploy/ssh.js'),
+git = require('./deploy/git.js'),
+dns = require('./deploy/dns.js'),
+util = require('util'),
+events = require('events'),
+fs = require('fs');
+
+// verify we have files we need
+
+// a class capable of deploying and emmitting events along the way
+function DevDeployer() {
+ events.EventEmitter.call(this);
+
+ this.sslpub = process.env['DEV_SSL_PUB'];
+ this.sslpriv = process.env['DEV_SSL_PRIV'];
+ this.keypairs = [];
+ if (process.env['ADDITIONAL_KEYPAIRS']) {
+ this.keypairs = process.env['ADDITIONAL_KEYPAIRS'].split(',');
+ }
+
+ if (!this.sslpub || !this.sslpriv) {
+ throw("you must provide ssl cert paths via DEV_SSL_PUB & DEV_SSL_PRIV");
+ }
+
+ if (!fs.statSync(this.sslpub).isFile() || !fs.statSync(this.sslpriv).isFile()) {
+ throw("DEV_SSL_PUB & DEV_SSL_PRIV must be paths to actual files. duh");
+ }
+}
+
+util.inherits(DevDeployer, events.EventEmitter);
+
+DevDeployer.prototype.setup = function(cb) {
+ var self = this;
+ git.currentSHA(function(err, r) {
+ if (err) return cb(err);
+ self.sha = r;
+ vm.startImage(function(err, r) {
+ if (err) return cb(err);
+ self.emit('progress', "starting new image");
+ vm.waitForInstance(r.instanceId, function(err, d) {
+ if (err) return cb(err);
+ self.deets = d;
+ self.emit('progress', "image started");
+ vm.setName(r.instanceId, "dev.diresworb.org (" + self.sha + ")", function(err, r) {
+ if (err) return cb(err);
+ self.emit('progress', "name set");
+ cb(null);
+ });
+ });
+ });
+ });
+}
+
+DevDeployer.prototype.configure = function(cb) {
+ var self = this;
+ var config = { public_url: "https://dev.diresworb.org" };
+ ssh.copyUpConfig(self.deets.ipAddress, config, function (err) {
+ if (err) return cb(err);
+ ssh.copySSL(self.deets.ipAddress, self.sslpub, self.sslpriv, function(err) {
+ if (err) return cb(err);
+
+ // now copy up addtional keypairs
+ var i = 0;
+ function copyNext() {
+ if (i == self.keypairs.length) return cb(null);
+ ssh.addSSHPubKey(self.deets.ipAddress, self.keypairs[i++], function(err) {
+ if (err) return cb(err);
+ self.emit('progress', "key added...");
+ copyNext();
+ });
+ }
+ copyNext();
+ });
+ });
+}
+
+DevDeployer.prototype.pushCode = function(cb) {
+ var self = this;
+ git.push(this.deets.ipAddress, function(d) { self.emit('build_output', d); }, cb);
+}
+
+DevDeployer.prototype.updateDNS = function(cb) {
+ var self = this;
+ dns.deleteRecord('dev.diresworb.org', function() {
+ dns.updateRecord('', 'dev.diresworb.org', self.deets.ipAddress, cb);
+ });
+}
+
+var deployer = new DevDeployer();
+
+deployer.on('progress', function(d) {
+ console.log("PR: " + d);
+});
+
+deployer.on('build_output', function(d) {
+ console.log("BO: " + d);
+});
+
+function checkerr(err) {
+ if (err) {
+ process.stderr.write("fatal error: " + err + "\n");
+ process.exit(1);
+ }
+}
+
+var startTime = new Date();
+deployer.setup(function(err) {
+ checkerr(err);
+ deployer.configure(function(err) {
+ checkerr(err);
+ deployer.updateDNS(function(err) {
+ checkerr(err);
+ deployer.pushCode(function(err) {
+ checkerr(err);
+ console.log("dev.diresworb.org (" + deployer.sha + ") deployed to " +
+ deployer.deets.ipAddress + " in " +
+ ((new Date() - startTime) / 1000.0).toFixed(2) + "s");
+ });
+ });
+ });
+});
diff --git a/scripts/deploy_server.js b/scripts/deploy_server.js
new file mode 100755
index 0000000..f38338e
--- /dev/null
+++ b/scripts/deploy_server.js
@@ -0,0 +1,293 @@
+#!/usr/bin/env node
+
+const
+temp = require('temp'),
+path = require('path'),
+util = require('util'),
+events = require('events'),
+git = require('./deploy/git.js'),
+https = require('https'),
+vm = require('./deploy/vm.js'),
+jsel = require('JSONSelect'),
+fs = require('fs'),
+express = require('express'),
+irc = require('irc');
+
+console.log("deploy server starting up");
+
+// a class capable of deploying and emmitting events along the way
+function Deployer() {
+ events.EventEmitter.call(this);
+
+ // a directory where we'll keep code
+ this._codeDir = process.env['CODE_DIR'] || temp.mkdirSync();
+ console.log("code dir is:", this._codeDir);
+ var self = this;
+
+ git.init(this._codeDir, function(err) {
+ if (err) {
+ console.log("can't init code dir:", err);
+ process.exit(1);
+ }
+ self.emit('ready');
+ });
+}
+
+util.inherits(Deployer, events.EventEmitter);
+
+Deployer.prototype._getLatestRunningSHA = function(cb) {
+ var self = this;
+
+ // failure is not fatal. maybe nothing is running?
+ var fail = function(err) {
+ self.emit('info', { msg: "can't get current running sha", reason: err });
+ cb(null, null);
+ }
+
+ https.get({ host: 'dev.diresworb.org', path: '/ver.txt' }, function(res) {
+ var buf = "";
+ res.on('data', function (c) { buf += c });
+ res.on('end', function() {
+ try {
+ var sha = buf.split(' ')[0];
+ if (sha.length == 7) {
+ self.emit('info', 'latest running is ' + sha);
+ return cb(null, sha);
+ }
+ fail('malformed ver.txt: ' + buf);
+ } catch(e) {
+ fail(e);
+ }
+ });
+ }).on('error', function(err) {
+ fail(err);
+ });
+
+}
+
+Deployer.prototype._cleanUpOldVMs = function() {
+ var self = this;
+ // what's our sha
+ git.currentSHA(self._codeDir, function(err, latest) {
+ if (err) return self.emit('info', err);
+ vm.list(function(err, r) {
+ if (err) return self.emit('info', err);
+ // only check the vms that have 'dev.diresworb.org' as a name
+ jsel.forEach("object:has(:root > .name:contains(?))", [ "dev.diresworb.org" ], r, function(o) {
+ // don't delete the current one
+ if (o.name.indexOf(latest) == -1) {
+ self.emit('info', 'decommissioning VM: ' + o.name + ' - ' + o.instanceId);
+ vm.destroy(o.name, function(err, r) {
+ if (err) self.emit('info', 'decomissioning failed: ' + err);
+ else self.emit('info', 'decomissioning succeeded of ' + r);
+ })
+ }
+ });
+ });
+ });
+}
+
+Deployer.prototype._deployNewCode = function(cb) {
+ var self = this;
+
+ function splitAndEmit(chunk) {
+ if (chunk) chunk = chunk.toString();
+ if (typeof chunk === 'string') {
+ chunk.split('\n').forEach(function (line) {
+ line = line.trim();
+ if (line.length) self.emit('progress', line);
+ });
+ }
+ }
+
+ var npmInstall = spawn('npm', [ 'install' ], { cwd: self._codeDir });
+
+ npmInstall.stdout.on('data', splitAndEmit);
+ npmInstall.stderr.on('data', splitAndEmit);
+
+ npmInstall.on('exit', function(code, signal) {
+ if (code != 0) {
+ self.emit('error', "can't npm install to prepare to run deploy_dev");
+ return;
+ }
+ var p = spawn('scripts/deploy_dev.js', [], { cwd: self._codeDir });
+
+ p.stdout.on('data', splitAndEmit);
+ p.stderr.on('data', splitAndEmit);
+
+ p.on('exit', function(code, signal) {
+ return cb(code != 0);
+ });
+ });
+};
+
+Deployer.prototype._pullLatest = function(cb) {
+ var self = this;
+ git.pull(this._codeDir, 'git://github.com/mozilla/browserid', 'dev', function(l) {
+ self.emit('progress', l);
+ }, function(err) {
+ if (err) return cb(err);
+ git.currentSHA(self._codeDir, function(err, latest) {
+ if (err) return cb(err);
+ self.emit('info', 'latest available sha is ' + latest);
+ self._getLatestRunningSHA(function(err, running) {
+ if (latest != running) {
+ self.emit('deployment_begins', {
+ sha: latest,
+ });
+ var startTime = new Date();
+
+ self._deployNewCode(function(err, res) {
+ if (err) return cb(err);
+ // deployment is complete!
+ self.emit('deployment_complete', {
+ sha: latest,
+ time: (new Date() - startTime)
+ });
+ // finally, let's clean up old servers
+ self._cleanUpOldVMs();
+ cb(null, null);
+ });
+ } else {
+ self.emit('info', 'up to date');
+ cb(null, null);
+ }
+ });
+ });
+ });
+}
+
+// may be invoked any time we suspect updates have occured to re-deploy
+// if needed
+Deployer.prototype.checkForUpdates = function() {
+ var self = this;
+
+ if (this._busy) return;
+
+ this._busy = true;
+ self.emit('info', 'checking for updates');
+
+ self._pullLatest(function(err, sha) {
+ if (err) self.emit('error', err);
+ self._busy = false;
+ });
+}
+
+var deployer = new Deployer();
+
+var currentLogFile = null;
+// a directory where we'll keep deployment logs
+var deployLogDir = process.env['DEPLOY_LOG_DIR'] || temp.mkdirSync();
+
+var deployingSHA = null;
+
+console.log("deployment log dir is:", deployLogDir);
+
+[ 'info', 'ready', 'error', 'deployment_begins', 'deployment_complete', 'progress' ].forEach(function(evName) {
+ deployer.on(evName, function(data) {
+ if (typeof data != 'string') data = JSON.stringify(data, null, 2);
+ var msg = evName + ": " + data;
+ console.log(msg)
+ if (currentLogFile) currentLogFile.write(msg + "\n");
+ });
+});
+
+// irc integration!
+var ircClient = null;
+const ircChannel = '#identity';
+function ircSend(msg) {
+ if (!ircClient) {
+ ircClient = new irc.Client('irc.mozilla.org', 'browserid_deployer', {
+ channels: [ircChannel]
+ });
+ ircClient.on('error', function(e) {
+ console.log('irc error: ', e);
+ });
+ ircClient.once('join' + ircChannel, function(e) {
+ ircClient.say(ircChannel, msg);
+ });
+ } else {
+ ircClient.say(ircChannel, msg);
+ }
+}
+
+function ircDisconnect() {
+ setTimeout(function() {
+ if (ircClient) {
+ ircClient.disconnect();
+ ircClient = null;
+ }
+ }, 1000);
+}
+
+
+// now when deployment begins, we log all events
+deployer.on('deployment_begins', function(r) {
+ currentLogFile = fs.createWriteStream(path.join(deployLogDir, r.sha + ".txt"));
+ currentLogFile.write("deployment of " + r.sha + " begins\n");
+ deployingSHA = r.sha;
+ ircSend("deploying " + r.sha + " - status https://deployer.hacksign.in/" + r.sha + ".txt");
+});
+
+function closeLogFile() {
+ if (currentLogFile) {
+ currentLogFile.end();
+ currentLogFile = null;
+ }
+}
+
+deployer.on('deployment_complete', function(r) {
+ ircSend("deployment of " + deployingSHA + " completed successfully in " +
+ (r.time / 1000.0).toFixed(2) + "s");
+ ircDisconnect();
+
+ closeLogFile();
+ deployingSHA = null;
+
+ // always check to see if we should try another deployment after one succeeds to handle rapid fire
+ // commits
+ deployer.checkForUpdates();
+});
+
+deployer.on('error', function(r) {
+ ircSend("deployment of " + deployingSHA + " failed. check logs for deets");
+ ircDisconnect();
+
+ closeLogFile();
+ deployingSHA = null;
+
+ // on error, try again in 2 minutes
+ setTimeout(function () {
+ deployer.checkForUpdates();
+ }, 2 * 60 * 1000);
+});
+
+
+// we check every 3 minutes no mattah what. (checks are cheap, github webhooks are flakey)
+setInterval(function () {
+ deployer.checkForUpdates();
+}, (1000 * 60 * 3));
+
+// check for updates at startup
+deployer.on('ready', function() {
+ deployer.checkForUpdates();
+
+ var app = express.createServer();
+
+ app.get('/check', function(req, res) {
+ deployer.checkForUpdates();
+ res.send('ok');
+ });
+
+ app.get('/', function(req, res) {
+ var what = "idle";
+ if (deployingSHA) what = "deploying " + deployingSHA;
+ res.send(what);
+ });
+
+ app.use(express.static(deployLogDir));
+
+ app.listen(process.env['PORT'] || 8080, function() {
+ console.log("deploy server bound");
+ });
+});
diff --git a/tests/i18n-tests.js b/tests/i18n-tests.js
index 9001546..4add4f4 100755
--- a/tests/i18n-tests.js
+++ b/tests/i18n-tests.js
@@ -61,7 +61,7 @@ suite.addBatch({
var supported = ['af', 'en-US', 'pa'];
var def = 'en-US';
return i18n.bestLanguage(
- i18n.parseAcceptLanguage(accept),
+ parseAcceptLanguage(accept),
supported, def);
},
"For Punjabi": function (err, locale) {
@@ -74,7 +74,7 @@ suite.addBatch({
var supported = ['af', 'en-US', 'pa'];
var def = 'en-US';
return i18n.bestLanguage(
- i18n.parseAcceptLanguage(accept),
+ parseAcceptLanguage(accept),
supported, def);
},
"For Punjabi (India) serve Punjabi": function (err, locale) {
@@ -87,7 +87,7 @@ suite.addBatch({
var supported = ['af', 'en-US', 'pa-IT'];
var def = 'en-US';
return i18n.bestLanguage(
- i18n.parseAcceptLanguage(accept),
+ parseAcceptLanguage(accept),
supported, def);
},
"Don't choose Punjabi (India)": function (err, locale) {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment