diff --git a/lib/browserid/primary.js b/lib/browserid/primary.js new file mode 100644 index 0000000000000000000000000000000000000000..c27ee3207ba31694e8c3612750da7319417668db --- /dev/null +++ b/lib/browserid/primary.js @@ -0,0 +1,141 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla BrowserID. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Lloyd Hilaiel <lloyd@hilaiel.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// this file is an abstraction around "primary identity authority" support, +// specifically checks and a cache to see if a primary supports browserid +// natively. + +const +https = require('https'), +logger = require('../logging.js').logger, +urlparse = require('urlparse'), +jwk = require('jwcrypto/jwk'); + +const WELL_KNOWN_URL = "/.well-known/vep"; + +// cache .well-known/vep for six hours +const MAX_CACHE_MS = (6 * 60 * 1000); + +function parseWellKnownBody(body, domain) { + var v = JSON.parse(body); + + const want = [ 'public-key', 'authentication', 'provisioning' ]; + var got = Object.keys(v); + + want.forEach(function(k) { + if (-1 === got.indexOf(k)) throw "missing required key: " + k; + }); + + var urls = { + auth: 'https://' + domain + v.authentication, + prov: 'https://' + domain + v.provisioning, + }; + + // validate the urls + urlparse(urls.auth).validate(); + urlparse(urls.prov).validate(); + + // parse the public key + return { + publicKey: jwk.PublicKey.fromSimpleObject(v['public-key']), + urls: urls + }; +} + +// a cache of network responses. We want to move this into +// fast and efficient external key/value storage as we scale +var g_cache = { }; + +exports.checkSupport = function(domain, cb) { + if (!cb) throw "missing required callback function"; + + if (typeof domain !== 'string' || !domain.length) { + return process.nextTick(function() { cb("invalid domain"); }); + } + + // check cache age + if (g_cache[domain]) { + if (!g_cache[domain].when || (new Date() - g_cache[domain].when) > MAX_CACHE_MS) { + delete g_cache[domain]; + } + + if (g_cache[domain]) { + logger.debug("returning primary support status for '" + domain + "' from cache"); + return process.nextTick(function() { cb(null, g_cache[domain].status); }); + } + } + + function cacheAndReturn(cacheValue, publicKey) { + g_cache[domain] = { + when: new Date(), + status: cacheValue, + publicKey: publicKey + }; + cb(null, cacheValue); + } + + // now we need to check to see if domain purports to being a primary for browserid + var req = https.get({ + host: domain, + path: WELL_KNOWN_URL, + agent: false + }, function (res) { + if (res.statusCode !== 200) { + logger.debug(domain + ' is not a browserid primary - non-200 response code to ' + WELL_KNOWN_URL); + return cacheAndReturn(false); + } + if (res.headers['content-type'].indexOf('application/json') !== 0) { + logger.debug(domain + ' is not a browserid primary - non "application/json" response to ' + WELL_KNOWN_URL); + return cacheAndReturn(false); + } + + var body = ""; + res.on('data', function(chunk) { body += chunk; }); + res.on('end', function() { + try { + var r = parseWellKnownBody(body, domain); + logger.info(domain + ' is a valid browserid primary'); + return cacheAndReturn(r.urls, r.publicKey); + } catch(e) { + logger.debug(domain + ' is a broken browserid primary, malformed dec of support: ' + e.toString()); + return cacheAndReturn(false); + } + }); + }).on('error', function(e) { + logger.debug(domain + ' is not a browserid primary: ' + e.toString()); + cacheAndReturn(false); + }); +}; diff --git a/scripts/check_primary_support b/scripts/check_primary_support new file mode 100755 index 0000000000000000000000000000000000000000..42c2bb98e2ba942655ca8828b9ac3b2b84174d1d --- /dev/null +++ b/scripts/check_primary_support @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +const +primary = require('../lib/browserid/primary'), +logging = require('../lib/logging.js'); + +logging.enableConsoleLogging(); + +if (process.argv.length !== 3) { + console.log('Checks to see if a domain has a proper declaration of support as a browserid primary'); + console.log('Usage:', process.argv[1], '<domain>'); + process.exit(1); +} + +primary.checkSupport(process.argv[2], function(err, rv) { + if (err) { + process.stderr.write("error: " + err + "\n"); + process.exit(1); + } + console.log(rv); +});