Source: core/StaticServer.js

const express   = require('express');
const path      = require('path');
const fs        = require('fs-extra');
const Utils     = require('../Utils.js');
const ip        = require('ip');
const http      = require('http');
const https     = require('https');
const replace   = require("replace");
const bodyParser = require("body-parser");
const session   = require('express-session');
// const history   = require('connect-history-api-fallback');
const openurl       = require('openurl');

const selfsigned = require('selfsigned');


/** Server for static files */
class StaticServer {

    constructor(jt) {
        this.jt = jt;
        var expApp = express();
        expApp.use(bodyParser.urlencoded({extended : true}));
        // expApp.use(history());
        expApp.use(session(
            {
                secret: 'keyboard cat',
                cookie: { maxAge: 60000 },
                resave: false,
                saveUninitialized: true
            }
        ))

        var self = this;
        this.expApp = expApp;

        //////////////////////////////
        // FILES TO SERVE
        expApp.use('', express.static(path.join(this.jt.path, jt.settings.participantUI)));
        expApp.use('/help', express.static(path.join(this.jt.path, jt.settings.helpPath)));
        expApp.use('/source', express.static(path.join(this.jt.path, jt.settings.adminUIsSharedPath)));
        expApp.use('/admin', express.static(this.defaultAdminUIPath()));
        // expApp.use('/adminShared', express.static(this.adminUIsSharedPath()));
        var adminUIs = fs.readdirSync(this.adminUIsPath());
        for (var i in adminUIs) {
            // Skip shared folder.
            // if (adminUIs[i] == 'shared') {
            //     continue;
            // }
            var pathToFolder = path.join(this.adminUIsPath(), adminUIs[i]);
            if (fs.lstatSync(pathToFolder).isDirectory()) {
                expApp.use('/admin/' + adminUIs[i], express.static(pathToFolder));
            }
        }
        expApp.use('/participant', express.static(path.join(this.jt.path, jt.settings.participantUI)));
        expApp.use('/shared', express.static(path.join(this.jt.path, jt.settings.sharedUI)));
        for (let i in jt.data.queues) {
            let queue = jt.data.queues[i];
            jt.log('serving files from ' + queue.parentFolderFullName() + ' as /' + queue.parentFolderName());
            expApp.use('/' + queue.parentFolderName(), express.static(queue.parentFolderFullName()));
        }
        // expApp.use(history());

        // END FILE SERVING
        //////////////////////////////

        //////////////////////////////
        // REQUESTS
        expApp.get('/', function(req, res) {
            let pId = null;
            if (req.query.id != null) {
                pId = req.query.id;
            }
            self.sendParticipantPage(req, res, pId, undefined);
        });

        expApp.get('/api/sessions', function(req, res) {
            let sessions = self.jt.data.loadSessions();
            let out = [];
            for (let i=0; i<sessions.length; i++) {
                out.push(sessions[i].shell());
            }
            res.send(out);
        });

        expApp.get('/api/apps', function(req, res) {
            let apps = self.jt.data.getApps();
            let out = [];
            for (let i=0; i<apps.length; i++) {
                out.push(apps[i].metaData());
            }
            res.send(out);
        });

        expApp.get('/api/clients', function(req, res) {
            let clients = self.jt.data.getClients(req.params.sessionId);
            // let out = [];
            // for (let i=0; i<clients.length; i++) {
            //     out.push(clients[i].shell());
            // }
            res.send(clients);
        });

        expApp.get('/:pId', this.handleRequest.bind(this));
        expApp.post('/:pId', this.handleRequest.bind(this));

        expApp.get('/room/:rId', function(req, res) {
            res.cookie('roomId', req.params.rId);
            var room = self.jt.data.room(req.params.rId);
            res.cookie('roomDN', room.displayName);
            res.cookie('hasSecret', room.useSecureURLs);
            res.sendFile(path.join(self.jt.path, self.jt.settings.clientUI, '/room.html'));
        });

        expApp.get('/session-download/:sId', function(req, res) {
            var session = self.jt.data.session(req.params.sId);
            var out = session.saveOutput();
            res.setHeader('Content-disposition', 'attachment; filename=' + session.csvFN());
            res.set('Content-Type', 'text/csv');
            res.status(200).send(out);
        });

        expApp.get('/room/:rId/:pId', function(req, res) {
            var room = self.jt.data.room(req.params.rId);
            if (room.isValidPId(req.params.pId, req.params.hash)) {
                res.sendFile(path.join(self.jt.path, self.jt.settings.participantUI + '/readyClient.html'));
            } else {
                res.cookie('roomId', req.params.rId);
                res.cookie('participantId', req.params.pId);
                res.cookie('roomDN', room.displayName);
                res.cookie('hasSecret', room.useSecureURLs);
                res.sendFile(path.join(self.jt.path, self.jt.settings.clientUI, '/room.html'));
            }

        });

        // GET /logout
        expApp.get('/users/logout', function(req, res, next) {
          if (req.session) {

              for (var i in self.jt.data.users) {
                  var user = self.jt.data.users[i];
                  if (user.id === req.session.userId) {
                      var ind = user.sessionIds.indexOf(req.session.id);
                      user.sessionIds.splice(ind, 1);
                  }
              }

            // delete session object
            req.session.destroy(function(err) {
              if(err) {
                return next(err);
              } else {
                return res.redirect('/admin');
              }
            });
          }
        });

        // expApp.get('/', function(req, res) {
        //     self.sendParticipantPage(req, res, req.query.id, undefined);
        // });
        //
        expApp.get('/session/:sId/:pId', function(req, res) {
            self.sendParticipantPage(req, res, req.params.pId, req.params.sId);
        });

        // Admin interfaces
        for (let i in adminUIs) {
            let pathToFolder = path.join(this.adminUIsPath(), adminUIs[i]);
            if (fs.lstatSync(pathToFolder).isDirectory()) {
                expApp.get('/admin/' + adminUIs[i], function(req, res) {
                    var ui = req.originalUrl.substring('/admin/'.length);
                    var id = req.query.id;
                    var pwd = req.query.pwd;
                    var admin = jt.data.getAdmin(id, pwd);
                    if (admin == null && jt.settings.adminLoginReq === true) {
                        res.sendFile(path.join(jt.staticServer.adminUIsPath(), ui, 'invalidAdminLogin.html'));
                    } else {
                        res.sendFile(path.join(jt.staticServer.adminUIsPath(), ui, 'admin.html'));
                    }
                });
            }
        }
        // END REQUESTS
        //////////////////////////////


        //////////////////////////////
        // START SERVER
        this.port = jt.settings.port;
        this.ip = ip.address();


        if (jt.settings.useHTTPS == false) {
            this.server = http.Server(expApp);
        } else {

            var attrs = [{ name: 'commonName', value: this.ip }];
            var pems = selfsigned.generate(attrs, { days: 365 });
            let options = {
                key: pems.private,
                cert: pems.cert,
            }

            this.server = https.createServer(options, expApp);
        }

        this.server.on('listening', () => {
            let protocol = 'http://';
            if (jt.settings.useHTTPS) {
                protocol = 'https://';
            }
            console.log('###############################################');
            jt.settings.server.ip = self.ip;
            jt.settings.server.port = self.port;
            console.log('jtree ' + jt.version + ', listening on ' + protocol + self.ip + ':' + self.port + '/admin');

            // pkg cannot include part of 'opn' package in executable.
            // const opn           = require('opn');
            if (jt.settings.openAdminOnStart) {
                //    opn('http://' + jt.staticServer.ip + ':' + jt.staticServer.port + '/admin');
                    try {
                        let protocol = 'http://';
                        if (jt.settings.useHTTPS) {
                            protocol = 'https://';
                        }
                        openurl.open(protocol + this.ip + ':' + this.port + '/admin');
                    } catch (err) {
                        console.error(err);
                    }
                }
  
        });

        let portsTried = [];

        this.server.on('error', (e) => {
            if (e.code === 'EADDRINUSE') {
                portsTried.push(jt.settings.port);
              this.server.close();
              let nextPort = 80;
              // On many OSses, ports below 1024 require admin access.
                for (let i=1025; i<9999; i++) {
                    if (!portsTried.includes(i)) {
                        nextPort = i;
                        break;
                    }
                }
              console.log('Something is already running on port ' + jt.settings.port + '. Retrying on port ' + nextPort + '...');
                jt.settings.port = nextPort;
                this.port = jt.settings.port;
                this.server.listen(this.port);
            }
          });

        this.server.listen(this.port);
        //////////////////////////////
        // Generate files used by clients.
        this.generateSharedJS(this.jt.settings.clientJSTemplateFile, this.jt.settings.clientJSFile);
        //////////////////////////////

    }

//     generateClientModels() {
// //        var file = babel.transformFileSync(path.join(this.jt.path, "../server/source/App.js"), {});
// //        fs.writeFileSync(path.join(this.jt.path, 'internal/clients/admin/shared/models.js'), file.code);
//     }

