Many server improvements & stability fixes.

This commit is contained in:
Ylian Saint-Hilaire 2019-02-14 14:44:38 -08:00
parent 66b1403eba
commit 8b3de82e6a
12 changed files with 178 additions and 24 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -258,7 +258,11 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
obj.send(obj.common.ShortToStr(1) + msg.substring(2, 50) + obj.nonce); // Command 1, hash + nonce. Use the web hash given by the agent.
} else {
// Check that the server hash matches our own web certificate hash (SHA384)
if ((getWebCertHash(obj.domain) != msg.substring(2, 50)) && (getWebCertFullHash(obj.domain) != msg.substring(2, 50))) { console.log('Agent bad web cert hash (Agent:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex').substring(0, 10)) + ' != Server:' + (Buffer.from(getWebCertHash(obj.domain), 'binary').toString('hex').substring(0, 10)) + ' or ' + (new Buffer(getWebCertFullHash(obj.domain), 'binary').toString('hex').substring(0, 10)) + '), holding connection (' + obj.remoteaddrport + ').'); return; }
if ((getWebCertHash(obj.domain) != msg.substring(2, 50)) && (getWebCertFullHash(obj.domain) != msg.substring(2, 50))) {
console.log('Agent bad web cert hash (Agent:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex').substring(0, 10)) + ' != Server:' + (Buffer.from(getWebCertHash(obj.domain), 'binary').toString('hex').substring(0, 10)) + ' or ' + (new Buffer(getWebCertFullHash(obj.domain), 'binary').toString('hex').substring(0, 10)) + '), holding connection (' + obj.remoteaddrport + ').');
console.log('Agent reported web cert hash:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex')) + '.');
return;
}
}
// Use our server private key to sign the ServerHash + AgentNonce + ServerNonce
@ -365,6 +369,31 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
obj.send(obj.common.ShortToStr(1) + getWebCertHash(obj.domain) + obj.nonce); // Command 1, hash + nonce
}
// Return the mesh for this device, in some cases, we may auto-create the mesh.
function getMeshAutoCreate() {
var mesh = obj.parent.meshes[obj.dbMeshKey];
if ((mesh == null) && (typeof obj.domain.orphanagentuser == 'string')) {
var adminUser = obj.parent.users['user/' + domain.id + '/' + obj.domain.orphanagentuser.toLowerCase()];
if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) {
// Mesh name is hex instead of base64
var meshname = Buffer.from(obj.meshid, 'base64').toString('hex').substring(0, 18);
// Create a new mesh for this device
var links = {};
links[adminUser._id] = { name: adminUser.name, rights: 0xFFFFFFFF };
mesh = { type: 'mesh', _id: obj.dbMeshKey, name: meshname, mtype: 2, desc: '', domain: domain.id, links: links };
obj.db.Set(obj.common.escapeLinksFieldName(mesh));
obj.parent.meshes[obj.dbMeshKey] = mesh;
if (adminUser.links == null) user.links = {};
adminUser.links[obj.dbMeshKey] = { rights: 0xFFFFFFFF };
obj.db.SetUser(adminUser);
obj.parent.parent.DispatchEvent(['*', obj.dbMeshKey, adminUser._id], obj, { etype: 'mesh', username: adminUser.name, meshid: obj.dbMeshKey, name: meshname, mtype: 2, desc: '', action: 'createmesh', links: links, msg: 'Mesh created: ' + obj.meshid, domain: domain.id });
}
}
return mesh;
}
// Once we get all the information about an agent, run this to hook everything up to the server
function completeAgentConnection() {
if ((obj.authenticated != 1) || (obj.meshid == null) || obj.pendingCompleteAgentConnection) return;
@ -380,21 +409,69 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
if (domainAgentSessionCount >= domain.limits.maxagentsessions) { return; } // Too many, hold the connection.
}
/*
// Check that the mesh exists
var mesh = obj.parent.meshes[obj.dbMeshKey];
if (mesh == null) { console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').'); return; } // If we disconnect, the agnet will just reconnect. We need to log this or tell agent to connect in a few hours.
if (mesh == null) {
var holdConnection = true;
if (typeof obj.domain.orphanagentuser == 'string') {
var adminUser = obj.parent.users['user/' + domain.id + '/' + obj.args.orphanagentuser];
if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) {
// Create a new mesh for this device
holdConnection = false;
var links = {};
links[user._id] = { name: adminUser.name, rights: 0xFFFFFFFF };
mesh = { type: 'mesh', _id: obj.dbMeshKey, name: obj.meshid, mtype: 2, desc: '', domain: domain.id, links: links };
obj.db.Set(obj.common.escapeLinksFieldName(mesh));
obj.parent.meshes[obj.meshid] = mesh;
obj.parent.parent.AddEventDispatch([obj.meshid], ws);
if (adminUser.links == null) user.links = {};
adminUser.links[obj.meshid] = { rights: 0xFFFFFFFF };
//adminUser.subscriptions = obj.parent.subscribe(adminUser._id, ws);
obj.db.SetUser(user);
obj.parent.parent.DispatchEvent(['*', meshid, user._id], obj, { etype: 'mesh', username: user.name, meshid: obj.meshid, name: obj.meshid, mtype: 2, desc: '', action: 'createmesh', links: links, msg: 'Mesh created: ' + obj.meshid, domain: domain.id });
}
}
if (holdConnection == true) {
// If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
return;
}
}
if (mesh.mtype != 2) { console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').'); return; } // If we disconnect, the agnet will just reconnect. We need to log this or tell agent to connect in a few hours.
*/
// Check that the node exists
obj.db.Get(obj.dbNodeKey, function (err, nodes) {
var device;
// Mark when we connected to this agent
obj.connectTime = Date.now();
obj.db.Set({ _id: 'lc' + obj.dbNodeKey, type: 'lastconnect', domain: domain.id, time: obj.connectTime, addr: obj.remoteaddrport });
// See if this node exists in the database
if (nodes.length == 0) {
// This device does not exist, use the meshid given by the device
// See if this mesh exists, if it does not we may want to create it.
var mesh = getMeshAutoCreate();
// Check if the mesh exists
if (mesh == null) {
// If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
return;
}
// Check if the mesh is the right type
if (mesh.mtype != 2) {
// If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
return;
}
// Mark when this device connected
obj.connectTime = Date.now();
obj.db.Set({ _id: 'lc' + obj.dbNodeKey, type: 'lastconnect', domain: domain.id, time: obj.connectTime, addr: obj.remoteaddrport });
// This node does not exist, create it.
device = { type: 'node', mtype: mesh.mtype, _id: obj.dbNodeKey, icon: obj.agentInfo.platformType, meshid: obj.dbMeshKey, name: obj.agentInfo.computerName, rname: obj.agentInfo.computerName, domain: domain.id, agent: { ver: obj.agentInfo.agentVersion, id: obj.agentInfo.agentId, caps: obj.agentInfo.capabilities }, host: null };
obj.db.Set(device);
@ -407,15 +484,42 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
obj.parent.parent.DispatchEvent(['*', obj.dbMeshKey], obj, { etype: 'node', action: 'addnode', node: device, msg: ('Added device ' + obj.agentInfo.computerName + ' to mesh ' + mesh.name), domain: domain.id });
}
} else {
// Device already exists, look if changes has occured
device = nodes[0];
// This device exists, meshid given by the device must be ignored, use the server side one.
if (device.meshid != obj.dbMeshKey) {
obj.dbMeshKey = device.meshid;
obj.meshid = device.meshid.split('/')[2];
}
// See if this mesh exists, if it does not we may want to create it.
var mesh = getMeshAutoCreate();
// Check if the mesh exists
if (mesh == null) {
// If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
return;
}
// Check if the mesh is the right type
if (mesh.mtype != 2) {
// If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
return;
}
// Mark when this device connected
obj.connectTime = Date.now();
obj.db.Set({ _id: 'lc' + obj.dbNodeKey, type: 'lastconnect', domain: domain.id, time: obj.connectTime, addr: obj.remoteaddrport });
// Device already exists, look if changes has occured
var changes = [], change = 0, log = 0;
if (device.agent == null) { device.agent = { ver: obj.agentInfo.agentVersion, id: obj.agentInfo.agentId, caps: obj.agentInfo.capabilities }; change = 1; }
if (device.rname != obj.agentInfo.computerName) { device.rname = obj.agentInfo.computerName; change = 1; changes.push('computer name'); }
if (device.agent.ver != obj.agentInfo.agentVersion) { device.agent.ver = obj.agentInfo.agentVersion; change = 1; changes.push('agent version'); }
if (device.agent.id != obj.agentInfo.agentId) { device.agent.id = obj.agentInfo.agentId; change = 1; changes.push('agent type'); }
if ((device.agent.caps & 24) != (obj.agentInfo.capabilities & 24)) { device.agent.caps = obj.agentInfo.capabilities; change = 1; changes.push('agent capabilities'); } // If agent console or javascript support changes, update capabilities
if (device.meshid != obj.dbMeshKey) { obj.dbMeshKey = device.meshid; obj.meshid = device.meshid.split('/')[2]; } // If the mesh does not match, the server mesh is the correct one. This is because we allow the server to change the mesh of a device server-side.
if (change == 1) {
obj.db.Set(device);

View File

@ -642,6 +642,22 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
obj.parent.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: deluserid, username: deluser.name, action: 'accountremove', msg: 'Account removed', domain: domain.id });
obj.parent.parent.DispatchEvent([deluserid], obj, 'close');
break;
}
case 'userbroadcast':
{
// Broadcast a message to all currently connected users.
if ((user.siteadmin & 2) == 0) break;
if (obj.common.validateUsername(command.msg, 1, 256) == false) break; // Notification message is between 1 and 256 characters
// Create the notification message
var notification = { "action": "msg", "type": "notify", "value": command.msg };
// Send the notification on all user sessions for this server
for (var i in obj.parent.wssessions2) { try { obj.parent.wssessions2[i].send(JSON.stringify(notification)); } catch (ex) { } }
// TODO: Notify all sessions on other peers.
break;
}
case 'adduser':
@ -879,7 +895,6 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
// We only create Agent-less Intel AMT mesh (Type1), or Agent mesh (Type2)
if ((command.meshtype == 1) || (command.meshtype == 2)) {
// Create a type 1 agent-less Intel AMT mesh.
obj.parent.crypto.randomBytes(48, function (err, buf) {
meshid = 'mesh/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
var links = {};
@ -1674,17 +1689,25 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
// Check is 2-step login is supported
const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true));
if ((twoStepLoginSupported == false) || (typeof command.otp != 'string')) break;
if ((twoStepLoginSupported == false) || (typeof command.otp != 'string')) {
ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: false, name: command.name }));
break;
}
// Check if Yubikey support is present
if ((typeof domain.yubikey != 'object') || (typeof domain.yubikey.id != 'string') || (typeof domain.yubikey.secret != 'string')) break;
// Check if Yubikey support is present or OTP no exactly 44 in length
if ((typeof domain.yubikey != 'object') || (typeof domain.yubikey.id != 'string') || (typeof domain.yubikey.secret != 'string') || (command.otp.length != 44)) {
ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: false, name: command.name }));
break;
}
// TODO: Check if command.otp is modhex encoded, reject if not.
// Query the YubiKey server to validate the OTP
var yubikeyotp = require('yubikeyotp');
var request = { otp: command.otp, id: domain.yubikey.id, key: domain.yubikey.secret, timestamp: true }
if (domain.yubikey.proxy) { request.requestParams = { proxy: domain.yubikey.proxy }; }
yubikeyotp.verifyOTP(request, function (err, results) {
if (results.status == 'OK') {
if ((results != null) && (results.status == 'OK')) {
var keyIndex = obj.parent.crypto.randomBytes(4).readUInt32BE(0);
var keyId = command.otp.substring(0, 12);
if (user.otphkeys == null) { user.otphkeys = []; }

View File

@ -1,6 +1,6 @@
{
"name": "meshcentral",
"version": "0.2.8-d",
"version": "0.2.8-g",
"keywords": [
"Remote Management",
"Intel AMT",

File diff suppressed because one or more lines are too long

View File

@ -299,9 +299,14 @@
<tr>
<td class=h1></td>
<td class=style14>
&nbsp;
<input type=button onclick=showCreateNewAccountDialog() value="New Account..." />&nbsp;
<input id=UserSearchInput type=text style=width:120px placeholder=Filter onchange=onUserSearchInputChanged() onkeyup=onUserSearchInputChanged() autocomplete=off onfocus=onUserSearchFocus(1) onblur=onUserSearchFocus(0) />&nbsp;
<div style="float:right">
<input type=button onclick=showUserBroadcastDialog() value="Broadcast" />&nbsp;
</div>
<div>
&nbsp;
<input type=button onclick=showCreateNewAccountDialog() value="New Account..." />&nbsp;
<input id=UserSearchInput type=text style=width:120px placeholder=Filter onchange=onUserSearchInputChanged() onkeyup=onUserSearchInputChanged() autocomplete=off onfocus=onUserSearchFocus(1) onblur=onUserSearchFocus(0) />&nbsp;
</div>
</td>
<td class=h2></td>
</tr>
@ -6349,6 +6354,17 @@
return false;
}
function showUserBroadcastDialog() {
if (xxdialogMode) return;
var x = 'Broadcast a message to all connected users.<textarea id=broadcastMessage value="" style=width:370px;height:100px;resize:none maxlength=256 /></textarea>';
setDialogMode(2, "Broadcast Message", 3, showUserBroadcastDialogEx, x);
Q('broadcastMessage').focus();
}
function showUserBroadcastDialogEx() {
meshserver.send({ action: 'userbroadcast', msg: Q('broadcastMessage').value });
}
function showCreateNewAccountDialog() {
if (xxdialogMode) return;
var x = '';

View File

@ -1076,10 +1076,20 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
*/
}
// Return true if it looks like we are using a real TLS certificate.
function isTrustedCert() {
if (obj.args.notls == true) return false; // We are not using TLS, so not trusted cert.
if (obj.args.tlsoffload != null) return true; // We are using TLS offload, a real cert is likely used.
if (obj.parent.config.letsencrypt != null) return true; // We are using Let's Encrypt, real cert in use.
if (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0) return false; // Our cert is issued by self-signed cert.
if (obj.certificates.CommonName == 'un-configured') return false; // Out cert is named with a fake name
return true; // This is a guess
}
// Get the link to the root certificate if needed
function getRootCertLink() {
// Check if the HTTPS certificate is issued from MeshCentralRoot, if so, add download link to root certificate.
if ((obj.args.notls == null) && (obj.tlsSniCredentials == null) && (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0) && (obj.certificates.CommonName != 'un-configured')) { return '<a href=/MeshServerRootCert.cer title="Download the root certificate for this server">Root Certificate</a>'; }
if ((obj.args.notls == null) && (obj.args.tlsoffload == null) && (obj.parent.config.letsencrypt == null) && (obj.tlsSniCredentials == null) && (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0) && (obj.certificates.CommonName != 'un-configured')) { return '<a href=/MeshServerRootCert.cer title="Download the root certificate for this server">Root Certificate</a>'; }
return '';
}
@ -2193,18 +2203,19 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// 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'
/*
var headers = null;
if (obj.args.notls) {
if (isTrustedCert() == false) {
// Default headers if no TLS is used
headers = { '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'" };
//headers = { '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
headers = { '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'" };
//headers = { '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'" };
// Set Strict-Transport-Security if we are using a trusted certificate or TLS offload.
headers = { 'Strict-Transport-Security': 'max-age=31536000;includeSubDomains' };
}
if (parent.config.settings.accesscontrolalloworigin != null) { headers['Access-Control-Allow-Origin'] = parent.config.settings.accesscontrolalloworigin; }
res.set(headers);
*/
return next();
}
});