mirror of
https://github.com/Ylianst/MeshCentral.git
synced 2024-12-24 22:25:52 -05:00
2131 lines
134 KiB
JavaScript
2131 lines
134 KiB
JavaScript
/**
|
|
* @description Meshcentral web server
|
|
* @author Ylian Saint-Hilaire
|
|
* @version v0.0.1
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
/*
|
|
class SerialTunnel extends require('stream').Duplex {
|
|
constructor(options) { super(options); this.forwardwrite = null; }
|
|
updateBuffer(chunk) { this.push(chunk); }
|
|
_write(chunk, encoding, callback) { if (this.forwardwrite != null) { this.forwardwrite(chunk); } else { console.err("Failed to fwd _write."); } if (callback) callback(); } // Pass data written to forward
|
|
_read(size) { } // Push nothing, anything to read should be pushed from updateBuffer()
|
|
}
|
|
*/
|
|
|
|
// Older NodeJS does not support the keyword "class", so we do without using this syntax
|
|
// TODO: Validate that it's the same as above and that it works.
|
|
function SerialTunnel(options) {
|
|
var obj = new require('stream').Duplex(options);
|
|
obj.forwardwrite = null;
|
|
obj.updateBuffer = function (chunk) { this.push(chunk); }
|
|
obj._write = function (chunk, encoding, callback) { if (obj.forwardwrite != null) { obj.forwardwrite(chunk); } else { console.err("Failed to fwd _write."); } if (callback) callback(); } // Pass data written to forward
|
|
obj._read = function(size) { } // Push nothing, anything to read should be pushed from updateBuffer()
|
|
return obj;
|
|
}
|
|
|
|
// ExpressJS login sample
|
|
// https://github.com/expressjs/express/blob/master/examples/auth/index.js
|
|
|
|
// Polyfill startsWith/endsWith for older NodeJS
|
|
if (!String.prototype.startsWith) { String.prototype.startsWith = function (searchString, position) { position = position || 0; return this.substr(position, searchString.length) === searchString; }; }
|
|
if (!String.prototype.endsWith) { String.prototype.endsWith = function (searchString, position) { var subjectString = this.toString(); if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) { position = subjectString.length; } position -= searchString.length; var lastIndex = subjectString.lastIndexOf(searchString, position); return lastIndex !== -1 && lastIndex === position; }; }
|
|
|
|
// Construct a HTTP web server object
|
|
module.exports.CreateWebServer = function (parent, db, args, secret, certificates) {
|
|
var obj = {};
|
|
|
|
// Modules
|
|
obj.fs = require('fs');
|
|
obj.net = require('net');
|
|
obj.tls = require('tls');
|
|
obj.path = require('path');
|
|
obj.hash = require('./pass').hash;
|
|
obj.bodyParser = require('body-parser');
|
|
obj.session = require('express-session');
|
|
obj.exphbs = require('express-handlebars');
|
|
obj.crypto = require('crypto');
|
|
obj.common = require('./common.js');
|
|
obj.express = require('express');
|
|
obj.meshAgentHandler = require('./meshagent.js');
|
|
obj.meshRelayHandler = require('./meshrelay.js')
|
|
obj.interceptor = require('./interceptor');
|
|
|
|
// Variables
|
|
obj.parent = parent;
|
|
obj.filespath = parent.filespath;
|
|
obj.db = db;
|
|
obj.app = obj.express();
|
|
obj.app.use(require('compression')());
|
|
obj.tlsServer = null;
|
|
obj.tcpServer;
|
|
obj.certificates = certificates;
|
|
obj.args = args;
|
|
obj.users = {};
|
|
obj.meshes = {};
|
|
|
|
// Perform hash on web certificate and agent certificate
|
|
obj.webCertificatHash = parent.certificateOperations.forge.pki.getPublicKeyFingerprint(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.web.cert).publicKey, { md: parent.certificateOperations.forge.md.sha256.create(), encoding: 'binary' });
|
|
obj.webCertificatHashHex = parent.certificateOperations.forge.pki.getPublicKeyFingerprint(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.web.cert).publicKey, { md: parent.certificateOperations.forge.md.sha256.create(), encoding: 'hex' });
|
|
obj.agentCertificatHashHex = parent.certificateOperations.forge.pki.getPublicKeyFingerprint(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.agent.cert).publicKey, { md: parent.certificateOperations.forge.md.sha256.create(), encoding: 'hex' });
|
|
obj.agentCertificatAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.agent.cert))).getBytes();
|
|
|
|
// Main lists
|
|
obj.wsagents = {};
|
|
obj.wssessions = {}; // UserId --> Array Of Sessions
|
|
obj.wssessions2 = {}; // UserId + SessionId --> Session
|
|
obj.wsrelays = {}; // Id -> Relay
|
|
obj.wsPeerRelays = {}; // Id -> { ServerId, Time }
|
|
|
|
// Setup randoms
|
|
obj.crypto.randomBytes(32, function (err, buf) { obj.httpAuthRandom = buf; });
|
|
obj.crypto.randomBytes(16, function (err, buf) { obj.httpAuthRealm = buf.toString('hex'); });
|
|
obj.crypto.randomBytes(32, function (err, buf) { obj.relayRandom = buf; });
|
|
|
|
function EscapeHtml(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, '''); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
|
|
function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, ''').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, ' '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
|
|
|
|
if (obj.args.notls) {
|
|
// Setup the HTTP server without TLS
|
|
obj.expressWs = require('express-ws')(obj.app);
|
|
} else {
|
|
// Setup the HTTP server with TLS
|
|
//var certOperations = require('./certoperations.js').CertificateOperations();
|
|
//var webServerCert = certOperations.GetWebServerCertificate('./data', 'SampleServer.org', 'US', 'SampleOrg');
|
|
obj.tlsServer = require('https').createServer({ cert: obj.certificates.web.cert, key: obj.certificates.web.key, ca: obj.certificates.calist, rejectUnauthorized: true }, obj.app);
|
|
obj.expressWs = require('express-ws')(obj.app, obj.tlsServer);
|
|
}
|
|
|
|
// Setup middleware
|
|
obj.app.engine('handlebars', obj.exphbs({})); // defaultLayout: 'main'
|
|
obj.app.set('view engine', 'handlebars');
|
|
obj.app.use(obj.bodyParser.urlencoded({ extended: false }));
|
|
obj.app.use(obj.session({
|
|
resave: false, // don't save session if unmodified
|
|
saveUninitialized: false, // don't create session until something stored
|
|
secret: secret // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances
|
|
}));
|
|
|
|
// Session-persisted message middleware
|
|
obj.app.use(function (req, res, next) {
|
|
if (req.session != null) {
|
|
var err = req.session.error;
|
|
var msg = req.session.success;
|
|
var passhint = req.session.passhint;
|
|
delete req.session.error;
|
|
delete req.session.success;
|
|
delete req.session.passhint;
|
|
}
|
|
res.locals.message = '';
|
|
if (err) res.locals.message = '<p class="msg error">' + err + '</p>';
|
|
if (msg) res.locals.message = '<p class="msg success">' + msg + '</p>';
|
|
if (passhint) res.locals.passhint = EscapeHtml(passhint);
|
|
next();
|
|
});
|
|
|
|
// Fetch all users from the database, keep this in memory
|
|
obj.db.GetAllType('user', function (err, docs) {
|
|
var domainUserCount = {};
|
|
for (var i in parent.config.domains) { domainUserCount[i] = 0; }
|
|
for (var i in docs) { var u = obj.users[docs[i]._id] = docs[i]; domainUserCount[u.domain]++; }
|
|
for (var i in parent.config.domains) {
|
|
if (domainUserCount[i] == 0) {
|
|
if (parent.config.domains[i].newAccounts == 0) { parent.config.domains[i].newAccounts = 2; }
|
|
console.log('Server ' + ((i == '') ? '' : (i + ' ')) + 'has no users, next new account will be site administrator.');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Fetch all meshes from the database, keep this in memory
|
|
obj.db.GetAllType('mesh', function (err, docs) { for (var i in docs) { obj.meshes[docs[i]._id] = docs[i]; } });
|
|
|
|
// Authenticate the user
|
|
obj.authenticate = function (name, pass, domain, fn) {
|
|
if (!module.parent) console.log('authenticating %s:%s:%s', domain.id, name, pass);
|
|
var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()];
|
|
// Query the db for the given username
|
|
if (!user) return fn(new Error('cannot find user'));
|
|
// Apply the same algorithm to the POSTed password, applying the hash against the pass / salt, if there is a match we found the user
|
|
if (user.salt == null) {
|
|
fn(new Error('invalid password'));
|
|
} else {
|
|
obj.hash(pass, user.salt, function (err, hash) {
|
|
if (err) return fn(err);
|
|
if (hash == user.hash) return fn(null, user._id);
|
|
fn(new Error('invalid password'), null, user.passhint);
|
|
});
|
|
}
|
|
}
|
|
|
|
/*
|
|
obj.restrict = function (req, res, next) {
|
|
console.log('restrict', req.url);
|
|
var domain = getDomain(req);
|
|
if (req.session.userid) {
|
|
next();
|
|
} else {
|
|
req.session.error = 'Access denied!';
|
|
res.redirect(domain.url + 'login');
|
|
}
|
|
}
|
|
*/
|
|
|
|
// Return the current domain of the request
|
|
function getDomain(req) {
|
|
var x = req.url.split('/');
|
|
if (x.length < 2) return parent.config.domains[''];
|
|
if (parent.config.domains[x[1].toLowerCase()]) return parent.config.domains[x[1].toLowerCase()];
|
|
return parent.config.domains[''];
|
|
}
|
|
|
|
function handleLogoutRequest(req, res) {
|
|
var domain = getDomain(req);
|
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0' });
|
|
// Destroy the user's session to log them out will be re-created next request
|
|
if (req.session.userid) {
|
|
var user = obj.users[req.session.userid]
|
|
obj.parent.DispatchEvent(['*'], obj, { etype: 'user', username: user.name, action: 'logout', msg: 'Account logout', domain: domain.id })
|
|
}
|
|
req.session.destroy(function () {
|
|
res.redirect(domain.url);
|
|
});
|
|
}
|
|
|
|
function handleLoginRequest(req, res) {
|
|
var domain = getDomain(req);
|
|
obj.authenticate(req.body.username, req.body.password, domain, function (err, userid, passhint) {
|
|
if (userid) {
|
|
var user = obj.users[userid];
|
|
|
|
// Save login time
|
|
user.login = Date.now();
|
|
obj.db.SetUser(user);
|
|
|
|
// Regenerate session when signing in to prevent fixation
|
|
req.session.regenerate(function () {
|
|
// Store the user's primary key in the session store to be retrieved, or in this case the entire user object
|
|
// req.session.success = 'Authenticated as ' + user.name + 'click to <a href="/logout">logout</a>. You may now access <a href="/restricted">/restricted</a>.';
|
|
delete req.session.loginmode;
|
|
req.session.userid = userid;
|
|
req.session.domainid = domain.id;
|
|
req.session.currentNode = '';
|
|
if (req.session.passhint) { delete req.session.passhint; }
|
|
if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; }
|
|
if (req.body.host) {
|
|
obj.db.GetAllType('node', function (err, docs) {
|
|
for (var i = 0; i < docs.length; i++) {
|
|
if (docs[i].name == req.body.host) {
|
|
req.session.currentNode = docs[i]._id;
|
|
break;
|
|
}
|
|
}
|
|
console.log("CurrentNode: " + req.session.currentNode);
|
|
// This redirect happens after finding node is completed
|
|
res.redirect(domain.url);
|
|
});
|
|
} else {
|
|
res.redirect(domain.url);
|
|
}
|
|
});
|
|
|
|
obj.parent.DispatchEvent(['*'], obj, { etype: 'user', username: user.name, action: 'login', msg: 'Account login', domain: domain.id })
|
|
} else {
|
|
delete req.session.loginmode;
|
|
req.session.error = '<b style=color:#8C001A>Login failed, check username and password.</b>';
|
|
if ((passhint != null) && (passhint.length > 0)) {
|
|
req.session.passhint = passhint;
|
|
} else {
|
|
if (req.session.passhint) { delete req.session.passhint; }
|
|
}
|
|
res.redirect(domain.url);
|
|
}
|
|
});
|
|
}
|
|
|
|
function handleCreateAccountRequest(req, res) {
|
|
var domain = getDomain(req);
|
|
if (domain.newAccounts == 0) { res.sendStatus(401); return; }
|
|
if (!req.body.username || !req.body.email || !req.body.password1 || !req.body.password2 || (req.body.password1 != req.body.password2) || req.body.username == '~') {
|
|
req.session.loginmode = 2;
|
|
req.session.error = '<b style=color:#8C001A>Unable to create account.</b>';;
|
|
res.redirect(domain.url);
|
|
} else {
|
|
// Check if there is domain.newAccountToken, check if supplied token is valid
|
|
if ((domain.newAccountsPass != null) && (domain.newAccountsPass != '') && (req.body.anewaccountpass != domain.newAccountsPass)) {
|
|
req.session.loginmode = 2;
|
|
req.session.error = '<b style=color:#8C001A>Invalid account creation token.</b>';
|
|
res.redirect(domain.url);
|
|
return;
|
|
}
|
|
// Check if user exists
|
|
if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) {
|
|
req.session.loginmode = 2;
|
|
req.session.error = '<b style=color:#8C001A>Username already exists.</b>';
|
|
} else {
|
|
var hint = req.body.apasswordhint;
|
|
if (hint.length > 250) hint = hint.substring(0, 250);
|
|
var user = { type: 'user', _id: 'user/' + domain.id + '/' + req.body.username.toLowerCase(), name: req.body.username, email: req.body.email, creation: Date.now(), login: Date.now(), domain: domain.id, passhint: hint };
|
|
var usercount = 0;
|
|
for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
|
|
if (usercount == 0) { user.siteadmin = 0xFFFFFFFF; if (domain.newAccounts == 2) { domain.newAccounts = 0; } } // If this is the first user, give the account site admin.
|
|
obj.users[user._id] = user;
|
|
req.session.userid = user._id;
|
|
req.session.domainid = domain.id;
|
|
// Create a user, generate a salt and hash the password
|
|
obj.hash(req.body.password1, function (err, salt, hash) {
|
|
if (err) throw err;
|
|
user.salt = salt;
|
|
user.hash = hash;
|
|
obj.db.SetUser(user);
|
|
});
|
|
obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', username: user.name, account: user, action: 'accountcreate', msg: 'Account created, email is ' + req.body.email, domain: domain.id })
|
|
}
|
|
res.redirect(domain.url);
|
|
}
|
|
}
|
|
|
|
function handleDeleteAccountRequest(req, res) {
|
|
var domain = getDomain(req);
|
|
// Check if the user is logged and we have all required parameters
|
|
if (!req.session || !req.session.userid || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.domainid != domain.id)) { res.redirect(domain.url); return; }
|
|
var user = obj.users[req.session.userid];
|
|
if (!user) return;
|
|
|
|
// Check if the password is correct
|
|
obj.authenticate(user.name, req.body.apassword1, domain, function (err, userid) {
|
|
var user = obj.users[userid];
|
|
if (user) {
|
|
obj.db.Remove(user._id);
|
|
delete obj.users[user._id];
|
|
req.session.destroy(function () { res.redirect(domain.url); });
|
|
obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', username: user.name, action: 'accountremove', msg: 'Account removed', domain: domain.id })
|
|
} else {
|
|
res.redirect(domain.url);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle password changes
|
|
function handlePasswordChangeRequest(req, res) {
|
|
var domain = getDomain(req);
|
|
// Check if the user is logged and we have all required parameters
|
|
if (!req.session || !req.session.userid || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.domainid != domain.id)) { res.redirect(domain.url); return; }
|
|
|
|
// Update the password
|
|
obj.hash(req.body.apassword1, function (err, salt, hash) {
|
|
if (err) throw err;
|
|
var hint = req.body.apasswordhint;
|
|
if (hint.length > 250) hint = hint.substring(0, 250);
|
|
var user = obj.users[req.session.userid];
|
|
user.salt = salt;
|
|
user.hash = hash;
|
|
user.passchange = Date.now();
|
|
user.passhint = req.body.apasswordhint;
|
|
obj.db.SetUser(user);
|
|
req.session.viewmode = 2;
|
|
res.redirect(domain.url);
|
|
obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', username: user.name, action: 'passchange', msg: 'Account password changed: ' + user.name, domain: domain.id })
|
|
});
|
|
}
|
|
|
|
// Indicates that any request to "/" should render "default" or "login" depending on login state
|
|
function handleRootRequest(req, res) {
|
|
if (!obj.args) { res.sendStatus(500); return; }
|
|
var domain = getDomain(req);
|
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0' });
|
|
// Check if we have an incomplete domain name in the path
|
|
if (domain.id != '' && req.url.split('/').length == 2) { res.redirect(domain.url); return; }
|
|
if (obj.args.nousers == true) {
|
|
// If in single user mode, setup things here.
|
|
if (req.session && req.session.loginmode) { delete req.session.loginmode; }
|
|
req.session.userid = 'user/' + domain.id + '/~';
|
|
req.session.domainid = domain.id;
|
|
req.session.currentNode = '';
|
|
if (obj.users[req.session.userid] == null) {
|
|
// Create the dummy user ~ with impossible password
|
|
obj.users[req.session.userid] = { type: 'user', _id: req.session.userid, name: '~', email: '~', domain: domain.id, siteadmin: 0xFFFFFFFF };
|
|
obj.db.SetUser(obj.users[req.session.userid]);
|
|
}
|
|
} else if (obj.args.user && (!req.session || !req.session.userid) && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
|
|
// If a default user is active, setup the session here.
|
|
if (req.session && req.session.loginmode) { delete req.session.loginmode; }
|
|
req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase();
|
|
req.session.domainid = domain.id;
|
|
req.session.currentNode = '';
|
|
}
|
|
// If a user is logged in, serve the default app, otherwise server the login app.
|
|
if (req.session && req.session.userid) {
|
|
if (req.session.domainid != domain.id) { req.session.destroy(function () { res.redirect(domain.url); }); return; } // Check is the session is for the correct domain
|
|
var viewmode = 1;
|
|
if (req.session.viewmode) {
|
|
viewmode = req.session.viewmode;
|
|
delete req.session.viewmode;
|
|
}
|
|
var currentNode = '';
|
|
if (req.session.currentNode) {
|
|
currentNode = req.session.currentNode;
|
|
delete req.session.currentNode;
|
|
}
|
|
var user;
|
|
var logoutcontrol;
|
|
if (!obj.args.nousers) {
|
|
user = obj.users[req.session.userid]
|
|
logoutcontrol = 'Welcome ' + user.name + '.';
|
|
}
|
|
var features = 0;
|
|
if (obj.args.wanonly == true) { features += 1; } // WAN-only mode
|
|
if (obj.args.lanonly == true) { features += 2; } // LAN-only mode
|
|
if (obj.args.nousers == true) { features += 4; } // Single user mode
|
|
if (domain.userQuota == -1) { features += 8; } // No server files mode
|
|
if ((!obj.args.user) && (!obj.args.nousers)) { logoutcontrol += ' <a href=' + domain.url + 'logout?' + Math.random() + ' style=color:white>Logout</a>'; } // If a default user is in use or no user mode, don't display the logout button
|
|
res.render(obj.path.join(__dirname, 'views/default'), { viewmode: viewmode, currentNode: currentNode, logoutControl: logoutcontrol, title: domain.title, title2: domain.title2, domainurl: domain.url, domain: domain.id, debuglevel: parent.debugLevel, serverDnsName: obj.certificates.CommonName, serverPublicPort: args.port, noServerBackup: (args.noserverbackup == 1 ? 1 : 0), features: features, mpspass: args.mpspass });
|
|
} else {
|
|
// Send back the login application
|
|
res.render(obj.path.join(__dirname, 'views/login'), { loginmode: req.session.loginmode, rootCertLink: getRootCertLink(), title: domain.title, title2: domain.title2, newAccount: domain.newAccounts, newAccountPass: ((domain.newAccountsPass == null)?0:1), serverDnsName: obj.certificates.CommonName, serverPublicPort: obj.args.port });
|
|
}
|
|
}
|
|
|
|
// Get the link to the root certificate if needed
|
|
function getRootCertLink() {
|
|
// TODO: This is not quite right, we need to check if the HTTPS certificate is issued from MeshCentralRoot, if so, add this download link.
|
|
if (obj.args.notls == null && obj.certificates.RootName.substring(0, 16) == 'MeshCentralRoot-') { return '<a href=/MeshServerRootCert.cer title="Download the root certificate for this server">Root Certificate</a>'; }
|
|
return '';
|
|
}
|
|
|
|
// Renter the terms of service.
|
|
function handleTermsRequest(req, res) {
|
|
var domain = getDomain(req);
|
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0' });
|
|
if (req.session && req.session.userid) {
|
|
if (req.session.domainid != domain.id) { req.session.destroy(function () { res.redirect(domain.url); }); return; } // Check is the session is for the correct domain
|
|
var user = obj.users[req.session.userid];
|
|
res.render(obj.path.join(__dirname, 'views/terms'), { logoutControl: 'Welcome ' + user.name + '. <a href=' + domain.url + 'logout?' + Math.random() + ' style=color:white>Logout</a>' });
|
|
} else {
|
|
res.render(obj.path.join(__dirname, 'views/terms'), { title: domain.title, title2: domain.title2 });
|
|
}
|
|
}
|
|
|
|
// Returns the server root certificate encoded in base64
|
|
function getRootCertBase64() {
|
|
var rootcert = obj.certificates.root.cert;
|
|
var i = rootcert.indexOf("-----BEGIN CERTIFICATE-----\r\n");
|
|
if (i >= 0) { rootcert = rootcert.substring(i + 29); }
|
|
i = rootcert.indexOf("-----END CERTIFICATE-----");
|
|
if (i >= 0) { rootcert = rootcert.substring(i, 0); }
|
|
return new Buffer(rootcert, 'base64').toString('base64');
|
|
}
|
|
|
|
// Returns the mesh server root certificate
|
|
function handleRootCertRequest(req, res) {
|
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename=' + certificates.RootName + '.cer' });
|
|
res.send(new Buffer(getRootCertBase64(), 'base64'));
|
|
}
|
|
|
|
// Returns an mescript for Intel AMT configuration
|
|
function handleMeScriptRequest(req, res) {
|
|
if (req.query.type == 1) {
|
|
var filename = 'cira_setup.mescript';
|
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename=' + filename });
|
|
var serverNameSplit = obj.certificates.CommonName.split('.');
|
|
if ((serverNameSplit.length == 4) && (parseInt(serverNameSplit[0]) == serverNameSplit[0]) && (parseInt(serverNameSplit[1]) == serverNameSplit[1]) && (parseInt(serverNameSplit[2]) == serverNameSplit[2]) && (parseInt(serverNameSplit[3]) == serverNameSplit[3])) {
|
|
// Server name is an IPv4 address
|
|
var filepath = obj.parent.path.join(__dirname, 'public/scripts/cira_setup_script_ip.mescript');
|
|
readEntireTextFile(filepath, function (data) {
|
|
if (data == null) { res.sendStatus(404); return; }
|
|
var scriptFile = JSON.parse(data);
|
|
|
|
// Change a few things in the script
|
|
scriptFile.scriptBlocks[2].vars.CertBin.value = getRootCertBase64(); // Set the root certificate
|
|
scriptFile.scriptBlocks[3].vars.IP.value = obj.certificates.CommonName; // Set the server IPv4 address name
|
|
scriptFile.scriptBlocks[3].vars.ServerName.value = obj.certificates.CommonName; // Set the server certificate name
|
|
scriptFile.scriptBlocks[3].vars.Port.value = obj.args.mpsport; // Set the server MPS port
|
|
scriptFile.scriptBlocks[3].vars.username.value = req.query.meshid; // Set the username
|
|
scriptFile.scriptBlocks[3].vars.password.value = obj.args.mpspass ? obj.args.mpspass : 'A@xew9rt'; // Set the password
|
|
scriptFile.scriptBlocks[4].vars.AccessInfo1.value = obj.certificates.CommonName + ':' + obj.args.mpsport; // Set the primary server name:port to set periodic timer
|
|
//scriptFile.scriptBlocks[4].vars.AccessInfo2.value = obj.certificates.CommonName + ':' + obj.args.mpsport; // Set the secondary server name:port to set periodic timer
|
|
if (obj.args.ciralocalfqdn != null) { scriptFile.scriptBlocks[6].vars.DetectionStrings.value = obj.args.ciralocalfqdn; } // Set the environment detection local FQDN's
|
|
|
|
// Compile the script
|
|
var scriptEngine = require('./amtscript.js').CreateAmtScriptEngine();
|
|
var runscript = scriptEngine.script_blocksToScript(scriptFile.blocks, scriptFile.scriptBlocks);
|
|
scriptFile.mescript = new Buffer(scriptEngine.script_compile(runscript), 'binary').toString('base64');
|
|
|
|
// Send the script
|
|
res.send(new Buffer(JSON.stringify(scriptFile, null, ' ')));
|
|
});
|
|
} else {
|
|
// Server name is a hostname
|
|
var filepath = obj.parent.path.join(__dirname, 'public/scripts/cira_setup_script_dns.mescript');
|
|
readEntireTextFile(filepath, function (data) {
|
|
if (data == null) { res.sendStatus(404); return; }
|
|
var scriptFile = JSON.parse(data);
|
|
|
|
// Change a few things in the script
|
|
scriptFile.scriptBlocks[2].vars.CertBin.value = getRootCertBase64(); // Set the root certificate
|
|
scriptFile.scriptBlocks[3].vars.FQDN.value = obj.certificates.CommonName; // Set the server DNS name
|
|
scriptFile.scriptBlocks[3].vars.Port.value = obj.args.mpsport; // Set the server MPS port
|
|
scriptFile.scriptBlocks[3].vars.username.value = req.query.meshid; // Set the username
|
|
scriptFile.scriptBlocks[3].vars.password.value = obj.args.mpspass ? obj.args.mpspass : 'A@xew9rt'; // Set the password
|
|
scriptFile.scriptBlocks[4].vars.AccessInfo1.value = obj.certificates.CommonName + ':' + obj.args.mpsport; // Set the primary server name:port to set periodic timer
|
|
//scriptFile.scriptBlocks[4].vars.AccessInfo2.value = obj.certificates.CommonName + ':' + obj.args.mpsport; // Set the secondary server name:port to set periodic timer
|
|
if (obj.args.ciralocalfqdn != null) { scriptFile.scriptBlocks[6].vars.DetectionStrings.value = obj.args.ciralocalfqdn; } // Set the environment detection local FQDN's
|
|
|
|
// Compile the script
|
|
var scriptEngine = require('./amtscript.js').CreateAmtScriptEngine();
|
|
var runscript = scriptEngine.script_blocksToScript(scriptFile.blocks, scriptFile.scriptBlocks);
|
|
scriptFile.mescript = new Buffer(scriptEngine.script_compile(runscript), 'binary').toString('base64');
|
|
|
|
// Send the script
|
|
res.send(new Buffer(JSON.stringify(scriptFile, null, ' ')));
|
|
});
|
|
}
|
|
}
|
|
else if (req.query.type == 2) {
|
|
var filename = 'cira_cleanup.mescript';
|
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename=' + filename });
|
|
var filepath = obj.parent.path.join(__dirname, 'public/scripts/cira_cleanup.mescript');
|
|
readEntireTextFile(filepath, function (data) {
|
|
if (data == null) { res.sendStatus(404); return; }
|
|
res.send(new Buffer(data));
|
|
});
|
|
}
|
|
}
|
|
|
|
// Handle user public file downloads
|
|
function handleDownloadUserFiles(req, res) {
|
|
var domain = getDomain(req), domainname = 'domain', spliturl = decodeURIComponent(req.path).split('/'), filename = '';
|
|
if ((spliturl.length < 3) || (obj.common.IsFilenameValid(spliturl[2]) == false) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
|
|
if (domain.id != '') { domainname = 'domain-' + domain.id; }
|
|
var path = obj.path.join(obj.filespath, domainname + "/user-" + spliturl[2] + "/Public");
|
|
for (var i = 3; i < spliturl.length; i++) { if (obj.common.IsFilenameValid(spliturl[i]) == true) { path += '/' + spliturl[i]; filename = spliturl[i]; } else { res.sendStatus(404); return; } }
|
|
|
|
var stat = null;
|
|
try { stat = obj.fs.statSync(path) } catch (e) { }
|
|
if ((stat != null) && ((stat.mode & 0x004000) == 0)) {
|
|
if (req.query.download == 1) {
|
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename=\"' + filename + '\"' });
|
|
try { res.sendFile(obj.path.resolve(__dirname, path)); } catch (e) { res.sendStatus(404); }
|
|
} else {
|
|
res.render(obj.path.join(__dirname, 'views/download'), { rootCertLink: getRootCertLink(), title: domain.title, title2: domain.title2, message: "<a href='" + req.path + "?download=1'>" + filename + "</a>, " + stat.size + " byte" + ((stat.size < 2)?'':'s') + "." });
|
|
}
|
|
} else {
|
|
res.render(obj.path.join(__dirname, 'views/download'), { rootCertLink: getRootCertLink(), title: domain.title, title2: domain.title2, message: "Invalid file link, please check the URL again." });
|
|
}
|
|
}
|
|
|
|
// Download a file from the server
|
|
function handleDownloadFile(req, res) {
|
|
var domain = getDomain(req);
|
|
if ((req.query.link == null) || (req.session == null) || (req.session.userid == null) || (domain == null) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
|
|
var user = obj.users[req.session.userid];
|
|
if (user == null) { res.sendStatus(404); return; }
|
|
var file = getServerFilePath(user, domain, req.query.link);
|
|
if (file == null) { res.sendStatus(404); return; }
|
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename=\"' + file.name + '\"' });
|
|
try { res.sendFile(file.fullpath); } catch (e) { res.sendStatus(404); }
|
|
}
|
|
|
|
// Upload a MeshCore.js file to the server
|
|
function handleUploadMeshCoreFile(req, res) {
|
|
var domain = getDomain(req);
|
|
if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
|
|
var user = obj.users[req.session.userid];
|
|
if (user.siteadmin != 0xFFFFFFFF) { res.sendStatus(401); return; } // Check if we have mesh core upload rights (Full admin only)
|
|
|
|
var multiparty = require('multiparty');
|
|
var form = new multiparty.Form();
|
|
form.parse(req, function (err, fields, files) {
|
|
if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
|
|
for (var i in files.files) {
|
|
var file = files.files[i];
|
|
readEntireTextFile(file.path, function (data) {
|
|
if (data == null) return;
|
|
data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
|
|
sendMeshAgentCore(user, domain, fields.attrib[0], data); // Upload the core
|
|
try { obj.fs.unlinkSync(file.path); } catch (e) { }
|
|
});
|
|
}
|
|
res.send('');
|
|
});
|
|
}
|
|
|
|
// Upload a file to the server
|
|
function handleUploadFile(req, res) {
|
|
var domain = getDomain(req);
|
|
if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid) || (domain.userQuota == -1)) { res.sendStatus(401); return; }
|
|
var user = obj.users[req.session.userid];
|
|
if ((user.siteadmin & 8) == 0) { res.sendStatus(401); return; } // Check if we have file rights
|
|
|
|
var multiparty = require('multiparty');
|
|
var form = new multiparty.Form();
|
|
form.parse(req, function (err, fields, files) {
|
|
if ((fields == null) || (fields.link == null) || (fields.link.length != 1)) { res.sendStatus(404); return; }
|
|
var xfile = getServerFilePath(user, domain, decodeURIComponent(fields.link[0]));
|
|
if (xfile == null) { res.sendStatus(404); return; }
|
|
// Get total bytes in the path
|
|
var totalsize = readTotalFileSize(xfile.fullpath);
|
|
if (totalsize < xfile.quota) { // Check if the quota is not already broken
|
|
if (fields.name != null) {
|
|
// Upload method where all the file data is within the fields.
|
|
var names = fields.name[0].split('*'), sizes = fields.size[0].split('*'), types = fields.type[0].split('*'), datas = fields.data[0].split('*');
|
|
if ((names.length == sizes.length) && (types.length == datas.length) && (names.length == types.length)) {
|
|
for (var i = 0; i < names.length; i++) {
|
|
if (obj.common.IsFilenameValid(names[i]) == false) { res.sendStatus(404); return; }
|
|
var filedata = new Buffer(datas[i].split(',')[1], 'base64');
|
|
if ((totalsize + filedata.length) < xfile.quota) { // Check if quota would not be broken if we add this file
|
|
obj.fs.writeFileSync(xfile.fullpath + '/' + names[i], filedata);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// More typical upload method, the file data is in a multipart mime post.
|
|
for (var i in files.files) {
|
|
var file = files.files[i], fpath = xfile.fullpath + '/' + file.originalFilename;
|
|
if (obj.common.IsFilenameValid(file.originalFilename) && ((totalsize + file.size) < xfile.quota)) { // Check if quota would not be broken if we add this file
|
|
obj.fs.rename(file.path, fpath);
|
|
} else {
|
|
try { obj.fs.unlinkSync(file.path); } catch (e) { }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
res.send('');
|
|
obj.parent.DispatchEvent([user._id], obj, 'updatefiles') // Fire an event causing this user to update this files
|
|
});
|
|
}
|
|
|
|
// Subscribe to all events we are allowed to receive
|
|
obj.subscribe = function (userid, target) {
|
|
var user = obj.users[userid];
|
|
var subscriptions = [userid, 'server-global'];
|
|
if (user.siteadmin != null) {
|
|
if (user.siteadmin == 0xFFFFFFFF) subscriptions.push('*');
|
|
if ((user.siteadmin & 2) != 0) subscriptions.push('server-users');
|
|
}
|
|
if (user.links != null) {
|
|
for (var i in user.links) { subscriptions.push(i); }
|
|
}
|
|
obj.parent.RemoveAllEventDispatch(target);
|
|
obj.parent.AddEventDispatch(subscriptions, target);
|
|
return subscriptions;
|
|
}
|
|
|
|
// Handle a web socket relay request
|
|
function handleRelayWebSocket(ws, req) {
|
|
var node, domain = getDomain(req);
|
|
// Check if this is a logged in user
|
|
var user, peering = true;
|
|
if (req.query.auth == null) {
|
|
// Use ExpressJS session
|
|
if (!req.session || !req.session.userid) { return; } // Web socket attempt without login, disconnect.
|
|
if (req.session.domainid != domain.id) { console.log('ERR: Invalid domain'); return; }
|
|
user = obj.users[req.session.userid];
|
|
} else {
|
|
// Get the session from the cookie
|
|
if (obj.parent.multiServer == null) { return; }
|
|
var session = obj.parent.multiServer.decodeCookie(req.query.auth);
|
|
if (session == null) { console.log('ERR: Invalid cookie'); return; }
|
|
if (session.domainid != domain.id) { console.log('ERR: Invalid domain'); return; }
|
|
user = obj.users[session.userid];
|
|
peering = false; // Don't allow the connection to jump again to a different server
|
|
}
|
|
if (!user) { console.log('ERR: Not a user'); return; }
|
|
Debug(1, 'Websocket relay connected from ' + user.name + ' for ' + req.query.host + '.');
|
|
|
|
// Hold this socket until we are ready.
|
|
ws.pause();
|
|
|
|
// Fetch information about the target
|
|
obj.db.Get(req.query.host, function (err, docs) {
|
|
if (docs.length == 0) { console.log('ERR: Node not found'); return; }
|
|
node = docs[0];
|
|
if (!node.intelamt) { console.log('ERR: Not AMT node'); return; }
|
|
|
|
// Check if this user has permission to manage this computer
|
|
var meshlinks = user.links[node.meshid];
|
|
if (!meshlinks || !meshlinks.rights) { console.log('ERR: Access denied (1)'); return; }
|
|
if ((meshlinks.rights & 8) == 0) { console.log('ERR: Access denied (2)'); return; }
|
|
|
|
// Check what connectivity is available for this node
|
|
var state = parent.GetConnectivityState(req.query.host);
|
|
var conn = 0;
|
|
if (!state || state.connectivity == 0) { Debug(1, 'ERR: No routing possible (1)'); try { ws.close(); } catch (e) { } return; } else { conn = state.connectivity; }
|
|
|
|
// Check what server needs to handle this connection
|
|
if ((obj.parent.multiServer != null) && (peering == true)) {
|
|
var server = obj.parent.GetRoutingServerId(req.query.host, 2); // Check for Intel CIRA connection
|
|
if (server != null) {
|
|
if (server.serverid != obj.parent.serverId) {
|
|
// Do local Intel CIRA routing using a different server
|
|
Debug(1, 'Route Intel AMT CIRA connection to peer server: ' + server.serverid);
|
|
obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
|
|
return;
|
|
}
|
|
} else {
|
|
server = obj.parent.GetRoutingServerId(req.query.host, 4); // Check for local Intel AMT connection
|
|
if ((server != null) && (server.serverid != obj.parent.serverId)) {
|
|
// Do local Intel AMT routing using a different server
|
|
Debug(1, 'Route Intel AMT direct connection to peer server: ' + server.serverid);
|
|
obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If Intel AMT CIRA connection is available, use it
|
|
if (((conn & 2) != 0) && (parent.mpsserver.ciraConnections[req.query.host] != null)) {
|
|
var ciraconn = parent.mpsserver.ciraConnections[req.query.host];
|
|
|
|
// Compute target port, look at the CIRA port mappings, if non-TLS is allowed, use that, if not use TLS
|
|
var port = 16993;
|
|
//if (node.intelamt.tls == 0) port = 16992; // DEBUG: Allow TLS flag to set TLS mode within CIRA
|
|
if (ciraconn.tag.boundPorts.indexOf(16992) >= 0) port = 16992; // RELEASE: Always use non-TLS mode if available within CIRA
|
|
if (req.query.p == 2) port += 2;
|
|
|
|
// Setup a new CIRA channel
|
|
if ((port == 16993) || (port == 16995)) {
|
|
// Perform TLS - ( TODO: THIS IS BROKEN on Intel AMT v7 but works on v10, Not sure why )
|
|
var ser = new SerialTunnel();
|
|
var chnl = parent.mpsserver.SetupCiraChannel(ciraconn, port);
|
|
|
|
// let's chain up the TLSSocket <-> SerialTunnel <-> CIRA APF (chnl)
|
|
// Anything that needs to be forwarded by SerialTunnel will be encapsulated by chnl write
|
|
ser.forwardwrite = function (msg) {
|
|
// Convert a buffer into a string, "msg = msg.toString('ascii');" does not work
|
|
// TLS ---> CIRA
|
|
var msg2 = "";
|
|
for (var i = 0; i < msg.length; i++) { msg2 += String.fromCharCode(msg[i]); }
|
|
chnl.write(msg2);
|
|
};
|
|
|
|
// When APF tunnel return something, update SerialTunnel buffer
|
|
chnl.onData = function (ciraconn, data) {
|
|
// CIRA ---> TLS
|
|
Debug(3, 'Relay TLS CIRA data', data.length);
|
|
if (data.length > 0) { try { ser.updateBuffer(Buffer.from(data, 'binary')); } catch (e) { } }
|
|
}
|
|
|
|
// Handke CIRA tunnel state change
|
|
chnl.onStateChange = function (ciraconn, state) {
|
|
Debug(2, 'Relay TLS CIRA state change', state);
|
|
if (state == 0) { try { ws.close(); } catch (e) { } }
|
|
}
|
|
|
|
// TLSSocket to encapsulate TLS communication, which then tunneled via SerialTunnel an then wrapped through CIRA APF
|
|
var TLSSocket = require('tls').TLSSocket;
|
|
var tlsock = new TLSSocket(ser, { secureProtocol: 'SSLv23_method', rejectUnauthorized: false }); // TLSv1_2_method
|
|
tlsock.on('error', function (err) { Debug(1, "CIRA TLS Connection Error ", err); });
|
|
tlsock.on('secureConnect', function () { Debug(2, "CIRA Secure TLS Connection"); ws.resume(); });
|
|
|
|
// Decrypted tunnel from TLS communcation to be forwarded to websocket
|
|
tlsock.on('data', function (data) {
|
|
// AMT/TLS ---> WS
|
|
try {
|
|
var data2 = "";
|
|
for (var i = 0; i < data.length; i++) { data2 += String.fromCharCode(data[i]); }
|
|
if (ws.interceptor) { data2 = ws.interceptor.processAmtData(data2); } // Run data thru interceptor
|
|
ws.send(data2);
|
|
} catch (e) { }
|
|
});
|
|
|
|
// If TLS is on, forward it through TLSSocket
|
|
ws.forwardclient = tlsock;
|
|
ws.forwardclient.xtls = 1;
|
|
} else {
|
|
// Without TLS
|
|
ws.forwardclient = parent.mpsserver.SetupCiraChannel(ciraconn, port);
|
|
ws.forwardclient.xtls = 0;
|
|
ws.resume();
|
|
}
|
|
|
|
// When data is received from the web socket, forward the data into the associated CIRA cahnnel.
|
|
// If the CIRA connection is pending, the CIRA channel has built-in buffering, so we are ok sending anyway.
|
|
ws.on('message', function (msg) {
|
|
// WS ---> AMT/TLS
|
|
// Convert a buffer into a string, "msg = msg.toString('ascii');" does not work
|
|
var msg2 = "";
|
|
for (var i = 0; i < msg.length; i++) { msg2 += String.fromCharCode(msg[i]); }
|
|
if (ws.interceptor) { msg2 = ws.interceptor.processBrowserData(msg2); } // Run data thru interceptor
|
|
if (ws.forwardclient.xtls == 1) { ws.forwardclient.write(Buffer.from(msg2, 'binary')); } else { ws.forwardclient.write(msg2); }
|
|
});
|
|
|
|
// If error, do nothing
|
|
ws.on('error', function (err) { console.log(err); });
|
|
|
|
// If the web socket is closed, close the associated TCP connection.
|
|
ws.on('close', function (req) {
|
|
Debug(1, 'Websocket relay closed.');
|
|
if (ws.forwardclient && ws.forwardclient.close) { ws.forwardclient.close(); } // TODO: If TLS is used, we need to close the socket that is wrapped by TLS
|
|
});
|
|
|
|
ws.forwardclient.onStateChange = function (ciraconn, state) {
|
|
Debug(2, 'Relay CIRA state change', state);
|
|
if (state == 0) { try { ws.close(); } catch (e) { } }
|
|
}
|
|
|
|
ws.forwardclient.onData = function (ciraconn, data) {
|
|
Debug(3, 'Relay CIRA data', data.length);
|
|
if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
|
|
if (data.length > 0) { try { ws.send(data); } catch (e) { } } // TODO: Add TLS support
|
|
}
|
|
|
|
ws.forwardclient.onSendOk = function (ciraconn) {
|
|
// TODO: Flow control? (Dont' really need it with AMT, but would be nice)
|
|
//console.log('onSendOk');
|
|
}
|
|
|
|
// Fetch Intel AMT credentials & Setup interceptor
|
|
if (req.query.p == 1) {
|
|
Debug(3, 'INTERCEPTOR1', { host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
|
|
ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
|
|
}
|
|
else if (req.query.p == 2) {
|
|
Debug(3, 'INTERCEPTOR2', { user: node.intelamt.user, pass: node.intelamt.pass });
|
|
ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass });
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// If Intel AMT direct connection is possible, option a direct socket
|
|
if ((conn & 4) != 0) { // We got a new web socket connection, initiate a TCP connection to the target Intel AMT host/port.
|
|
Debug(2, 'Opening relay TCP socket connection to ' + req.query.host + '.');
|
|
|
|
// When data is received from the web socket, forward the data into the associated TCP connection.
|
|
ws.on('message', function (msg) {
|
|
Debug(1, 'TCP relay data to ' + node.host + ', ' + msg.length + ' bytes'); // DEBUG
|
|
// Convert a buffer into a string, "msg = msg.toString('ascii');" does not work
|
|
var msg2 = "";
|
|
for (var i = 0; i < msg.length; i++) { msg2 += String.fromCharCode(msg[i]); }
|
|
if (ws.interceptor) { msg2 = ws.interceptor.processBrowserData(msg2); } // Run data thru interceptor
|
|
ws.forwardclient.write(new Buffer(msg2, "ascii")); // Forward data to the associated TCP connection.
|
|
});
|
|
|
|
// If error, do nothing
|
|
ws.on('error', function (err) { console.log(err); });
|
|
|
|
// If the web socket is closed, close the associated TCP connection.
|
|
ws.on('close', function (req) {
|
|
Debug(1, 'Closing relay web socket connection to ' + ws.upgradeReq.query.host + '.');
|
|
if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
|
|
});
|
|
|
|
// Compute target port
|
|
var port = 16992;
|
|
if (node.intelamt.tls > 0) port = 16993; // This is a direct connection, use TLS when possible
|
|
if (req.query.p == 2) port += 2;
|
|
|
|
if (node.intelamt.tls == 0) {
|
|
// If this is TCP (without TLS) set a normal TCP socket
|
|
ws.forwardclient = new obj.net.Socket();
|
|
ws.forwardclient.setEncoding('binary');
|
|
ws.forwardclient.xstate = 0;
|
|
ws.forwardclient.forwardwsocket = ws;
|
|
ws.resume();
|
|
} else {
|
|
// If TLS is going to be used, setup a TLS socket
|
|
ws.forwardclient = obj.tls.connect(port, node.host, { secureProtocol: 'TLSv1_method', rejectUnauthorized: false }, function () {
|
|
// The TLS connection method is the same as TCP, but located a bit differently.
|
|
Debug(2, 'TLS connected to ' + node.host + ':' + port + '.');
|
|
ws.forwardclient.xstate = 1;
|
|
ws.resume();
|
|
});
|
|
ws.forwardclient.setEncoding('binary');
|
|
ws.forwardclient.xstate = 0;
|
|
ws.forwardclient.forwardwsocket = ws;
|
|
}
|
|
|
|
// When we receive data on the TCP connection, forward it back into the web socket connection.
|
|
ws.forwardclient.on('data', function (data) {
|
|
Debug(1, 'TCP relay data from ' + node.host + ', ' + data.length + ' bytes.'); // DEBUG
|
|
if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
|
|
try { ws.send(new Buffer(data, 'binary')); } catch (e) { }
|
|
});
|
|
|
|
// If the TCP connection closes, disconnect the associated web socket.
|
|
ws.forwardclient.on('close', function () {
|
|
Debug(1, 'TCP relay disconnected from ' + node.host + '.');
|
|
try { ws.close(); } catch (e) { }
|
|
});
|
|
|
|
// If the TCP connection causes an error, disconnect the associated web socket.
|
|
ws.forwardclient.on('error', function (err) {
|
|
Debug(1, 'TCP relay error from ' + node.host + ': ' + err.errno);
|
|
try { ws.close(); } catch (e) { }
|
|
});
|
|
|
|
// Fetch Intel AMT credentials & Setup interceptor
|
|
if (req.query.p == 1) { ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass }); }
|
|
else if (req.query.p == 2) { ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass }); }
|
|
|
|
if (node.intelamt.tls == 0) {
|
|
// A TCP connection to Intel AMT just connected, start forwarding.
|
|
ws.forwardclient.connect(port, node.host, function () {
|
|
Debug(1, 'TCP relay connected to ' + node.host + ':' + port + '.');
|
|
ws.forwardclient.xstate = 1;
|
|
ws.resume();
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
// Handle the web socket echo request, just echo back the data sent
|
|
function handleEchoWebSocket(ws, req) {
|
|
// When data is received from the web socket, echo it back
|
|
ws.on('message', function (data) {
|
|
var cmd = data.toString('utf8');
|
|
if (cmd == 'close') {
|
|
try { ws.close(); } catch (e) { console.log(e); }
|
|
} else {
|
|
try { ws.send(data); } catch (e) { console.log(e); }
|
|
}
|
|
});
|
|
|
|
// If error, do nothing
|
|
ws.on('error', function (err) { console.log(err); });
|
|
|
|
// If closed, do nothing
|
|
ws.on('close', function (req) { });
|
|
}
|
|
|
|
// Read the folder and all sub-folders and serialize that into json.
|
|
function readFilesRec(path) {
|
|
var r = {}, dir = obj.fs.readdirSync(path);
|
|
for (var i in dir) {
|
|
var f = { t: 3, d: 111 };
|
|
var stat = obj.fs.statSync(path + '/' + dir[i])
|
|
if ((stat.mode & 0x004000) == 0) { f.s = stat.size; f.d = stat.mtime.getTime(); } else { f.t = 2; f.f = readFilesRec(path + '/' + dir[i]); }
|
|
r[dir[i]] = f;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
// Get the total size of all files in a folder and all sub-folders
|
|
function readTotalFileSize(path) {
|
|
var r = 0, dir = obj.fs.readdirSync(path);
|
|
for (var i in dir) {
|
|
var stat = obj.fs.statSync(path + '/' + dir[i])
|
|
if ((stat.mode & 0x004000) == 0) { r += stat.size; } else { r += readTotalFileSize(path + '/' + dir[i]); }
|
|
}
|
|
return r;
|
|
}
|
|
|
|
// Delete a folder and all sub items.
|
|
function deleteFolderRec(path) {
|
|
if (obj.fs.existsSync(path) == false) return;
|
|
obj.fs.readdirSync(path).forEach(function (file, index) {
|
|
var pathx = path + "/" + file;
|
|
if (obj.fs.lstatSync(pathx).isDirectory()) { deleteFolderRec(pathx); } else { obj.fs.unlinkSync(pathx); }
|
|
});
|
|
obj.fs.rmdirSync(path);
|
|
};
|
|
|
|
// Indicates we want to handle websocket requests on "/control.ashx".
|
|
function handleControlRequest(ws, req) {
|
|
var domain = getDomain(req);
|
|
try {
|
|
// Check if the user is logged in
|
|
if ((!req.session) || (!req.session.userid) || (req.session.domainid != domain.id)) { try { ws.close(); } catch (e) { } return; }
|
|
req.session.ws = ws; // Associate this websocket session with the web session
|
|
req.session.ws.userid = req.session.userid;
|
|
req.session.ws.domainid = domain.id;
|
|
var user = obj.users[req.session.userid];
|
|
if (user == null) { try { ws.close(); } catch (e) { } return; }
|
|
|
|
// Add this web socket session to session list
|
|
ws.sessionId = user._id + '/' + ('' + Math.random()).substring(2);
|
|
obj.wssessions2[ws.sessionId] = ws;
|
|
if (!obj.wssessions[user._id]) { obj.wssessions[user._id] = [ws]; } else { obj.wssessions[user._id].push(ws); }
|
|
obj.parent.DispatchEvent(['*'], obj, { action: 'wssessioncount', username: user.name, count: obj.wssessions[user._id].length, nolog: 1, domain: domain.id })
|
|
|
|
// Handle events
|
|
ws.HandleEvent = function (source, event) {
|
|
if (!event.domain || event.domain == domain.id) {
|
|
try {
|
|
if (event == 'close') { req.session.destroy(); ws.close(); }
|
|
else if (event == 'resubscribe') { user.subscriptions = obj.subscribe(user._id, ws); }
|
|
else if (event == 'updatefiles') { updateUserFiles(user, ws, domain); }
|
|
else { ws.send(JSON.stringify({ action: 'event', event: event })); }
|
|
} catch (e) { }
|
|
}
|
|
}
|
|
|
|
// Subscribe to events
|
|
user.subscriptions = obj.subscribe(user._id, ws);
|
|
|
|
// When data is received from the web socket
|
|
ws.on('message', function (msg) {
|
|
var user = obj.users[req.session.userid];
|
|
var command = JSON.parse(msg.toString('utf8'))
|
|
switch (command.action) {
|
|
case 'meshes':
|
|
{
|
|
// Request a list of all meshes this user as rights to
|
|
var docs = [];
|
|
for (var i in user.links) { if (obj.meshes[i]) { docs.push(obj.meshes[i]); } }
|
|
ws.send(JSON.stringify({ action: 'meshes', meshes: docs }));
|
|
break;
|
|
}
|
|
case 'nodes':
|
|
{
|
|
// Request a list of all meshes this user as rights to
|
|
var links = [];
|
|
for (var i in user.links) { links.push(i); }
|
|
|
|
// Request a list of all nodes
|
|
obj.db.GetAllTypeNoTypeFieldMeshFiltered(links, domain.id, 'node', function (err, docs) {
|
|
var r = {};
|
|
for (var i in docs) {
|
|
// Add the connection state
|
|
var state = parent.GetConnectivityState(docs[i]._id);
|
|
if (state) {
|
|
docs[i].conn = state.connectivity;
|
|
docs[i].pwr = state.powerState;
|
|
if ((state.connectivity & 1) != 0) { var agent = obj.wsagents[docs[i]._id]; if (agent != null) { docs[i].agct = agent.connectTime; } }
|
|
if ((state.connectivity & 2) != 0) { var cira = obj.parent.mpsserver.ciraConnections[docs[i]._id]; if (cira != null) { docs[i].cict = cira.tag.connectTime; } }
|
|
}
|
|
// Compress the meshid's
|
|
var meshid = docs[i].meshid;
|
|
if (!r[meshid]) { r[meshid] = []; }
|
|
delete docs[i].meshid;
|
|
r[meshid].push(docs[i]);
|
|
}
|
|
ws.send(JSON.stringify({ action: 'nodes', nodes: r }));
|
|
});
|
|
break;
|
|
}
|
|
case 'powertimeline':
|
|
{
|
|
// Query the database for the power timeline for a given node
|
|
// The result is a compacted array: [ startPowerState, startTimeUTC, powerState ] + many[ deltaTime, powerState ]
|
|
obj.db.getPowerTimeline(command.nodeid, function (err, docs) {
|
|
if (err == null && docs.length > 0) {
|
|
var timeline = [], time = null, previousPower;
|
|
for (var i in docs) {
|
|
var doc = docs[i];
|
|
if (time == null) {
|
|
// First element
|
|
time = doc.time;
|
|
if (doc.oldPower) { timeline.push(doc.oldPower); } else { timeline.push(0); }
|
|
timeline.push(time);
|
|
timeline.push(doc.power);
|
|
previousPower = doc.power;
|
|
} else {
|
|
// Delta element
|
|
if ((previousPower != doc.power) && ((doc.time - time) > 60000)) { // To boost speed, any blocks less than a minute get approximated.
|
|
// Create a new timeline
|
|
timeline.push(doc.time - time);
|
|
timeline.push(doc.power);
|
|
time = doc.time;
|
|
previousPower = doc.power;
|
|
} else {
|
|
// Extend the previous timeline
|
|
if ((timeline.length >= 6) && (timeline[timeline.length - 3] == doc.power)) { // We can merge the block with the previous block
|
|
timeline[timeline.length - 4] += (timeline[timeline.length - 2] + (doc.time - time));
|
|
timeline.pop();
|
|
timeline.pop();
|
|
} else { // Extend the last block in the timeline
|
|
timeline[timeline.length - 2] += (doc.time - time);
|
|
timeline[timeline.length - 1] = doc.power;
|
|
}
|
|
time = doc.time;
|
|
previousPower = doc.power;
|
|
}
|
|
}
|
|
}
|
|
ws.send(JSON.stringify({ action: 'powertimeline', nodeid: command.nodeid, timeline: timeline }));
|
|
} else {
|
|
// No records found, send current state if we have it
|
|
var state = obj.parent.GetConnectivityState(command.nodeid);
|
|
if (state != null) { ws.send(JSON.stringify({ action: 'powertimeline', nodeid: command.nodeid, timeline: [state.powerState, Date.now(), state.powerState] })); }
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case 'files':
|
|
{
|
|
// Send the full list of server files to the browser app
|
|
if ((user.siteadmin & 8) != 0) { updateUserFiles(user, ws, domain); }
|
|
break;
|
|
}
|
|
case 'fileoperation':
|
|
{
|
|
// Check permissions
|
|
if ((user.siteadmin & 8) != 0) {
|
|
// Perform a file operation (Create Folder, Delete Folder, Delete File...)
|
|
if ((command.path != null) && (typeof command.path == 'object') && command.path.length > 0) {
|
|
var rootfolder = command.path[0];
|
|
var rootfoldersplit = rootfolder.split('/'), domainx = 'domain';
|
|
if (rootfoldersplit[1].length > 0) domainx = 'domain-' + rootfoldersplit[1];
|
|
var path = obj.path.join(obj.filespath, domainx + "/" + rootfoldersplit[0] + "-" + rootfoldersplit[2]);
|
|
for (var i = 1; i < command.path.length; i++) { if (obj.common.IsFilenameValid(command.path[i]) == false) { path = null; break; } path += ("/" + command.path[i]); }
|
|
if (path == null) break;
|
|
|
|
if ((command.fileop == 'createfolder') && (obj.common.IsFilenameValid(command.newfolder) == true)) { try { obj.fs.mkdirSync(path + "/" + command.newfolder); } catch (e) { } } // Create a new folder
|
|
else if (command.fileop == 'delete') { for (var i in command.delfiles) { if (obj.common.IsFilenameValid(command.delfiles[i]) == true) { var fullpath = path + "/" + command.delfiles[i]; try { obj.fs.rmdirSync(fullpath); } catch (e) { try { obj.fs.unlinkSync(fullpath); } catch (e) { } } } } } // Delete
|
|
else if ((command.fileop == 'rename') && (obj.common.IsFilenameValid(command.oldname) == true) && (obj.common.IsFilenameValid(command.newname) == true)) { try { obj.fs.renameSync(path + "/" + command.oldname, path + "/" + command.newname); } catch (e) { } } // Rename
|
|
|
|
obj.parent.DispatchEvent([user._id], obj, 'updatefiles') // Fire an event causing this user to update this files
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'msg':
|
|
{
|
|
// Route a message.
|
|
// This this command has a nodeid, that is the target.
|
|
if (command.nodeid != null) {
|
|
var splitnodeid = command.nodeid.split('/');
|
|
// Check that we are in the same domain and the user has rights over this node.
|
|
if ((splitnodeid[0] == 'node') && (splitnodeid[1] == domain.id)) {
|
|
// See if the node is connected
|
|
var agent = obj.wsagents[command.nodeid];
|
|
if (agent != null) {
|
|
// Check if we have permission to send a message to that node
|
|
var rights = user.links[agent.dbMeshKey];
|
|
if (rights != null || ((rights & 16) != 0)) { // TODO: 16 is console permission, may need more gradular permission checking
|
|
command.sessionid = ws.sessionId; // Set the session id, required for responses.
|
|
command.rights = rights.rights; // Add user rights flags to the message
|
|
delete command.nodeid; // Remove the nodeid since it's implyed.
|
|
agent.send(JSON.stringify(command));
|
|
}
|
|
} else {
|
|
// Check if a peer server is connected to this agent
|
|
var routing = obj.parent.GetRoutingServerId(command.nodeid, 1); // 1 = MeshAgent routing type
|
|
if (routing != null) {
|
|
// Check if we have permission to send a message to that node
|
|
var rights = user.links[routing.meshid];
|
|
if (rights != null || ((rights & 16) != 0)) { // TODO: 16 is console permission, may need more gradular permission checking
|
|
command.fromSessionid = ws.sessionId; // Set the session id, required for responses.
|
|
command.rights = rights.rights; // Add user rights flags to the message
|
|
parent.multiServer.DispatchMessageSingleServer(command, routing.serverid);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'events':
|
|
{
|
|
// Send the list of events for this session
|
|
obj.db.GetEvents(user.subscriptions, domain.id, function (err, docs) { if (err != null) return; ws.send(JSON.stringify({ action: 'events', events: docs })); });
|
|
break;
|
|
}
|
|
case 'clearevents':
|
|
{
|
|
// Delete all events
|
|
if (user.siteadmin != 0xFFFFFFFF) break;
|
|
obj.db.RemoveAllEvents(domain.id);
|
|
obj.parent.DispatchEvent(['*', 'server-global'], obj, { action: 'clearevents', nolog: 1, domain: domain.id })
|
|
break;
|
|
}
|
|
case 'users':
|
|
{
|
|
// Request a list of all users
|
|
if ((user.siteadmin & 2) == 0) break;
|
|
var docs = [];
|
|
for (var i in obj.users) { if ((obj.users[i].domain == domain.id) && (obj.users[i].name != '~')) { docs.push(obj.users[i]); } }
|
|
ws.send(JSON.stringify({ action: 'users', users: docs }));
|
|
break;
|
|
}
|
|
case 'wssessioncount':
|
|
{
|
|
// Request a list of all web socket session count
|
|
if ((user.siteadmin & 2) == 0) break;
|
|
var wssessions = {};
|
|
for (var i in obj.wssessions) { if (obj.wssessions[i][0].domainid == domain.id) { wssessions[i] = obj.wssessions[i].length; } }
|
|
ws.send(JSON.stringify({ action: 'wssessioncount', wssessions: wssessions }));
|
|
break;
|
|
}
|
|
case 'deleteuser':
|
|
{
|
|
// Delete a user account
|
|
if ((user.siteadmin & 2) == 0) break;
|
|
var delusername = command.username, deluserid = command.userid, deluser = obj.users[deluserid];
|
|
if ((deluser.siteadmin != null) && (deluser.siteadmin > 0) && (user.siteadmin != 0xFFFFFFFF)) break; // Need full admin to remote another administrator
|
|
if ((deluserid.split('/').length != 3) || (deluserid.split('/')[1] != domain.id)) break; // Invalid domain, operation only valid for current domain
|
|
|
|
// Delete all files on the server for this account
|
|
try {
|
|
var deluserpath = getServerRootFilePath(deluser);
|
|
if (deluserpath != null) { deleteFolderRec(deluserpath); }
|
|
} catch (e) { }
|
|
|
|
obj.db.Remove(deluserid);
|
|
delete obj.users[deluserid];
|
|
obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: deluserid, username: delusername, action: 'accountremove', msg: 'Account removed', domain: domain.id })
|
|
obj.parent.DispatchEvent([deluserid], obj, 'close');
|
|
|
|
break;
|
|
}
|
|
case 'adduser':
|
|
{
|
|
// Add a new user account
|
|
if ((user.siteadmin & 2) == 0) break;
|
|
var newusername = command.username, newuserid = 'user/' + domain.id + '/' + command.username.toLowerCase();
|
|
if (newusername == '~') break; // This is a reserved user name
|
|
if (!obj.users[newuserid]) {
|
|
var newuser = { type: 'user', _id: newuserid, name: newusername, email: command.email, creation: Date.now(), domain: domain.id };
|
|
obj.users[newuserid] = newuser;
|
|
// Create a user, generate a salt and hash the password
|
|
obj.hash(command.pass, function (err, salt, hash) {
|
|
if (err) throw err;
|
|
newuser.salt = salt;
|
|
newuser.hash = hash;
|
|
obj.db.SetUser(newuser);
|
|
var newuser2 = obj.common.Clone(newuser);
|
|
if (newuser2.subscriptions) { delete newuser2.subscriptions; }
|
|
if (newuser2.salt) { delete newuser2.salt; }
|
|
if (newuser2.hash) { delete newuser2.hash; }
|
|
obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', username: newusername, account: newuser2, action: 'accountcreate', msg: 'Account created, email is ' + command.email, domain: domain.id })
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case 'edituser':
|
|
{
|
|
// Edit a user account, may involve changing email or administrator permissions
|
|
if (((user.siteadmin & 2) != 0) || (user.name == command.name)) {
|
|
var chguserid = 'user/' + domain.id + '/' + command.name.toLowerCase(), chguser = obj.users[chguserid], change = 0;
|
|
if (chguser) {
|
|
if (command.email && chguser.email != command.email) { chguser.email = command.email; change = 1; }
|
|
if (command.quota != chguser.quota) { chguser.quota = command.quota; if (chguser.quota == null) { delete chguser.quota; } change = 1; }
|
|
if ((user.siteadmin == 0xFFFFFFFF) && (command.siteadmin != null) && (chguser.siteadmin != command.siteadmin)) { chguser.siteadmin = command.siteadmin; change = 1 }
|
|
if (change == 1) {
|
|
obj.db.Set(chguser);
|
|
obj.parent.DispatchEvent([chguser._id], obj, 'resubscribe');
|
|
var chguser2 = obj.common.Clone(chguser);
|
|
delete chguser2.salt;
|
|
delete chguser2.hash;
|
|
obj.parent.DispatchEvent(['*', 'server-users', user._id, chguser._id], obj, { etype: 'user', username: user.name, account: chguser2, action: 'accountchange', msg: 'Account changed: ' + command.name, domain: domain.id })
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'serverversion':
|
|
{
|
|
// Check the server version
|
|
if ((user.siteadmin & 16) == 0) break;
|
|
obj.parent.getLatestServerVersion(function (currentVersion, latestVersion) { ws.send(JSON.stringify({ action: 'serverversion', current: currentVersion, latest: latestVersion })); });
|
|
break;
|
|
}
|
|
case 'serverupdate':
|
|
{
|
|
// Perform server update
|
|
if ((user.siteadmin & 16) == 0) break;
|
|
obj.parent.performServerUpdate();
|
|
break;
|
|
}
|
|
case 'createmesh':
|
|
{
|
|
// Create mesh
|
|
// TODO: Right now, we only create type 1 Agent-less Intel AMT mesh, or type 2 Agent mesh
|
|
if ((command.meshtype == 1) || (command.meshtype == 2)) {
|
|
// Create a type 1 agent-less Intel AMT mesh.
|
|
obj.crypto.randomBytes(32, function (err, buf) {
|
|
var meshid = 'mesh/' + domain.id + '/' + buf.toString('hex').toUpperCase();
|
|
var links = {}
|
|
links[user._id] = { name: user.name, rights: 0xFFFFFFFF };
|
|
var mesh = { type: 'mesh', _id: meshid, name: command.meshname, mtype: command.meshtype, desc: command.desc, domain: domain.id, links: links };
|
|
obj.db.Set(mesh);
|
|
obj.meshes[meshid] = mesh;
|
|
obj.parent.AddEventDispatch([meshid], ws);
|
|
if (user.links == null) user.links = {};
|
|
user.links[meshid] = { rights: 0xFFFFFFFF };
|
|
user.subscriptions = obj.subscribe(user._id, ws);
|
|
obj.db.SetUser(user);
|
|
obj.parent.DispatchEvent(['*', meshid, user._id], obj, { etype: 'mesh', username: user.name, meshid: meshid, name: command.meshname, mtype: command.meshtype, desc: command.desc, action: 'createmesh', links: links, msg: 'Mesh created: ' + command.meshname, domain: domain.id })
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case 'deletemesh':
|
|
{
|
|
// Delete a mesh and all computers within it
|
|
obj.db.Get(command.meshid, function (err, meshes) {
|
|
if (meshes.length != 1) return;
|
|
var mesh = meshes[0];
|
|
|
|
// Check if this user has rights to do this
|
|
if (mesh.links[user._id] == null || mesh.links[user._id].rights != 0xFFFFFFFF) return;
|
|
if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
|
|
|
|
// Fire the removal event first, because after this, the event will not route
|
|
obj.parent.DispatchEvent(['*', command.meshid], obj, { etype: 'mesh', username: user.name, meshid: command.meshid, name: command.meshname, action: 'deletemesh', msg: 'Mesh deleted: ' + command.meshname, domain: domain.id })
|
|
|
|
// Remove all user links to this mesh
|
|
for (var i in meshes) {
|
|
var links = meshes[i].links;
|
|
for (var j in links) {
|
|
var xuser = obj.users[j];
|
|
delete xuser.links[meshes[i]._id];
|
|
obj.db.Set(xuser);
|
|
obj.parent.DispatchEvent([xuser._id], obj, 'resubscribe');
|
|
}
|
|
}
|
|
|
|
// Delete all files on the server for this mesh
|
|
try {
|
|
var meshpath = getServerRootFilePath(mesh);
|
|
if (meshpath != null) { deleteFolderRec(meshpath); }
|
|
} catch (e) { }
|
|
|
|
obj.parent.RemoveEventDispatchId(command.meshid); // Remove all subscriptions to this mesh
|
|
obj.db.RemoveMesh(command.meshid); // Remove mesh from database
|
|
delete obj.meshes[command.meshid]; // Remove mesh from memory
|
|
});
|
|
break;
|
|
}
|
|
case 'editmesh':
|
|
{
|
|
// Change the name or description of a mesh
|
|
var mesh = obj.meshes[command.meshid], change = '';
|
|
if (mesh) {
|
|
// Check if this user has rights to do this
|
|
if (mesh.links[user._id] == null || ((mesh.links[user._id].rights & 1) == 0)) return;
|
|
if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
|
|
|
|
if (command.meshname && command.meshname != '' && command.meshname != mesh.name) { change = 'Mesh name changed from "' + mesh.name + '" to "' + command.meshname + '"'; mesh.name = command.meshname; }
|
|
if (command.desc != null && command.desc != mesh.desc) { if (change != '') change += ' and description changed'; else change += 'Mesh "' + mesh.name + '" description changed'; mesh.desc = command.desc; }
|
|
if (change != '') { obj.db.Set(mesh); obj.parent.DispatchEvent(['*', mesh._id, user._id], obj, { etype: 'mesh', username: user.name, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: change, domain: domain.id }) }
|
|
}
|
|
break;
|
|
}
|
|
case 'addmeshuser':
|
|
{
|
|
// Check if the user exists
|
|
var newuserid = 'user/' + domain.id + '/' + command.username.toLowerCase(), newuser = obj.users[newuserid];
|
|
if (newuser == null) {
|
|
// TODO: Send error back, user not found.
|
|
break;
|
|
}
|
|
|
|
// Get the mesh
|
|
var mesh = obj.meshes[command.meshid], change = '';
|
|
if (mesh) {
|
|
// Check if this user has rights to do this
|
|
if (mesh.links[user._id] == null || ((mesh.links[user._id].rights & 2) == 0)) return;
|
|
if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
|
|
|
|
// Add mesh to user
|
|
if (newuser.links == null) newuser.links = {};
|
|
newuser.links[command.meshid] = { rights: command.meshadmin };
|
|
obj.db.Set(newuser);
|
|
obj.parent.DispatchEvent([newuser._id], obj, 'resubscribe');
|
|
|
|
// Add a user to the mesh
|
|
mesh.links[newuserid] = { name: command.username, rights: command.meshadmin };
|
|
obj.db.Set(mesh);
|
|
|
|
// Notify mesh change
|
|
var change = 'Added user ' + command.username + ' to mesh ' + mesh.name;
|
|
obj.parent.DispatchEvent(['*', mesh._id, user._id, newuserid], obj, { etype: 'mesh', username: user.name, userid: command.userid, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: change, domain: domain.id })
|
|
}
|
|
break;
|
|
}
|
|
case 'removemeshuser':
|
|
{
|
|
if ((command.userid.split('/').length != 3) || (command.userid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
|
|
|
|
// Check if the user exists
|
|
var deluserid = command.userid, deluser = obj.users[deluserid];
|
|
if (deluser == null) {
|
|
// TODO: Send error back, user not found.
|
|
break;
|
|
}
|
|
|
|
// Get the mesh
|
|
var mesh = obj.meshes[command.meshid];
|
|
if (mesh) {
|
|
// Check if this user has rights to do this
|
|
if (mesh.links[user._id] == null || ((mesh.links[user._id].rights & 2) == 0)) return;
|
|
|
|
// Remove mesh from user
|
|
if (deluser.links != null && deluser.links[command.meshid] != null) {
|
|
var delmeshrights = deluser.links[command.meshid].rights;
|
|
if ((delmeshrights == 0xFFFFFFFF) && (mesh.links[user._id].rights != 0xFFFFFFFF)) return; // A non-admin can't kick out an admin
|
|
delete deluser.links[command.meshid];
|
|
obj.db.Set(deluser);
|
|
obj.parent.DispatchEvent([deluser._id], obj, 'resubscribe');
|
|
}
|
|
|
|
// Remove user from the mesh
|
|
if (mesh.links[command.userid] != null) {
|
|
delete mesh.links[command.userid];
|
|
obj.db.Set(mesh);
|
|
}
|
|
|
|
// Notify mesh change
|
|
var change = 'Removed user ' + deluser.name + ' from mesh ' + mesh.name;
|
|
obj.parent.DispatchEvent(['*', mesh._id, user._id, command.userid], obj, { etype: 'mesh', username: user.name, userid: command.userid, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: change, domain: domain.id })
|
|
}
|
|
break;
|
|
}
|
|
case 'addamtdevice':
|
|
{
|
|
if (obj.args.wanonly == true) return; // This is a WAN-only server, local Intel AMT computers can't be added
|
|
|
|
if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
|
|
|
|
// Get the mesh
|
|
var mesh = obj.meshes[command.meshid];
|
|
if (mesh) {
|
|
if (mesh.mtype != 1) return; // This operation is only allowed for mesh type 1, Intel AMT agentless mesh.
|
|
|
|
// Check if this user has rights to do this
|
|
if (mesh.links[user._id] == null || ((mesh.links[user._id].rights & 4) == 0)) return;
|
|
|
|
// Create a new nodeid
|
|
obj.crypto.randomBytes(32, function (err, buf) {
|
|
// create the new node
|
|
var nodeid = 'node/' + domain.id + '/' + buf.toString('hex').toUpperCase();
|
|
var device = { type: 'node', mtype: 1, _id: nodeid, meshid: command.meshid, name: command.devicename, host: command.hostname, domain: domain.id, intelamt: { user: command.amtusername, pass: command.amtpassword, tls: parseInt(command.amttls) } };
|
|
obj.db.Set(device);
|
|
|
|
// Event the new node
|
|
var device2 = obj.common.Clone(device);
|
|
delete device2.intelamt.pass; // Remove the Intel AMT password before eventing this.
|
|
var change = 'Added device ' + command.devicename + ' to mesh ' + mesh.name;
|
|
obj.parent.DispatchEvent(['*', command.meshid], obj, { etype: 'node', username: user.name, action: 'addnode', node: device2, msg: change, domain: domain.id })
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case 'scanamtdevice':
|
|
{
|
|
if (obj.args.wanonly == true) return; // This is a WAN-only server, this type of scanning is not allowed.
|
|
|
|
// Ask the RMCP scanning to scan a range of IP addresses
|
|
if (obj.parent.amtScanner) {
|
|
if (obj.parent.amtScanner.performRangeScan(ws.userid, command.range) == false) {
|
|
obj.parent.DispatchEvent(['*', ws.userid], obj, { action: 'scanamtdevice', range: command.range, results: null, nolog: 1 });
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'removedevices':
|
|
{
|
|
for (var i in command.nodeids) {
|
|
var nodeid = command.nodeids[i];
|
|
if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
|
|
|
|
// Get the device
|
|
obj.db.Get(nodeid, function (err, nodes) {
|
|
if (nodes.length != 1) return;
|
|
var node = nodes[0];
|
|
|
|
// Get the mesh for this device
|
|
var mesh = obj.meshes[node.meshid];
|
|
if (mesh) {
|
|
// Check if this user has rights to do this
|
|
if (mesh.links[user._id] == null || ((mesh.links[user._id].rights & 4) == 0)) return;
|
|
|
|
// Delete this node including network interface information and events
|
|
obj.db.Remove(node._id);
|
|
obj.db.Remove('if' + node._id);
|
|
|
|
// Event node deletion
|
|
var change = 'Removed device ' + node.name + ' from mesh ' + mesh.name;
|
|
obj.parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', username: user.name, action: 'removenode', nodeid: node._id, msg: change, domain: domain.id })
|
|
|
|
// Disconnect all connections if needed
|
|
var state = obj.parent.GetConnectivityState(command.nodeid);
|
|
if ((state != null) && (state.connectivity != null)) {
|
|
if ((state.connectivity & 1) != 0) { obj.wsagents[command.nodeid].close(); } // Disconnect mesh agent
|
|
if ((state.connectivity & 2) != 0) { obj.parent.mpsserver.close(obj.parent.mpsserver.ciraConnections[command.nodeid]); } // Disconnect CIRA connection
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
break;
|
|
}
|
|
case 'wakedevices':
|
|
{
|
|
// TODO: INPUT VALIDATION!!!
|
|
// TODO: We can optimize this a lot.
|
|
// - We should get a full list of all MAC's to wake first.
|
|
// - We should try to only have one agent per subnet (using Gateway MAC) send a wake-on-lan.
|
|
for (var i in command.nodeids) {
|
|
var nodeid = command.nodeids[i], wakeActions = 0;
|
|
if ((nodeid.split('/').length == 3) && (nodeid.split('/')[1] == domain.id)) { // Validate the domain, operation only valid for current domain
|
|
// Get the device
|
|
obj.db.Get(nodeid, function (err, nodes) {
|
|
if (nodes.length != 1) return;
|
|
var node = nodes[0];
|
|
|
|
// Get the mesh for this device
|
|
var mesh = obj.meshes[node.meshid];
|
|
if (mesh) {
|
|
|
|
// Check if this user has rights to do this
|
|
if (mesh.links[user._id] != null && ((mesh.links[user._id].rights & 64) != 0)) {
|
|
|
|
// Get the device interface information
|
|
obj.db.Get('if' + node._id, function (err, nodeifs) {
|
|
if (nodeifs.length == 1) {
|
|
var nodeif = nodeifs[0];
|
|
var macs = [];
|
|
for (var i in nodeif.netif) { if (nodeif.netif[i].mac) { macs.push(nodeif.netif[i].mac); } }
|
|
|
|
// Have the server send a wake-on-lan packet (Will not work in WAN-only)
|
|
if (obj.parent.meshScanner != null) { obj.parent.meshScanner.wakeOnLan(macs); wakeActions++; }
|
|
|
|
// Get the list of mesh this user as access to
|
|
var targetMeshes = [];
|
|
for (var i in user.links) { targetMeshes.push(i); }
|
|
|
|
// Go thru all the connected agents and send wake-on-lan on all the ones in the target mesh list
|
|
for (var i in obj.wsagents) {
|
|
var agent = obj.wsagents[i];
|
|
if ((targetMeshes.indexOf(agent.dbMeshKey) >= 0) && (agent.authenticated == 2)) {
|
|
//console.log('Asking agent ' + agent.dbNodeKey + ' to wake ' + macs.join(','));
|
|
agent.send(JSON.stringify({ action: 'wakeonlan', macs: macs }));
|
|
wakeActions++;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
}
|
|
}
|
|
});
|
|
}
|
|
// Confirm we may be doing something (TODO)
|
|
ws.send(JSON.stringify({ action: 'wakedevices' }));
|
|
}
|
|
|
|
break;
|
|
}
|
|
case 'poweraction':
|
|
{
|
|
// TODO: INPUT VALIDATION!!!
|
|
for (var i in command.nodeids) {
|
|
var nodeid = command.nodeids[i], powerActions = 0;
|
|
if ((nodeid.split('/').length == 3) && (nodeid.split('/')[1] == domain.id)) { // Validate the domain, operation only valid for current domain
|
|
// Get the device
|
|
obj.db.Get(nodeid, function (err, nodes) {
|
|
if (nodes.length != 1) return;
|
|
var node = nodes[0];
|
|
|
|
// Get the mesh for this device
|
|
var mesh = obj.meshes[node.meshid];
|
|
if (mesh) {
|
|
|
|
// Check if this user has rights to do this
|
|
if (mesh.links[user._id] != null && ((mesh.links[user._id].rights & 8) != 0)) { // "Remote Control permission"
|
|
|
|
// Get this device
|
|
var agent = obj.wsagents[node._id];
|
|
if (agent != null) {
|
|
// Send the power command
|
|
agent.send(JSON.stringify({ action: 'poweraction', actiontype: command.actiontype }));
|
|
powerActions++;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
// Confirm we may be doing something (TODO)
|
|
ws.send(JSON.stringify({ action: 'poweraction' }));
|
|
}
|
|
break;
|
|
}
|
|
case 'getnetworkinfo':
|
|
{
|
|
// Argument validation
|
|
if ((command.nodeid == null) || (typeof command.nodeid != 'string') || (command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
|
|
|
|
// Get the device
|
|
obj.db.Get(command.nodeid, function (err, nodes) {
|
|
if (nodes.length != 1) { ws.send(JSON.stringify({ action: 'getnetworkinfo', nodeid: command.nodeid, netif: null })); return; }
|
|
var node = nodes[0];
|
|
|
|
// Get the mesh for this device
|
|
var mesh = obj.meshes[node.meshid];
|
|
if (mesh) {
|
|
// Check if this user has rights to do this
|
|
if (mesh.links[user._id] == null || (mesh.links[user._id].rights == 0)) { ws.send(JSON.stringify({ action: 'getnetworkinfo', nodeid: command.nodeid, netif: null })); return; }
|
|
|
|
// Get network information about this node
|
|
obj.db.Get('if' + command.nodeid, function (err, netinfos) {
|
|
if (netinfos.length != 1) { ws.send(JSON.stringify({ action: 'getnetworkinfo', nodeid: command.nodeid, netif: null })); return; }
|
|
var netinfo = netinfos[0];
|
|
ws.send(JSON.stringify({ action: 'getnetworkinfo', nodeid: command.nodeid, updateTime: netinfo.updateTime, netif: netinfo.netif }));
|
|
});
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case 'changedevice':
|
|
{
|
|
// Argument validation
|
|
if ((command.nodeid == null) || (typeof command.nodeid != 'string') || (command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
|
|
|
|
// Change the device
|
|
obj.db.Get(command.nodeid, function (err, nodes) {
|
|
if (nodes.length != 1) return;
|
|
var node = nodes[0];
|
|
|
|
// Get the mesh for this device
|
|
var mesh = obj.meshes[node.meshid];
|
|
if (mesh) {
|
|
// Check if this user has rights to do this
|
|
if (mesh.links[user._id] == null || ((mesh.links[user._id].rights & 4) == 0)) return;
|
|
|
|
// Ready the node change event
|
|
var changes = [], change = 0, event = { etype: 'node', username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id };
|
|
event.msg = ": ";
|
|
|
|
// Look for a change
|
|
if (command.icon && (command.icon != node.icon)) { change = 1; node.icon = command.icon; changes.push('icon'); }
|
|
if (command.name && (command.name != node.name)) { change = 1; node.name = command.name; changes.push('name'); }
|
|
if (command.host && (command.host != node.host)) { change = 1; node.host = command.host; changes.push('host'); }
|
|
if (command.desc != null && (command.desc != node.desc)) { change = 1; node.desc = command.desc; changes.push('description'); }
|
|
if (command.intelamt != null) {
|
|
if ((command.intelamt.user != null) && (command.intelamt.pass != undefined) && ((command.intelamt.user != node.intelamt.user) || (command.intelamt.pass != node.intelamt.pass))) { change = 1; node.intelamt.user = command.intelamt.user; node.intelamt.pass = command.intelamt.pass; changes.push('Intel AMT credentials'); }
|
|
if (command.intelamt.tls && (command.intelamt.tls != node.intelamt.tls)) { change = 1; node.intelamt.tls = command.intelamt.tls; changes.push('Intel AMT TLS'); }
|
|
}
|
|
|
|
if (change == 1) {
|
|
// Save the node
|
|
obj.db.Set(node);
|
|
|
|
// Event the node change
|
|
event.msg = 'Changed device ' + node.name + ' from mesh ' + mesh.name + ': ' + changes.join(', ');
|
|
var node2 = obj.common.Clone(node);
|
|
if (node2.intelamt && node2.intelamt.pass) delete node2.intelamt.pass; // Remove the Intel AMT password before eventing this.
|
|
event.node = node2;
|
|
obj.parent.DispatchEvent(['*', node.meshid], obj, event);
|
|
}
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case 'uploadagentcore':
|
|
{
|
|
if (user.siteadmin != 0xFFFFFFFF) break;
|
|
if (command.path) {
|
|
if (command.path == '*') {
|
|
// Update the server default core and send a core hash request
|
|
// Load default mesh agent core if present, then perform a core update
|
|
parent.updateMeshCore(function () { sendMeshAgentCore(user, domain, command.nodeid, '*'); });
|
|
} else {
|
|
// Send a mesh agent core to the mesh agent
|
|
var file = getServerFilePath(user, domain, command.path);
|
|
if (file != null) {
|
|
readEntireTextFile(file.fullpath, function (data) {
|
|
if (data != null) {
|
|
data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
|
|
sendMeshAgentCore(user, domain, command.nodeid, data);
|
|
}
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
// Clear the mesh agent core on the mesh agent
|
|
sendMeshAgentCore(user, domain, command.nodeid, null);
|
|
}
|
|
break;
|
|
}
|
|
case 'agentdisconnect':
|
|
{
|
|
// Force mesh agent disconnection
|
|
forceMeshAgentDisconnect(user, domain, command.nodeid, command.disconnectMode);
|
|
break;
|
|
}
|
|
case 'close':
|
|
{
|
|
// Close the web socket session
|
|
if (req.session && req.session.ws && req.session.ws == ws) delete req.session.ws;
|
|
try { ws.close(); } catch (e) { }
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// If error, do nothing
|
|
ws.on('error', function (err) { console.log(err); });
|
|
|
|
// If the web socket is closed
|
|
ws.on('close', function (req) {
|
|
obj.parent.RemoveAllEventDispatch(ws);
|
|
if (req.session && req.session.ws && req.session.ws == ws) { delete req.session.ws; }
|
|
if (obj.wssessions2[ws.sessionId]) { delete obj.wssessions2[ws.sessionId]; }
|
|
if (obj.wssessions[ws.userid]) {
|
|
var i = obj.wssessions[ws.userid].indexOf(ws);
|
|
if (i >= 0) {
|
|
obj.wssessions[ws.userid].splice(i, 1);
|
|
var user = obj.users[ws.userid];
|
|
if (user) { obj.parent.DispatchEvent(['*'], obj, { action: 'wssessioncount', username: user.name, count: obj.wssessions[ws.userid].length, nolog: 1, domain: domain.id }) }
|
|
if (obj.wssessions[ws.userid].length == 0) { delete obj.wssessions[ws.userid]; }
|
|
}
|
|
}
|
|
});
|
|
|
|
// Send user information to web socket, this is the first thing we send
|
|
var userinfo = obj.common.Clone(obj.users[req.session.userid]);
|
|
delete userinfo.salt;
|
|
delete userinfo.hash;
|
|
ws.send(JSON.stringify({ action: 'userinfo', userinfo: userinfo }));
|
|
|
|
// Next, send server information
|
|
if (obj.args.notls == true) {
|
|
ws.send(JSON.stringify({ action: 'serverinfo', serverinfo: { name: obj.certificates.CommonName, mpsport: obj.args.mpsport, mpspass: obj.args.mpspass, port: obj.args.port, https: false } }));
|
|
} else {
|
|
ws.send(JSON.stringify({ action: 'serverinfo', serverinfo: { name: obj.certificates.CommonName, mpsport: obj.args.mpsport, mpspass: obj.args.mpspass, redirport: obj.args.redirport, port: obj.args.port, https: true } }));
|
|
}
|
|
} catch (e) { console.log(e); }
|
|
}
|
|
|
|
// Handle Intel AMT events
|
|
// To subscribe, add "http://server:port/amtevents.ashx" to Intel AMT subscriptions.
|
|
obj.handleAmtEventRequest = function (req, res) {
|
|
var domain = getDomain(req);
|
|
try {
|
|
if (req.headers['authorization']) {
|
|
var authstr = req.headers['authorization'];
|
|
if (authstr.substring(0, 7) == "Digest ") {
|
|
var auth = obj.common.parseNameValueList(obj.common.quoteSplit(authstr.substring(7)));
|
|
if ((req.url === auth.uri) && (obj.httpAuthRealm === auth.realm) && (auth.opaque === obj.crypto.createHmac('SHA256', obj.httpAuthRandom).update(auth.nonce).digest('hex'))) {
|
|
|
|
// Read the data, we need to get the arg field
|
|
var eventData = '';
|
|
req.on('data', function (chunk) { eventData += chunk; });
|
|
req.on('end', function () {
|
|
|
|
// Completed event read, let get the argument that must contain the nodeid
|
|
var i = eventData.indexOf('<m:arg xmlns:m="http://x.com">');
|
|
if (i > 0) {
|
|
var nodeid = eventData.substring(i + 30, i + 30 + 64).toUpperCase();
|
|
if (nodeid.length == 64) {
|
|
var nodekey = 'node/' + domain.id + '/' + nodeid;
|
|
|
|
// See if this node exists in the database
|
|
obj.db.Get(nodekey, function (err, nodes) {
|
|
if (nodes.length == 1) {
|
|
// Yes, the node exists, compute Intel AMT digest password
|
|
var node = nodes[0];
|
|
var amtpass = obj.crypto.createHash('sha256').update(auth.username.toLowerCase() + ":" + nodeid + ":" + obj.parent.dbconfig.amtWsEventSecret).digest("base64").substring(0, 12).split("/").join("x").split("\\").join("x");
|
|
|
|
// Check the MD5 hash
|
|
if (auth.response === obj.common.ComputeDigesthash(auth.username, amtpass, auth.realm, "POST", auth.uri, auth.qop, auth.nonce, auth.nc, auth.cnonce)) {
|
|
|
|
// This is an authenticated Intel AMT event, update the host address
|
|
var amthost = req.connection.remoteAddress;
|
|
if (amthost.substring(0, 7) === '::ffff:') { amthost = amthost.substring(7); }
|
|
if (node.host != amthost) {
|
|
// Get the mesh for this device
|
|
var mesh = obj.meshes[node.meshid];
|
|
if (mesh) {
|
|
// Update the database
|
|
var oldname = node.host;
|
|
node.host = amthost;
|
|
obj.db.Set(node);
|
|
|
|
// Event the node change
|
|
var event = { etype: 'node', action: 'changenode', nodeid: node._id, domain: domain.id, msg: 'Intel(R) AMT host change ' + node.name + ' from mesh ' + mesh.name + ': ' + oldname + ' to ' + amthost };
|
|
var node2 = obj.common.Clone(node);
|
|
if (node2.intelamt && node2.intelamt.pass) delete node2.intelamt.pass; // Remove the Intel AMT password before eventing this.
|
|
event.node = node2;
|
|
obj.parent.DispatchEvent(['*', node.meshid], obj, event);
|
|
}
|
|
}
|
|
|
|
parent.amtEventHandler.handleAmtEvent(eventData, nodeid, amthost);
|
|
//res.send('OK');
|
|
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} catch (e) { console.log(e); }
|
|
|
|
// Send authentication response
|
|
obj.crypto.randomBytes(32, function (err, buf) {
|
|
var nonce = buf.toString('hex'), opaque = obj.crypto.createHmac('SHA256', obj.httpAuthRandom).update(nonce).digest('hex');
|
|
res.set({ 'WWW-Authenticate': 'Digest realm="' + obj.httpAuthRealm + '", qop="auth,auth-int", nonce="' + nonce + '", opaque="' + opaque + '"' });
|
|
res.sendStatus(401);
|
|
});
|
|
}
|
|
|
|
// Handle a server backup request
|
|
function handleBackupRequest(req, res) {
|
|
var domain = getDomain(req);
|
|
if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid) || (obj.parent.args.noserverbackup == 1)) { res.sendStatus(401); return; }
|
|
var user = obj.users[req.session.userid];
|
|
if ((user.siteadmin & 1) == 0) { res.sendStatus(401); return; } // Check if we have server backup rights
|
|
|
|
// Require modules
|
|
var fs = require('fs');
|
|
var archive = require('archiver')('zip', { level: 9 }); // Sets the compression method to maximum.
|
|
|
|
// Good practice to catch this error explicitly
|
|
archive.on('error', function (err) { throw err; });
|
|
|
|
// Set the archive name
|
|
res.attachment(domain.title + '-Backup-' + new Date().toLocaleDateString().replace('/', '-').replace('/', '-') + '.zip');
|
|
|
|
// Pipe archive data to the file
|
|
archive.pipe(res);
|
|
|
|
// Append all of the files for this backup
|
|
var backupList = ['config.json', 'meshcentral.db', 'agentserver-cert-private.key', 'agentserver-cert-public.crt', 'mpsserver-cert-private.key', 'mpsserver-cert-public.crt', 'data/root-cert-private.key', 'root-cert-public.crt', 'webserver-cert-private.key', 'webserver-cert-public.crt'];
|
|
for (var i in backupList) {
|
|
var filename = backupList[i];
|
|
var filepath = obj.path.join(obj.parent.datapath, filename);
|
|
if (fs.existsSync(filepath)) { archive.file(filepath, { name: filename }); }
|
|
}
|
|
|
|
// Finalize the archive (ie we are done appending files but streams have to finish yet)
|
|
archive.finalize();
|
|
}
|
|
|
|
// Handle a server restore request
|
|
function handleRestoreRequest(req, res) {
|
|
var domain = getDomain(req);
|
|
if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid) || (obj.parent.args.noserverbackup == 1)) { res.sendStatus(401); return; }
|
|
var user = obj.users[req.session.userid];
|
|
if ((user.siteadmin & 4) == 0) { res.sendStatus(401); return; } // Check if we have server restore rights
|
|
|
|
var multiparty = require('multiparty');
|
|
var form = new multiparty.Form();
|
|
form.parse(req, function (err, fields, files) {
|
|
res.send('Server must be restarted, <a href="' + domain.url + '">click here to login</a>.');
|
|
parent.Stop(files.datafile[0].path);
|
|
});
|
|
}
|
|
|
|
// Handle a request to download a mesh agent
|
|
obj.handleMeshAgentRequest = function (req, res) {
|
|
if (req.query.id != null) {
|
|
// Send a specific mesh agent back
|
|
var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
|
|
if (argentInfo == null) { res.sendStatus(404); return; }
|
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename=' + argentInfo.rname });
|
|
res.sendFile(argentInfo.path);
|
|
} else if (req.query.script != null) {
|
|
// Send a specific mesh install script back
|
|
var scriptInfo = obj.parent.meshAgentInstallScripts[req.query.script];
|
|
if (scriptInfo == null) { res.sendStatus(404); return; }
|
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'text/plain', 'Content-Disposition': 'attachment; filename=' + scriptInfo.rname });
|
|
res.sendFile(scriptInfo.path);
|
|
} else {
|
|
// Send a list of available mesh agents
|
|
var response = '<html><head><title>Mesh Agents</title><style>table,th,td { border:1px solid black;border-collapse:collapse;padding:3px; }</style></head><body><table>';
|
|
response += '<tr style="background-color:lightgray"><th>ID</th><th>Description</th><th>Link</th><th>Size</th><th>SHA256</th></tr>';
|
|
for (var agentid in obj.parent.meshAgentBinaries) {
|
|
var agentinfo = obj.parent.meshAgentBinaries[agentid];
|
|
response += '<tr><td>' + agentinfo.id + '</td><td>' + agentinfo.desc + '</td>';
|
|
response += '<td><a target=_blank href="' + req.originalUrl + '?id=' + agentinfo.id + '">' + agentinfo.rname + '</a></td>';
|
|
response += '<td>' + agentinfo.size + '</td><td>' + agentinfo.hash + '</td></tr>';
|
|
}
|
|
response += '</table></body></html>';
|
|
res.send(response);
|
|
}
|
|
}
|
|
|
|
// Handle a request to download a mesh settings
|
|
obj.handleMeshSettingsRequest = function (req, res) {
|
|
var domain = getDomain(req);
|
|
//if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
|
|
|
|
// Delete a mesh and all computers within it
|
|
obj.db.Get('mesh/' + domain.id + '/' + req.query.id, function (err, meshes) {
|
|
if (meshes.length != 1) { res.sendStatus(401); return; }
|
|
var mesh = meshes[0];
|
|
|
|
// Check if this user has rights to do this
|
|
//var user = obj.users[req.session.userid];
|
|
//if ((user == null) || (mesh.links[user._id] == null) || ((mesh.links[user._id].rights & 1) == 0)) { res.sendStatus(401); return; }
|
|
//if (domain.id != mesh.domain) { res.sendStatus(401); return; }
|
|
|
|
var xdomain = domain.id;
|
|
if (xdomain != '') xdomain += "/";
|
|
var meshsettings = "MeshName=" + mesh.name + "\r\nMeshID=0x" + req.query.id.toUpperCase() + "\r\nServerID=" + obj.agentCertificatHashHex.toUpperCase() + "\r\n";
|
|
if (obj.args.lanonly != true) { meshsettings += "MeshServer=ws" + (obj.args.notls ? '' : 's') + "://" + certificates.CommonName + ":" + obj.args.port + "/" + xdomain + "agent.ashx\r\n"; } else { meshsettings += "MeshServer=local"; }
|
|
|
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename=meshagent.msh' });
|
|
res.send(meshsettings);
|
|
});
|
|
}
|
|
|
|
function updateUserFiles(user, ws, domain) {
|
|
// Request the list of server files
|
|
var files = { action: 'files', filetree: { n : 'Root', f : {} } };
|
|
|
|
// Add user files
|
|
files.filetree.f[user._id] = { t: 1, n: 'My Files', f: {} };
|
|
files.filetree.f[user._id].maxbytes = getQuota(user._id, domain);
|
|
var usersplit = user._id.split('/'), domainx = 'domain';
|
|
if (usersplit[1].length > 0) domainx = 'domain-' + usersplit[1];
|
|
|
|
// Read all files recursively
|
|
try {
|
|
files.filetree.f[user._id].f = readFilesRec(obj.path.join(obj.filespath, domainx + "/user-" + usersplit[2]));
|
|
} catch (e) {
|
|
// Got an error, try to create all the folders and try again...
|
|
try { obj.fs.mkdirSync(obj.filespath); } catch (e) { }
|
|
try { obj.fs.mkdirSync(obj.path.join(obj.filespath, domainx)); } catch (e) { }
|
|
try { obj.fs.mkdirSync(obj.path.join(obj.filespath, domainx + "/user-" + usersplit[2])); } catch (e) { }
|
|
try { obj.fs.mkdirSync(obj.path.join(obj.filespath, domainx + "/user-" + usersplit[2] + "/Public")); } catch (e) { }
|
|
try { files.filetree.f[user._id].f = readFilesRec(obj.path.join(obj.filespath, domainx + "/user-" + usersplit[2])); } catch (e) { }
|
|
}
|
|
|
|
// Add files for each mesh
|
|
for (var i in user.links) {
|
|
if ((user.links[i].rights & 32) != 0) { // Check that we have file permissions
|
|
var mesh = obj.meshes[i];
|
|
if (mesh) {
|
|
var meshsplit = mesh._id.split('/');
|
|
files.filetree.f[mesh._id] = { t: 1, n: mesh.name, f: {} };
|
|
files.filetree.f[mesh._id].maxbytes = getQuota(mesh._id, domain);
|
|
|
|
// Read all files recursively
|
|
try {
|
|
files.filetree.f[mesh._id].f = readFilesRec(obj.path.join(__dirname, "files/" + domainx + "/mesh-" + meshsplit[2]));
|
|
} catch (e) {
|
|
// Got an error, try to create all the folders and try again...
|
|
try { obj.fs.mkdirSync(obj.filespath); } catch (e) { }
|
|
try { obj.fs.mkdirSync(obj.path.join(obj.filespath, domainx)); } catch (e) { }
|
|
try { obj.fs.mkdirSync(obj.path.join(obj.filespath, domainx + "/mesh-" + meshsplit[2])); } catch (e) { }
|
|
try { files.filetree.f[mesh._id].f = readFilesRec(obj.path.join(obj.filespath, domainx + "/mesh-" + meshsplit[2])); } catch (e) { }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Respond
|
|
ws.send(JSON.stringify(files));
|
|
}
|
|
|
|
// Add HTTP security headers to all responses
|
|
obj.app.use(function (req, res, next) {
|
|
// Two more headers to take a look at:
|
|
// 'Public-Key-Pins': 'pin-sha256="X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg="; max-age=10'
|
|
// 'strict-transport-security': 'max-age=31536000; includeSubDomains'
|
|
res.removeHeader("X-Powered-By");
|
|
if (obj.args.notls) {
|
|
// Default headers if no TLS is used
|
|
res.set({ 'Referrer-Policy': 'no-referrer', 'x-frame-options': 'SAMEORIGIN', 'X-XSS-Protection': '1; mode=block', 'X-Content-Type-Options': 'nosniff', 'Content-Security-Policy': "default-src http: ws: data: 'self';script-src http: 'unsafe-inline';style-src http: 'unsafe-inline'" });
|
|
} else {
|
|
// Default headers if TLS is used
|
|
res.set({ 'Referrer-Policy': 'no-referrer', 'x-frame-options': 'SAMEORIGIN', 'X-XSS-Protection': '1; mode=block', 'X-Content-Type-Options': 'nosniff', 'Content-Security-Policy': "default-src https: wss: data: 'self';script-src https: 'unsafe-inline';style-src https: 'unsafe-inline'" });
|
|
}
|
|
return next();
|
|
});
|
|
|
|
// Setup all HTTP handlers
|
|
obj.app.get('/backup.zip', handleBackupRequest);
|
|
obj.app.post('/restoreserver.ashx', handleRestoreRequest);
|
|
if (parent.multiServer != null) { obj.app.ws('/meshserver.ashx', function (ws, req) { parent.multiServer.CreatePeerInServer(parent.multiServer, ws, req); } ); }
|
|
for (var i in parent.config.domains) {
|
|
var url = parent.config.domains[i].url;
|
|
obj.app.get(url, handleRootRequest);
|
|
obj.app.get(url + 'terms', handleTermsRequest);
|
|
obj.app.post(url + 'login', handleLoginRequest);
|
|
obj.app.get(url + 'logout', handleLogoutRequest);
|
|
obj.app.get(url + 'MeshServerRootCert.cer', handleRootCertRequest);
|
|
obj.app.get(url + 'mescript.ashx', handleMeScriptRequest);
|
|
obj.app.post(url + 'changepassword', handlePasswordChangeRequest);
|
|
obj.app.post(url + 'deleteaccount', handleDeleteAccountRequest);
|
|
obj.app.post(url + 'createaccount', handleCreateAccountRequest);
|
|
obj.app.post(url + 'amtevents.ashx', obj.handleAmtEventRequest);
|
|
obj.app.get(url + 'webrelay.ashx', function (req, res) { res.send('Websocket connection expected'); });
|
|
obj.app.ws(url + 'webrelay.ashx', handleRelayWebSocket);
|
|
obj.app.ws(url + 'control.ashx', handleControlRequest);
|
|
obj.app.get(url + 'meshagents', obj.handleMeshAgentRequest);
|
|
obj.app.get(url + 'meshsettings', obj.handleMeshSettingsRequest);
|
|
obj.app.get(url + 'downloadfile.ashx', handleDownloadFile);
|
|
obj.app.post(url + 'uploadfile.ashx', handleUploadFile);
|
|
obj.app.post(url + 'uploadmeshcorefile.ashx', handleUploadMeshCoreFile);
|
|
obj.app.get(url + 'userfiles/*', handleDownloadUserFiles);
|
|
obj.app.ws(url + 'echo.ashx', handleEchoWebSocket);
|
|
obj.app.ws(url + 'meshrelay.ashx', function (ws, req) { try { obj.meshRelayHandler.CreateMeshRelay(obj, ws, req); } catch (e) { console.log(e); } });
|
|
|
|
// Receive mesh agent connections
|
|
obj.app.ws(url + 'agent.ashx', function (ws, req) { try { var domain = getDomain(req); obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (e) { console.log(e); } });
|
|
|
|
obj.app.get(url + 'stop', function (req, res) { res.send('Stopping Server, <a href="' + url + '">click here to login</a>.'); setTimeout(function () { parent.Stop(); }, 500); });
|
|
|
|
/*
|
|
// DEBUG ONLY: Returns a mesh relay key
|
|
obj.app.get(url + 'getkey', function (req, res) {
|
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'text/plain' });
|
|
require('./meshrelay.js').CreateMeshRelayKey(obj, function (x) { res.send(x); });
|
|
});
|
|
*/
|
|
|
|
/*
|
|
// Test
|
|
obj.app.get(url + 'test.json', function (req, res) {
|
|
res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'text/plain' });
|
|
res.send('{ "glossary": { "title": "example glossary", "bob" : 5 } }');
|
|
});
|
|
*/
|
|
|
|
// Indicates to ExpressJS that the public folder should be used to serve static files.
|
|
obj.app.use(url, obj.express.static(obj.path.join(__dirname, 'public')));
|
|
}
|
|
|
|
// Find a free port starting with the specified one and going up.
|
|
function CheckListenPort(port, func) {
|
|
var s = obj.net.createServer(function (socket) { });
|
|
obj.tcpServer = s.listen(port, function () { s.close(function () { if (func) { func(port); } }); }).on('error', function (err) {
|
|
if (args.exactports) { console.error('ERROR: MeshCentral HTTPS web server port ' + port + ' not available.'); process.exit(); }
|
|
else { if (port < 65535) { CheckListenPort(port + 1, func); } else { if (func) { func(0); } } }
|
|
});
|
|
}
|
|
|
|
// Start the ExpressJS web server
|
|
function StartWebServer(port) {
|
|
if (port == 0 || port == 65535) return;
|
|
obj.args.port = port;
|
|
if (obj.tlsServer != null) {
|
|
if (obj.args.lanonly == true) {
|
|
obj.tcpServer = obj.tlsServer.listen(port, function () { console.log('MeshCentral HTTPS web server running on port ' + port + '.'); });
|
|
} else {
|
|
obj.tcpServer = obj.tlsServer.listen(port, function () { console.log('MeshCentral HTTPS web server running on ' + certificates.CommonName + ':' + port + '.'); });
|
|
}
|
|
} else {
|
|
obj.tcpServer = obj.app.listen(port, function () { console.log('MeshCentral HTTP web server running on port ' + port + '.'); });
|
|
}
|
|
}
|
|
|
|
// Force mesh agent disconnection
|
|
function forceMeshAgentDisconnect(user, domain, nodeid, disconnectMode) {
|
|
if (nodeid == null) return;
|
|
var splitnode = nodeid.split('/');
|
|
if ((splitnode.length != 3) || (splitnode[1] != domain.id)) return; // Check that nodeid is valid and part of our domain
|
|
var agent = obj.wsagents[nodeid];
|
|
if (agent == null) return;
|
|
|
|
// Check we have agent rights
|
|
var rights = user.links[agent.dbMeshKey].rights;
|
|
if ((rights != null) && ((rights & 16) != 0) && (user.siteadmin == 0xFFFFFFFF)) { agent.close(disconnectMode); }
|
|
}
|
|
|
|
// Send the core module to the mesh agent
|
|
function sendMeshAgentCore(user, domain, nodeid, core) {
|
|
if (nodeid == null) return;
|
|
var splitnode = nodeid.split('/');
|
|
if ((splitnode.length != 3) || (splitnode[1] != domain.id)) return; // Check that nodeid is valid and part of our domain
|
|
var agent = obj.wsagents[nodeid];
|
|
if (agent == null) return;
|
|
|
|
// Check we have agent rights
|
|
var rights = user.links[agent.dbMeshKey].rights;
|
|
if ((rights != null) && ((rights & 16) != 0) && (user.siteadmin == 0xFFFFFFFF)) {
|
|
if (core == null) {
|
|
// Clear the mesh agent core
|
|
agent.agentCoreCheck = 1000; // Tell the agent object we are not using a custom core.
|
|
agent.send(obj.common.ShortToStr(10) + obj.common.ShortToStr(0));
|
|
} else if (core == '*') {
|
|
agent.agentCoreCheck = 0; // Tell the agent object we are using a default code
|
|
// Reset the core to the server default
|
|
agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash.
|
|
} else {
|
|
agent.agentCoreCheck = 1000; // Tell the agent object we are not using a custom core.
|
|
// Perform a SHA256 hash on the core module
|
|
var buf = new Buffer(core, 'ascii');
|
|
var hash = obj.crypto.createHash('sha256').update(buf).digest(), hash2 = "";
|
|
for (var i = 0; i < hash.length; i++) { hash2 += String.fromCharCode(hash[i]); }
|
|
|
|
// Send the code module to the agent
|
|
agent.send(obj.common.ShortToStr(10) + obj.common.ShortToStr(0) + hash2 + core); // TODO: Add core encoding short
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the maximum number of bytes allowed in the user account "My Files".
|
|
function getQuota(objid, domain) {
|
|
if (objid == null) return 0;
|
|
if (objid.startsWith('user/')) {
|
|
var user = obj.users[objid];
|
|
if (user == null) return 0;
|
|
if ((user.quota != null) && (typeof user.quota == 'number')) { return user.quota; }
|
|
if ((domain != null) && (domain.userQuota != null) && (typeof domain.userQuota == 'number')) { return domain.userQuota; }
|
|
return 1048576; // By default, the server will have a 1 meg limit on user accounts
|
|
} else if (objid.startsWith('mesh/')) {
|
|
var mesh = obj.meshes[objid];
|
|
if (mesh == null) return 0;
|
|
if ((mesh.quota != null) && (typeof mesh.quota == 'number')) { return mesh.quota; }
|
|
if ((domain != null) && (domain.meshQuota != null) && (typeof domain.meshQuota == 'number')) { return domain.meshQuota; }
|
|
return 1048576; // By default, the server will have a 1 meg limit on mesh accounts
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Get the server path of a user or mesh object
|
|
function getServerRootFilePath(obj) {
|
|
if ((typeof obj != 'object') || (obj.domain == null) || (obj._id == null)) return null;
|
|
var domainname = 'domain', splitname = obj._id.split('/');
|
|
if (splitname.length != 3) return null;
|
|
if (obj.domain !== '') domainname = 'domain-' + obj.domain;
|
|
return obj.path.join(obj.filespath, domainname + "/" + splitname[0] + "-" + splitname[2]);
|
|
}
|
|
|
|
// Take a "user/domain/userid/path/file" format and return the actual server disk file path if access is allowed
|
|
function getServerFilePath(user, domain, path) {
|
|
var splitpath = path.split('/'), serverpath = obj.path.join(obj.filespath, 'domain'), filename = '';
|
|
if ((splitpath.length < 3) || (splitpath[0] != 'user' && splitpath[0] != 'mesh') || (splitpath[1] != domain.id)) return null; // Basic validation
|
|
var objid = splitpath[0] + '/' + splitpath[1] + '/' + splitpath[2];
|
|
if (splitpath[0] == 'user' && (objid != user._id)) return null; // User validation, only self allowed
|
|
if (splitpath[0] == 'mesh') { var link = user.links[objid]; if ((link == null) || (link.rights == null) || ((link.rights & 32) == 0)) { return null; } } // Check mesh server file rights
|
|
if (splitpath[1] != '') { serverpath += '-' + splitpath[1]; } // Add the domain if needed
|
|
serverpath += ('/' + splitpath[0] + '-' + splitpath[2]);
|
|
for (var i = 3; i < splitpath.length; i++) { if (obj.common.IsFilenameValid(splitpath[i]) == true) { serverpath += '/' + splitpath[i]; filename = splitpath[i]; } else { return null; } } // Check that each folder is correct
|
|
var fullpath = obj.path.resolve(obj.filespath, serverpath), quota = 0;
|
|
return { fullpath: fullpath, path: serverpath, name: filename, quota: getQuota(objid, domain) };
|
|
}
|
|
|
|
// Read entire file and return it in callback function
|
|
function readEntireTextFile(filepath, func) {
|
|
var called = false;
|
|
try {
|
|
obj.fs.open(filepath, 'r', function (err, fd) {
|
|
obj.fs.fstat(fd, function (err, stats) {
|
|
var bufferSize = stats.size, chunkSize = 512, buffer = new Buffer(bufferSize), bytesRead = 0;
|
|
while (bytesRead < bufferSize) {
|
|
if ((bytesRead + chunkSize) > bufferSize) { chunkSize = (bufferSize - bytesRead); }
|
|
obj.fs.readSync(fd, buffer, bytesRead, chunkSize, bytesRead);
|
|
bytesRead += chunkSize;
|
|
}
|
|
obj.fs.close(fd);
|
|
called = true;
|
|
func(buffer.toString('utf8', 0, bufferSize));
|
|
});
|
|
});
|
|
} catch (e) { if (called == false) { func(null); } }
|
|
}
|
|
|
|
// Debug
|
|
function Debug(lvl) {
|
|
if (lvl > obj.parent.debugLevel) return;
|
|
if (arguments.length == 2) { console.log(arguments[1]); }
|
|
else if (arguments.length == 3) { console.log(arguments[1], arguments[2]); }
|
|
else if (arguments.length == 4) { console.log(arguments[1], arguments[2], arguments[3]); }
|
|
else if (arguments.length == 5) { console.log(arguments[1], arguments[2], arguments[3], arguments[4]); }
|
|
else if (arguments.length == 6) { console.log(arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]); }
|
|
else if (arguments.length == 7) { console.log(arguments[1], arguments[2], arguments[3], arguments[4], arguments[5], arguments[6]); }
|
|
}
|
|
|
|
// Start server on a free port
|
|
CheckListenPort(obj.args.port, StartWebServer);
|
|
|
|
return obj;
|
|
}
|