    handleRequest(req, res) {
        var jt = this.jt;
        let pId = req.params.pId;
        let adminCall = req.originalUrl === '/admin/' || req.originalUrl === '/admin';
        var adminUIs = fs.readdirSync(this.adminUIsPath());
        for (let i in adminUIs) {
            let pathToFolder = path.join(this.adminUIsPath(), adminUIs[i]);
            if (fs.lstatSync(pathToFolder).isDirectory()) {
                if (req.originalUrl === '/admin/' + adminUIs[i]) {
                    adminCall = true;
                    break;
                }
            }
        }
        if (adminCall) {
                var id = req.body.uId;
                var pwd = req.body.pwd;
                var adminUser = jt.data.isValidAdmin(id, pwd);
                if (
                    adminUser !== null
                ) {
                    if (adminUser !== 'defaultAdmin') {
                        adminUser.sessionIds.push(req.session.id);
                        req.session.userId = adminUser.id;
                        res.cookie('userId', adminUser.id);
                    }
                    res.sendFile(this.getAdminPath(req) + '/admin.html');
                } else {
                    if (jt.settings.multipleUsers) {
                        res.sendFile(this.defaultAdminUIPath() + '/adminLogin.html');
                    } else {
                        res.sendFile(this.defaultAdminUIPath() + '/defaultAdminLogin.html');
                    }
                }
        } else if (pId === 'favicon.ico') {
            jt.log('asking for favicon.ico');
        } else {
            this.sendParticipantPage(req, res, req.params.pId, undefined);
        }
    }

