diff --git a/agents/MeshCmd-signed.exe b/agents/MeshCmd-signed.exe index 694c8e37..7988938f 100644 Binary files a/agents/MeshCmd-signed.exe and b/agents/MeshCmd-signed.exe differ diff --git a/agents/MeshCmd64-signed.exe b/agents/MeshCmd64-signed.exe index 400486e7..4a3cf412 100644 Binary files a/agents/MeshCmd64-signed.exe and b/agents/MeshCmd64-signed.exe differ diff --git a/agents/MeshService-signed.exe b/agents/MeshService-signed.exe index 8985bb9f..1db6b3b8 100644 Binary files a/agents/MeshService-signed.exe and b/agents/MeshService-signed.exe differ diff --git a/agents/MeshService.exe b/agents/MeshService.exe index fc808b22..1db6b3b8 100644 Binary files a/agents/MeshService.exe and b/agents/MeshService.exe differ diff --git a/agents/MeshService64-signed.exe b/agents/MeshService64-signed.exe index e68f08e3..b5998af5 100644 Binary files a/agents/MeshService64-signed.exe and b/agents/MeshService64-signed.exe differ diff --git a/agents/MeshService64.exe b/agents/MeshService64.exe index f64dbb33..9b64be44 100644 Binary files a/agents/MeshService64.exe and b/agents/MeshService64.exe differ diff --git a/meshagent.js b/meshagent.js index 862873e5..117378ee 100644 --- a/meshagent.js +++ b/meshagent.js @@ -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); diff --git a/meshuser.js b/meshuser.js index 45586a05..5419190f 100644 --- a/meshuser.js +++ b/meshuser.js @@ -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 = []; } diff --git a/package.json b/package.json index e1a72bc6..7befb4f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "0.2.8-d", + "version": "0.2.8-g", "keywords": [ "Remote Management", "Intel AMT", diff --git a/views/default-min.handlebars b/views/default-min.handlebars index ad81ff85..d98f98eb 100644 --- a/views/default-min.handlebars +++ b/views/default-min.handlebars @@ -1 +1 @@ - MeshCentral
{{{title}}}
{{{title2}}}

{{{logoutControl}}}

 

\ No newline at end of file + MeshCentral
{{{title}}}
{{{title2}}}

{{{logoutControl}}}

 

\ No newline at end of file diff --git a/views/default.handlebars b/views/default.handlebars index 70b95108..9c698ea1 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -299,9 +299,14 @@ -   -   -   +
+   +
+
+   +   +   +
@@ -6349,6 +6354,17 @@ return false; } + function showUserBroadcastDialog() { + if (xxdialogMode) return; + var x = 'Broadcast a message to all connected users.'; + 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 = ''; diff --git a/webserver.js b/webserver.js index a32857f8..05dc4e4f 100644 --- a/webserver.js +++ b/webserver.js @@ -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 'Root Certificate'; } + 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 'Root Certificate'; } 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(); } });