diff --git a/MeshCentralServer.njsproj b/MeshCentralServer.njsproj index 458e9bff..50b23648 100644 --- a/MeshCentralServer.njsproj +++ b/MeshCentralServer.njsproj @@ -260,6 +260,7 @@ + diff --git a/agents/MeshService-signed.exe b/agents/MeshService-signed.exe index 2589ba3d..9323abaa 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 15e20346..b0f08bf8 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 263a6417..6070eaa2 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 dae399b1..38cf75b2 100644 Binary files a/agents/MeshService64.exe and b/agents/MeshService64.exe differ diff --git a/meshcentral.js b/meshcentral.js index 2699dcbb..6122a7ec 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -1549,8 +1549,14 @@ function CreateMeshCentralServer(config, args) { if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug(1, 'ERR: Bad cookie due to invalid time'); return null; } o.time = o.time * 1000; // Decode the cookie creation time o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds) - if (timeout == null) { timeout = 2; } - if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug(1, 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right) + if ((o.expire) == null || (typeof o.expire != 'number')) { + // Use a fixed cookie expire time + if (timeout == null) { timeout = 2; } + if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug(1, 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right) + } else { + // An expire time is included in the cookie (in minutes), use this. + if ((o.dtime > (o.expire * 60000)) || (o.dtime < -30000)) { obj.debug(1, 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right) + } return o; } catch (ex) { obj.debug(1, 'ERR: Bad AESGCM cookie due to exception: ' + ex); return null; } }; @@ -1571,8 +1577,14 @@ function CreateMeshCentralServer(config, args) { if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug(1, 'ERR: Bad cookie due to invalid time'); return null; } o.time = o.time * 1000; // Decode the cookie creation time o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds) - if (timeout == null) { timeout = 2; } - if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug(1, 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right) + if ((o.expire) == null || (typeof o.expire != 'number')) { + // Use a fixed cookie expire time + if (timeout == null) { timeout = 2; } + if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug(1, 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right) + } else { + // An expire time is included in the cookie (in minutes), use this. + if ((o.dtime > (o.expire * 60000)) || (o.dtime < -30000)) { obj.debug(1, 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right) + } return o; } catch (ex) { obj.debug(1, 'ERR: Bad AESSHA cookie due to exception: ' + ex); return null; } }; diff --git a/meshuser.js b/meshuser.js index 9b80a399..75cefc72 100644 --- a/meshuser.js +++ b/meshuser.js @@ -2499,6 +2499,17 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use break; } + case 'createInviteLink': { + if (common.validateString(command.meshid, 8, 128) == false) break; // Check the meshid + if (common.validateInt(command.expire, 1, 99999) == false) break; // Check the expire time in hours + if (common.validateInt(command.flags, 0, 256) == false) break; // Check the flags + var mesh = parent.meshes[command.meshid]; + if (mesh == null) break; + const inviteCookie = parent.parent.encodeCookie({ a: 4, mid: command.meshid, f: command.flags, expire: command.expire * 60 }, parent.parent.loginCookieEncryptionKey); + if (inviteCookie == null) break; + ws.send(JSON.stringify({ action: 'createInviteLink', meshid: command.meshid, expire: command.expire, cookie: inviteCookie })); + break; + } default: { // Unknown user action console.log('Unknown action from user ' + user.name + ': ' + command.action + '.'); diff --git a/package.json b/package.json index 40b5c408..19eb02a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "0.3.5-v", + "version": "0.3.5-x", "keywords": [ "Remote Management", "Intel AMT", diff --git a/public/images/macosagent.png b/public/images/macosagent.png new file mode 100644 index 00000000..bd5cc5b2 Binary files /dev/null and b/public/images/macosagent.png differ diff --git a/public/images/winagent.png b/public/images/winagent.png new file mode 100644 index 00000000..a5bcbad0 Binary files /dev/null and b/public/images/winagent.png differ diff --git a/views/agentinvite.handlebars b/views/agentinvite.handlebars new file mode 100644 index 00000000..307361b6 --- /dev/null +++ b/views/agentinvite.handlebars @@ -0,0 +1,293 @@ + + + + + + + + + + + MeshCentral - Agent Installation + + + +
+ +
+
+ {{{title}}} +
+
+ {{{title2}}} +
+