    getAdminPath(req) {
        let defaultUI = req.originalUrl === '/admin/' || req.originalUrl === '/admin';
        if (defaultUI) {
            return path.join(this.defaultAdminUIPath());
        } else {
            return path.join(this.adminUIsPath(), req.originalUrl);
        }
    }

    defaultAdminUIPath() {
        return path.join(this.adminUIsPath(), this.jt.settings.defaultAdminUI);
    }

    adminUIsPath() {
        return path.join(this.jt.path, this.jt.settings.adminUIsPath);
    }

    /**
     * Folder containing content common to all admin UIs.
     */
    adminUIsSharedPath() {
        return path.join(this.adminUIsPath(), this.jt.settings.adminUIsSharedPath);
    }

    /**
     * Generates "shared.js", to be used by all clients to connect to server.
     * 1. Create a copy of 'sharedTemplate.js'.
     * 2. Overwrite the serverURL variable with the IP + port of the current machine.
     *
     * References:
     * http://stackoverflow.com/questions/3653065/get-local-ip-address-in-node-js
     * http://stackoverflow.com/questions/14177087/replace-a-string-in-a-file-with-nodejs
     */
    generateSharedJS(inFile, outFile) {
        var fn = path.join(this.jt.path, inFile) // file with marker
        var newFN = path.join(this.jt.path, outFile) // actual file to be sent to clients and admins
        try {
            fs.copySync(fn, newFN);
            replace({
                regex: '{{{SERVER_IP}}}',
                replacement: this.ip,
                paths: [newFN],
                recursive: true,
                silent: true,
            });
            replace({
                regex: '{{{SERVER_PORT}}}',
                replacement: this.port,
                paths: [newFN],
                recursive: true,
                silent: true,
            });
        } catch (err) {
            console.error(err);
        }
    }

    sendParticipantPage(req, res, pId, sessionId) {
        var session = null;
        if (sessionId == null || sessionId == undefined) {
            session = this.jt.data.getMostRecentActiveSession();
        } else {
            session = Utils.findByIdWOJQ(this.jt.data.sessions, sessionId);
        }

        // If asked for a particular session, and that session:
        // - does not exists, send invalid session page.
        // - does exist, send participant page for that session.
        // If did not ask for particular session,
        // - send default start page.
        if (sessionId != null && session === null) {
            res.sendFile(path.resolve(this.jt.path, './' + this.jt.settings.participantUI + '/invalidSession.html'));
        } else {
            if (session != null) {
                session.sendParticipantPage(req, res, pId);
            } else {
                res.sendFile(path.join(this.jt.path, this.jt.settings.participantUI + '/readyClient.html'));
            }
        }
    }

}

var exports = module.exports = {};
exports.new = StaticServer;