mgebbe@0: // app.js mgebbe@0: // mgebbe@0: // entrypoint for pump.io-enabled node.js apps mgebbe@0: // mgebbe@0: // Copyright 2013, E14N https://e14n.com/ mgebbe@0: // mgebbe@0: // Licensed under the Apache License, Version 2.0 (the "License"); mgebbe@0: // you may not use this file except in compliance with the License. mgebbe@0: // You may obtain a copy of the License at mgebbe@0: // mgebbe@0: // http://www.apache.org/licenses/LICENSE-2.0 mgebbe@0: // mgebbe@0: // Unless required by applicable law or agreed to in writing, software mgebbe@0: // distributed under the License is distributed on an "AS IS" BASIS, mgebbe@0: // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. mgebbe@0: // See the License for the specific language governing permissions and mgebbe@0: // limitations under the License. mgebbe@0: mgebbe@0: var fs = require("fs"), mgebbe@0: async = require("async"), mgebbe@0: path = require("path"), mgebbe@0: http = require("http"), mgebbe@0: https = require("https"), mgebbe@0: _ = require("underscore"), mgebbe@0: express = require('express'), mgebbe@0: DialbackClient = require("dialback-client"), mgebbe@0: Logger = require("bunyan"), mgebbe@0: routes = require('./routes'), mgebbe@0: databank = require("databank"), mgebbe@0: uuid = require("node-uuid"), mgebbe@0: Databank = databank.Databank, mgebbe@0: DatabankObject = databank.DatabankObject, mgebbe@0: DatabankStore = require('connect-databank')(express), mgebbe@0: RequestToken = require("./models/requesttoken"), mgebbe@0: RememberMe = require("./models/rememberme"), mgebbe@0: User = require("./models/user"), mgebbe@0: Host = require("./models/host"), mgebbe@0: site = require("./models/site"), mgebbe@0: auth = require("./auth.js"), mgebbe@0: defaults = { mgebbe@0: port: 4000, mgebbe@0: hostname: "localhost", mgebbe@0: driver: "memory", mgebbe@0: params: null, mgebbe@0: name: "An unconfigured pump.io client", mgebbe@0: description: "A pump.io client that is not correctly configured.", mgebbe@0: logfile: null, mgebbe@0: loglevel: "info", mgebbe@0: nologger: false, mgebbe@0: key: null, mgebbe@0: cert: null, mgebbe@0: sessionSecret: "insecure", mgebbe@0: static: null, mgebbe@0: address: null, mgebbe@0: useCDN: true mgebbe@0: }; mgebbe@0: mgebbe@0: var PumpIOClientApp = function(configArg) { mgebbe@0: mgebbe@0: var clap = this, mgebbe@0: config = _.defaults(configArg, defaults), mgebbe@0: log, mgebbe@0: db, mgebbe@0: app, mgebbe@0: setupLog = function() { mgebbe@0: var logParams = { mgebbe@0: serializers: { mgebbe@0: req: Logger.stdSerializers.req, mgebbe@0: res: Logger.stdSerializers.res, mgebbe@0: err: Logger.stdSerializers.err mgebbe@0: }, mgebbe@0: level: config.loglevel mgebbe@0: }; mgebbe@0: mgebbe@0: if (config.logfile) { mgebbe@0: logParams.streams = [{path: config.logfile}]; mgebbe@0: } else if (config.nologger) { mgebbe@0: logParams.streams = [{path: "/dev/null"}]; mgebbe@0: } else { mgebbe@0: logParams.streams = [{stream: process.stderr}]; mgebbe@0: } mgebbe@0: mgebbe@0: logParams.name = config.name; mgebbe@0: mgebbe@0: log = new Logger(logParams); mgebbe@0: mgebbe@0: log.debug("Initializing"); mgebbe@0: mgebbe@0: // Configure the service object mgebbe@0: mgebbe@0: log.debug({name: config.name, mgebbe@0: description: config.description, mgebbe@0: hostname: config.hostname}, mgebbe@0: "Initializing site object"); mgebbe@0: }, mgebbe@0: setupSite = function() { mgebbe@0: site.name = config.name; mgebbe@0: site.description = config.description; mgebbe@0: site.hostname = config.hostname; mgebbe@0: mgebbe@0: site.protocol = (config.key) ? "https" : "http"; mgebbe@0: }, mgebbe@0: setupDB = function(callback) { mgebbe@0: if (!config.params) { mgebbe@0: if (config.driver == "disk") { mgebbe@0: config.params = {dir: "/var/lib/"+config.hostname+"/"}; mgebbe@0: } else { mgebbe@0: config.params = {}; mgebbe@0: } mgebbe@0: } mgebbe@0: mgebbe@0: // Define the database schema mgebbe@0: mgebbe@0: if (!config.params.schema) { mgebbe@0: config.params.schema = {}; mgebbe@0: } mgebbe@0: mgebbe@0: _.extend(config.params.schema, DialbackClient.schema); mgebbe@0: _.extend(config.params.schema, DatabankStore.schema); mgebbe@0: mgebbe@0: // Now, our stuff mgebbe@0: mgebbe@0: _.each([User, Host, RequestToken, RememberMe], function(Cls) { mgebbe@0: config.params.schema[Cls.type] = Cls.schema; mgebbe@0: }); mgebbe@0: mgebbe@0: db = Databank.get(config.driver, config.params); mgebbe@0: mgebbe@0: log.debug({driver: config.driver, params: config.params}, mgebbe@0: "Connecting to DB"); mgebbe@0: mgebbe@0: // Set global databank info mgebbe@0: mgebbe@0: DatabankObject.bank = db; mgebbe@0: }, mgebbe@0: requestLogger = function(log) { mgebbe@0: return function(req, res, next) { mgebbe@0: var weblog = log.child({"req_id": uuid.v4(), component: "web"}); mgebbe@0: var end = res.end; mgebbe@0: req.log = weblog; mgebbe@0: res.end = function(chunk, encoding) { mgebbe@0: var rec; mgebbe@0: res.end = end; mgebbe@0: res.end(chunk, encoding); mgebbe@0: rec = {req: req, res: res}; mgebbe@0: weblog.info(rec); mgebbe@0: }; mgebbe@0: next(); mgebbe@0: }; mgebbe@0: }, mgebbe@0: setupApp = function() { mgebbe@0: var client; mgebbe@0: mgebbe@0: app = new express(); mgebbe@0: mgebbe@0: // Configuration mgebbe@0: mgebbe@0: var dbstore = new DatabankStore(db, log, 60000); mgebbe@0: mgebbe@0: log.debug("Configuring app"); mgebbe@0: mgebbe@0: app.configure(function(){ mgebbe@0: var serverVersion = site.userAgent() + ' express/'+express.version + ' node.js/'+process.version, mgebbe@0: versionStamp = function(req, res, next) { mgebbe@0: res.setHeader('Server', serverVersion); mgebbe@0: next(); mgebbe@0: }, mgebbe@0: appObject = function(req, res, next) { mgebbe@0: req.site = site; mgebbe@0: res.locals.site = site; mgebbe@0: res.locals.config = req.app.config; mgebbe@0: next(); mgebbe@0: }; mgebbe@0: mgebbe@0: app.set('views', path.join(__dirname, '../../../views')); mgebbe@0: app.set('view engine', 'jade'); mgebbe@0: app.use(requestLogger(log)); mgebbe@0: app.use(versionStamp); mgebbe@0: app.use(appObject); mgebbe@0: app.use(express.bodyParser()); mgebbe@0: app.use(express.cookieParser()); mgebbe@0: app.use(express.methodOverride()); mgebbe@0: app.use(express.session({secret: config.sessionSecret, mgebbe@0: cookie: {path: '/', httpOnly: true}, mgebbe@0: store: dbstore})); mgebbe@0: app.use(app.router); mgebbe@0: if (config.static) { mgebbe@0: app.use(express.static(config.static)); mgebbe@0: } mgebbe@0: app.use(express.static(path.join(__dirname, 'public'))); mgebbe@0: }); mgebbe@0: mgebbe@0: app.configure('development', function(){ mgebbe@0: app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); mgebbe@0: }); mgebbe@0: mgebbe@0: app.configure('production', function(){ mgebbe@0: app.use(express.errorHandler()); mgebbe@0: }); mgebbe@0: mgebbe@0: // Routes mgebbe@0: mgebbe@0: log.debug("Initializing routes"); mgebbe@0: mgebbe@0: app.get('/', auth.userAuth, auth.userOptional, routes.index); mgebbe@0: app.get('/login', auth.userAuth, auth.noUser, routes.login); mgebbe@0: app.post('/login', auth.userAuth, auth.noUser, routes.handleLogin); mgebbe@0: app.post('/logout', auth.userAuth, auth.userRequired, routes.handleLogout); mgebbe@0: app.get('/authorized/:hostname', routes.authorized); mgebbe@0: app.get('/.well-known/host-meta.json', routes.hostmeta); mgebbe@0: mgebbe@0: // Create a dialback client mgebbe@0: mgebbe@0: log.debug("Initializing dialback client"); mgebbe@0: mgebbe@0: client = new DialbackClient({ mgebbe@0: hostname: config.hostname, mgebbe@0: app: app, mgebbe@0: bank: db, mgebbe@0: userAgent: site.userAgent() mgebbe@0: }); mgebbe@0: mgebbe@0: // Configure this global object mgebbe@0: mgebbe@0: Host.dialbackClient = client; mgebbe@0: mgebbe@0: // Let Web stuff get to config mgebbe@0: mgebbe@0: app.config = config; mgebbe@0: mgebbe@0: // For handling errors mgebbe@0: mgebbe@0: app.log = function(obj) { mgebbe@0: if (obj instanceof Error) { mgebbe@0: log.error(obj); mgebbe@0: } else { mgebbe@0: log.info(obj); mgebbe@0: } mgebbe@0: }; mgebbe@0: }; mgebbe@0: mgebbe@0: // Dynamic default mgebbe@0: mgebbe@0: if (!config.address) { mgebbe@0: config.address = config.hostname; mgebbe@0: } mgebbe@0: mgebbe@0: // Set up aspects mgebbe@0: mgebbe@0: setupLog(); mgebbe@0: setupSite(); mgebbe@0: setupDB(); mgebbe@0: setupApp(); mgebbe@0: mgebbe@0: // Delegate mgebbe@0: mgebbe@0: _.each(_.functions(app), function(name) { mgebbe@0: clap[name] = function() { mgebbe@0: app[name].apply(app, arguments); mgebbe@0: }; mgebbe@0: }); mgebbe@0: mgebbe@0: // Expose the log so clients can use it mgebbe@0: mgebbe@0: clap.log = log; mgebbe@0: mgebbe@0: // Expose the site so clients can use it mgebbe@0: mgebbe@0: clap.site = site; mgebbe@0: mgebbe@0: // Run mgebbe@0: mgebbe@0: clap.run = function(callback) { mgebbe@0: mgebbe@0: var srv, mgebbe@0: bounce; mgebbe@0: mgebbe@0: if (config.key) { mgebbe@0: mgebbe@0: log.debug("Using SSL"); mgebbe@0: mgebbe@0: srv = https.createServer({key: fs.readFileSync(config.key), mgebbe@0: cert: fs.readFileSync(config.cert)}, mgebbe@0: app); mgebbe@0: mgebbe@0: bounce = http.createServer(function(req, res, next) { mgebbe@0: var host = req.headers.host, mgebbe@0: url = 'https://'+host+req.url; mgebbe@0: res.writeHead(301, {'Location': url, mgebbe@0: 'Content-Type': 'text/html'}); mgebbe@0: res.end(''+url+''); mgebbe@0: }); mgebbe@0: mgebbe@0: } else { mgebbe@0: log.debug("Not using SSL"); mgebbe@0: srv = http.createServer(app); mgebbe@0: } mgebbe@0: mgebbe@0: // Start the app mgebbe@0: mgebbe@0: async.waterfall([ mgebbe@0: function(callback) { mgebbe@0: db.connect(config.params, callback); mgebbe@0: }, mgebbe@0: function(callback) { mgebbe@0: // Wrapper function to give a callback-like interface to network servers mgebbe@0: var listenBack = function(server, port, address, callback) { mgebbe@0: var a, mgebbe@0: removeListeners = function() { mgebbe@0: server.removeListener("listening", listenSuccessHandler); mgebbe@0: server.removeListener("error", listenErrorHandler); mgebbe@0: }, mgebbe@0: listenErrorHandler = function(err) { mgebbe@0: removeListeners(); mgebbe@0: callback(err); mgebbe@0: }, mgebbe@0: listenSuccessHandler = function() { mgebbe@0: removeListeners(); mgebbe@0: callback(null); mgebbe@0: }; mgebbe@0: server.on("error", listenErrorHandler); mgebbe@0: server.on("listening", listenSuccessHandler); mgebbe@0: server.listen(port, address); mgebbe@0: }; mgebbe@0: mgebbe@0: async.parallel([ mgebbe@0: function(callback) { mgebbe@0: log.debug({port: config.port, address: config.address}, "Starting app listener"); mgebbe@0: listenBack(srv, config.port, config.address, callback); mgebbe@0: }, mgebbe@0: function(callback) { mgebbe@0: if (bounce) { mgebbe@0: log.debug({port: 80, address: config.address}, "Starting bounce listener"); mgebbe@0: listenBack(bounce, 80, config.address, callback); mgebbe@0: } else { mgebbe@0: callback(null, null); mgebbe@0: } mgebbe@0: } mgebbe@0: ], callback); mgebbe@0: } mgebbe@0: ], function(err, results) { mgebbe@0: // Ignore meaningless results mgebbe@0: callback(err); mgebbe@0: }); mgebbe@0: }; mgebbe@0: }; mgebbe@0: mgebbe@0: // Export the auth methods mgebbe@0: mgebbe@0: _.each(_.keys(auth), function(key) { mgebbe@0: PumpIOClientApp[key] = auth[key]; mgebbe@0: }); mgebbe@0: mgebbe@0: // Export the model classes mgebbe@0: mgebbe@0: PumpIOClientApp.User = User; mgebbe@0: PumpIOClientApp.Host = Host; mgebbe@0: PumpIOClientApp.RequestToken = RequestToken; mgebbe@0: PumpIOClientApp.RememberMe = RememberMe; mgebbe@0: mgebbe@0: module.exports = PumpIOClientApp;