Skip to content
Snippets Groups Projects
browserid 11.2 KiB
Newer Older
/* ***** 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):
 *
 * 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 ***** */

const
fs = require('fs'),
path = require('path'),
url = require('url'),
http = require('http');
sessions = require('connect-cookie-session'),
urlparse = require('urlparse'),
express = require('express');
wsapi = require('../lib/browserid/wsapi.js'),
httputils = require('../lib/httputils.js'),
secrets = require('../lib/secrets.js'),
db = require('../lib/db.js'),
config = require('../lib/configuration.js'),
heartbeat = require('../lib/heartbeat.js'),
metrics = require('../lib/metrics.js'),
logger = require('../lib/logging.js').logger
forward = require('../lib/browserid/http_forward'),
shutdown = require('../lib/shutdown');
app = express.createServer();
logger.info("browserid server starting up");

const COOKIE_SECRET = secrets.hydrateSecret('browserid_cookie', config.get('var_path'));
Lloyd Hilaiel's avatar
Lloyd Hilaiel committed
const COOKIE_KEY = 'browserid_state';

// verify that we have a keysigner configured
if (!config.get('keysigner_url')) {
  logger.error('missing required configuration - url for the keysigner (KEYSIGNER_URL in env)');
  process.exit(1);
}

function router(app) {
  app.set("views", path.join(__dirname, "..", "resources", "views"));
    production: config.get('use_minified_resources')
Ben Adida's avatar
Ben Adida committed
  // this should probably be an internal redirect
  // as soon as relative paths are figured out.
  app.get('/sign_in', function(req, res, next ) {
    res.render('dialog.ejs', {
      title: 'A Better Way to Sign In',
      layout: 'dialog_layout.ejs',
      useJavascript: true,
      production: config.get('use_minified_resources')
Ben Adida's avatar
Ben Adida committed

  app.get('/communication_iframe', function(req, res, next ) {
    res.removeHeader('x-frame-options');
    res.render('communication_iframe.ejs', {
      layout: false,
      production: config.get('use_minified_resources')
    });
  });

  app.get("/unsupported_dialog", function(req,res) {
    res.render('unsupported_dialog.ejs', {layout: 'dialog_layout.ejs', useJavascript: false});
  });

  // Used for a relay page for communication.
Ben Adida's avatar
Ben Adida committed
  app.get("/relay", function(req,res, next) {
    // Allow the relay to be run within a frame
    res.removeHeader('x-frame-options');
    res.render('relay.ejs', {
      layout: false,
      production: config.get('use_minified_resources')
    res.render('index.ejs', {title: 'A Better Way to Sign In', fullpage: true});
  });
  app.get("/signup", function(req, res) {
    res.render('signup.ejs', {title: 'Sign Up', fullpage: false});
Ben Adida's avatar
Ben Adida committed
  app.get("/forgot", function(req, res) {
    res.render('forgot.ejs', {title: 'Forgot Password', fullpage: false, email: req.query.email});
  });

  app.get("/signin", function(req, res) {
    res.render('signin.ejs', {title: 'Sign In', fullpage: false});
  app.get("/about", function(req, res) {
    res.render('about.ejs', {title: 'About', fullpage: false});
    res.render('tos.ejs', {title: 'Terms of Service', fullpage: false});
  });
  app.get("/privacy", function(req, res) {
    res.render('privacy.ejs', {title: 'Privacy Policy', fullpage: false});
  });
  app.get("/verify_email_address", function(req, res) {
Ben Adida's avatar
Ben Adida committed
    res.render('verifyuser.ejs', {title: 'Complete Registration', fullpage: true, token: req.query.token});
Ben Adida's avatar
Ben Adida committed

  app.get("/add_email_address", function(req,res) {
    res.render('verifyemail.ejs', {title: 'Verify Email Address', fullpage: false});
Ben Adida's avatar
Ben Adida committed
  // REDIRECTS
  REDIRECTS = {
    "/manage": "/",
    "/users": "/",
Ben Adida's avatar
Ben Adida committed
    "/primaries" : "/developers",
    "/primaries/" : "/developers",
    "/developers" : "https://github.com/mozilla/browserid/wiki/How-to-Use-BrowserID-on-Your-Site"
  };

  // set up all the redirects
  // oh my watch out for scope issues on var url - closure time
  for (var url in REDIRECTS) {
    (function(from,to) {
      app.get(from, function(req, res) {
        res.redirect(to);
      });
    })(url, REDIRECTS[url]);
  }

  // register all the WSAPI handlers
  wsapi.setup(app);
  heartbeat.setup(app, function(cb) {
    // let's check stuff!  first the heartbeat of our keysigner
    heartbeat.check(config.get('keysigner_url'), cb);
  });
Ben Adida's avatar
Ben Adida committed
  // the public key
  app.get("/pk", function(req, res) {
    res.json(config.get('public_key').toSimpleObject());
  // vep bundle of JavaScript
  app.get("/vepbundle", function(req, res) {
    fs.readFile(__dirname + "/../node_modules/jwcrypto/vepbundle.js", function(error, content) {
      if (error) {
        res.writeHead(500);
        res.end("oops");
        console.log(error);
      } else {
        res.writeHead(200, {'Content-Type': 'text/javascript'});
        res.write(content);
        res.end();
      }
    });
  });

  shutdown.installUpdateHandler(app, function(readyForShutdown) {
    logger.debug("closing database connection");
    db.close(readyForShutdown)
// request to logger, dev formatted which omits personal data in the requests
app.use(express.logger({
  format: config.get('express_log_format'),
  stream: {
    write: function(x) {
      logger.info(typeof x === 'string' ? x.trim() : x);
  }
}));

// if these are verify requests, we'll redirect them off
// to the verifier
if (config.get('verifier_url')) {
  app.use(function(req, res, next) {
    if (/^\/verify$/.test(req.url)) {
      forward(
        config.get('verifier_url'), req, res,
        function(err) {
          if (err) {
            logger.error("error forwarding request:", err);
          }
        });
// over SSL?
var overSSL = (config.get('scheme') == 'https');

app.use(express.cookieParser());

var cookieSessionMiddleware = sessions({
  secret: COOKIE_SECRET,
  key: COOKIE_KEY,
  cookie: {
    path: '/wsapi',
    httpOnly: true,
    // IMPORTANT: we allow users to go 1 weeks on the same device
    // without entering their password again
    maxAge: config.get('authentication_duration_ms'),
    secure: overSSL
  }
});

// cookie sessions && cache control
app.use(function(req, resp, next) {
  // cookie sessions are only applied to calls to /wsapi
  // as all other resources can be aggressively cached
  // by layers higher up based on cache control headers.
  // the fallout is that all code that interacts with sessions
  // should be under /wsapi
  if (/^\/wsapi/.test(req.url)) {
    // explicitly disallow caching on all /wsapi calls (issue #294)
    resp.setHeader('Cache-Control', 'no-cache, max-age=0');

    // we set this parameter so the connect-cookie-session
    // sends the cookie even though the local connection is HTTP
    // (the load balancer does SSL)
    if (overSSL)
      req.connection.proxySecure = true;

    return cookieSessionMiddleware(req, resp, next);

  } else {
config.performSubstitution(app);
// verify all JSON responses are objects - prevents regression on issue #217
app.use(function(req, resp, next) {
  var realRespJSON = resp.json;
  resp.json = function(obj) {
    if (!obj || typeof obj !== 'object') {
      logger.error("INTERNAL ERROR!  *all* json responses must be objects");
      throw "internal error";
    }
    realRespJSON.call(resp, obj);
  };
  return next();
});

app.use(express.bodyParser());

// Check CSRF token early.  POST requests are only allowed to
// /wsapi and they always must have a valid csrf token
app.use(function(req, resp, next) {
  // only on POSTs
  if (req.method == "POST") {
    var denied = false;
    if (!/^\/wsapi/.test(req.url)) { // post requests only allowed to /wsapi
      denied = true;
      logger.warn("CSRF validation failure: POST only allowed to /wsapi urls.  not '" + req.url + "'");
    }
    else if (req.session === undefined) { // there must be a session
      denied = true;
      logger.warn("CSRF validation failure: POST calls to /wsapi require an active session");
    }
    // the session must have a csrf token
    else if (typeof req.session.csrf !== 'string') {
      denied = true;
      logger.warn("CSRF validation failure: POST calls to /wsapi require an csrf token to be set");
    // and the token must match what is sent in the post body
    else if (req.body.csrf != req.session.csrf) {
      denied = true;
      // if any of these things are false, then we'll block the request
      logger.warn("CSRF validation failure, token mismatch. got:" + req.body.csrf + " want:" + req.session.csrf);
    if (denied) return httputils.badRequest(resp, "CSRF violation");
Ben Adida's avatar
Ben Adida committed

// Strict Transport Security
app.use(function(req, resp, next) {
  if (overSSL) {
    // expires in 30 days, include subdomains like www
    resp.setHeader("Strict-Transport-Security", "max-age=2592000; includeSubdomains");
  }
  next();
});
// prevent framing
app.use(function(req, resp, next) {
  resp.setHeader('x-frame-options', 'DENY');
  next();
});
// add the actual URL handlers other than static
router(app);

// use the express 'static' middleware for serving of static files (cache headers, HTTP range, etc)
app.use(express.static(path.join(__dirname, "..", "resources", "static")));

// open the databse
db.open(config.get('database'), function (error) {

  if (error) {
    logger.error("can't open database: " + error);
    // let async logging flush, then exit 1
    return setTimeout(function() { process.exit(1); }, 0);
  }

  // shut down express gracefully on SIGINT
  shutdown.handleTerminationSignals(app, function(readyForShutdown) {
    db.close(readyForShutdown)
  });

  var bindTo = config.get('bind_to');
  app.listen(bindTo.port, bindTo.host, function() {
    logger.info("running on http://" + app.address().address + ":" + app.address().port);
  });
Lloyd Hilaiel's avatar
Lloyd Hilaiel committed
});