'use strict' /* IMPORTS */ const express = require('express') const helmet = require('helmet') const csp = require('helmet-csp') const rateLimit = require('express-request-limit') const bodyParser = require('body-parser') const cookieParser = require('cookie-parser') const cookieSession = require('cookie-session') const csurf = require('csurf') const mongoose = require('mongoose') const nunjucks = require('nunjucks') const passport = require('passport') const flash = require('connect-flash-plus') const env = require('./config/env/env.js') const User = require('./config/models.js').user const mail = require('./config/mail.js') const demo = require('./config/demo.js') const app = express() const http = require('http').Server(app) const io = require('socket.io')(http) const sockets = require('./config/sockets.js') // Promises marking a ready server let ready_promise_list = [] /* Database */ { // Setup with native ES6 promises mongoose.Promise = global.Promise // Connect to database ready_promise_list.push( new Promise( async (resolve, reject) => { try { mongoose.connect(env.mongoSetup, { useMongoClient: true, socketTimeoutMS: 30000, //reconnectTries: 30, keepAlive: true }) console.log(` Mongoose connected to ${env.mongoSetup}`) resolve() } catch (err) { console.error(err.stack) reject() } }) ) } /* Templates */ { nunjucks.configure(__dirname + '/views', { autoescape: true, express: app, }) app.set('view engine', 'html') } /* Express session and settings */ app.use( helmet.referrerPolicy({ policy: 'strict-origin', }), csp({directives:{ 'default-src': ["'self'"], 'script-src': ["'self'", "'unsafe-inline'", // TODO: Get rid of this 'https://code.jquery.com', 'https://cdnjs.cloudflare.com/ajax/libs/socket.io/', 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/', 'https://www.google.com/recaptcha/', 'https://www.google-analytics.com', 'https://maps.googleapis.com', // 'https://coin-hive.com', // 'https://coinhive.com', ], // 'worker-src': ["'self'", // 'blob:', // for coinhive // ], 'connect-src': ["'self'", 'wss://*.tracman.org', // 'wss://*.coinhive.com', ], 'style-src': ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com', 'https://maxcdn.bootstrapcdn.com', ], 'font-src': [ 'https://fonts.gstatic.com', 'https://maxcdn.bootstrapcdn.com/font-awesome/*', 'https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/fonts/fontawesome-webfont.woff', 'https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/fonts/fontawesome-webfont.ttf', 'https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/fonts/fontawesome-webfont.woff2', ], 'img-src': ["'self'", 'https://www.google-analytics.com', 'https://maps.gstatic.com', 'https://*.googleapis.com', 'https://http.cat', ], 'object-src': ["'none'"], 'report-uri': '/csp-violation', }}), cookieParser(env.cookie), cookieSession({ cookie: { maxAge: 1000 * 60 * 60 * 24 * 7, // 1 week secure: true, httpOnly: true, domain: env.url.substring(env.url.indexOf('//')+2), }, secret: env.session, saveUninitialized: true, resave: true, }), bodyParser.json(), bodyParser.urlencoded({ extended: true, }), flash() ) /* Report CSP violations */ app.post('/csp-violation', (req, res) => { console.log(`CSP Violation: ${JSON.stringify(req.body)}`) res.status(204).end() }) /* Auth */ { require('./config/passport.js')(passport) app.use(passport.initialize(), passport.session()) } /* Routes */ { // Static files (keep this before default locals) app.use('/static', express.static(__dirname + '/static', {dotfiles: 'allow'})) // Default locals available to all views (keep this after static files) app.get('*', (req, res, next) => { // Rate limit rateLimit({ timeout: 1000 * 60 * 30, // 30 minutes exactPath: true, cleanUpInterval: 1000 * 60 * 60 * 24 * 7, // 1 week }) // User account res.locals.user = req.user // Flash messages res.locals.successes = req.flash('success') res.locals.dangers = req.flash('danger') res.locals.warnings = req.flash('warning') next() }) // Auth routes require('./config/routes/auth.js')(app, passport) // Main routes app.use('/', require('./config/routes/index.js')) // Contact form app.use('/contact', require('./config/routes/contact.js')) // Settings app.use('/settings', require('./config/routes/settings.js')) // Account settings app.use('/account', require('./config/routes/account.js')) // Map app.use(['/map', '/trac'], require('./config/routes/map.js')) // Site administration app.use('/admin', require('./config/routes/admin.js')) // Testing if (env.mode == 'development') app.use('/test', require('./config/routes/test.js')) } { // Catch-all for 404s app.use((req, res, next) => { if (!res.headersSent) { let err = new Error(`Not found: ${req.url}`) err.status = 404 next(err) } }) // Production handlers if (env.mode !== 'development') { app.use((err, req, res, next) => { if (err.status !== 404 && err.status !== 401) console.error(err.stack) if (res.headersSent) return next(err) res.status(err.status || 500) res.render('error', { code: err.status || 500, message: (err.status < 500) ? err.message : 'Server error' }) }) // Development handlers } else { app.use((err, req, res, next) => { if (err.status !== 404) console.error(err.stack) if (res.headersSent) return next(err) res.status(err.status || 500) res.render('error', { code: err.status || 500, message: err.message, stack: err.stack }) }) } } // CSRF Protection (keep after routes) app.use(csurf({ cookie: true, })) /* Sockets */ { sockets.init(io) } /* RUNTIME */ console.log(`Starting ${env.mode} server at ${__dirname}...`) // Test SMTP server ready_promise_list.push(mail.verify()) // Listen ready_promise_list.push( new Promise( (resolve, reject) => { http.listen(env.port, async () => { console.log(` Express listening on ${env.url}`) resolve() // Check for clients for each user ready_promise_list.push( new Promise( async (resolve, reject) => { try { let users = await User.find({}) users.forEach((user) => { sockets.checkForUsers(io, user.id) }) resolve() } catch (err) { console.error(err.stack) reject(err) } }) ) // Start transmitting demo ready_promise_list.push( demo(io) ) // Mark everything when working correctly try { await Promise.all(ready_promise_list.map( // Also wait for rejected promises // https://stackoverflow.com/a/36115549/3006854 p => p.catch(e => e) )) console.log('Tracman server is running properly\n') } catch (err) { console.error(err.message) console.log(`Tracman server is not running properly!\n`) } }) }) ) module.exports = app