{{{logoutControl}}}

+
+
+
+
+
+
+ ♦ + +
+
+
+

Agent Installation

+

+ You have been invited to install a software that will allow a remote operator to fully access your computer remotely including the desktop and files. + Only follow the instructions below if this invitation was expected and you know who will be accessing your computer. + Selecting your operation system and follow the instructions below. +

+
+
+ + + + +
+ +
+

Microsoft™ Windows 64bit

+

Download the software here, run it and press "Install" or "Connect".

+
+ +
+
+ +
+

Microsoft™ Windows 32bit

+

Download the software here, run it and press "Install" or "Connect".

+
+ +
+
+ +
+

Linux

+

To install, cut and paste the following command in a root terminal.

+
+ +

To uninstall, cut and paste the following command as root.

+
+ +

+
+ +
+

Apple™ MacOS

+

Download the installer here, right click on it and select "Open", then follow the instructions.

+
+ +
+
+
+
+ +
+ + + diff --git a/views/default.handlebars b/views/default.handlebars index 7c3268e3..622a7fea 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -1977,6 +1977,28 @@ } break; } + case 'createInviteLink': { // Agent installation invitation link + if (xxdialogTag != message.meshid) break; + var servername = serverinfo.name; + if ((servername.indexOf('.') == -1) || ((features & 2) != 0)) { servername = window.location.hostname; } // If the server name is not set or it's in LAN-only mode, use the URL hostname as server name. + var domainUrlNoSlash = domainUrl.substring(0, domainUrl.length - 1); + var url; + if (serverinfo.https == true) { + var portStr = (serverinfo.port == 443) ? '' : (":" + serverinfo.port); + url = "https://" + servername + portStr + domainUrl + "agentinvite?c=" + message.cookie; + } else { + var portStr = (serverinfo.port == 80) ? '' : (":" + serverinfo.port); + url = "http://" + servername + portStr + domainUrl + "agentinvite?c=" + message.cookie; + } + Q('agentInvitationLink').href = url; + var t = message.expire + ' hour' + addLetterS(message.expire); + if (message.expire == 24) { t = '1 day'; } + if (message.expire == 168) { t = '1 week'; } + if (message.expire == 5040) { t = '1 month'; } + QH('agentInvitationLink', 'Invitation Link (' + t + ')'); + QV('agentInvitationLinkDiv', true); + break; + } case 'stopped': { // Server is stopping. // Disconnect autoReconnect = false; @@ -2547,9 +2569,7 @@ } if (mesh.mtype == 2) { r += ' Add Agent'; - if (features & 64) { - r += ' Invite'; - } + r += ' Invite'; } return r; } @@ -2673,23 +2693,51 @@ function inviteAgentToMesh(meshid) { if (xxdialogMode) return; - var mesh = meshes[meshid]; - var x = "Invite someone to install the mesh agent. An email with be sent with the link to the mesh agent installation for " + EscapeHtml(mesh.name) + ".

"; - x += addHtmlValue('Name (optional)', ''); - x += addHtmlValue('Email', ''); - x += addHtmlValue('Operating System', ''); - x += addHtmlValue('Installation Type', ''); - x += addHtmlValue('Message
(optional)', ''); + var x = '', mesh = meshes[meshid]; + if (features & 64) { + x += addHtmlValue('Invitation Type', '') + "
"; + x += "'; + } + x += '
Invite someone to install the mesh agent by sharing a invitation link. This link points the user to installation instructions for the \"' + EscapeHtml(mesh.name) + '\" device group. The link is public and no account this server is needed.

'; + x += addHtmlValue('Link Expiration', ''); + x += '
'; setDialogMode(2, "Invite", 3, performAgentInvite, x, meshid); validateAgentInvite(); + d2RequestInvitationLink(); + } + + function d2RequestInvitationLink() { + meshserver.send({ action: 'createInviteLink', meshid: xxdialogTag, expire: parseInt(Q('d2inviteExpire').value), flags: 0 }); + } + + function d2ChangedInviteType() { + QV('urlInviteDiv', Q('d2InviteType').value == 0); + if (features & 64) { QV('emailInviteDiv', Q('d2InviteType').value == 1); } + validateAgentInvite(); } + function d2CopyInviteToClip() { navigator.clipboard.writeText(Q('agentInvitationLink').href); } + function validateAgentInvite() { - QE('idx_dlgOkButton', checkEmail(Q('agentInviteEmail').value)); + if ((features & 64) && (Q('d2InviteType').value == 1)) { + QE('idx_dlgOkButton', checkEmail(Q('agentInviteEmail').value)); + QV('idx_dlgCancelButton', true); + } else { + QE('idx_dlgOkButton', true); + QV('idx_dlgCancelButton', false); + } } function performAgentInvite(button, meshid) { - meshserver.send({ action: 'inviteAgent', meshid: meshid, email: Q('agentInviteEmail').value, name: Q('agentInviteName').value, os: Q('agentInviteNameOs').value, flags: Q('agentInviteType').value, msg: Q('agentInviteMessage').value }); + if ((features & 64) && (Q('d2InviteType').value == 1)) { + meshserver.send({ action: 'inviteAgent', meshid: meshid, email: Q('agentInviteEmail').value, name: Q('agentInviteName').value, os: Q('agentInviteNameOs').value, flags: Q('agentInviteType').value, msg: Q('agentInviteMessage').value }); + } } function addAgentToMesh(meshid) { @@ -6196,9 +6244,7 @@ } if (currentMesh.mtype == 2) { x += ' Install'; - if (features & 64) { - x += ' Invite'; - } + x += ' Invite'; } } diff --git a/webserver.js b/webserver.js index 79fa4363..6cdef490 100644 --- a/webserver.js +++ b/webserver.js @@ -1063,6 +1063,30 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } } + // Called to process an agent invite request + function handleAgentInviteRequest(req, res) { + const domain = checkUserIpAddress(req, res); + if ((domain == null) || ((req.query.m == null) && (req.query.c == null))) { res.sendStatus(404); return; } + if (req.query.c != null) { + // A cookie is specified in the query string, use that + var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey); + if (cookie == null) { res.sendStatus(404); return; } + var mesh = obj.meshes[cookie.mid]; + if (mesh == null) { res.sendStatus(404); return; } + var installflags = cookie.f; + if (typeof installflags != 'number') { installflags = 0; } + res.render(obj.path.join(obj.parent.webViewsPath, 'agentinvite'), { title: domain.title, title2: domain.title2, domainurl: domain.url, meshid: mesh._id.split('/')[2], serverport: ((args.aliasport != null) ? args.aliasport : args.port), serverhttps: ((args.notls == true) ? '0' : '1'), servernoproxy: ((domain.agentnoproxy === true) ? '1' : '0'), meshname: encodeURIComponent(mesh.name), installflags: installflags }); + } else if (req.query.m != null) { + // The MeshId is specified in the query string, use that + var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.m.toLowerCase()]; + if (mesh == null) { res.sendStatus(404); return; } + var installflags = 0; + if (req.query.f) { installflags = parseInt(req.query.f); } + if (typeof installflags != 'number') { installflags = 0; } + res.render(obj.path.join(obj.parent.webViewsPath, 'agentinvite'), { title: domain.title, title2: domain.title2, domainurl: domain.url, meshid: mesh._id.split('/')[2], serverport: ((args.aliasport != null) ? args.aliasport : args.port), serverhttps: ((args.notls == true) ? '0' : '1'), servernoproxy: ((domain.agentnoproxy === true) ? '1' : '0'), meshname: encodeURIComponent(mesh.name), installflags: installflags }); + } + } + function handleDeleteAccountRequest(req, res) { const domain = checkUserIpAddress(req, res); if ((domain == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap')) { res.sendStatus(404); return; } @@ -2670,6 +2694,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { obj.app.post(url + 'resetpassword', handleResetPasswordRequest); obj.app.post(url + 'resetaccount', handleResetAccountRequest); obj.app.get(url + 'checkmail', handleCheckMailRequest); + obj.app.get(url + 'agentinvite', handleAgentInviteRequest); obj.app.post(url + 'amtevents.ashx', obj.handleAmtEventRequest); obj.app.get(url + 'meshagents', obj.handleMeshAgentRequest); obj.app.get(url + 'messenger', handleMessengerRequest);