Merge branch 'master_upstream' into Feature/docker

This commit is contained in:
Simon Schön 2022-07-02 20:12:21 +02:00
commit 00f3e816b7
No known key found for this signature in database
GPG Key ID: FD1F8B45F65D5B10
65 changed files with 6330 additions and 3792 deletions

75
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,75 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Server Software (please complete the following information):**
- OS: [e.g. Ubuntu]
- Virtualization: [e.g. Docker]
- Network: [e.g. LAN/WAN, reverse proxy, cloudflare, ssl offload, etc...]
- Version: [e.g. 1.0.43]
- Node: [e.g. 18.4.0]
- Browser: [e.g. Google Chrome]
**Remote Device (please complete the following information):**
- Device: [e.g. Laptop]
- OS: [e.g. Windows 10]
- Version: [e.g. 21H2]
- Current Core Version (if known): [**HINT**: Go to a device then `console` Tab then type `info`]
**Additional context**
Add any other context about the problem here.
**Your config.json file**
```
{
"$schema": "http://info.meshcentral.com/downloads/meshcentral-config-schema.json",
"__comment1__": "This is a simple configuration file, all values and sections that start with underscore (_) are ignored. Edit a section and remove the _ in front of the name. Refer to the user's guide for details.",
"__comment2__": "See node_modules/meshcentral/sample-config-advanced.json for a more advanced example.",
"settings": {
"_cert": "myserver.mydomain.com",
"_WANonly": true,
"_LANonly": true,
"_sessionKey": "MyReallySecretPassword1",
"_port": 443,
"_aliasPort": 443,
"_redirPort": 80,
"_redirAliasPort": 80
},
"domains": {
"": {
"_title": "MyServer",
"_title2": "Servername",
"_minify": true,
"_newAccounts": true,
"_userNameIsEmail": true
}
},
"_letsencrypt": {
"__comment__": "Requires NodeJS 8.x or better, Go to https://letsdebug.net/ first before trying Let's Encrypt.",
"email": "myemail@mydomain.com",
"names": "myserver.mydomain.com",
"production": false
}
}
```

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

21
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: Release
on:
push:
branches:
- master
jobs:
build:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Release
uses: justincy/github-action-npm-release@2.0.2
id: release
- name: Print release output
if: ${{ steps.release.outputs.released == 'true' }}
run: echo Release ID ${{ steps.release.outputs.release_id }}

View File

@ -239,6 +239,7 @@
<Compile Include="translate\translate.js" />
<Compile Include="ua-parser.js" />
<Compile Include="webauthn.js" />
<Compile Include="webrelayserver.js" />
<Compile Include="webserver.js" />
<Compile Include="winservice.js" />
<Content Include="agents\codesign.cer" />
@ -261,10 +262,8 @@
<Content Include="agents\MeshCommander-Small.gz" />
<Content Include="agents\meshinstall-initd.sh" />
<Content Include="agents\meshinstall-linux.sh" />
<Content Include="agents\MeshService-signed.exe" />
<Content Include="agents\MeshService.exe" />
<Content Include="agents\MeshService.pdb" />
<Content Include="agents\MeshService64-signed.exe" />
<Content Include="agents\MeshService64.exe" />
<Content Include="agents\MeshService64.pdb" />
<Content Include="agents\modules_meshcore\coretranslations.json" />
@ -698,6 +697,7 @@
<Folder Include="typings\globals\localforage\" />
<Folder Include="typings\globals\lru-cache\" />
<Folder Include="typings\globals\marked\" />
<Folder Include="typings\globals\moment-timezone\" />
<Folder Include="typings\globals\moment\" />
<Folder Include="typings\globals\node-forge\" />
<Folder Include="typings\globals\nodemailer\" />
@ -738,6 +738,7 @@
<TypeScriptCompile Include="typings\globals\localforage\index.d.ts" />
<TypeScriptCompile Include="typings\globals\lru-cache\index.d.ts" />
<TypeScriptCompile Include="typings\globals\marked\index.d.ts" />
<TypeScriptCompile Include="typings\globals\moment-timezone\index.d.ts" />
<TypeScriptCompile Include="typings\globals\moment\index.d.ts" />
<TypeScriptCompile Include="typings\globals\node-forge\index.d.ts" />
<TypeScriptCompile Include="typings\globals\nodemailer\index.d.ts" />

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -7,14 +7,14 @@ MD modules_meshcore_min
"..\..\WebSiteCompiler\bin\Debug\WebSiteCompiler.exe" meshcmd.js
REM del meshcore.min.js
REM %LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node ..\translate\translate.js minify meshcore.js
REM %LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node ..\translate\translate.js minify meshcore.js
REM rename meshcore.js.min meshcore.min.js
REM del meshcmd.min.js
REM %LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node ..\translate\translate.js minify meshcmd.js
REM %LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node ..\translate\translate.js minify meshcmd.js
REM rename meshcmd.js.min meshcmd.min.js
REM Minify the translations
%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node ..\translate\translate.js minify modules_meshcore\coretranslations.json
%LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node ..\translate\translate.js minify modules_meshcore\coretranslations.json
COPY modules_meshcore\coretranslations.json.min modules_meshcore_min\coretranslations.json
DEL modules_meshcore\coretranslations.json.min

View File

@ -1 +1 @@
MeshService-signed.exe hashagents.js > hashagents.json
MeshService.exe hashagents.js > hashagents.json

View File

@ -1,8 +1,8 @@
var fs = require('fs');
var agents = {
'MeshService-signed.exe': 3,
'MeshService64-signed.exe': 4,
'MeshService.exe': 3,
'MeshService64.exe': 4,
'meshagent_x86': 5,
'meshagent_x86-64': 6,
'meshagent_arm': 9,

View File

@ -1,15 +1,15 @@
{
"3": {
"filename": "MeshService-signed.exe",
"filename": "MeshService.exe",
"hash": "C0E5DB22DE5DED510C48141D7CFE4807F98B8205D680F5FC8A5D15950F17A1465E0953B7BFA7FAEED72019E765E1C8E1",
"size": 3686192,
"mtime": "2022-05-03T20:43:54Z"
"size": 3680256,
"mtime": "2022-04-04T17:13:59Z"
},
"4": {
"filename": "MeshService64-signed.exe",
"filename": "MeshService64.exe",
"hash": "47A927806EDB6DFAC2C79467719FADA0F3625010D551C6D0EA6EA7DB99F088C088E70F562416FC1809B014913CFEA7E0",
"size": 3299120,
"mtime": "2022-05-03T20:43:56Z"
"size": 3293184,
"mtime": "2022-03-25T19:04:18Z"
},
"5": {
"filename": "meshagent_x86",

View File

@ -157,6 +157,8 @@ function run(argv) {
if ((typeof args.uuidoutput) == 'string' || args.uuidoutput) { settings.uuidoutput = args.uuidoutput; }
if ((typeof args.desc) == 'string') { settings.desc = args.desc; }
if ((typeof args.dnssuffix) == 'string') { settings.dnssuffix = args.dnssuffix; }
if ((typeof args.create) == 'string') { settings.create = args.create; }
if ((typeof args.delete) == 'string') { settings.delete = args.delete; }
if (args.bindany) { settings.bindany = true; }
if (args.emailtoken) { settings.emailtoken = true; }
if (args.smstoken) { settings.smstoken = true; }
@ -238,8 +240,12 @@ function run(argv) {
console.log('\r\nPossible arguments:\r\n');
console.log(' --json Display all Intel AMT state in JSON format.');
} else if (action == 'amthashes') {
console.log('Amthashes will display all trusted activations hashes for Intel AMT on this computer. The command must be run on a computer with Intel AMT, must run as administrator and the Intel management driver must be installed. These certificates hashes are used by Intel AMT when performing activation into ACM mode. Example usage:\r\n\r\n meshcmd amthashes');
console.log('Amthashes will display all trusted activations hashes for Intel AMT. If the host is not specified, the hashes are read using the local MEI driver is used. These certificates hashes are used by Intel AMT when performing activation into ACM mode. Example usage:\r\n\r\n meshcmd amthashes');
console.log('\r\nPossible arguments:\r\n');
console.log(' --host [hostname] The IP address or DNS name of Intel AMT, 127.0.0.1 is default.');
console.log(' --user [username] The Intel AMT login username, admin is default.');
console.log(' --pass [password] The Intel AMT login password.');
console.log(' --tls Specifies that TLS must be used.');
console.log(' --json Display all Intel AMT hashes in JSON format.');
} else if ((action == 'microlms') || (action == 'lms') || (action == 'amtlms')) {
console.log('Starts MicroLMS on this computer, allowing local access to Intel AMT on TCP ports 16992 and 16993 when applicable. The command must be run on a computer with Intel AMT, must run as administrator and the Intel management driver must be installed. These certificates hashes are used by Intel AMT when performing activation into ACM mode. Example usage:\r\n\r\n meshcmd microlms');
@ -528,23 +534,28 @@ function run(argv) {
return;
});
} else if (settings.action == 'amthashes') {
// Display Intel AMT list of trusted hashes
var amtMeiModule, amtMei, amtHashes = [];
try { amtMeiModule = require('amt-mei'); amtMei = new amtMeiModule(); } catch (ex) { console.log(ex); exit(1); return; }
amtMei.on('error', function (e) { console.log('amthashes error: ' + e); exit(1); return; });
amtMei.getHashHandles(function (handles) {
exitOnCount = handles.length;
for (var i = 0; i < handles.length; ++i) {
this.getCertHashEntry(handles[i], function (result) {
var certState = [];
if (result.isDefault) { certState.push('Default'); }
if (result.isActive) { certState.push('Active'); } else { certState.push('Disabled'); }
amtHashes.push(result);
if (!args.json) { console.log(result.name + ', (' + certState.join(', ') + ')\r\n ' + result.hashAlgorithmStr + ': ' + result.certificateHash); }
if (--exitOnCount == 0) { if (args.json) { console.log(JSON.stringify(amtHashes, null, 2)); } exit(0); }
});
}
});
if (settings.hostname == null) {
// Display Intel AMT list of trusted hashes from the MEI driver
var amtMeiModule, amtMei, amtHashes = [];
try { amtMeiModule = require('amt-mei'); amtMei = new amtMeiModule(); } catch (ex) { console.log(ex); exit(1); return; }
amtMei.on('error', function (e) { console.log('amthashes error: ' + e); exit(1); return; });
amtMei.getHashHandles(function (handles) {
exitOnCount = handles.length;
for (var i = 0; i < handles.length; ++i) {
this.getCertHashEntry(handles[i], function (result) {
var certState = [];
if (result.isDefault) { certState.push('Default'); }
if (result.isActive) { certState.push('Active'); } else { certState.push('Disabled'); }
amtHashes.push(result);
if (!args.json) { console.log(result.name + ', (' + certState.join(', ') + ')\r\n ' + result.hashAlgorithmStr + ': ' + result.certificateHash); }
if (--exitOnCount == 0) { if (args.json) { console.log(JSON.stringify(amtHashes, null, 2)); } exit(0); }
});
}
});
} else {
// We are going to use WSMAN to perform hash operations
performAmtTrustedHashes();
}
} else if (settings.action == 'netinfo') {
// Display network information
var interfaces = require('os').networkInterfaces();
@ -872,6 +883,104 @@ function run(argv) {
}
}
//
// Intel AMT Trusted Hashes
//
function performAmtTrustedHashes() {
// Check the settings
if ((settings.password == null) || (typeof settings.password != 'string') || (settings.password == '')) { console.log('No or invalid \"password\" specified, use --password [password].'); exit(1); return; }
if ((settings.hostname == null) || (typeof settings.hostname != 'string') || (settings.hostname == '')) { settings.hostname = '127.0.0.1'; }
if ((settings.username == null) || (typeof settings.username != 'string') || (settings.username == '')) { settings.username = 'admin'; }
if ((typeof settings.create == 'string')) {
if ((settings.name == null) || (typeof settings.name != 'string') || (settings.name == '')) { console.log('No or invalid \"name\" specified, use --name [name].'); exit(1); return; }
if ((settings.create.length != 32) && (settings.create.length != 40) && (settings.create.length != 64) && (settings.create.length != 96)) { console.log('No or invalid \"create\" hash, must be in HEX format of length 30, 40, 64, 96.'); exit(1); return; }
if (Buffer.from(settings.create, 'hex').toString('hex') != settings.create.toUpperCase()) { console.log('No or invalid \"create\" specified, must be in HEX format.'); exit(1); return; }
settings.create = Buffer.from(settings.create, 'hex').toString('hex');
}
if ((typeof settings.delete == 'string')) {
if ((settings.delete.length != 32) && (settings.delete.length != 40) && (settings.delete.length != 64) && (settings.delete.length != 96)) { console.log('No or invalid \"delete\" hash, must be in HEX format of length 30, 40, 64, 96.'); exit(1); return; }
if (Buffer.from(settings.delete, 'hex').toString('hex') != settings.delete.toUpperCase()) { console.log('No or invalid \"delete\" specified, must be in HEX format.'); exit(1); return; }
settings.delete = Buffer.from(settings.delete, 'hex').toString('hex');
}
// See if MicroLMS needs to be started
if ((settings.hostname == '127.0.0.1') || (settings.hostname.toLowerCase() == 'localhost')) {
settings.noconsole = true; startLms(performAmtTrustedHashesEx);
} else {
performAmtTrustedHashesEx();
}
}
function performAmtTrustedHashesEx(x) {
var transport = require('amt-wsman-duk');
var wsman = require('amt-wsman');
var amt = require('amt');
wsstack = new wsman(transport, settings.hostname, settings.tls ? 16993 : 16992, settings.username, settings.password, settings.tls);
amtstack = new amt(wsstack);
amtstack.BatchEnum(null, ['AMT_ProvisioningCertificateHash'], performAmtTrustedHashesEx2);
}
function performAmtTrustedHashesEx2(stack, name, responses, status) {
if (status != 200) {
console.log('Unable to get trusted hashes, status = ' + status + '.');
} else {
var r = responses['AMT_ProvisioningCertificateHash'].responses;
if (settings.create) {
// Create a new hash entry
var instanceId = null;
for (var i in r) { if (Buffer.from(r[i]['HashData'], 'base64').toString('hex') == settings.create) { instanceId = r[i]['InstanceID']; } }
if (instanceId != null) { console.log('This trusted hash is already present.'); exit(1); return; }
// Setup hash type
var hashtype = -1;
var hash = Buffer.from(settings.create, 'hex');
if (hash.length == 16) { hashtype = 0; } // MD5
if (hash.length == 20) { hashtype = 1; } // SHA1
if (hash.length == 32) { hashtype = 2; } // SHA256
if (hash.length == 48) { hashtype = 3; } // SHA384
if (hashtype == -1) { console.log('Invalid hash type', hash.length); exit(1); return; }
// Setup object instance
var instance = { "Description": settings.name, "Enabled": true, "HashData": hash.toString('base64'), "HashType": hashtype, "IsDefault": false, "InstanceID": '' };
// Perform WSMAN "CREATE" operation.
amtstack.Create('AMT_ProvisioningCertificateHash', instance, function (stack, name, response, status) {
if (status != 200) { console.log('ERROR: Failed to create trusted hash.', status, JSON.stringify(response, null, 2)); } else { console.log('Done.'); }
exit(0);
});
return;
} else if (settings.delete) {
// Delete a hash entry
var instance = null;
for (var i in r) { if (Buffer.from(r[i]['HashData'], 'base64').toString('hex') == settings.delete) { instance = r[i]; } }
if (instance == null) { console.log('This trusted hash not present.'); exit(1); return; }
// Perform WSMAN "DELETE" operation.
amtstack.Delete('AMT_ProvisioningCertificateHash', instance, function (stack, name, response, status) {
if (status != 200) { console.log('ERROR: Failed to delete trusted hash.', status, JSON.stringify(response, null, 2)); } else { console.log('Done.'); }
exit(0);
});
return;
} else if (settings.json) {
// List the hashes in JSON format
console.log(JSON.stringify(r, null, 2));
} else {
// List the hashes
for (var i in r) {
var certState = [];
var hashTypes = ['MD5', 'SHA1', 'SHA256', 'SHA384'];
if (r[i]['IsDefault']) { certState.push('Default'); }
if (r[i]['Enabled']) { certState.push('Active'); } else { certState.push('Disabled'); }
console.log(r[i]['Description'] + ', (' + certState.join(', ') + ')\r\n ' + hashTypes[r[i]['HashType']] + ': ' + Buffer.from(r[i]['HashData'], 'base64').toString('hex'));
}
}
exit(0);
}
}
//
// Intel AMT Agent Presence
//

View File

@ -1365,13 +1365,18 @@ function handleServerCommand(data) {
}
case 'setclip': {
// Set the load clipboard to a user value
if (typeof data.data == 'string') {
if (typeof data.data == 'string')
{
MeshServerLogEx(22, [data.data.length], "Setting clipboard content, " + data.data.length + " byte(s)", data);
if (require('MeshAgent').isService) {
if (process.platform != 'win32') {
if (require('MeshAgent').isService)
{
if (process.platform != 'win32')
{
require('clipboard').dispatchWrite(data.data);
mesh.SendCommand({ action: 'msg', type: 'setclip', sessionid: data.sessionid, success: true });
}
else {
else
{
var clipargs = data.data;
var uid = require('user-sessions').consoleUid();
var user = require('user-sessions').getUsername(uid);
@ -1381,20 +1386,24 @@ function handleServerCommand(data) {
this._dispatcher = require('win-dispatcher').dispatch({ user: user, modules: [{ name: 'clip-dispatch', script: "module.exports = { dispatch: function dispatch(val) { require('clipboard')(val); process.exit(); } };" }], launch: { module: 'clip-dispatch', method: 'dispatch', args: [clipargs] } });
this._dispatcher.parent = this;
//require('events').setFinalizerMetadata.call(this._dispatcher, 'clip-dispatch');
this._dispatcher.on('connection', function (c) {
this._dispatcher.on('connection', function (c)
{
this._c = c;
this._c.root = this.parent;
this._c.on('end', function () {
this._c.on('end', function ()
{
this.root._dispatcher = null;
this.root = null;
mesh.SendCommand({ action: 'msg', type: 'setclip', sessionid: data.sessionid, success: true });
});
});
}
}
else {
else
{
require("clipboard")(data.data);
mesh.SendCommand({ action: 'msg', type: 'setclip', sessionid: data.sessionid, success: true });
} // Set the clipboard
mesh.SendCommand({ action: 'msg', type: 'setclip', sessionid: data.sessionid, success: true });
}
break;
}

View File

@ -7,7 +7,6 @@ var tunnels = {};
var fs = require('fs');
var needStreamFix = (new Date(process.versions.meshAgent) < new Date('2020-01-21 13:27:45.000-08:00'));
try
{
Object.defineProperty(Array.prototype, 'find', {
@ -553,15 +552,15 @@ function agentUpdate_Start(updateurl, updateoptions) {
if (process.platform == 'win32')
{
// Special Processing for Temporary/Console Mode Agents on Windows
var parms = windows_getCommandLine();
if (parms.findIndex(function (val) { return (val.toUpperCase() == 'RUN' || val.toUpperCase() == 'CONNECT');})>=0)
var parms = windows_getCommandLine(); // This uses FFI to fetch the command line parameters that the agent was started with
if (parms.findIndex(function (val) { return (val != null && (val.toUpperCase() == 'RUN' || val.toUpperCase() == 'CONNECT')); }) >= 0)
{
// This is a Temporary/Console Mode Agent
sendConsoleText('This is a temporary/console agent, checking for conflicts with background services...');
// Check to see if our binary conflicts with an installed agent
var agents = _getPotentialServiceNames();
if(_getPotentialServiceNames().length>0)
if (_getPotentialServiceNames().length > 0)
{
sendConsoleText('Self update cannot continue because the installed agent (' + agents[0] + ') conflicts with the currently running Temp/Console agent...', sessionid);
return;
@ -648,11 +647,25 @@ function agentUpdate_Start(updateurl, updateoptions) {
sendAgentMessage('Self Update FAILED because the downloaded agent FAILED hash check (' + agentUpdate_Start._retryCount + '), URL: ' + updateurl, 3);
agentUpdate_Start._selfupdate = null;
try
{
// We are clearing these two properties, becuase some older agents may not cleanup correctly causing problems with the retry
require('https').globalAgent.sockets = {};
require('https').globalAgent.requests = {};
}
catch(z)
{}
if (needStreamFix)
{
sendConsoleText('This is an older agent that may have an httpstream bug. On next retry will try to fetch the update differently...');
needStreamFix = false;
}
if (agentUpdate_Start._retryCount < 4)
{
// Retry the download again
sendConsoleText('Self Update will try again in 60 seconds...', sessionid);
agentUpdate_Start._timeout = setTimeout(agentUpdate_Start, 60000, updateurl, updateoptions);
sendConsoleText('Self Update will try again in 20 seconds...', sessionid);
agentUpdate_Start._timeout = setTimeout(agentUpdate_Start, 20000, updateurl, updateoptions);
}
else
{
@ -708,8 +721,11 @@ function agentUpdate_Start(updateurl, updateoptions) {
}
catch (zz)
{
sendConsoleText('Self Update encountered an error trying to restart service', sessionid);
sendAgentMessage('Self Update encountered an error trying to restart service', 3);
if (zz.toString() != 'waitExit() aborted because thread is exiting')
{
sendConsoleText('Self Update encountered an error trying to restart service', sessionid);
sendAgentMessage('Self Update encountered an error trying to restart service', 3);
}
}
break;
}
@ -900,7 +916,8 @@ function onTunnelControlData(data, ws) {
}
require('MeshAgent').AddCommandHandler(function (data) {
require('MeshAgent').AddCommandHandler(function (data)
{
if (typeof data == 'object') {
// If this is a console command, parse it and call the console handler
switch (data.action) {

View File

@ -1525,55 +1525,55 @@ module.exports.CreateAmtManager = function (parent) {
dev.amtstack.Delete('CIM_WiFiEndpointSettings', { InstanceID: 'Intel(r) AMT:WiFi Endpoint Settings ' + profilesToRemove[i].ElementName }, function (stack, name, responses, status) { }, 0, 1);
}
}
}
// Check the 802.1x client certificate expiration time
// TODO: We are only getting the client cert from the wired 802.1x profile, need to get it for wireless too.
var netAuthClientCert = null;
if (netAuthClientCertInstanceId != null) {
netAuthClientCert = getInstance(responses['AMT_PublicKeyCertificate'].responses, netAuthClientCertInstanceId);
if (netAuthClientCert) {
var cert = null;
try { cert = obj.parent.certificateOperations.forge.pki.certificateFromAsn1(obj.parent.certificateOperations.forge.asn1.fromDer(obj.parent.certificateOperations.forge.util.decode64(netAuthClientCert.X509Certificate))); } catch (ex) { }
if (cert != null) {
const certStart = new Date(cert.validity.notBefore).getTime();
const certEnd = new Date(cert.validity.notAfter).getTime();
const certMidPoint = certStart + ((certEnd - certStart) / 2);
if (Date.now() > certMidPoint) { newNetAuthProfileRequested = true; } // Past mid-point or expired, request a new 802.1x certificate & profile
}
// Check the 802.1x client certificate expiration time
// TODO: We are only getting the client cert from the wired 802.1x profile, need to get it for wireless too.
var netAuthClientCert = null;
if (netAuthClientCertInstanceId != null) {
netAuthClientCert = getInstance(responses['AMT_PublicKeyCertificate'].responses, netAuthClientCertInstanceId);
if (netAuthClientCert) {
var cert = null;
try { cert = obj.parent.certificateOperations.forge.pki.certificateFromAsn1(obj.parent.certificateOperations.forge.asn1.fromDer(obj.parent.certificateOperations.forge.util.decode64(netAuthClientCert.X509Certificate))); } catch (ex) { }
if (cert != null) {
const certStart = new Date(cert.validity.notBefore).getTime();
const certEnd = new Date(cert.validity.notAfter).getTime();
const certMidPoint = certStart + ((certEnd - certStart) / 2);
if (Date.now() > certMidPoint) { newNetAuthProfileRequested = true; } // Past mid-point or expired, request a new 802.1x certificate & profile
}
}
}
// Figure out is there are no changes to 802.1x wired configuration
if ((wiredMatch == 0) && (newNetAuthProfileRequested == false)) { wiredConfig = false; }
// Figure out if there are no changes to 802.1x wired configuration
if ((wiredMatch == 0) && (newNetAuthProfileRequested == false)) { wiredConfig = false; }
// See if we need to ask MeshCentral Satellite for a new 802.1x profile
if (newNetAuthProfileRequested && (typeof srvNetAuthProfile.satellitecredentials == 'string')) {
// Credentials for this 802.1x profile are provided using MeshCentral Satellite
// Send a message to Satellite requesting a 802.1x profile for this device
dev.consoleMsg("Requesting 802.1x credentials for " + netAuthStrings[srvNetAuthProfile.authenticationprotocol] + " from MeshCentral Satellite...");
dev.netAuthSatReqId = Buffer.from(parent.crypto.randomBytes(16), 'binary').toString('base64'); // Generate a crypto-secure request id.
dev.netAuthSatReqData = { domain: domain, wiredConfig: wiredConfig, wirelessConfig: wirelessConfig, devNetAuthProfile: devNetAuthProfile, srvNetAuthProfile: srvNetAuthProfile, profilesToAdd: profilesToAdd, prioritiesInUse: prioritiesInUse, responses: responses, xxCertificates: xxCertificates, xxCertPrivateKeys: xxCertPrivateKeys }
const request = { action: 'satellite', subaction: '802.1x-ProFile-Request', satelliteFlags: 2, nodeid: dev.nodeid, icon: dev.icon, domain: dev.nodeid.split('/')[1], nolog: 1, reqid: dev.netAuthSatReqId, authProtocol: srvNetAuthProfile.authenticationprotocol, devname: dev.name, osname: dev.rname, ver: dev.intelamt.ver };
if (netAuthClientCert != null) { request.cert = netAuthClientCert.X509Certificate; request.certid = netAuthClientCertInstanceId; }
parent.DispatchEvent([srvNetAuthProfile.satellitecredentials], obj, request);
// See if we need to ask MeshCentral Satellite for a new 802.1x profile
if (newNetAuthProfileRequested && (typeof srvNetAuthProfile.satellitecredentials == 'string')) {
// Credentials for this 802.1x profile are provided using MeshCentral Satellite
// Send a message to Satellite requesting a 802.1x profile for this device
dev.consoleMsg("Requesting 802.1x credentials for " + netAuthStrings[srvNetAuthProfile.authenticationprotocol] + " from MeshCentral Satellite...");
dev.netAuthSatReqId = Buffer.from(parent.crypto.randomBytes(16), 'binary').toString('base64'); // Generate a crypto-secure request id.
dev.netAuthSatReqData = { domain: domain, wiredConfig: wiredConfig, wirelessConfig: wirelessConfig, devNetAuthProfile: devNetAuthProfile, srvNetAuthProfile: srvNetAuthProfile, profilesToAdd: profilesToAdd, prioritiesInUse: prioritiesInUse, responses: responses, xxCertificates: xxCertificates, xxCertPrivateKeys: xxCertPrivateKeys }
const request = { action: 'satellite', subaction: '802.1x-ProFile-Request', satelliteFlags: 2, nodeid: dev.nodeid, icon: dev.icon, domain: dev.nodeid.split('/')[1], nolog: 1, reqid: dev.netAuthSatReqId, authProtocol: srvNetAuthProfile.authenticationprotocol, devname: dev.name, osname: dev.rname, ver: dev.intelamt.ver };
if (netAuthClientCert != null) { request.cert = netAuthClientCert.X509Certificate; request.certid = netAuthClientCertInstanceId; }
parent.DispatchEvent([srvNetAuthProfile.satellitecredentials], obj, request);
// Set a response timeout
const netAuthTimeoutFunc = function netAuthTimeout() {
if (isAmtDeviceValid(netAuthTimeout.dev) == false) return; // Device no longer exists, ignore this request.
if (dev.netAuthSatReqId != null) {
delete netAuthTimeout.dev.netAuthSatReqId;
delete netAuthTimeout.dev.netAuthSatReqData;
netAuthTimeout.dev.consoleMsg("MeshCentral Satellite did not respond in time, 802.1x profile will not be set.");
devTaskCompleted(netAuthTimeout.dev);
}
// Set a response timeout
const netAuthTimeoutFunc = function netAuthTimeout() {
if (isAmtDeviceValid(netAuthTimeout.dev) == false) return; // Device no longer exists, ignore this request.
if (dev.netAuthSatReqId != null) {
delete netAuthTimeout.dev.netAuthSatReqId;
delete netAuthTimeout.dev.netAuthSatReqData;
netAuthTimeout.dev.consoleMsg("MeshCentral Satellite did not respond in time, 802.1x profile will not be set.");
devTaskCompleted(netAuthTimeout.dev);
}
netAuthTimeoutFunc.dev = dev;
dev.netAuthSatReqTimer = setTimeout(netAuthTimeoutFunc, 20000);
return;
} else {
// No need to call MeshCentral Satellite for a 802.1x profile, so configure everything now.
attempt8021xSyncEx(dev, { domain: domain, wiredConfig: wiredConfig, wirelessConfig: wirelessConfig, devNetAuthProfile: devNetAuthProfile, srvNetAuthProfile: srvNetAuthProfile, profilesToAdd: profilesToAdd, prioritiesInUse: prioritiesInUse, responses: responses, xxCertificates: xxCertificates, xxCertPrivateKeys: xxCertPrivateKeys });
}
netAuthTimeoutFunc.dev = dev;
dev.netAuthSatReqTimer = setTimeout(netAuthTimeoutFunc, 20000);
return;
} else {
// No need to call MeshCentral Satellite for a 802.1x profile, so configure everything now.
attempt8021xSyncEx(dev, { domain: domain, wiredConfig: wiredConfig, wirelessConfig: wirelessConfig, devNetAuthProfile: devNetAuthProfile, srvNetAuthProfile: srvNetAuthProfile, profilesToAdd: profilesToAdd, prioritiesInUse: prioritiesInUse, responses: responses, xxCertificates: xxCertificates, xxCertPrivateKeys: xxCertPrivateKeys });
}
});
}
@ -1813,31 +1813,34 @@ module.exports.CreateAmtManager = function (parent) {
function attemptWifiSyncEx2(dev, devNetAuthData) {
if (isAmtDeviceValid(dev) == false) return; // Device no longer exists, ignore this request.
const responses = devNetAuthData.responses;
const wirelessConfig = devNetAuthData.wirelessConfig;
// Check if local WIFI profile sync is enabled, if not, enabled it.
if ((responses['AMT_WiFiPortConfigurationService'] != null) && (responses['AMT_WiFiPortConfigurationService'].response != null) && (responses['AMT_WiFiPortConfigurationService'].response['localProfileSynchronizationEnabled'] == 0)) {
responses['AMT_WiFiPortConfigurationService'].response['localProfileSynchronizationEnabled'] = 1;
dev.amtstack.Put('AMT_WiFiPortConfigurationService', responses['AMT_WiFiPortConfigurationService'].response, function (stack, name, response, status) {
if (status != 200) { dev.consoleMsg("Unable to enable local WIFI profile sync."); } else { dev.consoleMsg("Enabled local WIFI profile sync."); }
});
}
if (wirelessConfig) {
// Check if local WIFI profile sync is enabled, if not, enabled it.
if ((responses['AMT_WiFiPortConfigurationService'] != null) && (responses['AMT_WiFiPortConfigurationService'].response != null) && (responses['AMT_WiFiPortConfigurationService'].response['localProfileSynchronizationEnabled'] == 0)) {
responses['AMT_WiFiPortConfigurationService'].response['localProfileSynchronizationEnabled'] = 1;
dev.amtstack.Put('AMT_WiFiPortConfigurationService', responses['AMT_WiFiPortConfigurationService'].response, function (stack, name, response, status) {
if (status != 200) { dev.consoleMsg("Unable to enable local WIFI profile sync."); } else { dev.consoleMsg("Enabled local WIFI profile sync."); }
});
}
// Change the WIFI state if needed. Right now, we always enable it.
// WifiState = { 3: "Disabled", 32768: "Enabled in S0", 32769: "Enabled in S0, Sx/AC" };
var wifiState = 32769; // For now, always enable WIFI
if (responses['CIM_WiFiPort'].responses.Body.EnabledState != 32769) {
if (wifiState == 3) {
dev.amtstack.CIM_WiFiPort_RequestStateChange(wifiState, null, function (stack, name, responses, status) {
const dev = stack.dev;
if (isAmtDeviceValid(dev) == false) return; // Device no longer exists, ignore this request.
if (status == 200) { dev.consoleMsg("Disabled WIFI."); }
});
} else {
dev.amtstack.CIM_WiFiPort_RequestStateChange(wifiState, null, function (stack, name, responses, status) {
const dev = stack.dev;
if (isAmtDeviceValid(dev) == false) return; // Device no longer exists, ignore this request.
if (status == 200) { dev.consoleMsg("Enabled WIFI."); }
});
// Change the WIFI state if needed. Right now, we always enable it.
// WifiState = { 3: "Disabled", 32768: "Enabled in S0", 32769: "Enabled in S0, Sx/AC" };
var wifiState = 32769; // For now, always enable WIFI
if (responses['CIM_WiFiPort'].responses.Body.EnabledState != 32769) {
if (wifiState == 3) {
dev.amtstack.CIM_WiFiPort_RequestStateChange(wifiState, null, function (stack, name, responses, status) {
const dev = stack.dev;
if (isAmtDeviceValid(dev) == false) return; // Device no longer exists, ignore this request.
if (status == 200) { dev.consoleMsg("Disabled WIFI."); }
});
} else {
dev.amtstack.CIM_WiFiPort_RequestStateChange(wifiState, null, function (stack, name, responses, status) {
const dev = stack.dev;
if (isAmtDeviceValid(dev) == false) return; // Device no longer exists, ignore this request.
if (status == 200) { dev.consoleMsg("Enabled WIFI."); }
});
}
}
}
@ -3010,17 +3013,20 @@ module.exports.CreateAmtManager = function (parent) {
function guidToStr(g) { return g.substring(6, 8) + g.substring(4, 6) + g.substring(2, 4) + g.substring(0, 2) + '-' + g.substring(10, 12) + g.substring(8, 10) + '-' + g.substring(14, 16) + g.substring(12, 14) + '-' + g.substring(16, 20) + '-' + g.substring(20); }
// Base64 to string conversion utility functions
function atob(x) { return Buffer.from(x, 'base64').toString('binary'); }
function btoa(x) { return Buffer.from(x, 'binary').toString('base64'); }
// Check which key pair matches the public key in the certificate
function amtcert_linkCertPrivateKey(certs, keys) {
if ((keys == null) || (keys.length == 0)) return;
for (var i in certs) {
var cert = certs[i];
try {
if (keys.length == 0) return;
var b = obj.parent.certificateOperations.forge.asn1.fromDer(cert.X509CertificateBin);
var a = obj.parent.certificateOperations.forge.pki.certificateFromAsn1(b).publicKey;
var publicKeyPEM = obj.parent.certificateOperations.forge.pki.publicKeyToPem(a).substring(28 + 32).replace(/(\r\n|\n|\r)/gm, "");
var publicKeyPEM = obj.parent.certificateOperations.forge.pki.publicKeyToPem(obj.parent.certificateOperations.forge.pki.certificateFromAsn1(obj.parent.certificateOperations.forge.asn1.fromDer(cert.X509CertificateBin)).publicKey).substring(28 + 32).replace(/(\r\n|\n|\r)/gm, "");
publicKeyPEM = publicKeyPEM.substring(0, publicKeyPEM.length - 24); // Remove the PEM footer
for (var j = 0; j < keys.length; j++) {
if (publicKeyPEM === (keys[j]['DERKey'] + '-----END PUBLIC KEY-----')) {
if ((publicKeyPEM === (keys[j]['DERKey'])) || (publicKeyPEM == btoa(atob(keys[j]['DERKey']).substring(24)))) { // Match directly or, new version of Intel AMT put the key type OID in the private key, skip that and match.
keys[j].XCert = cert; // Link the key pair to the certificate
cert.XPrivateKey = keys[j]; // Link the certificate to the key pair
}

View File

@ -13,13 +13,13 @@
/*jshint esversion: 6 */
"use strict";
/*
Protocol numbers
10 = RDP
11 = SSH-TERM
12 = VNC
13 - SSH-FILES
13 = SSH-FILES
14 = Web-TCP
*/
// Protocol Numbers
@ -58,6 +58,604 @@ const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288
const MESHRIGHT_DEVICEDETAILS = 0x00100000; // 1048576
const MESHRIGHT_ADMIN = 0xFFFFFFFF;
// SerialTunnel object is used to embed TLS within another connection.
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;
}
// Construct a Web relay object
module.exports.CreateWebRelaySession = function (parent, db, req, args, domain, userid, nodeid, addr, port, appid) {
const obj = {};
obj.parent = parent;
obj.lastOperation = Date.now();
obj.domain = domain;
obj.userid = userid;
obj.nodeid = nodeid;
obj.addr = addr;
obj.port = port;
obj.appid = appid;
var pendingRequests = [];
var nextTunnelId = 1;
var tunnels = {};
var errorCount = 0; // If we keep closing tunnels without processing requests, fail the requests
// Any HTTP cookie set by the device is going to be shared between all tunnels to that device.
obj.webCookies = {};
// Events
obj.closed = false;
obj.onclose = null;
// Check if any tunnels need to be cleaned up
obj.checkTimeout = function () {
const limit = Date.now() - (1 * 60 * 1000); // This is is 5 minutes before current time
// Close any old non-websocket tunnels
const tunnelToRemove = [];
for (var i in tunnels) { if ((tunnels[i].lastOperation < limit) && (tunnels[i].isWebSocket !== true)) { tunnelToRemove.push(tunnels[i]); } }
for (var i in tunnelToRemove) { tunnelToRemove[i].close(); }
// Close this session if no longer used
if (obj.lastOperation < limit) {
var count = 0;
for (var i in tunnels) { count++; }
if (count == 0) { close(); } // Time limit reached and no tunnels, clean up.
}
}
// Handle new HTTP request
obj.handleRequest = function (req, res) {
pendingRequests.push([req, res, false]);
handleNextRequest();
}
// Handle new websocket request
obj.handleWebSocket = function (ws, req) {
pendingRequests.push([req, ws, true]);
handleNextRequest();
}
// Handle request
function handleNextRequest() {
// if there are not pending requests, do nothing
if (pendingRequests.length == 0) return;
// If the errorCount is high, something is really wrong, we are opening lots of tunnels and not processing any requests.
if (errorCount > 5) { close(); return; }
// Check to see if any of the tunnels are free
var count = 0;
for (var i in tunnels) {
count += (tunnels[i].isWebSocket ? 0 : 1);
if ((tunnels[i].relayActive == true) && (tunnels[i].res == null) && (tunnels[i].isWebSocket == false)) {
// Found a free tunnel, use it
const x = pendingRequests.shift();
if (x[2] == true) { tunnels[i].processWebSocket(x[0], x[1]); } else { tunnels[i].processRequest(x[0], x[1]); }
return;
}
}
if (count > 0) return;
launchNewTunnel();
}
function launchNewTunnel() {
// Launch a new tunnel
const tunnel = module.exports.CreateWebRelay(obj, db, args, domain);
tunnel.onclose = function (tunnelId, processedCount) {
if (processedCount == 0) { errorCount++; } // If this tunnel closed without processing any requests, mark this as an error
delete tunnels[tunnelId];
handleNextRequest();
}
tunnel.onconnect = function (tunnelId) {
if (pendingRequests.length > 0) {
const x = pendingRequests.shift();
if (x[2] == true) { tunnels[tunnelId].processWebSocket(x[0], x[1]); } else { tunnels[tunnelId].processRequest(x[0], x[1]); }
}
}
tunnel.oncompleted = function (tunnelId) {
errorCount = 0; // Something got completed, clear any error count
if (pendingRequests.length > 0) {
const x = pendingRequests.shift();
if (x[2] == true) { tunnels[tunnelId].processWebSocket(x[0], x[1]); } else { tunnels[tunnelId].processRequest(x[0], x[1]); }
}
}
tunnel.connect(userid, nodeid, addr, port, appid);
tunnel.tunnelId = nextTunnelId++;
tunnels[tunnel.tunnelId] = tunnel;
}
// Close all tunnels
function close() {
// Set the session as closed
if (obj.closed == true) return;
obj.closed = true;
// Close all tunnels
for (var i in tunnels) { tunnels[i].close(); }
tunnels = null;
// Close any pending requests
for (var i in pendingRequests) { if (pendingRequests[i][2] == true) { pendingRequests[i][1].close(); } else { pendingRequests[i][1].end(); } }
// Notify of session closure
if (obj.onclose) { obj.onclose(obj.userid + '/' + obj.sessionId); }
// Cleanup
delete obj.userid;
delete obj.lastOperation;
}
return obj;
}
// Construct a Web relay object
module.exports.CreateWebRelay = function (parent, db, args, domain) {
//const Net = require('net');
const WebSocket = require('ws')
const obj = {};
obj.lastOperation = Date.now();
obj.relayActive = false;
obj.closed = false;
obj.isWebSocket = false;
obj.processedRequestCount = 0;
const constants = (require('crypto').constants ? require('crypto').constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
// Events
obj.onclose = null;
obj.oncompleted = null;
obj.onconnect = null;
// Process a HTTP request
obj.processRequest = function (req, res) {
if (obj.relayActive == false) { console.log("ERROR: Attempt to use an unconnected tunnel"); return false; }
parent.lastOperation = obj.lastOperation = Date.now();
// Construct the HTTP request
var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n';
const blockedHeaders = ['origin', 'cookie']; // These are headers we do not forward
for (var i in req.headers) { if (blockedHeaders.indexOf(i) == -1) { request += i + ': ' + req.headers[i] + '\r\n'; } }
var cookieStr = '';
for (var i in parent.webCookies) { if (cookieStr != '') { cookieStr += '; ' } cookieStr += (i + '=' + parent.webCookies[i].value); }
if (cookieStr.length > 0) { request += 'cookie: ' + cookieStr + '\r\n' } // If we have session cookies, set them in the header here
request += '\r\n';
if (req.headers['content-length'] != null) {
// Stream the HTTP request and body, this is a content-length HTTP request, just forward the body data
send(Buffer.from(request));
req.on('data', function (data) { send(data); }); // TODO: Flow control (Not sure how to do this in ExpressJS)
req.on('end', function () { });
} else if (req.headers['transfer-encoding'] != null) {
// Stream the HTTP request and body, this is a chunked encoded HTTP request
// TODO: Flow control (Not sure how to do this in ExpressJS)
send(Buffer.from(request));
req.on('data', function (data) { send(Buffer.concat([Buffer.from(data.length.toString(16) + '\r\n', 'binary'), data, send(Buffer.from('\r\n', 'binary'))])); });
req.on('end', function () { send(Buffer.from('0\r\n\r\n', 'binary')); });
} else {
// Request has no body, send it now
send(Buffer.from(request));
}
obj.res = res;
}
// Process a websocket request
obj.processWebSocket = function (req, ws) {
if (obj.relayActive == false) { console.log("ERROR: Attempt to use an unconnected tunnel"); return false; }
parent.lastOperation = obj.lastOperation = Date.now();
// Mark this tunnel as being a web socket tunnel
obj.isWebSocket = true;
obj.ws = ws;
// Pause the websocket until we get a tunnel connected
obj.ws._socket.pause();
// Remove the trailing '/.websocket' if needed
var baseurl = req.url, i = req.url.indexOf('?');
if (i > 0) { baseurl = req.url.substring(0, i); }
if (baseurl.endsWith('/.websocket')) { req.url = baseurl.substring(0, baseurl.length - 11) + ((i < 1) ? '' : req.url.substring(i)); }
// Construct the HTTP request
var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n';
const blockedHeaders = ['origin', 'cookie', 'sec-websocket-extensions']; // These are headers we do not forward
for (var i in req.headers) { if (blockedHeaders.indexOf(i) == -1) { request += i + ': ' + req.headers[i] + '\r\n'; } }
var cookieStr = '';
for (var i in parent.webCookies) { if (cookieStr != '') { cookieStr += '; ' } cookieStr += (i + '=' + parent.webCookies[i].value); }
if (cookieStr.length > 0) { request += 'cookie: ' + cookieStr + '\r\n' } // If we have session cookies, set them in the header here
request += '\r\n';
send(Buffer.from(request));
// Hook up the websocket events
obj.ws.on('message', function (data) {
// Setup opcode and payload
var op = 2, payload = data;
if (typeof data == 'string') { op = 1; payload = Buffer.from(data, 'binary'); } // Text frame
sendWebSocketFrameToDevice(op, payload);
});
obj.ws.on('ping', function (data) { sendWebSocketFrameToDevice(9, data); }); // Forward ping frame
obj.ws.on('pong', function (data) { sendWebSocketFrameToDevice(10, data); }); // Forward pong frame
obj.ws.on('close', function () { obj.close(); });
obj.ws.on('error', function (err) { obj.close(); });
}
function sendWebSocketFrameToDevice(op, payload) {
// Select a random mask
const mask = parent.parent.crypto.randomBytes(4)
// Setup header and mask
var header = null;
if (payload.length < 126) {
header = Buffer.alloc(6); // Header (2) + Mask (4)
header[0] = 0x80 + op; // FIN + OP
header[1] = 0x80 + payload.length; // Mask + Length
mask.copy(header, 2, 0, 4); // Copy the mask
} else if (payload.length <= 0xFFFF) {
header = Buffer.alloc(8); // Header (2) + Length (2) + Mask (4)
header[0] = 0x80 + op; // FIN + OP
header[1] = 0x80 + 126; // Mask + 126
header.writeInt16BE(payload.length, 2); // Payload size
mask.copy(header, 4, 0, 4); // Copy the mask
} else {
header = Buffer.alloc(14); // Header (2) + Length (8) + Mask (4)
header[0] = 0x80 + op; // FIN + OP
header[1] = 0x80 + 127; // Mask + 127
header.writeInt32BE(payload.length, 6); // Payload size
mask.copy(header, 10, 0, 4); // Copy the mask
}
// Mask the payload
for (var i = 0; i < payload.length; i++) { payload[i] = (payload[i] ^ mask[i % 4]); }
// Send the frame
//console.log(obj.tunnelId, '-->', op, payload.length);
send(Buffer.concat([header, payload]));
}
// Disconnect
obj.close = function (arg) {
if (obj.closed == true) return;
obj.closed = true;
if (obj.tls) {
try { obj.tls.end(); } catch (ex) { console.log(ex); }
delete obj.tls;
}
/*
// Event the session ending
if ((obj.startTime) && (obj.meshid != null)) {
// Collect how many raw bytes where received and sent.
// We sum both the websocket and TCP client in this case.
var inTraffc = obj.ws._socket.bytesRead, outTraffc = obj.ws._socket.bytesWritten;
if (obj.wsClient != null) { inTraffc += obj.wsClient._socket.bytesRead; outTraffc += obj.wsClient._socket.bytesWritten; }
const sessionSeconds = Math.round((Date.now() - obj.startTime) / 1000);
const user = parent.users[obj.cookie.userid];
const username = (user != null) ? user.name : null;
const event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, userid: obj.cookie.userid, username: username, sessionid: obj.sessionid, msgid: 123, msgArgs: [sessionSeconds, obj.sessionid], msg: "Left Web-SSH session \"" + obj.sessionid + "\" after " + sessionSeconds + " second(s).", protocol: PROTOCOL_WEBSSH, bytesin: inTraffc, bytesout: outTraffc };
parent.DispatchEvent(['*', obj.nodeid, obj.cookie.userid, obj.meshid], obj, event);
delete obj.startTime;
delete obj.sessionid;
}
*/
if (obj.wsClient) {
obj.wsClient.removeAllListeners('open');
obj.wsClient.removeAllListeners('message');
obj.wsClient.removeAllListeners('close');
try { obj.wsClient.close(); } catch (ex) { console.log(ex); }
delete obj.wsClient;
}
// Close any pending request
if (obj.res) { obj.res.end(); delete obj.res; }
if (obj.ws) { obj.ws.close(); delete obj.ws; }
// Event disconnection
if (obj.onclose) { obj.onclose(obj.tunnelId, obj.processedRequestCount); }
obj.relayActive = false;
};
// Start the looppback server
obj.connect = function (userid, nodeid, addr, port, appid) {
if (obj.relayActive || obj.closed) return;
obj.addr = addr;
obj.port = port;
obj.appid = appid;
// Encode a cookie for the mesh relay
const cookieContent = { userid: userid, domainid: domain.id, nodeid: nodeid, tcpport: port };
if (addr != null) { cookieContent.tcpaddr = addr; }
const cookie = parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey);
try {
// Setup the correct URL with domain and use TLS only if needed.
const options = { rejectUnauthorized: false };
const protocol = (args.tlsoffload) ? 'ws' : 'wss';
var domainadd = '';
if ((domain.dns == null) && (domain.id != '')) { domainadd = domain.id + '/' }
const url = protocol + '://localhost:' + args.port + '/' + domainadd + (((obj.mtype == 3) && (obj.relaynodeid == null)) ? 'local' : 'mesh') + 'relay.ashx?p=14&auth=' + cookie; // Protocol 14 is Web-TCP
parent.parent.debug('relay', 'TCP: Connection websocket to ' + url);
obj.wsClient = new WebSocket(url, options);
obj.wsClient.on('open', function () { parent.parent.debug('relay', 'TCP: Relay websocket open'); });
obj.wsClient.on('message', function (data) { // Make sure to handle flow control.
if (obj.tls) {
// WS --> TLS
processRawHttpData(data);
} else if (obj.relayActive == false) {
if ((data == 'c') || (data == 'cr')) {
if (appid == 2) {
// TLS needs to be setup
obj.ser = new SerialTunnel();
obj.ser.forwardwrite = function (data) { if (data.length > 0) { try { obj.wsClient.send(data); } catch (ex) { } } }; // TLS ---> WS
// TLSSocket to encapsulate TLS communication, which then tunneled via SerialTunnel
const tlsoptions = { socket: obj.ser, rejectUnauthorized: false };
obj.tls = require('tls').connect(tlsoptions, function () {
parent.parent.debug('relay', "Web Relay Secure TLS Connection");
obj.relayActive = true;
parent.lastOperation = obj.lastOperation = Date.now(); // Update time of last opertion performed
if (obj.onconnect) { obj.onconnect(obj.tunnelId); } // Event connection
});
obj.tls.setEncoding('binary');
obj.tls.on('error', function (err) { parent.parent.debug('relay', "Web Relay TLS Connection Error", err); obj.close(); });
// Decrypted tunnel from TLS communcation to be forwarded to the browser
obj.tls.on('data', function (data) { processHttpData(data); }); // TLS ---> Browser
} else {
// No TLS needed, tunnel is now active
obj.relayActive = true;
parent.lastOperation = obj.lastOperation = Date.now(); // Update time of last opertion performed
if (obj.onconnect) { obj.onconnect(obj.tunnelId); } // Event connection
}
}
} else {
processRawHttpData(data);
}
});
obj.wsClient.on('close', function () { parent.parent.debug('relay', 'TCP: Relay websocket closed'); obj.close(); });
obj.wsClient.on('error', function (err) { parent.parent.debug('relay', 'TCP: Relay websocket error: ' + err); obj.close(); });
} catch (ex) {
console.log(ex);
}
}
function processRawHttpData(data) {
if (typeof data == 'string') {
// Forward any ping/pong commands to the browser
var cmd = null;
try { cmd = JSON.parse(data); } catch (ex) { }
if ((cmd != null) && (cmd.ctrlChannel == '102938') && (cmd.type == 'ping')) { cmd.type = 'pong'; obj.wsClient.send(JSON.stringify(cmd)); }
return;
}
if (obj.tls) {
// If TLS is in use, WS --> TLS
if (data.length > 0) { try { obj.ser.updateBuffer(data); } catch (ex) { console.log(ex); } }
} else {
// Relay WS --> TCP, event data coming in
processHttpData(data.toString('binary'));
}
}
// Process incoming HTTP data
obj.socketAccumulator = '';
obj.socketParseState = 0;
obj.socketContentLengthRemaining = 0;
function processHttpData(data) {
obj.socketAccumulator += data;
while (true) {
//console.log('ACC(' + obj.socketAccumulator + '): ' + obj.socketAccumulator);
if (obj.socketParseState == 0) {
var headersize = obj.socketAccumulator.indexOf('\r\n\r\n');
if (headersize < 0) return;
//obj.Debug("Header: "+obj.socketAccumulator.substring(0, headersize)); // Display received HTTP header
obj.socketHeader = obj.socketAccumulator.substring(0, headersize).split('\r\n');
obj.socketAccumulator = obj.socketAccumulator.substring(headersize + 4);
obj.socketXHeader = { Directive: obj.socketHeader[0].split(' ') };
for (var i in obj.socketHeader) {
if (i != 0) {
var x2 = obj.socketHeader[i].indexOf(':');
const n = obj.socketHeader[i].substring(0, x2).toLowerCase();
const v = obj.socketHeader[i].substring(x2 + 2);
if (n == 'set-cookie') { // Since "set-cookie" can be present many times in the header, handle it as an array of values
if (obj.socketXHeader[n] == null) { obj.socketXHeader[n] = [v]; } else { obj.socketXHeader[n].push(v); }
} else {
obj.socketXHeader[n] = v;
}
}
}
// Check if this HTTP request has a body
if ((obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'close')) { obj.socketParseState = 1; }
if (obj.socketXHeader['content-length'] != null) { obj.socketParseState = 1; }
if ((obj.socketXHeader['transfer-encoding'] != null) && (obj.socketXHeader['transfer-encoding'].toLowerCase() == 'chunked')) { obj.socketParseState = 1; }
if (obj.isWebSocket) {
if ((obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'upgrade')) {
obj.processedRequestCount++;
obj.socketParseState = 2; // Switch to decoding websocket frames
obj.ws._socket.resume(); // Resume the browser's websocket
} else {
obj.close(); // Failed to upgrade to websocket
}
}
// Forward the HTTP request into the tunnel, if no body is present, close the request.
processHttpResponse(obj.socketXHeader, null, (obj.socketParseState == 0));
}
if (obj.socketParseState == 1) {
var csize = -1;
if ((obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'close')) {
// The body ends with a close, in this case, we will only process the header
processHttpResponse(null, null, true);
csize = 0;
} else if (obj.socketXHeader['content-length'] != null) {
// The body length is specified by the content-length
if (obj.socketContentLengthRemaining == 0) { obj.socketContentLengthRemaining = parseInt(obj.socketXHeader['content-length']); } // Set the remaining content-length if not set
var data = obj.socketAccumulator.substring(0, obj.socketContentLengthRemaining); // Grab the available data, not passed the expected content-length
obj.socketAccumulator = obj.socketAccumulator.substring(data.length); // Remove the data from the accumulator
obj.socketContentLengthRemaining -= data.length; // Substract the obtained data from the expected size
processHttpResponse(null, data, (obj.socketContentLengthRemaining == 0)); // Send any data we have, if we are done, signal the end of the response
if (obj.socketContentLengthRemaining > 0) return; // If more data is needed, return now so we exit the while() loop.
csize = 0; // We are done
}
else if ((obj.socketXHeader['transfer-encoding'] != null) && (obj.socketXHeader['transfer-encoding'].toLowerCase() == 'chunked')) {
// The body is chunked
var clen = obj.socketAccumulator.indexOf('\r\n');
if (clen < 0) { return; } // Chunk length not found, exit now and get more data.
// Chunk length if found, lets see if we can get the data.
csize = parseInt(obj.socketAccumulator.substring(0, clen), 16);
if (obj.socketAccumulator.length < clen + 2 + csize + 2) return;
// We got a chunk with all of the data, handle the chunck now.
var data = obj.socketAccumulator.substring(clen + 2, clen + 2 + csize);
obj.socketAccumulator = obj.socketAccumulator.substring(clen + 2 + csize + 2);
processHttpResponse(null, data, (csize == 0));
}
if (csize == 0) {
//obj.Debug("xxOnSocketData DONE: (" + obj.socketData.length + "): " + obj.socketData);
obj.socketParseState = 0;
obj.socketHeader = null;
}
}
if (obj.socketParseState == 2) {
// We are in websocket pass-thru mode, decode the websocket frame
if (obj.socketAccumulator.length < 2) return; // Need at least 2 bytes to decode a websocket header
//console.log('WebSocket frame', obj.socketAccumulator.length, Buffer.from(obj.socketAccumulator, 'binary'));
// Decode the websocket frame
const buf = Buffer.from(obj.socketAccumulator, 'binary');
const fin = ((buf[0] & 0x80) != 0);
const rsv = ((buf[0] & 0x70) != 0);
const op = buf[0] & 0x0F;
const mask = ((buf[1] & 0x80) != 0);
var len = buf[1] & 0x7F;
//console.log(obj.tunnelId, 'fin: ' + fin + ', rsv: ' + rsv + ', op: ' + op + ', len: ' + len);
// Calculate the total length
var payload = null;
if (len < 126) {
// 1 byte length
if (buf.length < (2 + len)) return; // Insuffisent data
payload = buf.slice(2, 2 + len);
obj.socketAccumulator = obj.socketAccumulator.substring(2 + len); // Remove data from accumulator
} else if (len == 126) {
// 2 byte length
if (buf.length < 4) return;
len = buf.readUInt16BE(2);
if (buf.length < (4 + len)) return; // Insuffisent data
payload = buf.slice(4, 4 + len);
obj.socketAccumulator = obj.socketAccumulator.substring(4 + len); // Remove data from accumulator
} if (len == 127) {
// 8 byte length
if (buf.length < 10) return;
len = buf.readUInt32BE(2);
if (len > 0) { obj.close(); return; } // This frame is larger than 4 gigabyte, close the connection.
len = buf.readUInt32BE(6);
if (buf.length < (10 + len)) return; // Insuffisent data
payload = buf.slice(10, 10 + len);
obj.socketAccumulator = obj.socketAccumulator.substring(10 + len); // Remove data from accumulator
}
if (buf.length < len) return;
// If the mask or reserved bit are true, we are not decoding this right, close the connection.
if ((mask == true) || (rsv == true)) { obj.close(); return; }
// TODO: If FIN is not set, we need to add support for continue frames
//console.log(obj.tunnelId, '<--', op, payload ? payload.length : 0);
// Perform operation
switch (op) {
case 0: { break; } // Continue frame (TODO)
case 1: { try { obj.ws.send(payload.toString('binary')); } catch (ex) { } break; } // Text frame
case 2: { try { obj.ws.send(payload); } catch (ex) { } break; } // Binary frame
case 8: { obj.close(); return; } // Connection close
case 9: { try { obj.ws.ping(payload); } catch (ex) { } break; } // Ping frame
case 10: { try { obj.ws.pong(payload); } catch (ex) { } break; } // Pong frame
}
}
}
}
// This is a fully parsed HTTP response from the remote device
function processHttpResponse(header, data, done) {
//console.log('processHttpResponse');
if (obj.isWebSocket == false) {
if (obj.res == null) return;
parent.lastOperation = obj.lastOperation = Date.now(); // Update time of last opertion performed
// If there is a header, send it
if (header != null) {
obj.res.status(parseInt(header.Directive[1])); // Set the status
const blockHeaders = ['Directive', 'sec-websocket-extensions']; // We do not forward these headers
for (var i in header) {
if (i == 'set-cookie') {
for (var ii in header[i]) {
// Decode the new cookie
//console.log('set-cookie', header[i][ii]);
const cookieSplit = header[i][ii].split(';');
var newCookieName = null, newCookie = {};
for (var j in cookieSplit) {
var l = cookieSplit[j].indexOf('='), k = null, v = null;
if (l == -1) { k = cookieSplit[j].trim(); } else { k = cookieSplit[j].substring(0, l).trim(); v = cookieSplit[j].substring(l + 1).trim(); }
if (j == 0) { newCookieName = k; newCookie.value = v; } else { newCookie[k.toLowerCase()] = (v == null) ? true : v; }
}
if (newCookieName != null) {
if ((typeof newCookie['max-age'] == 'string') && (parseInt(newCookie['max-age']) <= 0)) {
delete parent.webCookies[newCookieName]; // Remove a expired cookie
//console.log('clear-cookie', newCookieName);
} else if (((newCookie.secure != true) || (obj.tls != null))) {
parent.webCookies[newCookieName] = newCookie; // Keep this cookie in the session
if (newCookie.httponly != true) { obj.res.set(i, header[i]); } // if the cookie is not HTTP-only, forward it to the browser. We need to do this to allow JavaScript to read it.
//console.log('new-cookie', newCookieName, newCookie);
}
}
}
}
else if (blockHeaders.indexOf(i) == -1) { obj.res.set(i, header[i]); } // Set the headers if not blocked
}
obj.res.set('Content-Security-Policy', "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:;"); // Set an "allow all" policy, see if the can restrict this in the future
obj.res.set('Cache-Control', 'no-cache'); // Tell the browser not to cache the responses since since the relay port can be used for many relays
}
// If there is data, send it
if (data != null) { obj.res.write(data, 'binary'); }
// If we are done, close the response
if (done == true) {
// Close the response
obj.res.end();
delete obj.res;
// Event completion
obj.processedRequestCount++;
if (obj.oncompleted) { obj.oncompleted(obj.tunnelId); }
}
} else {
// Tunnel is now in web socket pass-thru mode
if ((typeof header.connection == 'string') && (header.connection.toLowerCase() == 'upgrade')) {
// Websocket upgrade succesful
obj.socketParseState = 2;
} else {
// Unable to upgrade to web socket
obj.close();
}
}
}
// Send data thru the relay tunnel. Written to use TLS if needed.
function send(data) { try { if (obj.tls) { obj.tls.write(data); } else { obj.wsClient.send(data); } } catch (ex) { } }
parent.parent.debug('relay', 'TCP: Request for web relay');
return obj;
};
// Construct a MSTSC Relay object, called upon connection
// This implementation does not have TLS support
// This is a bit of a hack as we are going to run the RDP connection thru a loopback connection.
@ -202,10 +800,16 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
delete bitmap.data;
send(['rdp-bitmap', bitmap]); // Send the bitmap metadata seperately, without bitmap data.
}).on('clipboard', function (content) {
// Clipboard data changed
send(['rdp-clipboard', content]);
send(['rdp-clipboard', content]); // The clipboard data has changed
}).on('pointer', function (cursorId, cursorStr) {
if (cursorStr == null) { cursorStr = 'default'; }
if (obj.lastCursorStrSent != cursorStr) {
obj.lastCursorStrSent = cursorStr;
//console.log('pointer', cursorStr);
send(['rdp-pointer', cursorStr]); // The mouse pointer has changed
}
}).on('close', function () {
send(['rdp-close']);
send(['rdp-close']); // This RDP session has closed
}).on('error', function (err) {
if (typeof err == 'string') { send(['rdp-error', err]); }
if ((typeof err == 'object') && (err.err) && (err.code)) { send(['rdp-error', err.err, err.code]); }

View File

@ -69,6 +69,11 @@ function loadCertificates(pemFileNames) {
var k = PemKeys[j].indexOf('-----END RSA PRIVATE KEY-----');
if (k >= 0) { keys.push(pki.privateKeyFromPem('-----BEGIN RSA PRIVATE KEY-----' + PemKeys[j].substring(0, k) + '-----END RSA PRIVATE KEY-----')); }
}
PemKeys = pem.split('-----BEGIN PRIVATE KEY-----');
for (var j in PemKeys) {
var k = PemKeys[j].indexOf('-----END PRIVATE KEY-----');
if (k >= 0) { keys.push(pki.privateKeyFromPem('-----BEGIN PRIVATE KEY-----' + PemKeys[j].substring(0, k) + '-----END PRIVATE KEY-----')); }
}
} catch (ex) { }
}
if ((certs.length == 0) || (keys.length != 1)) return; // No certificates or private keys
@ -288,80 +293,218 @@ function createAuthenticodeHandler(path) {
var derlen = forge.asn1.getBerValueLength(forge.util.createBuffer(pkcs7raw.slice(1, 5))) + 4;
if (derlen != pkcs7raw.length) { pkcs7raw = pkcs7raw.slice(0, derlen); }
//console.log('pkcs7raw', Buffer.from(pkcs7raw, 'binary').toString('base64'));
// Decode the signature block and check that it's valid
var pkcs7der = null, valid = false;
try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(pkcs7raw)); } catch (ex) { }
try { valid = ((pkcs7der != null) && (forge.asn1.derToOid(pkcs7der.value[1].value[0].value[2].value[0].value) == "1.3.6.1.4.1.311.2.1.4")); } catch (ex) { }
if (pkcs7der == null) {
// Can't decode the signature
obj.header.sigpos = 0;
obj.header.siglen = 0;
obj.header.signed = false;
} else {
// To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future
// Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1"
pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data;
// Decode the signature block
var pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(pkcs7raw));
// Decode the PKCS7 message
var pkcs7 = null, pkcs7content = null;
try {
pkcs7 = p7.messageFromAsn1(pkcs7der);
pkcs7content = pkcs7.rawCapture.content.value[0];
} catch (ex) { }
// To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForjeJS adds support for it in the future
// Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1"
pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data;
if ((pkcs7 == null) || (pkcs7content == null)) {
// Can't decode the signature
obj.header.sigpos = 0;
obj.header.siglen = 0;
obj.header.signed = false;
} else {
// Verify a PKCS#7 signature
// Verify is not currently supported in node-forge, but if implemented in the future, this code could work.
//var caStore = forge.pki.createCaStore();
//for (var i in obj.certificates) { caStore.addCertificate(obj.certificates[i]); }
// Return is true if all signatures are valid and chain up to a provided CA
//if (!pkcs7.verify(caStore)) { throw ('Executable file has an invalid signature.'); }
// Decode the PKCS7 message
var pkcs7 = p7.messageFromAsn1(pkcs7der);
var pkcs7content = pkcs7.rawCapture.content.value[0];
// Verify a PKCS#7 signature
// Verify is not currently supported in node-forge, but if implemented in the future, this code could work.
//var caStore = forge.pki.createCaStore();
//for (var i in obj.certificates) { caStore.addCertificate(obj.certificates[i]); }
// Return is true if all signatures are valid and chain up to a provided CA
//if (!pkcs7.verify(caStore)) { throw ('Executable file has an invalid signature.'); }
// Get the signing attributes
obj.signingAttribs = [];
try {
for (var i in pkcs7.rawCapture.authenticatedAttributes) {
if (
(pkcs7.rawCapture.authenticatedAttributes[i].value != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[0] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[0].value != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value != null) &&
(forge.asn1.derToOid(pkcs7.rawCapture.authenticatedAttributes[i].value[0].value) == obj.Oids.SPC_SP_OPUS_INFO_OBJID)) {
for (var j in pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value) {
// Get the signing attributes
obj.signingAttribs = [];
try {
for (var i in pkcs7.rawCapture.authenticatedAttributes) {
if (
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0].value != null)
) {
var v = pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0].value;
if (v.startsWith('http://') || v.startsWith('https://') || ((v.length % 2) == 1)) { obj.signingAttribs.push(v); } else {
var r = ""; // This string value is in UCS2 format, convert it to a normal string.
for (var k = 0; k < v.length; k += 2) { r += String.fromCharCode((v.charCodeAt(k + 8) << 8) + v.charCodeAt(k + 1)); }
obj.signingAttribs.push(r);
(pkcs7.rawCapture.authenticatedAttributes[i].value != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[0] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[0].value != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value != null) &&
(forge.asn1.derToOid(pkcs7.rawCapture.authenticatedAttributes[i].value[0].value) == obj.Oids.SPC_SP_OPUS_INFO_OBJID)) {
for (var j in pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value) {
if (
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0].value != null)
) {
var v = pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0].value;
if (v.startsWith('http://') || v.startsWith('https://') || ((v.length % 2) == 1)) { obj.signingAttribs.push(v); } else {
var r = ''; // This string value is in UCS2 format, convert it to a normal string.
for (var k = 0; k < v.length; k += 2) { r += String.fromCharCode((v.charCodeAt(k + 8) << 8) + v.charCodeAt(k + 1)); }
obj.signingAttribs.push(r);
}
}
}
}
}
} catch (ex) { }
// Set the certificate chain
obj.certificates = pkcs7.certificates;
// Set the signature
obj.signature = Buffer.from(pkcs7.rawCapture.signature, 'binary');
// Get the file hashing algorithm
var hashAlgoOid = forge.asn1.derToOid(pkcs7content.value[1].value[0].value[0].value);
switch (hashAlgoOid) {
case forge.pki.oids.sha256: { obj.fileHashAlgo = 'sha256'; break; }
case forge.pki.oids.sha384: { obj.fileHashAlgo = 'sha384'; break; }
case forge.pki.oids.sha512: { obj.fileHashAlgo = 'sha512'; break; }
case forge.pki.oids.sha224: { obj.fileHashAlgo = 'sha224'; break; }
case forge.pki.oids.md5: { obj.fileHashAlgo = 'md5'; break; }
}
// Get the signed file hash
obj.fileHashSigned = Buffer.from(pkcs7content.value[1].value[1].value, 'binary')
// Compute the actual file hash
if (obj.fileHashAlgo != null) { obj.fileHashActual = obj.getHash(obj.fileHashAlgo); }
}
} catch (ex) { }
// Set the certificate chain
obj.certificates = pkcs7.certificates;
// Get the file hashing algorithm
var hashAlgoOid = forge.asn1.derToOid(pkcs7content.value[1].value[0].value[0].value);
switch (hashAlgoOid) {
case forge.pki.oids.sha256: { obj.fileHashAlgo = 'sha256'; break; }
case forge.pki.oids.sha384: { obj.fileHashAlgo = 'sha384'; break; }
case forge.pki.oids.sha512: { obj.fileHashAlgo = 'sha512'; break; }
case forge.pki.oids.sha224: { obj.fileHashAlgo = 'sha224'; break; }
case forge.pki.oids.md5: { obj.fileHashAlgo = 'md5'; break; }
}
// Get the signed file hash
obj.fileHashSigned = Buffer.from(pkcs7content.value[1].value[1].value, 'binary')
// Compute the actual file hash
if (obj.fileHashAlgo != null) { obj.fileHashActual = obj.getHash(obj.fileHashAlgo); }
}
return true;
}
// Make a timestamp signature request
obj.timeStampRequest = function (args, func) {
// Create the timestamp request in DER format
const asn1 = forge.asn1;
const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data;
const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data;
const asn1obj =
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid),
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid),
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, obj.signature.toString('binary')) // Signature here
])
])
]);
// Serialize an ASN.1 object to DER format in Base64
const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64');
// Make an HTTP request
const options = { url: args.time, proxy: args.proxy };
// Make a request to the time server
httpRequest(options, requestBody, function (err, data) {
if (err != null) { func(err); return; }
// Decode the timestamp signature block
var timepkcs7der = null;
try { timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(data, 'base64').toString('binary'))); } catch (ex) { func("Unable to parse time-stamp response: " + ex); return; }
// Decode the executable signature block
var pkcs7der = null;
try {
var pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(obj.getRawSignatureBlock(), 'base64').toString('binary')));
// Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable
// TODO: We could look to see if the certificate is already present in the executable
const timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value;
for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); }
// Remove any existing time stamp signatures
var newValues = [];
for (var i in pkcs7der.value[1].value[0].value[4].value[0].value) {
const j = pkcs7der.value[1].value[0].value[4].value[0].value[i];
if ((j.tagClass != 128) || (j.type != 1)) { newValues.push(j); } // If this is not a time stamp, add it to out new list.
}
pkcs7der.value[1].value[0].value[4].value[0].value = newValues; // Set the new list
// Get the time signature and add it to the executables PKCS7
const timeasn1Signature = timepkcs7der.value[1].value[0].value[4];
const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data;
const asn1obj2 =
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid),
timeasn1Signature
])
]);
pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2);
// Re-encode the executable signature block
const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary');
// Open the output file
var output = null;
try { output = fs.openSync(args.out, 'w+'); } catch (ex) { }
if (output == null) return false;
var tmp, written = 0;
var executableSize = obj.header.sigpos ? obj.header.sigpos : this.filesize;
// Compute pre-header length and copy that to the new file
var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
var tmp = readFileSlice(written, preHeaderLen);
fs.writeSync(output, tmp);
written += tmp.length;
// Quad Align the results, adding padding if necessary
var len = executableSize + p7signature.length;
var padding = (8 - ((len) % 8)) % 8;
// Write the signature header
var addresstable = Buffer.alloc(8);
addresstable.writeUInt32LE(executableSize);
addresstable.writeUInt32LE(8 + p7signature.length + padding, 4);
fs.writeSync(output, addresstable);
written += addresstable.length;
// Copy the rest of the file until the start of the signature block
while ((executableSize - written) > 0) {
tmp = readFileSlice(written, Math.min(executableSize - written, 65536));
fs.writeSync(output, tmp);
written += tmp.length;
}
// Write the signature block header and signature
var win = Buffer.alloc(8); // WIN CERTIFICATE Structure
win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length
win.writeUInt16LE(512, 4); // WORD revision
win.writeUInt16LE(2, 6); // WORD type
fs.writeSync(output, win);
fs.writeSync(output, p7signature);
if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); }
written += (p7signature.length + padding + 8);
// Compute the checksum and write it in the PE header checksum location
var tmp = Buffer.alloc(4);
tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4)));
fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64);
// Close the file
fs.closeSync(output);
// Indicate we are done
func(null);
} catch (ex) { func('' + ex); return; }
});
}
// Read a resource table.
// ptr: The pointer to the start of the resource section
// offset: The offset start of the resource table to read
@ -568,6 +711,15 @@ function createAuthenticodeHandler(path) {
'configurationFiles': 24
}
// Return the raw signature block buffer with padding removed
obj.getRawSignatureBlock = function () {
if ((obj.header.sigpos == 0) || (obj.header.siglen == 0)) return null;
var pkcs7raw = readFileSlice(obj.header.sigpos + 8, obj.header.siglen - 8);
var derlen = forge.asn1.getBerValueLength(forge.util.createBuffer(pkcs7raw.slice(1, 5))) + 4;
if (derlen != pkcs7raw.length) { pkcs7raw = pkcs7raw.slice(0, derlen); }
return pkcs7raw;
}
// Get icon information from resource
obj.getIconInfo = function () {
const r = {}, ptr = obj.header.sections['.rsrc'].rawAddr;
@ -945,8 +1097,9 @@ function createAuthenticodeHandler(path) {
//function padPointer(ptr) { return ptr + (ptr % 4); }
// Hash the file using the selected hashing system
// This hash skips the executables CRC and code signing data and signing block
obj.getHash = function(algo) {
var hash = crypto.createHash(algo);
const hash = crypto.createHash(algo);
runHash(hash, 0, obj.header.peHeaderLocation + 88);
runHash(hash, obj.header.peHeaderLocation + 88 + 4, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
runHash(hash, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16) + 8, obj.header.sigpos > 0 ? obj.header.sigpos : obj.filesize);
@ -954,14 +1107,41 @@ function createAuthenticodeHandler(path) {
}
// Hash of an open file using the selected hashing system
obj.getHashOfFile = function (fd, algo, filesize) {
var hash = crypto.createHash(algo);
// This hash skips the executables CRC and code signing data and signing block
obj.getHashOfFile = function(fd, algo, filesize) {
const hash = crypto.createHash(algo);
runHashOnFile(fd, hash, 0, obj.header.peHeaderLocation + 88);
runHashOnFile(fd, hash, obj.header.peHeaderLocation + 88 + 4, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
runHashOnFile(fd, hash, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16) + 8, obj.header.sigpos > 0 ? obj.header.sigpos : filesize);
return hash.digest();
}
// Hash the file using the selected hashing system skipping resource section
// This hash skips the executables CRC, sections table, resource section, code signing data and signing block
obj.getHashNoResources = function (algo) {
if (obj.header.sections['.rsrc'] == null) { return obj.getHash(algo); } // No resources in this executable, return a normal hash
// Get the sections table start and size
const sectionHeaderPtr = obj.header.SectionHeadersPtr;
const sectionHeaderSize = obj.header.coff.numberOfSections * 40;
// Get the resource section start and size
const resPtr = obj.header.sections['.rsrc'].rawAddr;
const resSize = obj.header.sections['.rsrc'].rawSize;
// Get the end-of-file location
const eof = obj.header.sigpos > 0 ? obj.header.sigpos : obj.filesize;
// Hash the remaining data
const hash = crypto.createHash(algo);
runHash(hash, 0, obj.header.peHeaderLocation + 88);
runHash(hash, obj.header.peHeaderLocation + 88 + 4, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
runHash(hash, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16) + 8, sectionHeaderPtr);
runHash(hash, sectionHeaderPtr + sectionHeaderSize, resPtr);
runHash(hash, resPtr + resSize, eof);
return hash.digest();
}
// Hash the file from start to end loading 64k chunks
function runHash(hash, start, end) {
var ptr = start;
@ -971,8 +1151,8 @@ function createAuthenticodeHandler(path) {
// Hash the open file loading 64k chunks
// TODO: Do chunks on this!!!
function runHashOnFile(fd, hash, start, end) {
var buf = Buffer.alloc(end - start);
var len = fs.readSync(fd, buf, 0, buf.length, start);
const buf = Buffer.alloc(end - start);
const len = fs.readSync(fd, buf, 0, buf.length, start);
if (len != buf.length) { console.log('BAD runHashOnFile'); }
hash.update(buf);
}
@ -1044,7 +1224,7 @@ function createAuthenticodeHandler(path) {
}
// Sign the file using the certificate and key. If none is specified, generate a dummy one
obj.sign = function (cert, args) {
obj.sign = function (cert, args, func) {
if (cert == null) { cert = createSelfSignedCert({ cn: 'Test' }); }
// Set the hash algorithm hash OID
@ -1055,16 +1235,16 @@ function createAuthenticodeHandler(path) {
if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHash('sha512'); }
if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHash('sha224'); }
if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHash('md5'); }
if (hashOid == null) return false;
if (hashOid == null) { func(false); return; };
// Create the signature block
var p7 = forge.pkcs7.createSignedData();
var xp7 = forge.pkcs7.createSignedData();
var content = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.15').data }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000', 'bitStringContents': '\u0000', 'original': { 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000' } }, { 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 2, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': '' }] }] }] }] }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer(hashOid).data }, { 'tagClass': 0, 'type': 5, 'constructed': false, 'composed': false, 'value': '' }] }, { 'tagClass': 0, 'type': 4, 'constructed': false, 'composed': false, 'value': fileHash.toString('binary') }] }] };
p7.contentInfo = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').getBytes())]);
p7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content]));
p7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign.
p7.addCertificate(cert.cert);
if (cert.extraCerts) { for (var i = 0; i < cert.extraCerts.length; i++) { p7.addCertificate(cert.extraCerts[0]); } } // Add any extra certificates that form the cert chain
xp7.contentInfo = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').getBytes())]);
xp7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content]));
xp7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign.
xp7.addCertificate(cert.cert);
if (cert.extraCerts) { for (var i = 0; i < cert.extraCerts.length; i++) { xp7.addCertificate(cert.extraCerts[0]); } } // Add any extra certificates that form the cert chain
// Build authenticated attributes
var authenticatedAttributes = [
@ -1083,22 +1263,198 @@ function createAuthenticodeHandler(path) {
}
// Add the signer and sign
p7.addSigner({
xp7.addSigner({
key: cert.key,
certificate: cert.cert,
digestAlgorithm: forge.pki.oids.sha384,
authenticatedAttributes: authenticatedAttributes
});
p7.sign();
var p7signature = Buffer.from(forge.pkcs7.messageToPem(p7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64');
//console.log('Signature', Buffer.from(p7signature, 'binary').toString('base64'));
xp7.sign();
var p7signature = Buffer.from(forge.pkcs7.messageToPem(xp7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64');
if (args.time == null) {
// Sign the executable without timestamp
signEx(args, p7signature, obj.filesize, func);
} else {
// Decode the signature block
var pkcs7der = null;
try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; }
// To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future
// Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1"
pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data;
// Decode the PKCS7 message
var pkcs7 = p7.messageFromAsn1(pkcs7der);
// Create the timestamp request in DER format
const asn1 = forge.asn1;
const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data;
const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data;
const asn1obj =
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid),
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid),
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, pkcs7.rawCapture.signature.toString('binary')) // Signature here
])
])
]);
// Re-decode the PKCS7 from the executable, this time, no workaround needed
try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; }
// Serialize an ASN.1 object to DER format in Base64
const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64');
// Make an HTTP request
const options = { url: args.time, proxy: args.proxy };
// Make a request to the time server
httpRequest(options, requestBody, function (err, data) {
if (err != null) { func(err); return; }
// Decode the timestamp signature block
var timepkcs7der = null;
try { timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(data, 'base64').toString('binary'))); } catch (ex) { func("Unable to parse time-stamp response: " + ex); return; }
try {
// Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable
// TODO: We could look to see if the certificate is already present in the executable
const timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value;
for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); }
// Get the time signature and add it to the executables PKCS7
const timeasn1Signature = timepkcs7der.value[1].value[0].value[4];
const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data;
const asn1obj2 =
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid),
timeasn1Signature
])
]);
pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2);
// Re-encode the executable signature block
const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary');
// Write the file with the signature block
signEx(args, p7signature, obj.filesize, func);
} catch (ex) { func('' + ex); }
});
}
}
// Make a HTTP request, use a proxy if needed
function httpRequest(options, requestBody, func) {
// Decode the URL
const timeServerUrl = new URL(options.url);
options.protocol = timeServerUrl.protocol;
options.hostname = timeServerUrl.hostname;
options.path = timeServerUrl.pathname;
options.port = ((timeServerUrl.port == '') ? 80 : parseInt(timeServerUrl.port));
if (options.proxy == null) {
// No proxy needed
// Setup the options
delete options.url;
options.method = 'POST';
options.headers = {
'accept': 'application/octet-stream',
'cache-control': 'no-cache',
'user-agent': 'Transport',
'content-type': 'application/octet-stream',
'content-length': Buffer.byteLength(requestBody)
};
// Set up the request
var responseAccumulator = '';
var req = require('http').request(options, function (res) {
res.setEncoding('utf8');
res.on('data', function (chunk) { responseAccumulator += chunk; });
res.on('end', function () { func(null, responseAccumulator); });
});
// Post the data
req.on('error', function (err) { func('' + err); });
req.write(requestBody);
req.end();
} else {
// We are using a proxy
// This is a fairly basic proxy implementation, should work most of the time.
// Setup the options and decode the proxy URL
var proxyOptions = { method: 'CONNECT' };
if (options.proxy) {
const proxyUrl = new URL(options.proxy);
proxyOptions.protocol = proxyUrl.protocol;
proxyOptions.hostname = proxyUrl.hostname;
proxyOptions.path = options.hostname + ':' + options.port;
proxyOptions.port = ((proxyUrl.port == '') ? 80 : parseInt(proxyUrl.port));
}
// Set up the proxy request
var responseAccumulator = '';
var req = require('http').request(proxyOptions);
req.on('error', function (err) { func('' + err); });
req.on('connect', function (res, socket, head) {
// Make a request over the HTTP tunnel
socket.write('POST ' + options.path + ' HTTP/1.1\r\n' +
'host: ' + options.hostname + ':' + options.port + '\r\n' +
'accept: application/octet-stream\r\n' +
'cache-control: no-cache\r\n' +
'user-agent: Transport\r\n' +
'content-type: application/octet-stream\r\n' +
'content-length: ' + Buffer.byteLength(requestBody) + '\r\n' +
'\r\n' + requestBody);
socket.on('data', function (chunk) {
responseAccumulator += chunk.toString();
var responseData = parseHttpResponse(responseAccumulator);
if (responseData != null) { try { socket.end(); } catch (ex) { console.log('ex', ex); } socket.xdone = true; func(null, responseData); }
});
socket.on('end', function () {
if (socket.xdone == true) return;
var responseData = parseHttpResponse(responseAccumulator);
if (responseData != null) { func(null, responseData); } else { func("Unable to parse response."); }
});
});
req.end();
}
}
// Parse the HTTP response and return data if available
function parseHttpResponse(data) {
var dataSplit = data.split('\r\n\r\n');
if (dataSplit.length < 2) return null;
// Parse the HTTP header
var headerSplit = dataSplit[0].split('\r\n'), headers = {};
for (var i in headerSplit) {
if (i != 0) {
var x = headerSplit[i].indexOf(':');
headers[headerSplit[i].substring(0, x).toLowerCase()] = headerSplit[i].substring(x + 2);
}
}
// If there is a content-length in the header, keep accumulating data until we have the right length
if (headers['content-length'] != null) {
const contentLength = parseInt(headers['content-length']);
if (dataSplit[1].length < contentLength) return null; // Wait for more data
return dataSplit[1];
}
return dataSplit[1];
}
// Complete the signature of an executable
function signEx(args, p7signature, filesize, func) {
// Open the output file
var output = null;
try { output = fs.openSync(args.out, 'w+'); } catch (ex) { }
if (output == null) return false;
var tmp, written = 0;
var executableSize = obj.header.sigpos ? obj.header.sigpos : this.filesize;
if (output == null) { func(false); return; }
var tmp, written = 0, executableSize = obj.header.sigpos ? obj.header.sigpos : filesize;
// Compute pre-header length and copy that to the new file
var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
@ -1141,7 +1497,7 @@ function createAuthenticodeHandler(path) {
// Close the file
fs.closeSync(output);
return true;
func(null);
}
// Save an executable without the signature
@ -1176,7 +1532,7 @@ function createAuthenticodeHandler(path) {
}
// Save the executable
obj.writeExecutable = function (args, cert) {
obj.writeExecutable = function (args, cert, func) {
// Open the file
var output = fs.openSync(args.out, 'w+');
var tmp, written = 0;
@ -1232,13 +1588,13 @@ function createAuthenticodeHandler(path) {
}
// Write the entire header to the destination file
//console.log('Write header', fullHeader.length);
//console.log('Write header', fullHeader.length, written);
fs.writeSync(output, fullHeader);
written += fullHeader.length;
// Write the entire executable until the start to the resource segment
var totalWrite = resPtr;
//console.log('Write until res', totalWrite);
//console.log('Write until res', totalWrite, written);
while ((totalWrite - written) > 0) {
tmp = readFileSlice(written, Math.min(totalWrite - written, 65536));
fs.writeSync(output, tmp);
@ -1249,15 +1605,24 @@ function createAuthenticodeHandler(path) {
var rsrcSection = generateResourceSection(obj.resources);
fs.writeSync(output, rsrcSection);
written += rsrcSection.length;
//console.log('Write res', rsrcSection.length, written);
// Write until the signature block
totalWrite = obj.header.sigpos + resDeltaSize;
//console.log('Write until signature', totalWrite);
if (obj.header.sigpos > 0) {
// Since the original file was signed, write from the end of the resources to the start of the signature block.
totalWrite = obj.header.sigpos + resDeltaSize;
} else {
// The original file was not signed, write from the end of the resources to the end of the file.
totalWrite = obj.filesize + resDeltaSize;
}
//console.log('Write until signature', totalWrite, written);
while ((totalWrite - written) > 0) {
tmp = readFileSlice(written - resDeltaSize, Math.min(totalWrite - written, 65536));
fs.writeSync(output, tmp);
written += tmp.length;
}
//console.log('Write to signature', written);
// Write the signature if needed
if (cert != null) {
@ -1271,16 +1636,16 @@ function createAuthenticodeHandler(path) {
if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHashOfFile(output, 'sha512', written); }
if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHashOfFile(output, 'sha224', written); }
if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHashOfFile(output, 'md5', written); }
if (hashOid == null) return false;
if (hashOid == null) { func('Bad hash method OID'); return; }
// Create the signature block
var p7 = forge.pkcs7.createSignedData();
var xp7 = forge.pkcs7.createSignedData();
var content = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.15').data }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000', 'bitStringContents': '\u0000', 'original': { 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000' } }, { 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 2, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': '' }] }] }] }] }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer(hashOid).data }, { 'tagClass': 0, 'type': 5, 'constructed': false, 'composed': false, 'value': '' }] }, { 'tagClass': 0, 'type': 4, 'constructed': false, 'composed': false, 'value': fileHash.toString('binary') }] }] };
p7.contentInfo = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').getBytes())]);
p7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content]));
p7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign.
p7.addCertificate(cert.cert);
if (cert.extraCerts) { for (var i = 0; i < cert.extraCerts.length; i++) { p7.addCertificate(cert.extraCerts[0]); } } // Add any extra certificates that form the cert chain
xp7.contentInfo = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').getBytes())]);
xp7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content]));
xp7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign.
xp7.addCertificate(cert.cert);
if (cert.extraCerts) { for (var i = 0; i < cert.extraCerts.length; i++) { xp7.addCertificate(cert.extraCerts[0]); } } // Add any extra certificates that form the cert chain
// Build authenticated attributes
var authenticatedAttributes = [
@ -1299,45 +1664,131 @@ function createAuthenticodeHandler(path) {
}
// Add the signer and sign
p7.addSigner({
xp7.addSigner({
key: cert.key,
certificate: cert.cert,
digestAlgorithm: forge.pki.oids.sha384,
authenticatedAttributes: authenticatedAttributes
});
p7.sign();
var p7signature = Buffer.from(forge.pkcs7.messageToPem(p7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64');
xp7.sign();
var p7signature = Buffer.from(forge.pkcs7.messageToPem(xp7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64');
//console.log('Signature', Buffer.from(p7signature, 'binary').toString('base64'));
// Quad Align the results, adding padding if necessary
var len = written + p7signature.length;
var padding = (8 - ((len) % 8)) % 8;
if (args.time == null) {
// Write the signature block to the output executable without time stamp
writeExecutableEx(output, p7signature, written, func);
} else {
// Decode the signature block
var pkcs7der = null;
try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; }
// Write the signature block header and signature
var win = Buffer.alloc(8); // WIN CERTIFICATE Structure
win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length
win.writeUInt16LE(512, 4); // WORD revision
win.writeUInt16LE(2, 6); // WORD type
fs.writeSync(output, win);
fs.writeSync(output, p7signature);
if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); }
// To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future
// Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1"
pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data;
// Write the signature header
var addresstable = Buffer.alloc(8);
addresstable.writeUInt32LE(written);
addresstable.writeUInt32LE(8 + p7signature.length + padding, 4);
var signatureHeaderLocation = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
fs.writeSync(output, addresstable, 0, 8, signatureHeaderLocation);
written += (p7signature.length + padding + 8); // Add the signature block to written counter
// Decode the PKCS7 message
var pkcs7 = p7.messageFromAsn1(pkcs7der);
// Compute the checksum and write it in the PE header checksum location
var tmp = Buffer.alloc(4);
tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4)));
fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64);
// Create the timestamp request in DER format
const asn1 = forge.asn1;
const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data;
const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data;
const asn1obj =
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid),
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid),
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, pkcs7.rawCapture.signature.toString('binary')) // Signature here
])
])
]);
// Re-decode the PKCS7 from the executable, this time, no workaround needed
try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; }
// Serialize an ASN.1 object to DER format in Base64
const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64');
// Make an HTTP request
const options = { url: args.time, proxy: args.proxy };
// Make a request to the time server
httpRequest(options, requestBody, function (err, data) {
if (err != null) { func(err); return; }
// Decode the timestamp signature block
var timepkcs7der = null;
try { timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(data, 'base64').toString('binary'))); } catch (ex) { func("Unable to parse time-stamp response: " + ex); return; }
// Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable
// TODO: We could look to see if the certificate is already present in the executable
try {
var timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value;
for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); }
// Get the time signature and add it to the executables PKCS7
const timeasn1Signature = timepkcs7der.value[1].value[0].value[4];
const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data;
const asn1obj2 =
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid),
timeasn1Signature
])
]);
pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2);
// Re-encode the executable signature block
const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary');
// Write the file with the signature block
writeExecutableEx(output, p7signature, written, func);
} catch (ex) { func('' + ex); return; } // Something failed
});
}
return;
}
// Close the file
fs.closeSync(output);
// Indicate success
func(null);
}
function writeExecutableEx(output, p7signature, written, func) {
// Quad Align the results, adding padding if necessary
var len = written + p7signature.length;
var padding = (8 - ((len) % 8)) % 8;
// Write the signature block header and signature
var win = Buffer.alloc(8); // WIN CERTIFICATE Structure
win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length
win.writeUInt16LE(512, 4); // WORD revision
win.writeUInt16LE(2, 6); // WORD type
fs.writeSync(output, win);
fs.writeSync(output, p7signature);
if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); }
// Write the signature header
var addresstable = Buffer.alloc(8);
addresstable.writeUInt32LE(written);
addresstable.writeUInt32LE(8 + p7signature.length + padding, 4);
var signatureHeaderLocation = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
fs.writeSync(output, addresstable, 0, 8, signatureHeaderLocation);
written += (p7signature.length + padding + 8); // Add the signature block to written counter
// Compute the checksum and write it in the PE header checksum location
var tmp = Buffer.alloc(4);
tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4)));
fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64);
// Close the file
fs.closeSync(output);
// Indicate success
func(null);
}
// Return null if we could not open the file
@ -1364,6 +1815,8 @@ function start() {
console.log(" --desc [description] Description string to embbed into signature.");
console.log(" --url [url] URL to embbed into signature.");
console.log(" --hash [method] Default is SHA384, possible value: MD5, SHA224, SHA256, SHA384 or SHA512.");
console.log(" --time [url] The time signing server URL.");
console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://");
console.log(" unsign: Remove the signature from the executable.");
console.log(" --exe [file] Required executable to un-sign.");
console.log(" --out [file] Resulting executable with signature removed.");
@ -1376,6 +1829,11 @@ function start() {
console.log(" --org [value] Certificate organization name.");
console.log(" --ou [value] Certificate organization unit name.");
console.log(" --serial [value] Certificate serial number.");
console.log(" timestamp: Add a signed timestamp to an already signed executable.");
console.log(" --exe [file] Required executable to sign.");
console.log(" --out [file] Resulting signed executable.");
console.log(" --time [url] The time signing server URL.");
console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://");
console.log("");
console.log("Note that certificate PEM files must first have the signing certificate,");
console.log("followed by all certificates that form the trust chain.");
@ -1393,9 +1851,9 @@ function start() {
}
// Check that a valid command is passed in
if (['info', 'sign', 'unsign', 'createcert', 'icons', 'saveicon', 'header', 'test'].indexOf(process.argv[2].toLowerCase()) == -1) {
if (['info', 'sign', 'unsign', 'createcert', 'icons', 'saveicon', 'header', 'timestamp', 'signblock'].indexOf(process.argv[2].toLowerCase()) == -1) {
console.log("Invalid command: " + process.argv[2]);
console.log("Valid commands are: info, sign, unsign, createcert");
console.log("Valid commands are: info, sign, unsign, createcert, timestamp");
return;
}
@ -1410,13 +1868,16 @@ function start() {
}
// Parse the resources and make any required changes
var resChanges = false, versionStrings = exe.getVersionInfo();
var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
for (var i in versionProperties) {
const prop = versionProperties[i], propl = prop.toLowerCase();
if (args[propl] && (args[propl] != versionStrings[prop])) { versionStrings[prop] = args[propl]; resChanges = true; }
var resChanges = false, versionStrings = null;
if (exe != null) {
versionStrings = exe.getVersionInfo();
var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
for (var i in versionProperties) {
const prop = versionProperties[i], propl = prop.toLowerCase();
if (args[propl] && (args[propl] != versionStrings[prop])) { versionStrings[prop] = args[propl]; resChanges = true; }
}
if (resChanges == true) { exe.setVersionInfo(versionStrings); }
}
if (resChanges == true) { exe.setVersionInfo(versionStrings); }
// Execute the command
var command = process.argv[2].toLowerCase();
@ -1463,12 +1924,19 @@ function start() {
if (cert == null) { console.log("Unable to load certificate and/or private key, generating test certificate."); cert = createSelfSignedCert({ cn: 'Test' }); }
if (resChanges == false) {
console.log("Signing to " + args.out);
exe.sign(cert, args); // Simple signing, copy most of the original file.
exe.sign(cert, args, function (err) { // Simple signing, copy most of the original file.
if (err == null) { console.log("Done."); } else { console.log(err); }
if (exe != null) { exe.close(); }
});
return;
} else {
console.log("Changing resources and signing to " + args.out);
exe.writeExecutable(args, cert); // Signing with resources decoded and re-encoded.
exe.writeExecutable(args, cert, function (err) { // Signing with resources decoded and re-encoded.
if (err == null) { console.log("Done."); } else { console.log(err); }
if (exe != null) { exe.close(); }
});
return;
}
console.log("Done.");
}
if (command == 'unsign') { // Unsign an executable
if (typeof args.exe != 'string') { console.log("Missing --exe [filename]"); return; }
@ -1483,7 +1951,10 @@ function start() {
}
} else {
console.log("Changing resources and unsigning to " + args.out);
exe.writeExecutable(args, null); // Unsigning with resources decoded and re-encoded.
exe.writeExecutable(args, null, function (err) { // Unsigning with resources decoded and re-encoded.
if (err == null) { console.log("Done."); } else { console.log(err); }
if (exe != null) { exe.close(); }
});
}
}
if (command == 'createcert') { // Create a code signing certificate and private key
@ -1511,6 +1982,7 @@ function start() {
}
}
if (command == 'saveicon') { // Save an icon to file
if (exe == null) { console.log("Missing --exe [filename]"); return; }
if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; }
if (typeof args.icon != 'number') { console.log("Missing or incorrect --icon [number]"); return; }
const iconInfo = exe.getIconInfo();
@ -1534,6 +2006,23 @@ function start() {
fs.writeFileSync(args.out, Buffer.concat([buf, icon.icon]));
console.log("Done.");
}
if (command == 'signblock') { // Display the raw signature block of the executable in hex
if (exe == null) { console.log("Missing --exe [filename]"); return; }
var buf = exe.getRawSignatureBlock();
if (buf == null) { console.log("Executable is not signed."); return } else { console.log(buf.toString('hex')); return }
}
if (command == 'timestamp') {
if (exe == null) { console.log("Missing --exe [filename]"); return; }
if (exe.signature == null) { console.log("Executable is not signed."); return; }
if (typeof args.time != 'string') { console.log("Missing --time [url]"); return; }
createOutFile(args, args.exe);
console.log("Requesting time signature...");
exe.timeStampRequest(args, function (err) {
if (err == null) { console.log("Done."); } else { console.log(err); }
if (exe != null) { exe.close(); }
})
return;
}
// Close the file
if (exe != null) { exe.close(); }
@ -1545,3 +2034,4 @@ if (require.main === module) { start(); }
// Exports
module.exports.createAuthenticodeHandler = createAuthenticodeHandler;
module.exports.loadCertificates = loadCertificates;

40
db.js
View File

@ -109,7 +109,13 @@ module.exports.CreateDB = function (parent, func) {
obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
} else if ((obj.databaseType == 4) || (obj.databaseType == 5)) { // MariaDB or MySQL
sqlDbQuery('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expireEventsSeconds
sqlDbQuery('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expirePowerSeconds
sqlDbQuery('DELETE FROM serverstats WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
sqlDbQuery('DELETE FROM smbios WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
}
obj.removeInactiveDevices();
}
@ -1198,23 +1204,20 @@ module.exports.CreateDB = function (parent, func) {
obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); };
obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
if (id && (id != '')) {
sqlDbQuery('SELECT doc FROM main WHERE id = $1 AND type = $2 AND domain = $3 AND (extra = ANY ($4))', [id, type, domain, meshes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4))', [id, type, domain, meshes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
} else {
if (extrasids == null) {
sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, meshes], function (err, docs) {
if (err == null) { for (var i in docs) { delete docs[i].type } }
func(err, performTypedRecordDecrypt(docs));
}, true);
sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, meshes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }, true);
} else {
sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2 AND ((extra = ANY ($3)) OR (id = ANY ($4)))', [type, domain, meshes, extrasids], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4)))', [type, domain, meshes, extrasids], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
}
}
};
obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
if (id && (id != '')) {
sqlDbQuery('SELECT doc FROM main WHERE id = $1 AND type = $2 AND domain = $3 AND (extra = ANY ($4))', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4))', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
} else {
sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2 AND (extra = ANY ($3))', [type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
}
};
obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
@ -1367,17 +1370,16 @@ module.exports.CreateDB = function (parent, func) {
obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = ?', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ?', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); };
obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
if (id && (id != '')) {
sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, meshes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
} else {
if (extrasids == null) {
sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND extra IN (?)', [type, domain, meshes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
} else {
sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?))', [type, domain, meshes, extrasids], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
}
sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?))', [type, domain, meshes, extrasids], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
}
};
obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
if ((nodes == null) || (nodes.length == 0)) { nodes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
if (id && (id != '')) {
sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
} else {
@ -1385,7 +1387,10 @@ module.exports.CreateDB = function (parent, func) {
}
};
obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ?', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
obj.GetAllIdsOfType = function (ids, domain, type, func) { sqlDbQuery('SELECT doc FROM main WHERE id IN (?) AND domain = ? AND type = ?', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
obj.GetAllIdsOfType = function (ids, domain, type, func) {
if ((ids == null) || (ids.length == 0)) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
sqlDbQuery('SELECT doc FROM main WHERE id IN (?) AND domain = ? AND type = ?', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
}
obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = ?', [id], func); };
@ -1413,6 +1418,7 @@ module.exports.CreateDB = function (parent, func) {
if (ids.indexOf('*') >= 0) {
sqlDbQuery('SELECT doc FROM events WHERE (domain = ?) ORDER BY time DESC', [domain], func);
} else {
if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)) GROUP BY id ORDER BY time DESC', [domain, ids], func);
}
};
@ -1420,6 +1426,7 @@ module.exports.CreateDB = function (parent, func) {
if (ids.indexOf('*') >= 0) {
sqlDbQuery('SELECT doc FROM events WHERE (domain = ?) ORDER BY time DESC LIMIT ?', [domain, limit], func);
} else {
if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)) GROUP BY id ORDER BY time DESC LIMIT ?', [domain, ids, limit], func);
}
};
@ -1427,6 +1434,7 @@ module.exports.CreateDB = function (parent, func) {
if (ids.indexOf('*') >= 0) {
sqlDbQuery('SELECT doc FROM events WHERE (domain = ? AND userid = ?) ORDER BY time DESC', [domain, userid], func);
} else {
if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)) GROUP BY id ORDER BY time DESC', [domain, userid, ids], func);
}
};
@ -1434,6 +1442,7 @@ module.exports.CreateDB = function (parent, func) {
if (ids.indexOf('*') >= 0) {
sqlDbQuery('SELECT doc FROM events WHERE (domain = ? AND userid = ?) ORDER BY time DESC LIMIT ?', [domain, userid, limit], func);
} else {
if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)) GROUP BY id ORDER BY time DESC LIMIT ?', [domain, userid, ids, limit], func);
}
};
@ -1441,6 +1450,7 @@ module.exports.CreateDB = function (parent, func) {
if (ids.indexOf('*') >= 0) {
sqlDbQuery('SELECT doc FROM events WHERE ((domain = ?) AND (time BETWEEN ? AND ?)) ORDER BY time', [domain, start, end], func);
} else {
if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = ?) AND (target IN (?)) AND (time BETWEEN ? AND ?)) GROUP BY id ORDER BY time', [domain, ids, start, end], func);
}
};
@ -1467,7 +1477,7 @@ module.exports.CreateDB = function (parent, func) {
// Database actions on the Server Stats collection
obj.SetServerStats = function (data, func) { sqlDbQuery('REPLACE INTO serverstats VALUE (?, ?, ?)', [data.time, data.expire, JSON.stringify(data)], func); };
obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > ?', [t], func); }; // TODO: Expire old entries
obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > ?', [t], func); };
// Read a configuration file from the database
obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }

View File

@ -81,6 +81,12 @@ The main takeaway is that MeshCentral is mostly an ExpressJS application. This i
MeshCentral will run `npm install` automatically when any of these optional modules are needed but not currently available.
## Understanding the different modes: LAN, WAN and Hybrid
<div class="video-wrapper">
<iframe width="320" height="180" src="https://www.youtube.com/embed/gx5Fh3pQOns" frameborder="0" allowfullscreen></iframe>
</div>
## Code files and folders
Someone would think the server is rather simple when taking a look at the MeshCentral server code files. At a high level, the entire server has 3 folders, 3 text files and a manageable number of .js files that are fairly self-descriptive. Here is a list of the source files and folders.

View File

@ -24,3 +24,9 @@ You can run the MeshCentral Server with --help to get options for background ins
Once you get MeshCentral installed, the first user account that is created will be the server administrator. So, don't delay and navigate to the login page and create a new account. You can then start using your server right away. A lot of the fun with MeshCentral is the 100's of configuration options that are available in the config.json file. You can put your own branding on the web pages, setup a SMTP email server, SMS services and much more.
You can look [here for simple config.json](https://raw.githubusercontent.com/Ylianst/MeshCentral/master/sample-config.json), [here for a more advanced configuration](https://raw.githubusercontent.com/Ylianst/MeshCentral/master/sample-config-advanced.json) and [here for all possible configuration options](https://raw.githubusercontent.com/Ylianst/MeshCentral/master/meshcentral-config-schema.json). You can also take a look at the [MeshCentral User's Guide](https://meshcentral.com/info/docs/MeshCentral2InstallGuide.pdf) and [tutorial videos](https://meshcentral.com/info/tutorials.html) for additional help.
## Video Walkthru
<div class="video-wrapper">
<iframe width="320" height="180" src="https://www.youtube.com/embed/LSiWuu71k_U" frameborder="0" allowfullscreen></iframe>
</div>

View File

@ -13,6 +13,12 @@ Intel AMT Guide [as .odt](https://github.com/Ylianst/MeshCentral/blob/master/doc
This user guide contains all essential information for activating and using Intel® Active Management Technology (Intel® AMT) with MeshCentral. We will review how to activate, connect to and use Intel AMT features and how this benefit administrators that want to manage computers remotely. This document expect the reader to already be familiar with how to install and operate MeshCentral and have a basic understanding of how Intel® AMT works.
## History of AMT
<div class="video-wrapper">
<iframe width="320" height="180" src="https://www.youtube.com/embed/_SXT0Gr4Mls" frameborder="0" allowfullscreen></iframe>
</div>
## Introduction
MeshCentral is a free open source web-based remote computer management software and it fully supports Intel® Active Management Technology (Intel® AMT). MeshCentral does not require that computers it manages support Intel AMT, but if a remote computer has this capability, MeshCentral will make use of it.
@ -173,3 +179,19 @@ Once Intel AMT is in a situation where ACM activation can occur, the activation
![](images/2022-05-16-23-16-05.png)
The best way to test this feature is to create an “Intel AMT only” device group and run the MeshCMD command on the remote system to perform activation. If there is a problem, this process should clearly display why ACM activation fails.
## Intel AMT MEI and LMS
Intel Active Management Technology (Intel AMT) can communicate to the local platform using the Management Engine Interface (MEI). We show how your can use that to get Intel AMT information. For more advanced usages, you need to connect using TCP and TLS which requires Intel Local Manageability Service (LMS). We show how MeshCentral's Mesh Agent and MeshCMD have a small version of LMS built-in and how it works
<div class="video-wrapper">
<iframe width="320" height="180" src="https://www.youtube.com/embed/mStyhe-fSC0" frameborder="0" allowfullscreen></iframe>
</div>
## Intel AMT System Defense
As part of Intel AMT there are hardware filters in the network interface you can setup to match and perform actions on packets. This happens at Ethernet speeds with no slow down and independent of the OS.
<div class="video-wrapper">
<iframe width="320" height="180" src="https://www.youtube.com/embed/q7RyboI4uew" frameborder="0" allowfullscreen></iframe>
</div>

View File

@ -6,6 +6,63 @@ Nodejs Code Signing module
<iframe width="320" height="180" src="https://www.youtube.com/embed/xteKscs_Jgo" frameborder="0" allowfullscreen></iframe>
</div>
MeshCentral comes with authenticode.js, you can run it like this:
```bash
node node_modules/meshcentral/authenticode-js
```
and you will get
```
MeshCentral Authenticode Tool.
Usage:
node authenticode.js [command] [options]
Commands:
info: Show information about an executable.
--exe [file] Required executable to view information.
--json Show information in JSON format.
sign: Sign an executable.
--exe [file] Required executable to sign.
--out [file] Resulting signed executable.
--pem [pemfile] Certificate & private key to sign the executable with.
--desc [description] Description string to embbed into signature.
--url [url] URL to embbed into signature.
--hash [method] Default is SHA384, possible value: MD5, SHA224, SHA256, SHA384 or SHA512.
--time [url] The time signing server URL.
--proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://
unsign: Remove the signature from the executable.
--exe [file] Required executable to un-sign.
--out [file] Resulting executable with signature removed.
createcert: Create a code signging self-signed certificate and key.
--out [pemfile] Required certificate file to create.
--cn [value] Required certificate common name.
--country [value] Certificate country name.
--state [value] Certificate state name.
--locality [value] Certificate locality name.
--org [value] Certificate organization name.
--ou [value] Certificate organization unit name.
--serial [value] Certificate serial number.
timestamp: Add a signed timestamp to an already signed executable.
--exe [file] Required executable to sign.
--out [file] Resulting signed executable.
--time [url] The time signing server URL.
--proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://
Note that certificate PEM files must first have the signing certificate,
followed by all certificates that form the trust chain.
When doing sign/unsign, you can also change resource properties of the generated file.
--filedescription [value]
--fileversion [value]
--internalname [value]
--legalcopyright [value]
--originalfilename [value]
--productname [value]
--productversion [value]
```
## Automatic Agent Code Signing
If you want to self-sign the mesh agent so you can whitelist the software in your AV, and lock it to your server and organization.
@ -13,3 +70,6 @@ If you want to self-sign the mesh agent so you can whitelist the software in you
<div class="video-wrapper">
<iframe width="320" height="180" src="https://www.youtube.com/embed/qMAestNgCwc" frameborder="0" allowfullscreen></iframe>
</div>
!!!note
If you generate your private key on windows with use `BEGIN PRIVATE KEY` and openssl needs `BEGIN RSA PRIVATE KEY` you can convert your private key to rsa private key using `openssl rsa -in server.key -out server_new.key`

View File

@ -6,7 +6,22 @@ Make sure you understand how MeshCentral works with your browser using chrome de
<iframe width="320" height="180" src="https://www.youtube.com/embed/3vI4URd3VzU" frameborder="0" allowfullscreen></iframe>
</div>
## Enabling trace in your browser Dev Tools
## MeshCentral Server
### Useful config.js settings
<https://github.com/Ylianst/MeshCentral/blob/master/meshcentral-config-schema.json>
```json
"AgentsInRAM": false,
"AgentUpdateBlockSize": 2048,
"agentUpdateSystem": 1,
"noAgentUpdate": 1,
"WsCompression": false,
"AgentWsCompression": false,
```
### Enabling trace in your browser Dev Tools
`Trace=1` as a parameter in chrome dev tools for debugging
@ -34,7 +49,7 @@ If you want to change node to meshcentral in journalctl, add this to /etc/system
SyslogIdentifier=meshcentral
```
## Server: Logging it all
### Logging it all
To log everything that's possible, prepare the log directory.
@ -84,7 +99,7 @@ You'll then have 3 files:
`log.txt` will now log everything in the Trace tab
## Restricting server to specific IP(s)
### Restricting server to specific IP(s)
When doing debugging on my development server, I use this line in the settings section to block all agent connections except the agent I want:
@ -94,8 +109,101 @@ When doing debugging on my development server, I use this line in the settings s
Of course, this is just for debugging.
## Finding system ID types
### Finding system ID types
<https://serverurl/meshagents> aka trying figure out what this is
![ID](images/determine-id.png)
### Pull down cert .crt file from internet
[See #1662](https://github.com/Ylianst/MeshCentral/issues/1662#issuecomment-666559391) We have run into this challenge before, where our .crt file expired and then all our agents were unable to connect. In our case, the TLS cert was available on the internet, and thus, we were able to use these commands to update it:
```bash
echo -n \| openssl s_client -connect yourdomain.com:443 2> /dev/null\| sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /opt/meshcentral/meshcentral-data/webserver-cert-public.crt
service meshcentral restart
```
## MeshAgent
### Agent Debug Logs to server
This automatically downloads all agent error logs into `meshcentral-data/agenterrorlogs.txt`
[Set](https://github.com/Ylianst/MeshCentral/blob/aa58afcc3a5d738177ab7a7b6d0228d72af82b85/meshcentral-config-schema.json#L100) in `config.json`
```json
"agentLogDump": true
```
### Determine Agent capabilities
On the server goto the agents console tab. Type:
```
info
```
### Useful MeshAgent.msh flags
<https://github.com/Ylianst/MeshAgent/blob/master/meshcore/agentcore.h#L190>
```json
controlChannelDebug=1
logUpdate=1
```
### Obtain generated .msh File
If you need a trick to get the .msh file, you can add ?debug=1 to the URL and click "Add Agent", there will be an extra link to download it.
### MeshAgent Commands
```
MeshAgent run
MeshAgent dbTool.js list
```
Forcing Core version from Cmdline
* Download meschore.js and rename to CoreModule.js and put it alongside MeshAgent.exe
* Stop MeshAgent service
* Run `MeshAgent.exe dbTool.js import CoreModule`
### On the fly Patching MeshAgent
[MeshAgent#89 (comment)](https://github.com/Ylianst/MeshAgent/issues/89#issuecomment-949901720)
There are two ways to do this... When debugging, and making changes, you can modify the .js file directly, and just save it in the same folder as the agent binary... The agent will use the .js file from disc if it's there, if it's newer than the one compiled in the binary. You don't even need to restart the agent. You can just clear the core, and reload the core.....
When you are satisfied with your changes to the .js file, you can use the clipboard, in the following fashion:
```bash
meshagent -exec "require('clipboard').nativeAddCompressedModule('foo');process.exit();"
```
if the file you modified isn't in the same folder as the agent binary, you can use the following command if you don't want to move the file, and edit it directly in the modules folder:
```bash
meshagent -exec "setModulePath('pathToFolder');require('clipboard').nativeAddCompressedModule('foo');process.exit();"
```
This command is just like the previous, except it searches for modules in the path specified.
Just substitute foo, with the name of the module that you modified. It will load the module from disc, compress it, and save it into the clipboard.. So you can just load up your editor for ILibDuktape_Polyfills.c, and find where that particular module is defined... and paste directly from the clipboard... The clipboard will contain all the necessary C code to uncompress and load the module.
If the compressed result is relatively long, it will auto break it up into multiple lines to work around an issue with visual studio's maximum string literal limitations.
### Agent Debugging using MeshCore JS Debugger
[(#119)](https://github.com/Ylianst/MeshAgent/issues/119) How to test changes to the meshagent and recompile them.
* Copy duktape-debugger.js to the mesh directory on the target machine.
* From the console tab of the agent, enter this command, substituting the port number you want to use instead of 9999
`eval "attachDebugger({ webport: 9999 })"`
Then open your browser to http://localhost:9999 or whatever port you used.
!!!note
If you pause the debugger, and happen to forget about it, the agent will automatically kill itself and restart because it will think that a thread is stuck. Default debugger timeout is 10 minutes, you may find a log entry saved to disk saying "Microstack Thread STUCK", or something similar.

View File

@ -0,0 +1,26 @@
# Device Tabs
## General
### 7 Day Power State
Legend
1. Black color: device is powered om
2. purple color: device is in sleep state
3. blue/green color : device is connected trough amt/cira, but not powered on
4. grey color: device is powered off
![](images/7daypowerstate.png)
## Desktop
## Terminal
## Files
## Events
## Details
## Console

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -111,6 +111,16 @@ Click on any computer and go into the “Desktop” and “Files” tabs to remo
For advance users with console/command line interface experience, go into “Terminal” to perform scripting or quick tasks with CLI tools.
### Desktop Control
<div class="video-wrapper">
<iframe width="320" height="180" src="https://www.youtube.com/embed/alknFiojQPM" frameborder="0" allowfullscreen></iframe>
</div>
Depending on how the agent is connected to the server, there are multiple methods to remote control. Mesh Agent, RDP, and AMT
For RDP connections, if you have previously saved the credentials that is usable by all users on the system. If you want to remove those saved credentials that's under the `General Tab` > `Credentials`. Click pen to clear them.
## Server Certificate
As seen in the previous chapter, MeshCentral is setup with a self-signed certificate by default and the web browser will issue a warning concerning the validity of the certificate.
@ -448,6 +458,34 @@ This first line will load many of the “meshcentral-data” files into the data
Note that MeshCentral does not currently support placing a Lets Encrypt certificate in the database. Generally, one would use a reverse proxy with Lets Encrypt support and TLS offload in the reverse proxy and then run MeshCentral in state-less mode in a Docket container.
## Commandline Options
In general, doing `--option value` is the same as adding `"option": value` in the settings section of the config.json.
Here are the most common options found by running `meshcentral --help`
```
Run as a background service
--install/uninstall Install MeshCentral as a background service.
--start/stop/restart Control MeshCentral background service.
Run standalone, console application
--user [username] Always login as [username] if account exists.
--port [number] Web server port number.
--redirport [number] Creates an additional HTTP server to redirect users to the HTTPS server.
--exactports Server must run with correct ports or exit.
--noagentupdate Server will not update mesh agent native binaries.
--nedbtodb Transfer all NeDB records into current database.
--listuserids Show a list of a user identifiers in the database.
--cert [name], (country), (org) Create a web server certificate with [name] server name.
country and organization can optionally be set.
Server recovery commands, use only when MeshCentral is offline.
--createaccount [userid] Create a new user account.
--resetaccount [userid] Unlock an account, disable 2FA and set a new account password.
--adminaccount [userid] Promote account to site administrator.
```
## TLS Offloading
A good way for MeshCentral to handle a high traffic is to setup a TLS offload device at front of the server that takes care of doing all the TLS negotiation and encryption so that the server could offload this. There are many vendors who offer TLS or SSL offload as a software module (Nginx* or Apache*) so please contact your network administrator for the best solution that suits your setup.
@ -517,8 +555,6 @@ If you successfully setup a Lets Encrypt certificate using the Lets Encryp
If Lets Encrypt works for you, please consider donating to them as they provide a critical service to the Internet community.
## Server IP filtering
For improved security, its good to limit access to MeshCentral with IP address. For example, we want to allow mesh agents and Intel AMT computers to connect from anywhere, but whitelist IP address for users that we allow to access MeshCentral.
@ -706,12 +742,19 @@ MeshCentral supports the local device group allowing devices that do not have an
![](images/2022-05-31-10-30-42.png)
To enable SSH support, add this line to the domain section of your config.json:
```json
"ssh": true
```
Video Walkthru
<div class="video-wrapper">
<iframe width="320" height="180" src="https://www.youtube.com/embed/TtW5-g6SeZQ" frameborder="0" allowfullscreen></iframe>
</div>
### Raritan and WebPowerSwitch with Relay
In addition to local device groups, the IP-KVM/Power switch device group was also improved to support a MeshAgent as a relay. This is big news for Raritan IP-KVM switch owners as you can now monitor your IP-KVM ports and access them remotely from the Internet. The same can be done with WebPowerSwitch allowing full out-of-band remote access to devices from anywhere in the world.
@ -724,6 +767,12 @@ In addition to local device groups, the IP-KVM/Power switch device group was als
## NGINX Reverse-Proxy Setup
### Video Walkthru
<div class="video-wrapper">
<iframe width="320" height="180" src="https://www.youtube.com/embed/YSmiLyKSX2I" frameborder="0" allowfullscreen></iframe>
</div>
Sometimes its useful to setup MeshCentral with a reverse-proxy in front of it. This is useful if you need to host many services on a single public IP address, if you want to offload TLS and perform extra web caching. In this section we will setup NGINX, a popular reverse-proxy, in front of MeshCentral. NGNIX is available at: https://www.nginx.com/
![](images/2022-05-19-00-23-11.png)
@ -1180,6 +1229,38 @@ mongorestore --archive=backup.archive
This will re-import the database from the backup. You can then start MeshCentral again.
### Backup to Google Drive
```bash
sudo systemctl stop meshcentral.service
nano /opt/meshcentral/meshcentral-data/config.json
```
Remove underscored items
![](images/2022-06-17-15-56-14.png)
```bash
sudo systemctl start meshcentral.service
sudo systemctl status meshcentral.service
```
Log into your MC:
![](images/2022-06-17-15-56-55.png)
![](images/2022-06-17-15-57-03.png)
Create desktop app
![](images/2022-06-17-15-57-15.png)
Enter the Client ID and Client Secret into MC
![](images/2022-06-17-15-57-30.png)
![](images/2022-06-17-15-57-52.png)
## HashiCorp Vault support
MeshCentral has built-in support for HashiCorp Vault so that all configuration and certificates used by MeshCentral are retrieved from a Vault server. Vault is a secret store server and when used with MeshCentral, the MeshCentral server will not be storing any secrets locally. You can get started with Vault here: https://www.vaultproject.io/
@ -1685,3 +1766,11 @@ su -c '/bin/bash -i' myOtherUser
```
This will run bash in interactive mode and work correctly.
#### SSH and SFTP integration to the Terminal
MeshCentral has built-in web-based integration of SSH in the "Termina" tab and SFTP in the "Files" tab.
<div class="video-wrapper">
<iframe width="320" height="180" src="https://www.youtube.com/embed/7qAbl2OuZEU" frameborder="0" allowfullscreen></iframe>
</div>

View File

@ -0,0 +1,9 @@
# Plugins
## Installation
1. Enable plugins in the configuration and restart MC as described.
2. Log into MC as full administrator.
3. Go my `My Server` -> `Plugins`, hit the Download plugin button.
4. A dialog opens requesting an URL, put in: <https://github.com/ryanblenis/MeshCentral-ScriptTask>
5. The plugin pops up in the plugin list below the download button, you can now configure and enable/disable it.

View File

@ -416,6 +416,12 @@ In this example, the CIRA setup script was run on a remote computer. After the s
## IDE Redirection
## Video Walkthru
<div class="video-wrapper">
<iframe width="320" height="180" src="https://www.youtube.com/embed/iG73j7Toz1c" frameborder="0" allowfullscreen></iframe>
</div>
MeshCmd has all the code needed to perform Intel AMT IDE Redirection from the command line. This allows disk images on the administrators computer to be remotely mounted to an Intel AMT computer. You need to start with a floppy disk .img file and/or an .iso CDROM file.
![](images/2022-05-15-15-42-01.png)

View File

@ -9,6 +9,7 @@ nav:
- MeshCentral2:
- 'MeshCentral2 Guide': 'meshcentral/index.md'
- 'All Configuration Options': 'meshcentral/config.md'
- 'Device Tabs': 'meshcentral/devicetabs.md'
- 'Tokens': 'meshcentral/tokens.md'
- 'Assistant': 'meshcentral/assistant.md'
- 'Code Signing': 'meshcentral/codesigning.md'

View File

@ -0,0 +1 @@
Please place Powerpoints and slides here

View File

@ -1889,7 +1889,10 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
if (!device.intelamt) { device.intelamt = {}; }
if ((command.intelamt.Versions != null) && (typeof command.intelamt.Versions == 'object')) {
if ((command.intelamt.Versions.AMT != null) && (typeof command.intelamt.Versions.AMT == 'string') && (command.intelamt.Versions.AMT.length < 12) && (device.intelamt.ver != command.intelamt.Versions.AMT)) { changes.push('AMT version'); device.intelamt.ver = command.intelamt.Versions.AMT; change = 1; log = 1; }
if ((command.intelamt.Versions.Sku != null) && (typeof command.intelamt.Versions.Sku == 'string')) { var sku = parseInt(command.intelamt.Versions.Sku); if (device.intelamt.sku !== command.intelamt.sku) { device.intelamt.sku = sku; change = 1; log = 1; } }
if ((command.intelamt.Versions.Sku != null) && (typeof command.intelamt.Versions.Sku == 'string')) {
const sku = parseInt(command.intelamt.Versions.Sku);
if (device.intelamt.sku !== sku) { device.intelamt.sku = sku; change = 1; log = 1; }
}
}
if ((command.intelamt.ProvisioningState != null) && (typeof command.intelamt.ProvisioningState == 'number') && (device.intelamt.state != command.intelamt.ProvisioningState)) { changes.push('AMT state'); device.intelamt.state = command.intelamt.ProvisioningState; change = 1; log = 1; }
if ((command.intelamt.Flags != null) && (typeof command.intelamt.Flags == 'number') && (device.intelamt.flags != command.intelamt.Flags)) {

View File

@ -101,6 +101,8 @@
"agentCoreDump": { "type": "boolean", "default": false, "description": "Automatically activates and transfers any agent crash dump files to the server in meshcentral-data/coredumps." },
"agentCoreDumpUsers": { "type": "array", "description": "List of non-administrator users that have access to mesh agent crash dumps." },
"agentSignLock": { "type": "boolean", "default": false, "description": "When code signing an agent using authenticode, lock the agent to only allow connection to this server. (This is in testing, the default value will change to true in the future)." },
"agentTimeStampServer": { "type": [ "boolean", "string" ], "default": "http://timestamp.comodoca.com/authenticode", "description": "The time stamping server to use when code signing Windows executables. When set to false, the executables are not time stamped." },
"agentTimeStampProxy": { "type": [ "boolean", "string" ], "description": "The HTTP proxy to use when contacting the time stamping server, if false, no proxy is used. By default, the npmproxy value is used." },
"ignoreAgentHashCheck": { "type": [ "boolean", "string" ], "default": false, "description": "When true, the agent no longer checked the TLS certificate of the server. This should be used for debugging only. You can also set this to a comma seperated list of IP addresses to ignore, for example: \"192.168.2.100,192.168.1.0/24\"." },
"exactPorts": { "type": "boolean", "default": false, "description": "When set to true, MeshCentral will only grab the required TCP listening ports or fail. It will not try to use the next available port of it's busy." },
"allowLoginToken": { "type": "boolean", "default": false },
@ -542,6 +544,20 @@
"backgroundColor": { "type": "string", "default": null, "description": "Background color, valid values are RBG in format 0,0,0 to 255,255,255 or format #000000 to #FFFFFF." }
}
},
"agentFileInfo": {
"type": "object",
"additionalProperties": false,
"description": "Use this section to set resource metadata of the Windows agents prior to signing. In Windows, you can right-click and select properties to view these values.",
"properties": {
"fileDescription": { "type": "string", "description": "Executable file description." },
"fileVersion": { "type": "string", "description": "Executable file version, generally in the form of 1.2.3.4." },
"internalName": { "type": "string", "description": "Executable internal name." },
"legalCopyright": { "type": "string", "description": "Executable legal copyright." },
"originalFilename": { "type": "string", "description": "Executable original file name." },
"productName": { "type": "string", "description": "Executable product name." },
"productVersion": { "type": "string", "description": "Executable product version, generally in the form of 1.2.3.4." }
}
},
"assistantCustomization": {
"type": "object",
"additionalProperties": false,

View File

@ -145,7 +145,7 @@ function CreateMeshCentralServer(config, args) {
if ((obj.args.help == true) || (obj.args['?'] == true)) {
console.log('MeshCentral v' + getCurrentVersion() + ', remote computer management web portal.');
console.log('This software is open source under Apache 2.0 license.');
console.log('Details at: https://www.meshcommander.com/meshcentral2\r\n');
console.log('Details at: https://www.meshcentral.com\r\n');
if ((obj.platform == 'win32') || (obj.platform == 'linux')) {
console.log('Run as a background service');
console.log(' --install/uninstall Install MeshCentral as a background service.');
@ -1617,275 +1617,282 @@ function CreateMeshCentralServer(config, args) {
// Load the list of mesh agents and install scripts
if ((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true)) { for (i in obj.meshAgentsArchitectureNumbers) { obj.meshAgentsArchitectureNumbers[i].update = false; } }
obj.updateMeshAgentsTable(obj.config.domains[''], function () {
obj.updateMeshAgentInstallScripts();
obj.signMeshAgents(obj.config.domains[''], function () {
obj.updateMeshAgentsTable(obj.config.domains[''], function () {
obj.updateMeshAgentInstallScripts();
// Setup and start the web server
obj.crypto.randomBytes(48, function (err, buf) {
// Setup Mesh Multi-Server if needed
obj.multiServer = require('./multiserver.js').CreateMultiServer(obj, obj.args);
if (obj.multiServer != null) {
if ((obj.db.databaseType != 3) || (obj.db.changeStream != true)) { console.log("ERROR: Multi-server support requires use of MongoDB with ReplicaSet and ChangeStream enabled."); process.exit(0); return; }
if (typeof obj.args.sessionkey != 'string') { console.log("ERROR: Multi-server support requires \"SessionKey\" be set in the settings section of config.json, same key for all servers."); process.exit(0); return; }
obj.serverId = obj.multiServer.serverid;
for (var serverid in obj.config.peers.servers) { obj.peerConnectivityByNode[serverid] = {}; }
}
// Setup and start the web server
obj.crypto.randomBytes(48, function (err, buf) {
// Setup Mesh Multi-Server if needed
obj.multiServer = require('./multiserver.js').CreateMultiServer(obj, obj.args);
if (obj.multiServer != null) {
if ((obj.db.databaseType != 3) || (obj.db.changeStream != true)) { console.log("ERROR: Multi-server support requires use of MongoDB with ReplicaSet and ChangeStream enabled."); process.exit(0); return; }
if (typeof obj.args.sessionkey != 'string') { console.log("ERROR: Multi-server support requires \"SessionKey\" be set in the settings section of config.json, same key for all servers."); process.exit(0); return; }
obj.serverId = obj.multiServer.serverid;
for (var serverid in obj.config.peers.servers) { obj.peerConnectivityByNode[serverid] = {}; }
}
// If the server is set to "nousers", allow only loopback unless IP filter is set
if ((obj.args.nousers == true) && (obj.args.userallowedip == null)) { obj.args.userallowedip = "::1,127.0.0.1"; }
// If the server is set to "nousers", allow only loopback unless IP filter is set
if ((obj.args.nousers == true) && (obj.args.userallowedip == null)) { obj.args.userallowedip = "::1,127.0.0.1"; }
// Set the session length to 60 minutes if not set and set a random key if needed
if ((obj.args.sessiontime != null) && ((typeof obj.args.sessiontime != 'number') || (obj.args.sessiontime < 1))) { delete obj.args.sessiontime; }
if (typeof obj.args.sessionkey != 'string') { obj.args.sessionkey = buf.toString('hex').toUpperCase(); }
// Set the session length to 60 minutes if not set and set a random key if needed
if ((obj.args.sessiontime != null) && ((typeof obj.args.sessiontime != 'number') || (obj.args.sessiontime < 1))) { delete obj.args.sessiontime; }
if (typeof obj.args.sessionkey != 'string') { obj.args.sessionkey = buf.toString('hex').toUpperCase(); }
// Create MQTT Broker to hook into webserver and mpsserver
if ((typeof obj.config.settings.mqtt == 'object') && (typeof obj.config.settings.mqtt.auth == 'object') && (typeof obj.config.settings.mqtt.auth.keyid == 'string') && (typeof obj.config.settings.mqtt.auth.key == 'string')) { obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj, obj.db, obj.args); }
// Create MQTT Broker to hook into webserver and mpsserver
if ((typeof obj.config.settings.mqtt == 'object') && (typeof obj.config.settings.mqtt.auth == 'object') && (typeof obj.config.settings.mqtt.auth.keyid == 'string') && (typeof obj.config.settings.mqtt.auth.key == 'string')) { obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj, obj.db, obj.args); }
// Start the web server and if needed, the redirection web server.
obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates, obj.StartEx5);
if (obj.redirserver != null) { obj.redirserver.hookMainWebServer(obj.certificates); }
// Start the web server and if needed, the redirection web server.
obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates, obj.StartEx5);
if (obj.redirserver != null) { obj.redirserver.hookMainWebServer(obj.certificates); }
// Update proxy certificates
if (obj.supportsProxyCertificatesRequest == true) { obj.updateProxyCertificates(true); }
// Start the HTTP relay web server if needed
if ((obj.args.relayport != null) && (obj.args.relayport != 0)) {
obj.webrelayserver = require('./webrelayserver.js').CreateWebRelayServer(obj, obj.db, obj.args, obj.certificates, function () { });
}
// Setup the Intel AMT event handler
obj.amtEventHandler = require('./amtevents.js').CreateAmtEventsHandler(obj);
// Update proxy certificates
if (obj.supportsProxyCertificatesRequest == true) { obj.updateProxyCertificates(true); }
// Setup the Intel AMT local network scanner
if (obj.args.wanonly != true) {
if (obj.args.amtscanner != false) { obj.amtScanner = require('./amtscanner.js').CreateAmtScanner(obj).start(); }
if (obj.args.meshscanner != false) { obj.meshScanner = require('./meshscanner.js').CreateMeshScanner(obj).start(); }
}
// Setup the Intel AMT event handler
obj.amtEventHandler = require('./amtevents.js').CreateAmtEventsHandler(obj);
// Setup and start the MPS server
obj.mpsserver = require('./mpsserver.js').CreateMpsServer(obj, obj.db, obj.args, obj.certificates);
// Setup the Intel AMT local network scanner
if (obj.args.wanonly != true) {
if (obj.args.amtscanner != false) { obj.amtScanner = require('./amtscanner.js').CreateAmtScanner(obj).start(); }
if (obj.args.meshscanner != false) { obj.meshScanner = require('./meshscanner.js').CreateMeshScanner(obj).start(); }
}
// Setup the Intel AMT manager
if (obj.args.amtmanager !== false) {
obj.amtManager = require('./amtmanager.js').CreateAmtManager(obj);
}
// Setup and start the MPS server
obj.mpsserver = require('./mpsserver.js').CreateMpsServer(obj, obj.db, obj.args, obj.certificates);
// Setup and start the legacy swarm server
if ((obj.certificates.swarmserver != null) && (obj.args.swarmport != null) && (obj.args.swarmport !== 0)) {
obj.swarmserver = require('./swarmserver.js').CreateSwarmServer(obj, obj.db, obj.args, obj.certificates);
}
// Setup the Intel AMT manager
if (obj.args.amtmanager !== false) {
obj.amtManager = require('./amtmanager.js').CreateAmtManager(obj);
}
// Setup the main email server
if (obj.config.sendgrid != null) {
// Sendgrid server
obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
obj.mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
} else if (obj.config.smtp != null) {
// SMTP server
obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
obj.mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
} else if (obj.config.sendmail != null) {
// Sendmail server
obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
obj.mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
}
// Setup and start the legacy swarm server
if ((obj.certificates.swarmserver != null) && (obj.args.swarmport != null) && (obj.args.swarmport !== 0)) {
obj.swarmserver = require('./swarmserver.js').CreateSwarmServer(obj, obj.db, obj.args, obj.certificates);
}
// Setup the email server for each domain
for (i in obj.config.domains) {
if (obj.config.domains[i].sendgrid != null) {
// Setup the main email server
if (obj.config.sendgrid != null) {
// Sendgrid server
obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
obj.config.domains[i].mailserver.verify();
obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
obj.mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
} else if ((obj.config.domains[i].smtp != null) && (obj.config.domains[i].smtp.host != null) && (obj.config.domains[i].smtp.from != null)) {
} else if (obj.config.smtp != null) {
// SMTP server
obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
obj.config.domains[i].mailserver.verify();
obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
obj.mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
} else if (obj.config.domains[i].sendmail != null) {
} else if (obj.config.sendmail != null) {
// Sendmail server
obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
obj.config.domains[i].mailserver.verify();
obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
obj.mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
} else {
// Setup the parent mail server for this domain
if (obj.mailserver != null) { obj.config.domains[i].mailserver = obj.mailserver; }
}
}
// Setup SMS gateway
if (config.sms != null) {
obj.smsserver = require('./meshsms.js').CreateMeshSMS(obj);
if ((obj.smsserver != null) && (obj.args.lanonly == true)) { addServerWarning("SMS gateway has limited use in LAN mode.", 19); }
}
// Setup web based push notifications
if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) {
obj.webpush = require('web-push');
var vapidKeys = null;
try { vapidKeys = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, 'vapid.json')).toString()); } catch (ex) { }
if ((vapidKeys == null) || (typeof vapidKeys.publicKey != 'string') || (typeof vapidKeys.privateKey != 'string')) {
console.log("Generating web push VAPID keys...");
vapidKeys = obj.webpush.generateVAPIDKeys();
obj.fs.writeFileSync(obj.path.join(obj.datapath, 'vapid.json'), JSON.stringify(vapidKeys));
}
obj.webpush.vapidPublicKey = vapidKeys.publicKey;
obj.webpush.setVapidDetails('mailto:' + config.settings.webpush.email, vapidKeys.publicKey, vapidKeys.privateKey);
if (typeof config.settings.webpush.gcmapi == 'string') { webpush.setGCMAPIKey(config.settings.webpush.gcmapi); }
}
// Setup Firebase
if ((config.firebase != null) && (typeof config.firebase.senderid == 'string') && (typeof config.firebase.serverkey == 'string')) {
obj.firebase = require('./firebase').CreateFirebase(obj, config.firebase.senderid, config.firebase.serverkey);
} else if ((typeof config.firebaserelay == 'object') && (typeof config.firebaserelay.url == 'string')) {
// Setup the push messaging relay
obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key);
} else if (obj.config.settings.publicpushnotifications === true) {
// Setup the Firebase push messaging relay using https://meshcentral.com, this is the public push notification server.
obj.firebase = require('./firebase').CreateFirebaseRelay(obj, 'https://meshcentral.com/firebaserelay.aspx');
}
// Start periodic maintenance
obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
// Dispatch an event that the server is now running
obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' });
// Plugin hook. Need to run something at server startup? This is the place.
if (obj.pluginHandler) { obj.pluginHandler.callHook('server_startup'); }
// Setup the login cookie encryption key
if ((obj.config) && (obj.config.settings) && (typeof obj.config.settings.logincookieencryptionkey == 'string')) {
// We have a string, hash it and use that as a key
try { obj.loginCookieEncryptionKey = Buffer.from(obj.config.settings.logincookieencryptionkey, 'hex'); } catch (ex) { }
if ((obj.loginCookieEncryptionKey == null) || (obj.loginCookieEncryptionKey.length != 80)) { addServerWarning("Invalid \"LoginCookieEncryptionKey\" in config.json.", 20); obj.loginCookieEncryptionKey = null; }
}
// Login cookie encryption key not set, use one from the database
if (obj.loginCookieEncryptionKey == null) {
obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
// Setup the email server for each domain
for (i in obj.config.domains) {
if (obj.config.domains[i].sendgrid != null) {
// Sendgrid server
obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
obj.config.domains[i].mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
} else if ((obj.config.domains[i].smtp != null) && (obj.config.domains[i].smtp.host != null) && (obj.config.domains[i].smtp.from != null)) {
// SMTP server
obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
obj.config.domains[i].mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
} else if (obj.config.domains[i].sendmail != null) {
// Sendmail server
obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
obj.config.domains[i].mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
} else {
obj.loginCookieEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() });
// Setup the parent mail server for this domain
if (obj.mailserver != null) { obj.config.domains[i].mailserver = obj.mailserver; }
}
}
// Setup SMS gateway
if (config.sms != null) {
obj.smsserver = require('./meshsms.js').CreateMeshSMS(obj);
if ((obj.smsserver != null) && (obj.args.lanonly == true)) { addServerWarning("SMS gateway has limited use in LAN mode.", 19); }
}
// Setup web based push notifications
if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) {
obj.webpush = require('web-push');
var vapidKeys = null;
try { vapidKeys = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, 'vapid.json')).toString()); } catch (ex) { }
if ((vapidKeys == null) || (typeof vapidKeys.publicKey != 'string') || (typeof vapidKeys.privateKey != 'string')) {
console.log("Generating web push VAPID keys...");
vapidKeys = obj.webpush.generateVAPIDKeys();
obj.fs.writeFileSync(obj.path.join(obj.datapath, 'vapid.json'), JSON.stringify(vapidKeys));
}
obj.webpush.vapidPublicKey = vapidKeys.publicKey;
obj.webpush.setVapidDetails('mailto:' + config.settings.webpush.email, vapidKeys.publicKey, vapidKeys.privateKey);
if (typeof config.settings.webpush.gcmapi == 'string') { webpush.setGCMAPIKey(config.settings.webpush.gcmapi); }
}
// Setup Firebase
if ((config.firebase != null) && (typeof config.firebase.senderid == 'string') && (typeof config.firebase.serverkey == 'string')) {
obj.firebase = require('./firebase').CreateFirebase(obj, config.firebase.senderid, config.firebase.serverkey);
} else if ((typeof config.firebaserelay == 'object') && (typeof config.firebaserelay.url == 'string')) {
// Setup the push messaging relay
obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key);
} else if (obj.config.settings.publicpushnotifications === true) {
// Setup the Firebase push messaging relay using https://meshcentral.com, this is the public push notification server.
obj.firebase = require('./firebase').CreateFirebaseRelay(obj, 'https://meshcentral.com/firebaserelay.aspx');
}
// Start periodic maintenance
obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
// Dispatch an event that the server is now running
obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' });
// Plugin hook. Need to run something at server startup? This is the place.
if (obj.pluginHandler) { obj.pluginHandler.callHook('server_startup'); }
// Setup the login cookie encryption key
if ((obj.config) && (obj.config.settings) && (typeof obj.config.settings.logincookieencryptionkey == 'string')) {
// We have a string, hash it and use that as a key
try { obj.loginCookieEncryptionKey = Buffer.from(obj.config.settings.logincookieencryptionkey, 'hex'); } catch (ex) { }
if ((obj.loginCookieEncryptionKey == null) || (obj.loginCookieEncryptionKey.length != 80)) { addServerWarning("Invalid \"LoginCookieEncryptionKey\" in config.json.", 20); obj.loginCookieEncryptionKey = null; }
}
// Login cookie encryption key not set, use one from the database
if (obj.loginCookieEncryptionKey == null) {
obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
} else {
obj.loginCookieEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() });
}
});
}
// Load the invitation link encryption key from the database
obj.db.Get('InvitationLinkEncryptionKey', function (err, docs) {
if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (docs[0].key.length >= 160)) {
obj.invitationLinkEncryptionKey = Buffer.from(docs[0].key, 'hex');
} else {
obj.invitationLinkEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'InvitationLinkEncryptionKey', key: obj.invitationLinkEncryptionKey.toString('hex'), time: Date.now() });
}
});
}
// Load the invitation link encryption key from the database
obj.db.Get('InvitationLinkEncryptionKey', function (err, docs) {
if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (docs[0].key.length >= 160)) {
obj.invitationLinkEncryptionKey = Buffer.from(docs[0].key, 'hex');
} else {
obj.invitationLinkEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'InvitationLinkEncryptionKey', key: obj.invitationLinkEncryptionKey.toString('hex'), time: Date.now() });
// Setup Intel AMT hello server
if ((typeof config.settings.amtprovisioningserver == 'object') && (typeof config.settings.amtprovisioningserver.devicegroup == 'string') && (typeof config.settings.amtprovisioningserver.newmebxpassword == 'string') && (typeof config.settings.amtprovisioningserver.trustedfqdn == 'string') && (typeof config.settings.amtprovisioningserver.ip == 'string')) {
obj.amtProvisioningServer = require('./amtprovisioningserver').CreateAmtProvisioningServer(obj, config.settings.amtprovisioningserver);
}
});
// Setup Intel AMT hello server
if ((typeof config.settings.amtprovisioningserver == 'object') && (typeof config.settings.amtprovisioningserver.devicegroup == 'string') && (typeof config.settings.amtprovisioningserver.newmebxpassword == 'string') && (typeof config.settings.amtprovisioningserver.trustedfqdn == 'string') && (typeof config.settings.amtprovisioningserver.ip == 'string')) {
obj.amtProvisioningServer = require('./amtprovisioningserver').CreateAmtProvisioningServer(obj, config.settings.amtprovisioningserver);
}
// Start collecting server stats every 5 minutes
obj.trafficStats = obj.webserver.getTrafficStats();
setInterval(function () {
obj.serverStatsCounter++;
var hours = 720; // Start with all events lasting 30 days.
if (((obj.serverStatsCounter) % 2) == 1) { hours = 3; } // Half of the event get removed after 3 hours.
else if ((Math.floor(obj.serverStatsCounter / 2) % 2) == 1) { hours = 8; } // Another half of the event get removed after 8 hours.
else if ((Math.floor(obj.serverStatsCounter / 4) % 2) == 1) { hours = 24; } // Another half of the event get removed after 24 hours.
else if ((Math.floor(obj.serverStatsCounter / 8) % 2) == 1) { hours = 48; } // Another half of the event get removed after 48 hours.
else if ((Math.floor(obj.serverStatsCounter / 16) % 2) == 1) { hours = 72; } // Another half of the event get removed after 72 hours.
const expire = new Date();
expire.setTime(expire.getTime() + (60 * 60 * 1000 * hours));
// Start collecting server stats every 5 minutes
obj.trafficStats = obj.webserver.getTrafficStats();
setInterval(function () {
obj.serverStatsCounter++;
var hours = 720; // Start with all events lasting 30 days.
if (((obj.serverStatsCounter) % 2) == 1) { hours = 3; } // Half of the event get removed after 3 hours.
else if ((Math.floor(obj.serverStatsCounter / 2) % 2) == 1) { hours = 8; } // Another half of the event get removed after 8 hours.
else if ((Math.floor(obj.serverStatsCounter / 4) % 2) == 1) { hours = 24; } // Another half of the event get removed after 24 hours.
else if ((Math.floor(obj.serverStatsCounter / 8) % 2) == 1) { hours = 48; } // Another half of the event get removed after 48 hours.
else if ((Math.floor(obj.serverStatsCounter / 16) % 2) == 1) { hours = 72; } // Another half of the event get removed after 72 hours.
const expire = new Date();
expire.setTime(expire.getTime() + (60 * 60 * 1000 * hours));
// Get traffic data
var trafficStats = obj.webserver.getTrafficDelta(obj.trafficStats);
obj.trafficStats = trafficStats.current;
// Get traffic data
var trafficStats = obj.webserver.getTrafficDelta(obj.trafficStats);
obj.trafficStats = trafficStats.current;
var data = {
time: new Date(),
expire: expire,
mem: process.memoryUsage(),
conn: {
ca: Object.keys(obj.webserver.wsagents).length,
cu: Object.keys(obj.webserver.wssessions).length,
us: Object.keys(obj.webserver.wssessions2).length,
rs: obj.webserver.relaySessionCount
},
traffic: trafficStats.delta
};
try { data.cpu = require('os').loadavg(); } catch (ex) { }
if (obj.mpsserver != null) {
data.conn.am = 0;
for (var i in obj.mpsserver.ciraConnections) { data.conn.am += obj.mpsserver.ciraConnections[i].length; }
}
if (obj.firstStats === true) { delete obj.firstStats; data.first = true; }
if (obj.multiServer != null) { data.s = obj.multiServer.serverid; }
obj.db.SetServerStats(data); // Save the stats to the database
obj.DispatchEvent(['*'], obj, { action: 'servertimelinestats', data: data }); // Event the server stats
}, 300000);
var data = {
time: new Date(),
expire: expire,
mem: process.memoryUsage(),
conn: {
ca: Object.keys(obj.webserver.wsagents).length,
cu: Object.keys(obj.webserver.wssessions).length,
us: Object.keys(obj.webserver.wssessions2).length,
rs: obj.webserver.relaySessionCount
},
traffic: trafficStats.delta
};
try { data.cpu = require('os').loadavg(); } catch (ex) { }
if (obj.mpsserver != null) {
data.conn.am = 0;
for (var i in obj.mpsserver.ciraConnections) { data.conn.am += obj.mpsserver.ciraConnections[i].length; }
obj.debug('main', "Server started");
if (obj.args.nousers == true) { obj.updateServerState('nousers', '1'); }
obj.updateServerState('state', "running");
// Setup auto-backup defaults
if (obj.config.settings.autobackup == null) { obj.config.settings.autobackup = { backupintervalhours: 24, keeplastdaysbackup: 10 }; }
else if (obj.config.settings.autobackup === false) { delete obj.config.settings.autobackup; }
// Check that autobackup path is not within the "meshcentral-data" folder.
if ((typeof obj.config.settings.autobackup == 'object') && (typeof obj.config.settings.autobackup.backuppath == 'string') && (obj.path.normalize(obj.config.settings.autobackup.backuppath).startsWith(obj.path.normalize(obj.datapath)))) {
addServerWarning("Backup path can't be set within meshcentral-data folder, backup settings ignored.", 21);
delete obj.config.settings.autobackup;
}
if (obj.firstStats === true) { delete obj.firstStats; data.first = true; }
if (obj.multiServer != null) { data.s = obj.multiServer.serverid; }
obj.db.SetServerStats(data); // Save the stats to the database
obj.DispatchEvent(['*'], obj, { action: 'servertimelinestats', data: data }); // Event the server stats
}, 300000);
obj.debug('main', "Server started");
if (obj.args.nousers == true) { obj.updateServerState('nousers', '1'); }
obj.updateServerState('state', "running");
// Load Intel AMT passwords from the "amtactivation.log" file
obj.loadAmtActivationLogPasswords(function (amtPasswords) {
obj.amtPasswords = amtPasswords;
});
// Setup auto-backup defaults
if (obj.config.settings.autobackup == null) { obj.config.settings.autobackup = { backupintervalhours: 24, keeplastdaysbackup: 10 }; }
else if (obj.config.settings.autobackup === false) { delete obj.config.settings.autobackup; }
// Check that autobackup path is not within the "meshcentral-data" folder.
if ((typeof obj.config.settings.autobackup == 'object') && (typeof obj.config.settings.autobackup.backuppath == 'string') && (obj.path.normalize(obj.config.settings.autobackup.backuppath).startsWith(obj.path.normalize(obj.datapath)))) {
addServerWarning("Backup path can't be set within meshcentral-data folder, backup settings ignored.", 21);
delete obj.config.settings.autobackup;
}
// Load Intel AMT passwords from the "amtactivation.log" file
obj.loadAmtActivationLogPasswords(function (amtPasswords) {
obj.amtPasswords = amtPasswords;
});
// Setup users that can see all device groups
if (typeof obj.config.settings.managealldevicegroups == 'string') { obj.config.settings.managealldevicegroups = obj.config.settings.managealldevicegroups.split(','); }
else if (Array.isArray(obj.config.settings.managealldevicegroups) == false) { obj.config.settings.managealldevicegroups = []; }
for (i in obj.config.domains) {
if (Array.isArray(obj.config.domains[i].managealldevicegroups)) {
for (var j in obj.config.domains[i].managealldevicegroups) {
if (typeof obj.config.domains[i].managealldevicegroups[j] == 'string') {
const u = 'user/' + i + '/' + obj.config.domains[i].managealldevicegroups[j];
if (obj.config.settings.managealldevicegroups.indexOf(u) == -1) { obj.config.settings.managealldevicegroups.push(u); }
// Setup users that can see all device groups
if (typeof obj.config.settings.managealldevicegroups == 'string') { obj.config.settings.managealldevicegroups = obj.config.settings.managealldevicegroups.split(','); }
else if (Array.isArray(obj.config.settings.managealldevicegroups) == false) { obj.config.settings.managealldevicegroups = []; }
for (i in obj.config.domains) {
if (Array.isArray(obj.config.domains[i].managealldevicegroups)) {
for (var j in obj.config.domains[i].managealldevicegroups) {
if (typeof obj.config.domains[i].managealldevicegroups[j] == 'string') {
const u = 'user/' + i + '/' + obj.config.domains[i].managealldevicegroups[j];
if (obj.config.settings.managealldevicegroups.indexOf(u) == -1) { obj.config.settings.managealldevicegroups.push(u); }
}
}
}
}
}
obj.config.settings.managealldevicegroups.sort();
obj.config.settings.managealldevicegroups.sort();
// Start watchdog timer if needed
// This is used to monitor if NodeJS is servicing IO correctly or getting held up a lot. Add this line to the settings section of config.json
// "watchDog": { "interval": 100, "timeout": 150 }
// This will check every 100ms, if the timer is more than 150ms late, it will warn.
if ((typeof config.settings.watchdog == 'object') && (typeof config.settings.watchdog.interval == 'number') && (typeof config.settings.watchdog.timeout == 'number') && (config.settings.watchdog.interval >= 50) && (config.settings.watchdog.timeout >= 50)) {
obj.watchdogtime = Date.now();
obj.watchdogmax = 0;
obj.watchdogmaxtime = null;
obj.watchdogtable = [];
obj.watchdog = setInterval(function () {
const now = Date.now(), delta = now - obj.watchdogtime - config.settings.watchdog.interval;
if (delta > obj.watchdogmax) { obj.watchdogmax = delta; obj.watchdogmaxtime = new Date().toLocaleString(); }
if (delta > config.settings.watchdog.timeout) {
const msg = obj.common.format("Watchdog timer timeout, {0}ms.", delta);
obj.watchdogtable.push(new Date().toLocaleString() + ', ' + delta + 'ms');
while (obj.watchdogtable.length > 10) { obj.watchdogtable.shift(); }
obj.debug('main', msg);
try {
var errlogpath = null;
if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
obj.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + msg + '\r\n');
} catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
}
obj.watchdogtime = now;
}, config.settings.watchdog.interval);
obj.debug('main', "Started watchdog timer.");
}
// Start watchdog timer if needed
// This is used to monitor if NodeJS is servicing IO correctly or getting held up a lot. Add this line to the settings section of config.json
// "watchDog": { "interval": 100, "timeout": 150 }
// This will check every 100ms, if the timer is more than 150ms late, it will warn.
if ((typeof config.settings.watchdog == 'object') && (typeof config.settings.watchdog.interval == 'number') && (typeof config.settings.watchdog.timeout == 'number') && (config.settings.watchdog.interval >= 50) && (config.settings.watchdog.timeout >= 50)) {
obj.watchdogtime = Date.now();
obj.watchdogmax = 0;
obj.watchdogmaxtime = null;
obj.watchdogtable = [];
obj.watchdog = setInterval(function () {
const now = Date.now(), delta = now - obj.watchdogtime - config.settings.watchdog.interval;
if (delta > obj.watchdogmax) { obj.watchdogmax = delta; obj.watchdogmaxtime = new Date().toLocaleString(); }
if (delta > config.settings.watchdog.timeout) {
const msg = obj.common.format("Watchdog timer timeout, {0}ms.", delta);
obj.watchdogtable.push(new Date().toLocaleString() + ', ' + delta + 'ms');
while (obj.watchdogtable.length > 10) { obj.watchdogtable.shift(); }
obj.debug('main', msg);
try {
var errlogpath = null;
if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
obj.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + msg + '\r\n');
} catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
}
obj.watchdogtime = now;
}, config.settings.watchdog.interval);
obj.debug('main', "Started watchdog timer.");
}
});
});
});
};
@ -2841,46 +2848,60 @@ function CreateMeshCentralServer(config, args) {
10006: { id: 10006, localname: 'MeshCentralAssistant.exe', rname: 'MeshCentralAssistant.exe', desc: 'MeshCentral Assistant for Windows', update: false, amt: false, platform: 'win32' } // MeshCentral Assistant
};
// Update the list of available mesh agents
obj.updateMeshAgentsTable = function (domain, func) {
// Sign windows agents
obj.signMeshAgents = function (domain, func) {
// Setup the domain is specified
var objx = domain, suffix = '';
if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
// Check if a custom agent signing certificate is available
var agentSignCertInfo = require('./authenticode.js').loadCertificates([ obj.path.join(obj.datapath, 'agentsigningcert.pem') ]);
var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
// If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
agentSignCertInfo = {
cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert) ]
extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
}
}
if (agentSignCertInfo == null) { func(); return; } // No code signing certificate, nothing to do.
// Setup the domain is specified
var objx = domain, suffix = '';
if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
// Generate the agent signature description and URL
var serverSignedAgentsPath, signDesc, signUrl;
if (agentSignCertInfo != null) {
serverSignedAgentsPath = obj.path.join(obj.datapath, 'signedagents' + suffix);
signDesc = (domain.title ? domain.title : agentSignCertInfo.cert.subject.hash);
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
signUrl = 'https://' + ((domain.dns != null) ? domain.dns : obj.certificates.CommonName);
if (httpsPort != 443) { signUrl += ':' + httpsPort; }
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += '/';
signUrl += '/' + xdomain;
const serverSignedAgentsPath = obj.path.join(obj.datapath, 'signedagents' + suffix);
const signDesc = (domain.title ? domain.title : agentSignCertInfo.cert.subject.hash);
const httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
var httpsHost = ((domain.dns != null) ? domain.dns : obj.certificates.CommonName);
if (obj.args.agentaliasdns != null) { httpsHost = obj.args.agentaliasdns; }
var signUrl = 'https://' + httpsHost;
if (httpsPort != 443) { signUrl += ':' + httpsPort; }
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += '/';
signUrl += '/' + xdomain;
// If requested, lock the agent to this server
if (obj.config.settings.agentsignlock) { signUrl += '?ServerID=' + obj.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert).toUpperCase(); }
}
// If requested, lock the agent to this server
if (obj.config.settings.agentsignlock) { signUrl += '?ServerID=' + obj.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert).toUpperCase(); }
// Load agent information file. This includes the data & time of the agent.
const agentInfo = [];
try { agentInfo = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'hashagents.json'), 'utf8')); } catch (ex) { }
// Setup the time server
var timeStampUrl = 'http://timestamp.comodoca.com/authenticode';
if (args.agenttimestampserver === false) { timeStampUrl = null; }
else if (typeof args.agenttimestampserver == 'string') { timeStampUrl = args.agenttimestampserver; }
// Setup the time server proxy
var timeStampProxy = null;
if (typeof args.agenttimestampproxy == 'string') { timeStampProxy = args.agenttimestampproxy; }
else if ((args.agenttimestampproxy !== false) && (typeof args.npmproxy == 'string')) { timeStampProxy = args.npmproxy; }
// Setup the pending operations counter
var pendingOperations = 1;
var archcount = 0;
for (var archid in obj.meshAgentsArchitectureNumbers) {
if (obj.meshAgentsArchitectureNumbers[archid].codesign !== true) continue;
var agentpath;
if (domain.id == '') {
// Load all agents when processing the default domain
@ -2893,46 +2914,137 @@ function CreateMeshCentralServer(config, args) {
if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
}
// Open the original agent with authenticode
const signeedagentpath = obj.path.join(serverSignedAgentsPath, obj.meshAgentsArchitectureNumbers[archid].localname);
const originalAgent = require('./authenticode.js').createAuthenticodeHandler(agentpath);
if (originalAgent != null) {
// Check if the agent is already signed correctly
const destinationAgent = require('./authenticode.js').createAuthenticodeHandler(signeedagentpath);
var destinationAgentOk = (
(destinationAgent != null) &&
(destinationAgent.fileHashSigned != null) &&
(Buffer.compare(destinationAgent.fileHashSigned, destinationAgent.fileHashActual) == 0) &&
(destinationAgent.signingAttribs.indexOf(signUrl) >= 0) &&
(destinationAgent.signingAttribs.indexOf(signDesc) >= 0)
);
if (destinationAgent != null) {
// If the agent is signed correctly, look to see if the resources in the destination agent are correct
var orgVersionStrings = originalAgent.getVersionInfo();
if (destinationAgentOk == true) {
var versionStrings = destinationAgent.getVersionInfo();
var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
for (var i in versionProperties) {
const prop = versionProperties[i], propl = prop.toLowerCase();
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo[propl] == 'string')) {
if (domain.agentfileinfo[propl] != versionStrings[prop]) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
} else {
if (orgVersionStrings[prop] != versionStrings[prop]) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
}
}
}
// If everything looks ok, runs a hash of the original and destination agent skipping the CRC, resource and signature blocks. If different, sign the agent again.
if ((destinationAgentOk == true) && (originalAgent.getHashNoResources('sha384').compare(destinationAgent.getHashNoResources('sha384')) != 0)) { destinationAgentOk = false; }
// We are done comparing the destination agent, close it.
destinationAgent.close();
}
if (destinationAgentOk == false) {
// If not signed correctly, sign it. First, create the server signed agent folder if needed
try { obj.fs.mkdirSync(serverSignedAgentsPath); } catch (ex) { }
const xagentSignedFunc = function agentSignedFunc(err, size) {
if (err == null) {
// Agent was signed succesfuly
console.log(obj.common.format('Code signed agent {0}.', agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname));
} else {
// Failed to sign agent
addServerWarning('Failed to sign agent \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [ agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err ]);
}
if (--pendingOperations === 0) { agentSignedFunc.func(); }
}
pendingOperations++;
xagentSignedFunc.func = func;
xagentSignedFunc.objx = objx;
xagentSignedFunc.archid = archid;
xagentSignedFunc.signeedagentpath = signeedagentpath;
// Parse the resources in the executable and make any required changes
var resChanges = false, versionStrings = null;
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object')) {
versionStrings = originalAgent.getVersionInfo();
var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
for (var i in versionProperties) {
const prop = versionProperties[i], propl = prop.toLowerCase();
if (domain.agentfileinfo[propl] && (domain.agentfileinfo[propl] != versionStrings[prop])) { versionStrings[prop] = domain.agentfileinfo[propl]; resChanges = true; }
}
if (resChanges == true) { originalAgent.setVersionInfo(versionStrings); }
}
const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone
obj.debug('main', "Code signing agent with arguments: " + JSON.stringify(signingArguments));
if (resChanges == false) {
// Sign the agent the simple way, without changing any resources.
originalAgent.sign(agentSignCertInfo, signingArguments, xagentSignedFunc);
} else {
// Change the agent resources and sign the agent, this is a much more involved process.
// NOTE: This is experimental and could corupt the agent.
originalAgent.writeExecutable(signingArguments, agentSignCertInfo, xagentSignedFunc);
}
} else {
// Signed agent is already ok, use it.
originalAgent.close();
}
}
}
if (--pendingOperations === 0) { func(); }
}
// Update the list of available mesh agents
obj.updateMeshAgentsTable = function (domain, func) {
// Check if a custom agent signing certificate is available
var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
// If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
agentSignCertInfo = {
cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
}
}
// Setup the domain is specified
var objx = domain, suffix = '';
if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
// Load agent information file. This includes the data & time of the agent.
const agentInfo = [];
try { agentInfo = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'hashagents.json'), 'utf8')); } catch (ex) { }
var archcount = 0;
for (var archid in obj.meshAgentsArchitectureNumbers) {
var agentpath;
if (domain.id == '') {
// Load all agents when processing the default domain
agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
const agentpath2 = obj.path.join(obj.datapath, 'signedagents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; } // If the agent is present in "meshcentral-data/signedagents", use that one instead.
const agentpath3 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
if (obj.fs.existsSync(agentpath3)) { agentpath = agentpath3; } // If the agent is present in "meshcentral-data/agents", use that one instead.
} else {
// When processing an extra domain, only load agents that are specific to that domain
var agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
}
// Fetch agent binary information
var stats = null;
try { stats = obj.fs.statSync(agentpath); } catch (ex) { }
if ((stats == null)) continue; // If this agent does not exist, skip it.
// Check if we need to sign this agent, if so, check if it's already been signed
if ((obj.meshAgentsArchitectureNumbers[archid].codesign === true) && (agentSignCertInfo != null)) {
// Open the original agent with authenticode
var signeedagentpath = obj.path.join(serverSignedAgentsPath, obj.meshAgentsArchitectureNumbers[archid].localname);
const originalAgent = require('./authenticode.js').createAuthenticodeHandler(agentpath);
if (originalAgent != null) {
// Check if the agent is already signed correctly
const destinationAgent = require('./authenticode.js').createAuthenticodeHandler(signeedagentpath);
var destinationAgentOk = (
(destinationAgent != null) &&
(destinationAgent.fileHashSigned != null) &&
(Buffer.compare(destinationAgent.fileHashSigned, destinationAgent.fileHashActual) == 0) &&
((Buffer.compare(destinationAgent.fileHashSigned, originalAgent.getHash(destinationAgent.fileHashAlgo))) == 0) &&
(destinationAgent.signingAttribs.indexOf(signUrl) >= 0) &&
(destinationAgent.signingAttribs.indexOf(signDesc) >= 0)
);
if (destinationAgent != null) { destinationAgent.close(); }
if (destinationAgentOk == false) {
// If not signed correctly, sign it. First, create the server signed agent folder if needed
try { obj.fs.mkdirSync(serverSignedAgentsPath); } catch (ex) { }
if (originalAgent.sign(agentSignCertInfo, { out: signeedagentpath, desc: signDesc, url: signUrl }) == true) {
// Agent was signed succesfuly
agentpath = signeedagentpath;
console.log(obj.common.format('Code signed agent {0}.', obj.meshAgentsArchitectureNumbers[archid].localname));
} else {
console.log(obj.common.format('Failed to sign agent {0}.', obj.meshAgentsArchitectureNumbers[archid].localname));
}
} else {
// Signed agent is already ok, use it.
agentpath = signeedagentpath;
}
originalAgent.close();
}
}
// Setup agent information
archcount++;
objx.meshAgentBinaries[archid] = Object.assign({}, obj.meshAgentsArchitectureNumbers[archid]);
@ -2952,7 +3064,7 @@ function CreateMeshCentralServer(config, args) {
// Load the agent with a random msh added to it.
const outStream = new require('stream').Duplex();
outStream.meshAgentBinary = objx.meshAgentBinaries[archid];
outStream.meshAgentBinary.randomMsh = agentSignCertInfo.cert.subject.hash;
if (agentSignCertInfo) { outStream.meshAgentBinary.randomMsh = agentSignCertInfo.cert.subject.hash; } else { outStream.meshAgentBinary.randomMsh = obj.crypto.randomBytes(16).toString('hex'); }
outStream.bufferList = [];
outStream._write = function (chunk, encoding, callback) { this.bufferList.push(chunk); if (callback) callback(); }; // Append the chuck.
outStream._read = function (size) { }; // Do nothing, this is not going to be called.

View File

@ -43,6 +43,8 @@ const MESHRIGHT_ADMIN = 0xFFFFFFFF;
// 10 = Web-RDP
// 11 = Web-SSH
// 12 = Web-VNC
// 13 = Web-SSH-Files
// 14 = Web-TCP
// 100 = Intel AMT WSMAN
// 101 = Intel AMT Redirection
// 200 = Messenger

View File

@ -696,7 +696,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
// Request a list of all nodes
db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', command.id, function (err, docs) {
//console.log(docs);
//console.log(err, docs);
if (docs == null) { docs = []; }
parent.common.unEscapeAllLinksFieldName(docs);
@ -2867,8 +2867,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
if ((command.actiontype == 400) && common.validateInt(command.time, 1, 30000)) { routeCommandToNode({ action: 'msg', type: 'console', nodeid: node._id, value: 'flash ' + command.time }, MESHRIGHT_ADMIN, 0); }
if ((command.actiontype == 401) && common.validateInt(command.time, 1, 30000)) { routeCommandToNode({ action: 'msg', type: 'console', nodeid: node._id, value: 'vibrate ' + command.time }, MESHRIGHT_ADMIN, 0); }
} else {
// Check we have the rights to delete this device
if ((rights & MESHRIGHT_RESETOFF) == 0) return;
// Check we have the rights to perform this operation
if ((command.actiontype == 302) && ((rights & MESHRIGHT_WAKEDEVICE) == 0)) return; // This is a Intel AMT power on operation, check if we have WAKE rights
if ((command.actiontype != 302) && ((rights & MESHRIGHT_RESETOFF) == 0)) return; // For all other operations, check that we have RESET/OFF rights
// If this device is connected on MQTT, send a power action.
if ((parent.parent.mqttbroker != null) && (command.actiontype >= 0) && (command.actiontype <= 4)) { parent.parent.mqttbroker.publish(node._id, 'powerAction', ['', '', 'poweroff', 'reset', 'sleep'][command.actiontype]); }
@ -2913,7 +2914,6 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
// Perform input validation
try {
if (common.validateStrArray(command.nodeids, 1, 256) == false) { err = "Invalid nodeids"; } // Check nodeids
else if (common.validateString(command.title, 1, 512) == false) { err = "Invalid title"; } // Check title
else if (common.validateString(command.msg, 1, 4096) == false) { err = "Invalid message"; } // Check message
else {
var nodeids = [];
@ -2928,6 +2928,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
break;
}
// Check the title, if needed, use a default one
if (common.validateString(command.title, 1, 512) == false) { delete command.title } // Check title
if ((command.title == null) && (typeof domain.notificationmessages == 'object') && (typeof domain.notificationmessages.title == 'string')) { command.title = domain.notificationmessages.title; }
if ((command.title == null) && (typeof domain.title == 'string')) { command.title = domain.title; }
if (command.title == null) { command.title = "MeshCentral"; }
for (i in command.nodeids) {
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeids[i], function (node, rights, visible) {
@ -3020,6 +3026,22 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
}
}
if ((typeof command.httpport == 'number') && (command.httpport > 0) && (command.httpport < 65536)) {
if ((command.httpport == 80) && (node.httpport != null)) {
delete node.httpport; change = 1; changes.push('httpport'); // Delete the HTTP port
} else {
node.httpport = command.httpport; change = 1; changes.push('httpport'); // Set the HTTP port
}
}
if ((typeof command.httpsport == 'number') && (command.httpsport > 0) && (command.httpsport < 65536)) {
if ((command.httpsport == 443) && (node.httpsport != null)) {
delete node.httpsport; change = 1; changes.push('httpsport'); // Delete the HTTPS port
} else {
node.httpsport = command.httpsport; change = 1; changes.push('httpsport'); // Set the HTTPS port
}
}
if ((typeof command.ssh == 'number') && (command.ssh == 0)) {
if ((node.ssh != null) && (node.ssh[user._id] != null)) { delete node.ssh[user._id]; change = 1; changes.push('ssh'); } // Delete the SSH cendentials
}

View File

@ -484,7 +484,10 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
// We are under the limit, create the new device.
// Node is not in the database, add it. Credentials will be empty until added by the user.
var device = { type: 'node', mtype: 1, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: socket.tag.name, icon: (socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: domainid, intelamt: { user: (typeof socket.tag.meiState.amtuser == 'string') ? socket.tag.meiState.amtuser : '', pass: (typeof socket.tag.meiState.amtpass == 'string') ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if (socket.tag.meiState != null) {
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
}
obj.db.Set(device);
// Event the new node
@ -506,7 +509,10 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
// Node is not in the database, add it. Credentials will be empty until added by the user.
var device = { type: 'node', mtype: 1, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: socket.tag.name, icon: (socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: domainid, intelamt: { user: (typeof socket.tag.meiState.amtuser == 'string') ? socket.tag.meiState.amtuser : '', pass: (typeof socket.tag.meiState.amtpass == 'string') ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if (socket.tag.meiState != null) {
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
}
obj.db.Set(device);
// Event the new node
@ -707,7 +713,10 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
// We are under the limit, create the new device.
// Node is not in the database, add it. Credentials will be empty until added by the user.
var device = { type: 'node', mtype: 1, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: socket.tag.name, icon: (socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: initialMesh.domain, intelamt: { user: (typeof socket.tag.meiState.amtuser == 'string') ? socket.tag.meiState.amtuser : '', pass: (typeof socket.tag.meiState.amtpass == 'string') ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if (socket.tag.meiState != null) {
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
}
obj.db.Set(device);
// Event the new node
@ -733,7 +742,10 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
// Node is not in the database, add it. Credentials will be empty until added by the user.
var device = { type: 'node', mtype: 1, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: socket.tag.name, icon: (socket.tag.meiState && socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: initialMesh.domain, intelamt: { user: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtuser == 'string')) ? socket.tag.meiState.amtuser : '', pass: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtpass == 'string')) ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
if ((socket.tag.meiState != null) && (typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if (socket.tag.meiState != null) {
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
}
obj.db.Set(device);
// Event the new node
@ -793,7 +805,10 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
// Node is not in the database, add it. Credentials will be empty until added by the user.
var device = { type: 'node', mtype: 2, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: hostname, icon: (socket.tag.meiState && socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: initialMesh.domain, intelamt: { user: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtuser == 'string')) ? socket.tag.meiState.amtuser : '', pass: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtpass == 'string')) ? socket.tag.meiState.amtpass : '', tls: 0, state: 2, agent: { id: 0, caps: 0 } } };
if ((socket.tag.meiState != null) && (typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if (socket.tag.meiState != null) {
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
}
obj.db.Set(device);
// Event the new node
@ -827,9 +842,11 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
// Node is not in the database, add it. Credentials will be empty until added by the user.
var device = { type: 'node', mtype: 2, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: hostname, icon: (socket.tag.meiState && socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: initialMesh.domain, agent: { ver: 0, id: 0, caps: 0 }, intelamt: { uuid: socket.tag.SystemId, user: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtuser == 'string')) ? socket.tag.meiState.amtuser : '', pass: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtpass == 'string')) ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
if ((socket.tag.meiState != null) && (typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if (socket.tag.meiState != null) {
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
}
obj.db.Set(device);
console.log('ADDED', device);
// Event the new node
addedDeviceCount++;

View File

@ -1,6 +1,6 @@
{
"name": "meshcentral",
"version": "1.0.36",
"version": "1.0.45",
"keywords": [
"Remote Device Management",
"Remote Device Monitoring",

File diff suppressed because one or more lines are too long

View File

@ -533,6 +533,9 @@ var CreateAgentRemoteDesktop = function (canvasid, scrolldiv) {
else if (Button == obj.MouseButton.RIGHT) { Button = obj.MouseButton.LEFT; }
}
// Reverse mouse wheel if needed
if (obj.ReverseMouseWheel) { Delta = -1 * Delta; }
var MouseMsg = "";
if (Action == obj.KeyAction.DBLCLICK) {
MouseMsg = String.fromCharCode(0x00, obj.InputType.MOUSE, 0x00, 0x0A, 0x00, 0x88, ((X / 256) & 0xFF), (X & 0xFF), ((Y / 256) & 0xFF), (Y & 0xFF));

File diff suppressed because one or more lines are too long

View File

@ -18,6 +18,10 @@ var CreateRDPDesktop = function (canvasid) {
obj.m.onClipboardChanged = null;
obj.onConsoleMessageChange = null;
var xMouseCursorActive = true;
var xMouseCursorCurrent = 'default';
obj.mouseCursorActive = function (x) { if (xMouseCursorActive == x) return; xMouseCursorActive = x; obj.CanvasId.style.cursor = ((x == true) ? xMouseCursorCurrent : 'default'); }
function mouseButtonMap(button) {
// Swap mouse buttons if needed
if (obj.m.SwapMouse === true) return [2, 0, 1, 0, 0][button];
@ -75,6 +79,12 @@ var CreateRDPDesktop = function (canvasid) {
obj.render.update(bitmap);
break;
}
case 'rdp-pointer': {
var pointer = msg[1];
xMouseCursorCurrent = pointer;
if (xMouseCursorActive) { obj.CanvasId.style.cursor = pointer; }
break;
}
case 'rdp-close': {
obj.Stop();
break;
@ -198,6 +208,7 @@ var CreateRDPDesktop = function (canvasid) {
if (obj.mouseNagleTimer != null) { clearTimeout(obj.mouseNagleTimer); obj.mouseNagleTimer = null; }
var delta = 0;
if (e.detail) { delta = (e.detail * 120); } else if (e.wheelDelta) { delta = (e.wheelDelta * 3); }
if (obj.m.ReverseMouseWheel) { delta = -1 * delta; } // Reverse the mouse wheel
if (delta != 0) { obj.socket.send(JSON.stringify(['wheel', m.x, m.y, delta, false, false])); }
e.preventDefault();
return false;

File diff suppressed because one or more lines are too long

View File

@ -931,6 +931,10 @@ var CreateAmtRemoteDesktop = function (divid, scrolldiv) {
else if (typeof e.detail == 'number') { v = -1 * e.detail; }
else if (typeof e.wheelDelta == 'number') { v = e.wheelDelta; }
if (v == 0) return;
// Reverse mouse wheel if needed
if (obj.ReverseMouseWheel) { v = -1 * v; }
var tmpmask = obj.buttonmask;
obj.buttonmask |= (1 << ((v > 0) ? 3 : 4));
obj.mousemove(e, 1);

View File

@ -1,7 +1,7 @@
@ECHO OFF
CD ..\translate
%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js minifyall
%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js translateall
%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js extractall
%LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node translate.js minifyall
%LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node translate.js translateall
%LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node translate.js extractall
DEL ..\emails\translations\*-min_*
Pause

View File

@ -22,7 +22,7 @@ var fs = require('fs');
var type = require('./type');
var log = require('./log');
var tls = require('tls');
var crypto = require('crypto');
//var crypto = require('crypto');
var events = require('events');
/**

View File

@ -186,7 +186,7 @@ var FastPathUpdateType = {
FASTPATH_UPDATETYPE_PTR_NULL : 0x5,
FASTPATH_UPDATETYPE_PTR_DEFAULT : 0x6,
FASTPATH_UPDATETYPE_PTR_POSITION : 0x8,
FASTPATH_UPDATETYPE_COLOR : 0x9,
FASTPATH_UPDATETYPE_COLOR : 0x9, // Mouse cursor
FASTPATH_UPDATETYPE_CACHED : 0xA,
FASTPATH_UPDATETYPE_POINTER : 0xB
};
@ -1075,7 +1075,7 @@ function clipPDU() {
* @param opt {object} type option
* @returns {type.Component}
*/
function fastPathBitmapUpdateDataPDU (opt) {
function fastPathBitmapUpdateDataPDU(opt) {
var self = {
__FASTPATH_UPDATE_TYPE__ : FastPathUpdateType.FASTPATH_UPDATETYPE_BITMAP,
header : new type.UInt16Le(FastPathUpdateType.FASTPATH_UPDATETYPE_BITMAP, { constant : true }),
@ -1093,13 +1093,67 @@ function fastPathBitmapUpdateDataPDU (opt) {
return new type.Component(self, opt);
}
// This is a table of cursorid to cursor name.
// Created by movering the mouse over this page: https://www.w3schools.com/csSref/tryit.asp?filename=trycss_cursor
const cursorIdTable = {
// Normal style mouse cursor
903013897: 'alias',
370524792: 'all-scroll',
853046751: 'cell',
2101250798: 'col-resize',
703681364: 'copy',
992638936: 'crosshair',
1539083673: 'ew-resize',
1919796298: 'grab',
1010243511: 'grabbing',
1247283057: 'help',
1390892051: 'none',
885751489: 'not-allowed',
1732952247: 'row-resize',
747144997: 'url',
2018345610: 'zoom-in',
347367048: 'zoom-out',
1872942890: 'default',
1737852989: 'text',
1932827019: 'ns-resize',
1884471290: 'nesw-resize',
1204065391: 'nwse-resize',
2030531519: 'progress',
1050842114: 'pointer',
// Black style cursors
1258195498: 'default',
219484254: 'all-scroll',
399295089: 'text',
1912613597: 'wait',
864127801: 'ew-resize',
23245044: 'nesw-resize',
1966995494: 'not-allowed',
1873216615: 'help',
255126408: 'nesw-resize',
157191894: 'ns-resize',
1768446509: 'pointer',
1032011501: 'crosshair'
}
function fastPathPointerUpdateDataPDU(opt, cursorId, cursorStr) {
var self = {
__FASTPATH_UPDATE_TYPE__: FastPathUpdateType.FASTPATH_UPDATETYPE_COLOR,
header: new type.UInt16Le(FastPathUpdateType.FASTPATH_UPDATETYPE_COLOR, { constant: true }),
cursorId: cursorId,
cursorStr: cursorStr
};
return new type.Component(self, opt);
}
/**
* @see http://msdn.microsoft.com/en-us/library/cc240622.aspx
* @param updateData {type.Component}
* @param opt {object} type option
* @returns {type.Component}
*/
function fastPathUpdatePDU (updateData, opt) {
function fastPathUpdatePDU(updateData, opt) {
var self = {
updateHeader : new type.UInt8( function () {
return self.updateData.obj.__FASTPATH_UPDATE_TYPE__;
@ -1116,12 +1170,27 @@ function fastPathUpdatePDU (updateData, opt) {
}) };
switch (self.updateHeader.value & 0xf) {
case FastPathUpdateType.FASTPATH_UPDATETYPE_BITMAP:
self.updateData = fastPathBitmapUpdateDataPDU(options).read(s);
break;
default:
self.updateData = new type.BinaryString(null, options).read(s);
log.debug('unknown fast path pdu type ' + (self.updateHeader.value & 0xf));
case FastPathUpdateType.FASTPATH_UPDATETYPE_BITMAP: {
self.updateData = fastPathBitmapUpdateDataPDU(options).read(s);
break;
}
case FastPathUpdateType.FASTPATH_UPDATETYPE_COLOR: {
var data = new type.BinaryString(null, options).read(s);
// Hash the data to get a cursor id.
// This is a hack since the cursor bitmap is sent but we can't use that, we has hash the bitmap and use that as a hint as to what cursor we need to display
const hasher = require('crypto').createHash('sha384');
hasher.update(data.value);
const cursorid = Math.abs(hasher.digest().readInt32BE(0));
const cursorStr = cursorIdTable[cursorid];
//if (cursorStr == null) { console.log('Unknown cursorId: ' + cursorid); }
self.updateData = fastPathPointerUpdateDataPDU(options, cursorid, cursorStr);
break;
}
default: {
self.updateData = new type.BinaryString(null, options).read(s);
log.debug('unknown fast path pdu type ' + (self.updateHeader.value & 0xf));
}
}
})
};

View File

@ -268,11 +268,16 @@ Client.prototype.recvServerFontMapPDU = function(s) {
*/
Client.prototype.recvFastPath = function (secFlag, s) {
while (s.availableLength() > 0) {
var pdu = data.fastPathUpdatePDU().read(s);
var pdu = data.fastPathUpdatePDU().read(s);
switch (pdu.obj.updateHeader.value & 0xf) {
case data.FastPathUpdateType.FASTPATH_UPDATETYPE_BITMAP:
this.emit('bitmap', pdu.obj.updateData.obj.rectangles.obj);
break;
case data.FastPathUpdateType.FASTPATH_UPDATETYPE_BITMAP: {
this.emit('bitmap', pdu.obj.updateData.obj.rectangles.obj);
break;
}
case data.FastPathUpdateType.FASTPATH_UPDATETYPE_COLOR: {
this.emit('pointer', pdu.obj.updateData.obj.cursorId, pdu.obj.updateData.obj.cursorStr);
break;
}
default:
}
}

View File

@ -160,6 +160,8 @@ function RdpClient(config) {
}).on('close', function () {
self.connected = false;
self.emit('close');
}).on('pointer', function (cursorId, cursorStr) {
self.emit('pointer', cursorId, cursorStr);
}).on('bitmap', function (bitmaps) {
for (var bitmap in bitmaps) {
var bitmapData = bitmaps[bitmap].obj.bitmapDataStream.value;

View File

@ -142,7 +142,7 @@ module.exports.CreateRedirServer = function (parent, db, args, func) {
obj.parent.updateServerState('redirect-port', port);
func(obj.port);
}).on('error', function (err) {
if ((err.code == 'EACCES') && (port < 65535)) { StartRedirServer(port + 1); } else { console.log(err); func(obj.port); }
if ((err.code == 'EACCES') && (port < 65535)) { StartRedirServer(port + 1, addr); } else { console.log(err); func(obj.port); }
});
}

View File

@ -10,7 +10,7 @@
"_WANonly": true,
"_LANonly": true,
"_maintenanceMode": true,
"_certificatePrivateKeyPassword": [ "password1", "password2" ],
"_certificatePrivateKeyPassword": ["password1", "password2"],
"_sessionTime": 60,
"_sessionKey": "MyReallySecretPassword1",
"_sessionSameSite": "strict",
@ -38,6 +38,8 @@
"_agentCoreDump": true,
"_agentCoreDumpUsers": "user1,user2",
"_agentSignLock": true,
"_agentTimeStampServer": "http://timestamp.digicert.com",
"_agentTimeStampProxy": "http://1.2.3.4:80",
"_ignoreAgentHashCheck": true,
"_exactPorts": true,
"_allowLoginToken": true,
@ -72,14 +74,15 @@
"_webPush": { "email": "xxxxx@xxxxx.com" },
"_publicPushNotifications": true,
"_desktopMultiplex": true,
"_ipBlockedUserRedirect": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"_userAllowedIP": "127.0.0.1,192.168.1.0/24",
"_userBlockedIP": "127.0.0.1,::1,192.168.0.100",
"_agentAllowedIP": "192.168.0.100/24",
"_agentBlockedIP": "127.0.0.1,::1",
"_authLog": "c:\\temp\\auth.log",
"_InterUserMessaging": [ "user//admin" ],
"_manageAllDeviceGroups": [ "user//admin" ],
"_manageCrossDomain": [ "user//admin" ],
"_InterUserMessaging": ["user//admin"],
"_manageAllDeviceGroups": ["user//admin"],
"_manageCrossDomain": ["user//admin"],
"_localDiscovery": {
"name": "Local server name",
"info": "Information about this server"
@ -91,6 +94,7 @@
"_mpsAliasPort": 4433,
"_mpsAliasHost": "mps.mydomain.com",
"_mpsTlsOffload": true,
"_mpsHighSecurity": true,
"_no2FactorAuth": true,
"_runOnServerStarted": "c:\\tmp\\mcstart.bat",
"_runOnServerUpdated": "c:\\tmp\\mcupdate.bat",
@ -162,19 +166,21 @@
"title2": "Servername",
"_titlePicture": "title-sample.png",
"_loginPicture": "title-sample.png",
"_rootRedirect": "https://www.youtube.com/watch?v=Gs069dndIYk",
"_mobileSite": false,
"_unknownUserRootRedirect": "https://www.youtube.com/watch?v=2Q_ZzBGPdqE",
"_nightMode": 1,
"_userQuota": 1048576,
"_meshQuota": 248576,
"_loginKey": ["abc", "123"],
"_agentKey": ["abc", "123"],
"_ipkvm": false,
"minify": true,
"_guestDeviceSharing" : false,
"_AutoRemoveInactiveDevices": 37,
"_DeviceSearchBarServerAndClientName": false,
"_loginKey": [ "abc", "123" ],
"_agentKey": [ "abc", "123" ],
"_newAccounts": true,
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
"_userNameIsEmail": true,
"_newAccountEmailDomains": [ "sample.com" ],
"_newAccountsRights": [ "nonewgroups", "notools" ],
"_newAccountEmailDomains": ["sample.com"],
"_newAccountsRights": ["nonewgroups", "notools"],
"_welcomeText": "Sample Text on Login Page.",
"_welcomePicture": "mainwelcome.jpg",
"_welcomePictureFullScreen": false,
@ -184,6 +190,13 @@
"_hide": 4,
"_footer": "<a href='https://twitter.com/mytwitter'>Twitter</a>",
"_loginfooter": "This is a private server.",
"_allowSavingDeviceCredentials": false,
"_guestDeviceSharing": false,
"_AutoRemoveInactiveDevices": 37,
"_DeviceSearchBarServerAndClientName": false,
"_agentSelfGuestSharing": {
"expire": 120
},
"_certUrl": "https://192.168.2.106:443/",
"_altMessenging": {
"name": "Jitsi",
@ -199,7 +212,7 @@
"protocol": "http",
"port": 80,
"_ip": "192.168.1.100",
"_filter": [ "mesh/(domainid)/(meshid)", "node/(domainid)/(nodeid)" ]
"_filter": ["mesh/(domainid)/(meshid)", "node/(domainid)/(nodeid)"]
},
{
"name": "HTTPS",
@ -210,7 +223,7 @@
},
"PreconfiguredRemoteInput": [
{
"name": "CompagnyUrl",
"name": "CompanyUrl",
"value": "https://help.mycompany.com/"
},
{
@ -221,7 +234,7 @@
"name": "Welcome",
"value": "Default welcome text"
}
],
],
"myServer": {
"Backup": false,
"Restore": false,
@ -274,6 +287,15 @@
"image": "agent-logo.png",
"fileName": "compagnyagent"
},
"_agentFileInfo": {
"_filedescription": "sample_filedescription",
"_fileversion": "0.1.2.3",
"_internalname": "sample_internalname",
"_legalcopyright": "sample_legalcopyright",
"_originalfilename": "sample_originalfilename",
"_productname": "sample_productname",
"_productversion": "0.1.2.3"
},
"_assistantCustomization": {
"title": "Company® Product™",
"image": "assistant-logo.png",
@ -324,14 +346,24 @@
"log": "amtactivation.log",
"certs": {
"mycertname": {
"certfiles": [ "amtacm-leafcert.crt", "amtacm-intermediate1.crt", "amtacm-intermediate2.crt", "amtacm-rootcert.crt" ],
"certfiles": [
"amtacm-leafcert.crt",
"amtacm-intermediate1.crt",
"amtacm-intermediate2.crt",
"amtacm-rootcert.crt"
],
"keyfile": "amtacm-leafcert.key"
}
}
},
"_amtManager": {
"adminAccounts": [{ "user": "admin", "pass": "MyP@ssw0rd" }],
"environmentDetection": [ "domain1.com", "domain2.com", "domain3.com", "domain4.com" ],
"environmentDetection": [
"domain1.com",
"domain2.com",
"domain3.com",
"domain4.com"
],
"wifiProfiles": [
{
"name": "Profile1",
@ -354,8 +386,8 @@
"Strict-Transport-Security": "max-age=360000",
"x-frame-options": "SAMEORIGIN"
},
"_agentConfig": [ "webSocketMaskOverride=1", "coreDumpEnabled=1" ],
"_assistantConfig": [ "disableUpdate=1" ],
"_agentConfig": ["webSocketMaskOverride=1", "coreDumpEnabled=1"],
"_assistantConfig": ["disableUpdate=1"],
"_sessionRecording": {
"_onlySelectedUsers": true,
"_onlySelectedUserGroups": true,
@ -366,42 +398,42 @@
"_maxRecordingDays": 15,
"_maxRecordingSizeMegabytes": 3,
"__protocols__": "Is an array: 1 = Terminal, 2 = Desktop, 5 = Files, 100 = Intel AMT WSMAN, 101 = Intel AMT Redirection, 200 = Messenger",
"protocols": [ 1, 2, 101 ]
"protocols": [1, 2, 101]
},
"_authStrategies": {
"__comment__": "This section is used to allow users to login using other accounts. You will need to get an API key from the services and register callback URL's",
"twitter": {
"_callbackurl": "https://server/auth-twitter-callback",
"newAccounts": true,
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
"clientid": "xxxxxxxxxxxxxxxxxxxxxxx",
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"google": {
"_callbackurl": "https://server/auth-google-callback",
"newAccounts": true,
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
"clientid": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxx"
},
"github": {
"_callbackurl": "https://server/auth-github-callback",
"newAccounts": true,
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
"clientid": "xxxxxxxxxxxxxxxxxxxxxxx",
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"reddit": {
"_callbackurl": "https://server/auth-reddit-callback",
"newAccounts": true,
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
"clientid": "xxxxxxxxxxxxxxxxxxxxxxx",
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"azure": {
"_callbackurl": "https://server/auth-azure-callback",
"newAccounts": true,
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
"clientid": "00000000-0000-0000-0000-000000000000",
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"tenantid": "00000000-0000-0000-0000-000000000000"
@ -409,7 +441,7 @@
"jumpcloud": {
"_callbackurl": "https://server/auth-jumpcloud-callback",
"newAccounts": true,
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
"entityid": "meshcentral",
"idpurl": "https://sso.jumpcloud.com/saml2/saml2",
"cert": "jumpcloud-saml.pem"
@ -418,8 +450,8 @@
"_callbackurl": "https://server/auth-saml-callback",
"_disableRequestedAuthnContext": true,
"newAccounts": true,
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
"_newAccountsRights": [ "nonewgroups", "notools" ],
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
"_newAccountsRights": ["nonewgroups", "notools"],
"entityid": "meshcentral",
"idpurl": "https://server/saml2",
"cert": "saml.pem"
@ -455,7 +487,7 @@
"uid": "anneonyme",
"mail": "anneonyme@example.com",
"email": "anneonyme@example.com",
"otherMail": [ "other.anneonyme@example.com", "anneonyme@example.com" ]
"otherMail": ["other.anneonyme@example.com", "anneonyme@example.com"]
},
"so": {
"displayName": "Sticker Sophie",
@ -463,7 +495,7 @@
"uid": "ssticker",
"mail": "ssticker@example.com",
"email": "ssticker@example.com",
"otherMail": [ "other.ssticker@example.com", "ssticker@example.com" ]
"otherMail": ["other.ssticker@example.com", "ssticker@example.com"]
}
},
"__LDAPOptions": {
@ -512,7 +544,7 @@
"_sendmail": {
"newline": "unix",
"path": "/usr/sbin/sendmail",
"_args": [ "-f", "foo@example.com" ]
"_args": ["-f", "foo@example.com"]
},
"_sms": {
"provider": "twilio",

View File

@ -1,4 +0,0 @@
@ECHO OFF
%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js minifyall
%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js translateall
%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js extractall

View File

@ -0,0 +1,4 @@
@ECHO OFF
%LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node translate.js minifyall
%LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node translate.js translateall
%LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node translate.js extractall

File diff suppressed because it is too large Load Diff

View File

@ -3541,7 +3541,7 @@
x += '</table><br />';
// Show action button, only show if we have permissions 4, 8, 64
if (((meshrights & (4 + 8 + 64)) != 0) && (node.mtype < 3)) { x += '<input type=button value="' + "Actions" + '" onclick=deviceActionFunction() />'; }
if (((meshrights & (4 + 8 + 64 + 262144)) != 0) && (node.mtype < 3)) { x += '<input type=button value="' + "Actions" + '" onclick=deviceActionFunction() />'; }
x += '<input type=button value="' + "Notes" + '" onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponent(node._id) + '") />';
//if ((connectivity & 1) && (meshrights & 8) && (node.agent.id < 5)) { x += '<input type=button value=Toast onclick=deviceToastFunction() />'; }
@ -3676,35 +3676,6 @@
if (currentDevicePanel == 1) { deskAdjust(); }
}
/*
function deviceActionFunction() {
if (xxdialogMode) return;
var rights = GetNodeRights(currentNode), count = 0;
var x = "Select an operation to perform on this device." + '<br /><br />';
var y = '<select id=d2deviceop style=float:right;width:170px>';
if ((rights & 64) != 0) { count++; y += '<option value=100>' + "Wake-up" + '</option>'; } // Wake-up permission
//if (((currentNode.conn & 1) != 0) && ((rights & 131072) != 0)) { count++; y += '<option value=106>' + "Run Commands" + '</option>'; } // Remote command permission
if ((currentNode.conn != 0) && ((rights & 262144) != 0)) { count++; y += '<option value=4>' + "Sleep" + '</option><option value=3>' + "Reset" + '</option><option value=2>' + "Power off" + '</option>'; }
//if ((currentNode.conn & 16) != 0) { count++; y += '<option value=103>' + "Send MQTT Message" + '</option>'; }
//if (((currentNode.conn & 1) != 0) && ((rights & 32768) != 0)) { count++; y += '<option value=104>' + "Uninstall Agent" + '</option>'; }
y += '</select>';
x += addHtmlValue("Operation", y);
if (count == 0) { x = "No actions currently available for this device."; }
setDialogMode(2, "Device Action", (count == 0) ? 1 : 3, deviceActionFunctionEx, x);
}
function deviceActionFunctionEx() {
var op = Q('d2deviceop').value;
if (op == 100) {
// Device wake
meshserver.send({ action: 'wakedevices', nodeids: [currentNode._id] });
} else {
// Power operation
meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: op });
}
}
*/
function deviceActionFunction() {
if (xxdialogMode) return;
var rights = GetNodeRights(currentNode), count = 0;
@ -3723,12 +3694,15 @@
//if (((currentNode.conn & 1) != 0) && ((rights & 131072) != 0)) { count++; y += '<option value=106>' + "Run Commands" + '</option>'; } // Remote command permission
if ((currentNode.conn != 0) && ((rights & 262144) != 0)) { count++; y += '<option value=4>' + "Sleep" + '</option><option value=3>' + "Reset" + '</option><option value=2>' + "Power off" + '</option>'; }
//if ((currentNode.conn & 16) != 0) { count++; y += '<option value=103>' + "Send MQTT Message" + '</option>'; }
if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && (rights == 0xFFFFFFFF)) {
if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 262144) != 0)) {
count++;
y += '<option value=310>' + "Intel&reg; AMT Reset" + '</option>';
y += '<option value=302>' + "Intel&reg; AMT Power on" + '</option>';
y += '<option value=308>' + "Intel&reg; AMT Power off" + '</option>';
}
if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 64) != 0)) {
count++;
y += '<option value=302>' + "Intel&reg; AMT Power on" + '</option>';
}
//if ((getNodeAmtVersion(currentNode) >= 15) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && (rights == 0xFFFFFFFF) && ((features & 0x00000400) == 0)) { count++; y += '<option value=107>' + "Intel&reg; AMT One Click Recovery" + '</option>'; } // CIRA (2) or AMT (4) connected
//if (((currentNode.conn & 1) != 0) && ((rights & 32768) != 0)) { count++; y += '<option value=104>' + "Uninstall Agent" + '</option>'; }
}
@ -4070,7 +4044,7 @@
);
// Show the right settings
QV('d7amtkvm', (currentNode.intelamt != null && ((currentNode.intelamt.ver != null) || (currentNode.agent == null))) && ((deskState == 0) || (desktop.contype == 2)));
QV('d7amtkvm', (currentNode.intelamt != null && ((typeof currentNode.intelamt.sku != 'number') || ((currentNode.intelamt.sku & 16) == 0)) && ((currentNode.intelamt.ver != null) || (currentNode.agent == null))) && ((deskState == 0) || (desktop.contype == 2)));
QV('d7meshkvm', ((currentNode.agent != null) && (currentNode.agent.caps & 1) && ((deskState == false) || (desktop.contype == 1))));
// Enable buttons
@ -4241,7 +4215,7 @@
}
function applyDesktopSettings() {
var r = '', ops = (features & 512) ? [90, 70, 50, 40, 30, 20, 10, 5, 1] : [50, 40, 30, 20, 10, 5, 1];
var r = '', ops = (features & 512) ? [100, 90, 70, 50, 40, 30, 20, 10, 5, 1] : [50, 40, 30, 20, 10, 5, 1];
for (var i in ops) { r += '<option value=' + ops[i] + '>' + ops[i] + '%</option>'; }
QH('d7bitmapquality', r);
d7desktopmode.value = desktopsettings.encoding;
@ -4695,7 +4669,7 @@
// Enable action button if mesh type is not "local devices"
QV('termActionsBtn', terminalNode.mtype != 3);
if (((termState == true) && (terminal.contype != 3)) || (terminalNode.agent.id == 3) || (terminalNode.agent.id == 4)) {
if (((termState == true) && (terminal.contype != 3)) || (terminalNode.agent == null) || (terminalNode.agent.id == 3) || (terminalNode.agent.id == 4)) {
QH('terminalCustomUpperRight', '');
} else {
QH('terminalCustomUpperRight', '<a style=cursor:pointer onclick=cmsshportaction(1,event)>' + format("SSH Port {0}", (terminalNode.sshport ? terminalNode.sshport : 22)) + '</a>');
@ -5321,7 +5295,7 @@
QE('p13PasteButton', advancedFeatures && (currentNode.mtype != 3) && ((p13filetreelocation.length > 0) || (winAgent == false)) && ((p13clipboard != null) && (p13clipboard.length > 0)));
}
var filesState = ((files != null) && (files.state != 0));
if (((filesState == true) && (files.contype != 2)) || (filesNode.agent.id == 3) || (filesNode.agent.id == 4)) {
if (((filesState == true) && (files.contype != 2)) || (filesNode.agent == null) || (filesNode.agent.id == 3) || (filesNode.agent.id == 4)) {
QH('filesCustomUpperRight', '');
} else {
QH('filesCustomUpperRight', '<a style=cursor:pointer onclick=cmsshportaction(1,event)>' + format("SSH Port {0}", (filesNode.sshport ? filesNode.sshport : 22)) + '</a>');
@ -5798,20 +5772,13 @@
}
}
for (var j = 0; j < m.length; j++) {
var iplayer = m[j];
if (iplayer.family == 'IPv4') {
if (iplayer.gateway && iplayer.netmask) {
x += addDetailItem("IPv4 Layer", format("{0}, Mask: {1}, Gateway: {2}", EscapeHtml(iplayer.address), EscapeHtml(iplayer.netmask), EscapeHtml(iplayer.gateway)));
} else {
if (iplayer.address) { x += addDetailItem("IPv4 Layer", format("{0}", EscapeHtml(iplayer.address))); }
}
}
if (iplayer.family == 'IPv6') {
if (iplayer.gateway && iplayer.netmask) {
x += addDetailItem("IPv6 Layer", format("{0}, Mask: {1}, Gateway: {2}", EscapeHtml(iplayer.address), EscapeHtml(iplayer.netmask), EscapeHtml(iplayer.gateway)));
} else {
if (iplayer.address) { x += addDetailItem("IPv6 Layer", format("{0}", EscapeHtml(iplayer.address))); }
}
var iplayer = m[j], items = [];
if (iplayer.address) { items.push(format("IP: {0}", EscapeHtml(iplayer.address))); }
if (iplayer.netmask) { items.push(format("Mask: {0}", EscapeHtml(iplayer.netmask))); }
if (iplayer.gateway) { items.push(format("Gateway: {0}", EscapeHtml(iplayer.gateway))); }
if (items.length > 0) {
if (iplayer.family == 'IPv4') { x += addDetailItem("IPv4 Layer", items.join(", ")); }
if (iplayer.family == 'IPv6') { x += addDetailItem("IPv6 Layer", items.join(", ")); }
}
}
x += '</div>';
@ -5831,7 +5798,13 @@
x += addDetailItem("Security", (node.intelamt.tls == 1) ? "Secured using TLS" : "TLS is not setup", s);
// Check that the Intel AMT user is setup and there is no warnings (1 = invalid credentials, 8 = trying)
x += addDetailItem("Admin Credentials", ((node.intelamt.user) == null || (node.intelamt.user == '') || ((node.intelamt.warn != null) && ((node.intelamt.warn & 9) != 0))) ? "Not Known" : "Known", s);
if (x != '') { sections.push({ name: "Intel&reg; Active Management Technology (Intel&reg; AMT)", html: x, img: 'amt' }); }
if (x != '') {
if ((typeof node.intelamt.sku == 'number') && ((node.intelamt.sku & 16) != 0)) {
sections.push({ name: "Intel&reg; Standard Manageability (Intel&reg; SM)", html: x, img: 'amt' });
} else {
sections.push({ name: "Intel&reg; Active Management Technology (Intel&reg; AMT)", html: x, img: 'amt' });
}
}
}
if (hardware.identifiers) {

View File

@ -112,6 +112,12 @@
<div id="rfbPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
<div class="cmtext" onclick="cmrfbportaction(1,event)">Alternate Port</div>
</div>
<div id="httpPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
<div class="cmtext" onclick="cmhttpportaction(1,event)">Alternate Port</div>
</div>
<div id="httpsPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
<div class="cmtext" onclick="cmhttpsportaction(1,event)">Alternate Port</div>
</div>
<div id="filesContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
<div class="cmtext" onclick="cmfilesaction(1,event)">Rename</div>
<div class="cmtext" onclick="cmfilesaction(2,event)">Edit</div>
@ -619,7 +625,7 @@
</div>
<div id="p11warning" onclick="showFeaturesDlg()">
<div class="icon2"></div>
<div class="warningbox">Intel&reg; AMT Redirection port or KVM feature is disabled<span id="p11warninga">, click here to enable it.</span></div>
<div class="warningbox">Redirection port or KVM feature is disabled<span id="p11warninga">, click here to enable it.</span></div>
</div>
<div id="p11warning2" onclick="showPowerActionDlg()">
<div class="icon2"></div>
@ -655,7 +661,7 @@
<input type="button" id="autoconnectbutton1" value="AutoConnect" onclick=autoConnectDesktop(event) onkeypress="return false" onkeydown="return false" style="display:none;margin-right:4px" />
<span id=connectbutton1span><input type=button id=connectbutton1 cmenu="deskConnectButton" title="Connect using MeshAgent remote desktop" value="Connect" onclick=connectDesktop(event,3) onkeypress="return false" onkeydown="return false" disabled="disabled" /></span>
<span id=connectbutton1rspan><input type=button id=connectbutton1r cmenu="altPortContextMenu" value="RDP Connect" title="Connect using RDP" onclick=askRdpCredentials() onkeypress="return false" onkeydown="return false" disabled="disabled" /></span>
<span id=connectbutton1hspan><input type=button id=connectbutton1h value="HW Connect" title="Connect using Intel&reg; AMT hardware KVM" onclick=connectDesktop(event,2) onkeypress="return false" onkeydown="return false" disabled="disabled" /></span>
<span id=connectbutton1hspan><input type=button id=connectbutton1h value="HW Connect" title="Connect using hardware KVM" onclick=connectDesktop(event,2) onkeypress="return false" onkeydown="return false" disabled="disabled" /></span>
<span id=disconnectbutton1span><input type=button id=disconnectbutton1 cmenu="deskDisconnectButton" value="Disconnect" onclick=connectDesktop(event,0) onkeypress="return false" onkeydown="return false" /></span>
<span id="deskstatus" style="line-height:22px">Disconnected</span><span id="deskmetadata"></span>
</div>
@ -696,7 +702,7 @@
<span id="DeskLatency" title="Desktop Session Latency"></span>
<span id="DeskTimer" style="line-height:22px" title="Session time"></span>
<input id=DeskToolsButton type=button value=Tools title="Toggle tools view" onkeypress="return false" onkeydown="return false" onclick="toggleDeskTools()" />
<span>&nbsp</span>
<span>&nbsp;</span>
<span id=DeskChatButton class="deskarea" title="Open chat window to this computer"><img class="desktopButtons" src='images/icon-chat.png' onclick=deviceChat(event) height=16 width=16 style=padding-top:2px /></span>
<span id=DeskNotifyButton title="Display a notification on the remote computer"><img class="desktopButtons" src='images/icon-notify.png' onclick=deviceToastFunction() height=16 width=16 style=padding-top:2px /></span>
<span id=DeskLockButton title="Lock the remote computer"><img src='images/icon-lock.png' class="desktopButtons" onclick=deviceLockFunction() height=16 width=16 /></span>
@ -731,7 +737,7 @@
</div>
<div id="p12warning" onclick=showFeaturesDlg()>
<div class="icon2"></div>
<div class="warningbox">Intel&reg; AMT Redirection port or KVM feature is disabled<span id="p12warninga">, click here to enable it.</span></div>
<div class="warningbox">Redirection port or KVM feature is disabled<span id="p12warninga">, click here to enable it.</span></div>
</div>
<div id="p12warning2" onclick=showPowerActionDlg()>
<div class="icon2"></div>
@ -864,7 +870,7 @@
<div id="p14title">
<div id="p14BackButton" style="float:left"><div class="backButton" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"><div class="backButtonEx"></div></div></div>
<div id="devListToolbarViewIcons"><div class="viewSelector" onclick=deskToggleFull(event) title="Full Screen. Hold shift to browser full screen."><div class="viewSelector5"></div></div></div>
<h1>Intel&reg; AMT - <span id=p14deviceName></span></h1>
<h1><span id=p14deviceNamePrefix>Intel&reg; AMT</span> - <span id=p14deviceName></span></h1>
</div>
<iframe id=p14iframe src="{{{domainurl}}}commander.htm"></iframe>
</div>
@ -1323,6 +1329,7 @@
<div>Other Settings</div>
<div id="d7otherset2" style="display:block">
<label style="display:block"><input type="checkbox" id="d7deskSwapMouse" />Swap Mouse Buttons</label>
<label style="display:block"><input type="checkbox" id="d7deskrmw" />Reverse Mouse Wheel</label>
<label style="display:block"><input type="checkbox" id="d7deskRemoteKeyMap" />Use Remote Keyboard Map</label>
<label style="display:block" id="d7deskAutoClipboardLabel"><input type="checkbox" id="d7deskAutoClipboard" />Automatic Clipboard</label>
<label style="display:block" id="d7deskAutoLockLabel"><input type="checkbox" id="d7deskAutoLock" />Lock on Disconnect</label>
@ -1346,6 +1353,7 @@
<label style="display:block"><input type="checkbox" id="d7showfocus" />Show Focus Tool</label>
<label style="display:block"><input type="checkbox" id="d7showcursor" />Show Local Mouse Cursor</label>
<label style="display:block"><input type="checkbox" id="d7localKeyMap" />Local Keyboard Map</label>
<label style="display:block"><input type="checkbox" id="d7kvmrmw" />Reverse Mouse Wheel</label>
</div>
</div>
</div>
@ -1379,6 +1387,7 @@
<label style="display:block"><input type="checkbox" id="d7rdp9" />Enable Desktop Composision</label>
<label style="display:block"><input type="checkbox" id="d7rdpclip" />Automatic Clipboard</label>
<label style="display:block"><input type="checkbox" id="d7rdpsmb" />Swap Mouse Buttons</label>
<label style="display:block"><input type="checkbox" id="d7rdprmw" />Reverse Mouse Wheel</label>
</div>
</div>
</div>
@ -1446,6 +1455,7 @@
var features = parseInt('{{{features}}}');
var features2 = parseInt('{{{features2}}}');
var sessionTime = parseInt('{{{sessiontime}}}');
var webRelayPort = parseInt('{{{webRelayPort}}}');
var sessionRefreshTimer = null;
var domain = '{{{domain}}}';
var domainUrl = '{{{domainurl}}}';
@ -2316,7 +2326,8 @@
18: "SMTP server has limited use in LAN mode.",
19: "SMS gateway has limited use in LAN mode.",
20: "Invalid \"LoginCookieEncryptionKey\" in config.json.",
21: "Backup path can't be set within meshcentral-data folder, backup settings ignored."
21: "Backup path can't be set within meshcentral-data folder, backup settings ignored.",
22: "Failed to sign agent {0}: {1}"
};
var x = '';
for (var i in message.warnings) {
@ -2325,7 +2336,7 @@
x += '<div style=color:red;padding-bottom:6px><b>' + "WARNING: " + y + '</b></div>';
} else {
var z = ServerWarnings[y.id];
if (z == null) { z = y.msg; } else { z = format(z, y.args); }
if (z == null) { z = y.msg; } else { z = format(z, ...y.args); }
x += '<div style=color:red;padding-bottom:6px><b>' + "WARNING: " + z + '</b></div>';
}
}
@ -2733,7 +2744,7 @@
if (message.name != null) { url += ('&name=' + encodeURIComponentEx(message.name)); }
if (message.ip != null) { url += ('&remoteip=' + message.ip); }
url += ('&appid=' + message.protocol + '&autoexit=1'); // Protocol: 0 = Custom, 1 = HTTP, 2 = HTTPS, 3 = RDP, 4 = PuTTY, 5 = WinSCP, 6 = MCRDesktop, 7 = MCRFiles
console.log(url);
//console.log(url);
downloadFile(url, '');
} else if (message.tag == 'novnc') {
var vncurl = window.location.origin + domainUrl + 'novnc/vnc.html?ws=wss%3A%2F%2F' + window.location.host + encodeURIComponentEx(domainUrl) + (message.localRelay?'local':'mesh') + 'relay.ashx%3Fauth%3D' + message.cookie + '&show_dot=1' + (urlargs.key?('&key=' + urlargs.key):'') + '&l={{{lang}}}';
@ -3235,6 +3246,8 @@
node.rdpport = message.event.node.rdpport;
node.rfbport = message.event.node.rfbport;
node.sshport = message.event.node.sshport;
node.httpport = message.event.node.httpport;
node.httpsport = message.event.node.httpsport;
node.consent = message.event.node.consent;
node.pmt = message.event.node.pmt;
if (message.event.node.links != null) { node.links = message.event.node.links; } else { delete node.links; }
@ -4569,6 +4582,10 @@
// RDP link, show this link only of the remote machine is Windows.
if ((((node.conn & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0) && (node.agent.id != 14)) {
if (webRelayPort != 0) {
x += '<a href=# onclick=p10WebRouter("' + node._id + '",1,' + (node.httpport ? node.httpport : 80) + ')>' + "HTTP" + ((node.httpport && (node.httpport != 80)) ? '/' + node.httpport : '') + '</a>&nbsp;';
x += '<a href=# onclick=p10WebRouter("' + node._id + '",2,' + (node.httpsport ? node.httpsport : 443) + ')>' + "HTTPS" + ((node.httspport && (node.httpsport != 443)) ? '/' + node.httpsport : '') + '</a>&nbsp;';
}
if ((node.agent.id > 0) && (node.agent.id < 5)) {
if (navigator.platform.toLowerCase() == 'win32') {
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.rdp != false)) {
@ -4579,12 +4596,12 @@
if (node.agent.id > 4) {
if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) {
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.ssh != false)) {
x += '<a href=# onclick=p10MCRouter("' + node._id + '",4,22)>' + "SSH" + '</a>&nbsp;';
x += '<a href=# onclick=p10MCRouter("' + node._id + '",4,' + (node.sshport ? node.sshport : 22) + ')>' + "SSH" + '</a>&nbsp;';
}
}
if (navigator.platform.toLowerCase() == 'win32') {
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.scp != false)) {
x += '<a href=# onclick=p10MCRouter("' + node._id + '",5,22)>' + "SCP" + '</a>&nbsp;';
x += '<a href=# onclick=p10MCRouter("' + node._id + '",5,' + (node.sshport ? node.sshport : 22) + ')>' + "SCP" + '</a>&nbsp;';
}
}
}
@ -4851,6 +4868,7 @@
desk.m.ScalingLevel = multidesktopsettings.scaling;
if (multidesktopsettings.framerate) { desk.m.FrameRateTimer = multidesktopsettings.framerate; }
if (multidesktopsettings.swapmouse) { desk.m.SwapMouse = multidesktopsettings.swapmouse; }
if (multidesktopsettings.rmw) { desk.m.ReverseMouseWheel = multidesktopsettings.rmw; }
if (multidesktopsettings.remotekeymap == true) { desk.m.remoteKeyMap = multidesktopsettings.remotekeymap; }
//desk.m.onDisplayinfo = deskDisplayInfo;
desk.m.onScreenSizeChange = mdeskAdjust; // Multi-Desktop Adjust
@ -5546,6 +5564,7 @@
var op = Q('d2deviceop').value, title = Q('dp2notifyTitle').value, msg = Q('d2notifyMsg').value, chkNodeIds = getCheckedDevices();
if (msg.length == 0) return;
if (title == '') { title = decodeURIComponent('{{{extitle}}}'); }
if (title == '') { title = "MeshCentral"; }
if (op == 1) { // MessageBox
for (var i = 0; i < chkNodeIds.length; i++) { meshserver.send({ action: 'msg', type: 'messagebox', nodeid: chkNodeIds[i], title: title, msg: msg }); }
} else if (op == 2) { // Toast
@ -6055,6 +6074,32 @@
if (currentNode.rfbport != null) { Q('d10rfbport').value = currentNode.rfbport; }
}
function cmhttpportaction(action) {
if (xxdialogMode) return;
var x = "HTTP remote connection port:" + '<br /><br /><input type=text placeholder="80" inputmode="numeric" pattern="[0-9]*" onkeypress="return (event.keyCode == 8) || (event.charCode >= 48 && event.charCode <= 57)" maxlength=5 id=d10httpport type=text>';
setDialogMode(2, "HTTP Connection", 3, function() {
// Save the new HTTP port to the server
var httpport = ((Q('d10httpport').value.length > 0) ? parseInt(Q('d10httpport').value) : 80);
meshserver.send({ action: 'changedevice', nodeid: currentNode._id, httpport: httpport });
//if (currentNode != null) { p10rfb(currentNode._id, httpport); }
}, x, currentNode);
Q('d10httpport').focus();
if (currentNode.httpport != null) { Q('d10httpport').value = currentNode.httpport; }
}
function cmhttpsportaction(action) {
if (xxdialogMode) return;
var x = "HTTPS remote connection port:" + '<br /><br /><input type=text placeholder="443" inputmode="numeric" pattern="[0-9]*" onkeypress="return (event.keyCode == 8) || (event.charCode >= 48 && event.charCode <= 57)" maxlength=5 id=d10httpsport type=text>';
setDialogMode(2, "HTTPS Connection", 3, function() {
// Save the new HTTP port to the server
var httpsport = ((Q('d10httpsport').value.length > 0) ? parseInt(Q('d10httpsport').value) : 443);
meshserver.send({ action: 'changedevice', nodeid: currentNode._id, httpsport: httpsport });
//if (currentNode != null) { p10rfb(currentNode._id, httpsport); }
}, x, currentNode);
Q('d10httpsport').focus();
if (currentNode.httpsport != null) { Q('d10httpsport').value = currentNode.httpsport; }
}
function cmfilesaction(action) {
if (xxdialogMode) return;
var filetreexx = p13sort_files(p13filetree.dir);
@ -6144,6 +6189,8 @@
QV('altPortContextMenu', false);
QV('rfbPortContextMenu', false);
QV('sshPortContextMenu', false);
QV('httpPortContextMenu', false);
QV('httpsPortContextMenu', false);
QV('filesContextMenu', false);
QV('deskPlayerContextMenu', false);
QV('deskKeyShortcutContextMenu', false);
@ -7066,7 +7113,7 @@
x += '</table><br />';
// Show action button, only show if we have permissions 4, 8, 64
if (((meshrights & (4 + 8 + 64)) != 0) && (node.mtype < 3) && ((node.agent == null) || (node.agent.id != 34))) { x += '<input type=button value="' + "Actions" + '" title="' + "Perform power actions on the device" + '" onclick=deviceActionFunction() />'; }
if (((meshrights & (4 + 8 + 64 + 262144)) != 0) && (node.mtype < 3) && ((node.agent == null) || (node.agent.id != 34))) { x += '<input type=button value="' + "Actions" + '" title="' + "Perform power actions on the device" + '" onclick=deviceActionFunction() />'; }
x += '<input type=button value="' + "Notes" + '" title="' + "View notes about this device" + '" onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponentEx(node._id) + '") />';
x += '<input type=button value="' + "Log Event" + '" title="' + "Write an event for this device" + '" onclick=writeDeviceEvent("' + encodeURIComponentEx(node._id) + '") />';
if (node.mtype != 4) {
@ -7135,22 +7182,26 @@
// RDP link, show this link only of the remote machine is Windows.
if ((((connectivity & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0)) {
if (webRelayPort != 0) {
x += '<a href=# cmenu=httpPortContextMenu onclick=p10WebRouter("' + node._id + '",1,' + (node.httpport ? node.httpport : 80) + ')>' + "HTTP" + ((node.httpport && (node.httpport != 80)) ? '/' + node.httpport : '') + '</a>&nbsp;';
x += '<a href=# cmenu=httpsPortContextMenu onclick=p10WebRouter("' + node._id + '",2,' + (node.httpsport ? node.httpsport : 443) + ')>' + "HTTPS" + ((node.httpsport && (node.httpsport != 443)) ? '/' + node.httpsport : '') + '</a>&nbsp;';
}
if ((node.agent.id > 0) && (node.agent.id < 5)) {
if (navigator.platform.toLowerCase() == 'win32') {
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.rdp != false)) {
x += '<a href=# cmenu=altPortContextMenu id=rdpMCRouterLink onclick=p10MCRouter("' + node._id + '",3) title="' + "Requires installation of MeshCentral Router" + '.">' + "RDP" + '</a>&nbsp;';
x += '<a href=# cmenu=altPortContextMenu id=rdpMCRouterLink onclick=p10MCRouter("' + node._id + '",3) title="' + "Requires installation of MeshCentral Router" + '.">' + "RDP" + ((node.rdpport && (node.rdpport != 3389)) ? '/' + node.rdpport : '') + '</a>&nbsp;';
}
}
}
if (node.agent.id > 4) {
if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) {
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.ssh != false)) {
x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",4,22) title="' + "Requires installation of MeshCentral Router." + '">' + "SSH" + '</a>&nbsp;';
x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",4,' + (node.sshport ? node.sshport : 22) + ') title="' + "Requires installation of MeshCentral Router." + '">' + "SSH" + ((node.sshport && (node.sshport != 22)) ? '/' + node.sshport : '') + '</a>&nbsp;';
}
}
if (navigator.platform.toLowerCase() == 'win32') {
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.scp != false)) {
x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",5,22) title="' + "Requires installation of MeshCentral Router." + '">' + "SCP" + '</a>&nbsp;';
x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",5,' + (node.sshport ? node.sshport : 22) + ') title="' + "Requires installation of MeshCentral Router." + '">' + "SCP" + ((node.sshport && (node.sshport != 22)) ? '/' + node.sshport : '') + '</a>&nbsp;';
}
}
}
@ -7254,6 +7305,15 @@
QV('p15uploadCore', (node.agent != null) && (node.agent.caps != null) && ((node.agent.caps & 16) != 0));
QH('p15coreName', ((node.agent != null) && (node.agent.core != null))?node.agent.core:'');
// Set the Intel AMT / Intel SM tab name
if ((node.intelamt != null) && (typeof node.intelamt.sku == 'number') && ((node.intelamt.sku & 16) != 0)) {
QH('MainDevAmt', "Intel&reg;SM");
QH('p14deviceNamePrefix', "Intel&reg; SM");
} else {
QH('MainDevAmt', "Intel&reg;AMT");
QH('p14deviceNamePrefix', "Intel&reg; AMT");
}
// Setup/Refresh Intel AMT tab
var amtFrameNode = Q('p14iframe').contentWindow.getCurrentMeshNode();
if ((amtFrameNode != null) && (amtFrameNode._id != currentNode._id)) { Q('p14iframe').contentWindow.disconnect(); }
@ -7585,10 +7645,12 @@
}
function deviceMessageFunctionEx() {
var title = decodeURIComponent('{{{extitle}}}');
if (title == '') { title = "MeshCentral"; }
if (currentNode.pmt == 1) {
meshserver.send({ action: 'pushmessage', nodeid: currentNode._id, title: decodeURIComponent('{{{extitle}}}'), msg: Q('d2devMessage').value });
meshserver.send({ action: 'pushmessage', nodeid: currentNode._id, title: title, msg: Q('d2devMessage').value });
} else {
meshserver.send({ action: 'msg', type: 'messagebox', nodeid: currentNode._id, title: decodeURIComponent('{{{extitle}}}'), msg: Q('d2devMessage').value });
meshserver.send({ action: 'msg', type: 'messagebox', nodeid: currentNode._id, title: title, msg: Q('d2devMessage').value });
}
}
@ -7749,12 +7811,15 @@
if (((currentNode.conn & 1) != 0) && ((rights & 131072) != 0)) { count++; y += '<option value=106>' + "Run Commands" + '</option>'; } // Remote command permission
if ((currentNode.conn != 0) && ((rights & 262144) != 0)) { count++; y += '<option value=4>' + "Sleep" + '</option><option value=3>' + "Reset" + '</option><option value=2>' + "Power off" + '</option>'; }
if ((currentNode.conn & 16) != 0) { count++; y += '<option value=103>' + "Send MQTT Message" + '</option>'; }
if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && (rights == 0xFFFFFFFF)) {
if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 262144) != 0)) {
count++;
y += '<option value=310>' + "Intel&reg; AMT Reset" + '</option>';
y += '<option value=302>' + "Intel&reg; AMT Power on" + '</option>';
y += '<option value=308>' + "Intel&reg; AMT Power off" + '</option>';
}
if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 64) != 0)) {
count++;
y += '<option value=302>' + "Intel&reg; AMT Power on" + '</option>';
}
if ((getNodeAmtVersion(currentNode) >= 15) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && (rights == 0xFFFFFFFF) && ((features & 0x00000400) == 0)) { count++; y += '<option value=107>' + "Intel&reg; AMT One Click Recovery" + '</option>'; } // CIRA (2) or AMT (4) connected
if (((currentNode.conn & 1) != 0) && ((rights & 32768) != 0)) { count++; y += '<option value=104>' + "Uninstall Agent" + '</option>'; }
}
@ -8043,6 +8108,22 @@
meshserver.send({ action: 'removedevices', nodeids: [ nodeid ] });
}
function p10WebRouter(nodeid, protocol, port, addr) {
var relayid = null;
var node = getNodeFromId(nodeid);
if (node.mtype == 3) { // Setup device relay if needed
var mesh = meshes[node.meshid];
if (mesh && mesh.relayid) { relayid = mesh.relayid; addr = node.host; }
}
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 url = 'https://' + servername + ':' + webRelayPort + '/control-redirect.ashx?n=' + nodeid + '&p=' + port + '&appid=' + protocol; // Protocol: 1 = HTTP, 2 = HTTPS
if (addr != null) { url += '&addr=' + addr; }
if (relayid != null) { url += '&relayid=' + relayid; }
safeNewWindow(url, 'WebRelay');
return false;
}
function p10MCRouter(nodeid, protocol, port, addr, localport) {
var node = getNodeFromId(nodeid);
var mesh = meshes[node.meshid];
@ -8370,7 +8451,7 @@
);
}
// Show the right settings
QV('td7amtkvm', (currentNode.intelamt != null && ((currentNode.intelamt.ver != null) || (currentNode.agent == null))) && ((deskState == 0) || (desktop.contype == 2)));
QV('td7amtkvm', ((currentNode.intelamt != null) && ((typeof currentNode.intelamt.sku != 'number') || ((currentNode.intelamt.sku & 16) == 0)) && ((currentNode.intelamt.ver != null) || (currentNode.agent == null))) && ((deskState == 0) || (desktop.contype == 2)));
QV('td7meshkvm', (webRtcDesktop) || ((currentNode.agent != null) && (currentNode.agent.caps & 1) && ((deskState == 0) || (desktop.contype == 1))));
QV('td7rdpkvm', ((currentNode.agent != null) && ((currentNode.agent.id == 3) || (currentNode.agent.id == 4)) && ((deskState == 0) || (desktop.contype == 4))));
@ -8456,6 +8537,7 @@
desktop.m.bpp = (desktopsettings.encoding == 1 || desktopsettings.encoding == 3) ? 1 : 2;
desktop.m.useZRLE = (desktopsettings.encoding < 3);
desktop.m.localKeyMap = desktopsettings.localkeymap;
desktop.m.ReverseMouseWheel = desktopsettings.kvmrmw;
desktop.m.showmouse = desktopsettings.showmouse;
desktop.m.onScreenSizeChange = deskAdjust;
desktop.m.onKvmData = function (x) {
@ -8568,6 +8650,7 @@
desktop.m.ScalingLevel = desktopsettings.scaling;
if (desktopsettings.framerate) { desktop.m.FrameRateTimer = desktopsettings.framerate; }
if (desktopsettings.swapmouse) { desktop.m.SwapMouse = desktopsettings.swapmouse; }
if (desktopsettings.rmw) { desktop.m.ReverseMouseWheel = desktopsettings.rmw; }
if (desktopsettings.remotekeymap == true) { desktop.m.remoteKeyMap = desktopsettings.remotekeymap; }
desktop.m.onDisplayinfo = deskDisplayInfo;
desktop.m.onScreenSizeChange = deskAdjust;
@ -8584,6 +8667,7 @@
desktop.m.onScreenSizeChange = mdeskAdjust;
desktop.m.onClipboardChanged = function(text) { if ((text != null) && (desktopsettings.rdpautoclipboard) && (navigator.clipboard != null)) { navigator.clipboard.writeText(text).then(function() { }).catch(function(err) { console.log(err); }) } } // Put remote clipboard data into our clipboard
if (desktopsettings.rdpsmb) { desktop.m.SwapMouse = desktopsettings.rdpsmb; }
if (desktopsettings.rdprmw) { desktop.m.ReverseMouseWheel = desktopsettings.rdprmw; }
desktop.Start(desktopNode._id, currentNode.rdpport ? currentNode.rdpport : 3389, tsid);
desktop.contype = 4;
desktop.onConsoleMessageChange = function () {
@ -8866,12 +8950,15 @@
desktopsettings.scaling = d7bitmapscaling.value;
desktopsettings.framerate = d7framelimiter.value;
desktopsettings.swapmouse = d7deskSwapMouse.checked;
desktopsettings.rmw = d7deskrmw.checked;
desktopsettings.remotekeymap = d7deskRemoteKeyMap.checked;
desktopsettings.autoclipboard = d7deskAutoClipboard.checked;
desktopsettings.autolock = d7deskAutoLock.checked;
desktopsettings.localkeymap = d7localKeyMap.checked;
desktopsettings.kvmrmw = d7kvmrmw.checked;
desktopsettings.rdpsize = d7rdpsize.value;
desktopsettings.rdpsmb = d7rdpsmb.checked;
desktopsettings.rdprmw = d7rdprmw.checked;
desktopsettings.rdpautoclipboard = d7rdpclip.checked;
var rdpflags = 0;
for (var i = 1; i < 10; i++) { if ((i != 5) && (Q('d7rdp' + i).checked)) { rdpflags |= (1 << (i - 1)); } }
@ -8880,23 +8967,29 @@
applyDesktopSettings();
updateDesktopButtons();
if (desktop) {
if (desktop.contype == 1) {
if (desktop.contype == 1) { // Intel AMT KVM
desktop.m.SwapMouse = desktopsettings.swapmouse;
desktop.m.ReverseMouseWheel = desktopsettings.rmw;
desktop.m.remoteKeyMap = desktopsettings.remotekeymap;
if (desktop.State != 0) {
desktop.m.SendCompressionLevel(webpSupport?4:1, desktopsettings.quality, desktopsettings.scaling, desktopsettings.framerate);
desktop.sendCtrlMsg('{"ctrlChannel":"102938","type":"autolock","value":' + desktopsettings.autolock + '}');
}
}
if (desktop.contype == 2) {
if (desktop.contype == 2) { // Mesh Agent Remote Desktop
desktop.m.ReverseMouseWheel = desktopsettings.kvmrmw;
if (desktopsettings.showfocus == false) { desktop.m.focusmode = 0; deskFocusBtn.value = "All Focus"; }
if (desktop.State != 0) { desktop.Stop(); setTimeout(function () { connectDesktop(null, 2); }, 50); }
}
if (desktop.contype == 4) { // Web-RDP
desktop.m.SwapMouse = desktopsettings.rdpsmb;
desktop.m.ReverseMouseWheel = desktopsettings.rdprmw;
}
}
}
function applyDesktopSettings() {
var r = '', ops = (features & 512)?[90,80,70,60,50,40,30,20,10,5,1]:[60,50,40,30,20,10,5,1];
var r = '', ops = (features & 512)?[100,90,80,70,60,50,40,30,20,10,5,1]:[60,50,40,30,20,10,5,1];
for (var i in ops) { r += '<option value=' + ops[i] + '>' + ops[i] + '%</option>'; }
QH('d7bitmapquality', r);
d7desktopmode.value = desktopsettings.encoding;
@ -8907,14 +9000,17 @@
d7bitmapscaling.value = desktopsettings.scaling;
if (desktopsettings.framerate) { d7framelimiter.value = desktopsettings.framerate; } else { d7framelimiter.value = 100; }
if (desktopsettings.swapmouse != null) { d7deskSwapMouse.checked = desktopsettings.swapmouse; }
if (desktopsettings.rmw != null) { d7deskrmw.checked = desktopsettings.rmw; }
if (desktopsettings.remotekeymap != null) { d7deskRemoteKeyMap.checked = desktopsettings.remotekeymap; }
if (desktopsettings.autoclipboard != null) { d7deskAutoClipboard.checked = desktopsettings.autoclipboard; }
if (desktopsettings.autolock != null) { d7deskAutoLock.checked = desktopsettings.autolock; }
if (desktopsettings.localkeymap) { d7localKeyMap.checked = desktopsettings.localkeymap; }
if (desktopsettings.kvmrmw) { d7kvmrmw.checked = desktopsettings.kvmrmw; }
QV('deskFocusBtn', (desktop != null) && (desktop.contype == 2) && (desktop.state != 0) && (desktopsettings.showfocus));
if (desktopsettings.rdpsize != null) { d7rdpsize.value = desktopsettings.rdpsize; }
if (desktopsettings.rdpflags == null) { desktopsettings.rdpflags = 0x2F; }
if (desktopsettings.rdpsmb != null) { d7rdpsmb.checked = desktopsettings.rdpsmb; }
if (desktopsettings.rdprmw != null) { d7rdprmw.checked = desktopsettings.rdprmw; }
if (desktopsettings.rdpautoclipboard != null) { d7rdpclip.checked = desktopsettings.rdpautoclipboard; }
for (var i = 1; i < 10; i++) { if (i != 5) { Q('d7rdp' + i).checked = ((desktopsettings.rdpflags & (1 << (i - 1))) != 0); } }
}
@ -9639,7 +9735,7 @@
// Enable action button if mesh type is not "local devices"
QV('termActionsBtn', terminalNode.mtype != 3);
if (((termState == true) && (terminal.contype != 3)) || (terminalNode.agent.id == 3) || (terminalNode.agent.id == 4)) {
if (((termState == true) && (terminal.contype != 3)) || (terminalNode.agent == null) || (terminalNode.agent.id == 3) || (terminalNode.agent.id == 4)) {
QH('terminalCustomUpperRight', '');
} else {
QH('terminalCustomUpperRight', '<a style=cursor:pointer onclick=cmsshportaction(1,event)>' + format("SSH Port {0}", (terminalNode.sshport?terminalNode.sshport:22)) + '</a>');
@ -10431,7 +10527,7 @@
QE('p13PasteButton', advancedFeatures && ((p13filetreelocation.length > 0) || (winAgent == false)) && ((p13clipboard != null) && (p13clipboard.length > 0)));
}
var filesState = ((files != null) && (files.state != 0));
if (((filesState == true) && (files.contype != 2)) || (filesNode.agent.id == 3) || (filesNode.agent.id == 4)) {
if (((filesState == true) && (files.contype != 2)) || (filesNode.agent == null) || (filesNode.agent.id == 3) || (filesNode.agent.id == 4)) {
QH('filesCustomUpperRight', '');
} else {
QH('filesCustomUpperRight', '<a style=cursor:pointer onclick=cmsshportaction(1,event)>' + format("SSH Port {0}", (filesNode.sshport?filesNode.sshport:22)) + '</a>');
@ -11130,20 +11226,13 @@
}
}
for (var j = 0; j < m.length; j++) {
var iplayer = m[j];
if (iplayer.family == 'IPv4') {
if (iplayer.gateway && iplayer.netmask) {
x += addDetailItem("IPv4 Layer", format("IP: {0}, Mask: {1}, Gateway: {2}", EscapeHtml(iplayer.address), EscapeHtml(iplayer.netmask), EscapeHtml(iplayer.gateway)));
} else {
if (iplayer.address) { x += addDetailItem("IPv4 Layer", format("IP: {0}", EscapeHtml(iplayer.address))); }
}
}
if (iplayer.family == 'IPv6') {
if (iplayer.gateway && iplayer.netmask) {
x += addDetailItem("IPv6 Layer", format("IP: {0}, Mask: {1}, Gateway: {2}", EscapeHtml(iplayer.address), EscapeHtml(iplayer.netmask), EscapeHtml(iplayer.gateway)));
} else {
if (iplayer.address) { x += addDetailItem("IPv6 Layer", format("IP: {0}", EscapeHtml(iplayer.address))); }
}
var iplayer = m[j], items = [];
if (iplayer.address) { items.push(format("IP: {0}", EscapeHtml(iplayer.address))); }
if (iplayer.netmask) { items.push(format("Mask: {0}", EscapeHtml(iplayer.netmask))); }
if (iplayer.gateway) { items.push(format("Gateway: {0}", EscapeHtml(iplayer.gateway))); }
if (items.length > 0) {
if (iplayer.family == 'IPv4') { x += addDetailItem("IPv4 Layer", items.join(", ")); }
if (iplayer.family == 'IPv6') { x += addDetailItem("IPv6 Layer", items.join(", ")); }
}
}
x += '</div>';
@ -11163,7 +11252,13 @@
x += addDetailItem("Security", (node.intelamt.tls == 1)?"Secured using TLS":"TLS is not setup", s);
// Check that the Intel AMT user is setup and there is no warnings (1 = invalid credentials, 8 = trying)
x += addDetailItem("Admin Credentials", ((node.intelamt.user) == null || (node.intelamt.user == '') || ((node.intelamt.warn != null) && ((node.intelamt.warn & 9) != 0)))?"Not Known":"Known", s);
if (x != '') { sections.push({ name: "Intel&reg; Active Management Technology (Intel&reg; AMT)", html: x, img: 'amt64.png' }); }
if (x != '') {
if ((typeof node.intelamt.sku == 'number') && ((node.intelamt.sku & 16) != 0)) {
sections.push({ name: "Intel&reg; Standard Manageability (Intel&reg; SM)", html: x, img: 'amt64.png' });
} else {
sections.push({ name: "Intel&reg; Active Management Technology (Intel&reg; AMT)", html: x, img: 'amt64.png' });
}
}
}
if (hardware.identifiers) {

View File

@ -1188,7 +1188,7 @@
}
function applyDesktopSettings() {
var r = '', ops = (features & 512) ? [90, 70, 50, 40, 30, 20, 10, 5, 1] : [50, 40, 30, 20, 10, 5, 1];
var r = '', ops = (features & 512) ? [100, 90, 70, 50, 40, 30, 20, 10, 5, 1] : [50, 40, 30, 20, 10, 5, 1];
for (var i in ops) { r += '<option value=' + ops[i] + '>' + ops[i] + '%</option>'; }
QH('d7bitmapquality', r);
d7desktopmode.value = desktopsettings.encoding;

View File

@ -780,7 +780,7 @@
}
function applyDesktopSettings() {
var r = '', ops = (features2 & 1) ? [90, 80, 70, 60, 50, 40, 30, 20, 10, 5, 1] : [60, 50, 40, 30, 20, 10, 5, 1]
var r = '', ops = (features2 & 1) ? [100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 5, 1] : [60, 50, 40, 30, 20, 10, 5, 1]
for (var i in ops) { r += '<option value=' + ops[i] + '>' + ops[i] + '%</option>'; }
QH('d7bitmapquality', r);
d7desktopmode.value = desktopsettings.encoding;

272
webrelayserver.js Normal file
View File

@ -0,0 +1,272 @@
/**
* @description Meshcentral web relay server
* @author Ylian Saint-Hilaire
* @copyright Intel Corporation 2018-2022
* @license Apache-2.0
* @version v0.0.1
*/
/*jslint node: true */
/*jshint node: true */
/*jshint strict:false */
/*jshint -W097 */
/*jshint esversion: 6 */
"use strict";
// Construct a HTTP redirection web server object
module.exports.CreateWebRelayServer = function (parent, db, args, certificates, func) {
var obj = {};
obj.parent = parent;
obj.db = db;
obj.express = require('express');
obj.session = require('cookie-session');
obj.expressWs = null;
obj.tlsServer = null;
obj.net = require('net');
obj.app = obj.express();
if (args.compression !== false) { obj.app.use(require('compression')()); }
obj.app.disable('x-powered-by');
obj.webRelayServer = null;
obj.port = 0;
obj.cleanupTimer = null;
var nextSessionId = 1;
var relaySessions = {} // RelayID --> Web Mutli-Tunnel
const constants = (require('crypto').constants ? require('crypto').constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
var tlsSessionStore = {}; // Store TLS session information for quick resume.
var tlsSessionStoreCount = 0; // Number of cached TLS session information in store.
function serverStart() {
if (args.trustedproxy) {
// Reverse proxy should add the "X-Forwarded-*" headers
try {
obj.app.set('trust proxy', args.trustedproxy);
} catch (ex) {
// If there is an error, try to resolve the string
if ((args.trustedproxy.length == 1) && (typeof args.trustedproxy[0] == 'string')) {
require('dns').lookup(args.trustedproxy[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); args.trustedproxy = [address]; } });
}
}
}
else if (typeof args.tlsoffload == 'object') {
// Reverse proxy should add the "X-Forwarded-*" headers
try {
obj.app.set('trust proxy', args.tlsoffload);
} catch (ex) {
// If there is an error, try to resolve the string
if ((Array.isArray(args.tlsoffload)) && (args.tlsoffload.length == 1) && (typeof args.tlsoffload[0] == 'string')) {
require('dns').lookup(args.tlsoffload[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); args.tlsoffload = [address]; } });
}
}
}
// Setup cookie session
var sessionOptions = {
name: 'xid', // Recommended security practice to not use the default cookie name
httpOnly: true,
keys: [args.sessionkey], // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances
secure: (args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
sameSite: args.sessionsamesite
}
if (args.sessiontime != null) { sessionOptions.maxAge = (args.sessiontime * 60 * 1000); }
obj.app.use(obj.session(sessionOptions));
// Add HTTP security headers to all responses
obj.app.use(function (req, res, next) {
parent.debug('webrequest', req.url + ' (RelayServer)');
res.removeHeader('X-Powered-By');
res.set({
'strict-transport-security': 'max-age=60000; includeSubDomains',
'Referrer-Policy': 'no-referrer',
'x-frame-options': 'SAMEORIGIN',
'X-XSS-Protection': '1; mode=block',
'X-Content-Type-Options': 'nosniff',
'Content-Security-Policy': "default-src 'none'; style-src 'self' 'unsafe-inline';"
});
// Set the real IP address of the request
// If a trusted reverse-proxy is sending us the remote IP address, use it.
var ipex = '0.0.0.0', xforwardedhost = req.headers.host;
if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
if (
(args.trustedproxy === true) || (args.tlsoffload === true) ||
((typeof args.trustedproxy == 'object') && (isIPMatch(ipex, args.trustedproxy))) ||
((typeof args.tlsoffload == 'object') && (isIPMatch(ipex, args.tlsoffload)))
) {
// Get client IP
if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
} else if (req.headers['x-forwarded-for']) {
req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
} else if (req.headers['x-real-ip']) {
req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
} else {
req.clientIp = ipex;
}
// If there is a port number, remove it. This will only work for IPv4, but nice for people that have a bad reverse proxy config.
const clientIpSplit = req.clientIp.split(':');
if (clientIpSplit.length == 2) { req.clientIp = clientIpSplit[0]; }
// Get server host
if (req.headers['x-forwarded-host']) { xforwardedhost = req.headers['x-forwarded-host'].split(',')[0]; } // If multiple hosts are specified with a comma, take the first one.
} else {
req.clientIp = ipex;
}
// If this is a session start or a websocket, have the application handle this
if ((req.headers.upgrade == 'websocket') || (req.url.startsWith('/control-redirect.ashx?n='))) {
return next();
} else {
// If this is a normal request (GET, POST, etc) handle it here
if ((req.session.userid != null) && (req.session.rid != null)) {
var relaySession = relaySessions[req.session.userid + '/' + req.session.rid];
if (relaySession != null) {
// The web relay session is valid, use it
relaySession.handleRequest(req, res);
} else {
// No web relay ession with this relay identifier, close the HTTP request.
res.end();
}
} else {
// The user is not logged in or does not have a relay identifier, close the HTTP request.
res.end();
}
}
});
// Start the server, only after users and meshes are loaded from the database.
if (args.tlsoffload) {
// Setup the HTTP server without TLS
obj.expressWs = require('express-ws')(obj.app, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
} else {
// Setup the HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
const tlsOptions = { cert: certificates.web.cert, key: certificates.web.key, ca: certificates.web.ca, rejectUnauthorized: true, ciphers: "HIGH:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_8_SHA256:TLS_AES_128_CCM_SHA256:TLS_CHACHA20_POLY1305_SHA256", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 };
obj.tlsServer = require('https').createServer(tlsOptions, obj.app);
obj.tlsServer.on('secureConnection', function () { /*console.log('tlsServer secureConnection');*/ });
obj.tlsServer.on('error', function (err) { console.log('tlsServer error', err); });
obj.tlsServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
obj.tlsServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
obj.expressWs = require('express-ws')(obj.app, obj.tlsServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
}
// Handle incoming web socket calls
obj.app.ws('/*', function (ws, req) {
if ((req.session.userid != null) && (req.session.rid != null)) {
var relaySession = relaySessions[req.session.userid + '/' + req.session.rid];
if (relaySession != null) {
// The multi-tunnel session is valid, use it
relaySession.handleWebSocket(ws, req);
} else {
// No multi-tunnel session with this relay identifier, close the websocket.
ws.close();
}
} else {
// The user is not logged in or does not have a relay identifier, close the websocket.
ws.close();
}
});
// This is the magic URL that will setup the relay session
obj.app.get('/control-redirect.ashx', function (req, res) {
if ((req.session == null) || (req.session.userid == null)) { res.redirect('/'); return; }
res.set({ 'Cache-Control': 'no-store' });
parent.debug('web', 'webRelaySetup');
// Check that all the required arguments are present
if ((req.session.userid == null) || (req.query.n == null) || (req.query.p == null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; }
// Get the user and domain information
const userid = req.session.userid;
const domainid = userid.split('/')[1];
const domain = parent.config.domains[domainid];
const nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
const addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
const port = parseInt(req.query.p);
const appid = parseInt(req.query.appid);
// Check to see if we already have a multi-relay session that matches exactly this device and port for this user
var relaySession = null;
for (var i in relaySessions) {
const xrelaySession = relaySessions[i];
if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
relaySession = xrelaySession; // We found an exact match
}
}
if (relaySession != null) {
// Since we found a match, use it
req.session.rid = relaySession.sessionId;
} else {
// Create a web relay session
relaySession = require('./apprelays.js').CreateWebRelaySession(parent, db, req, args, domain, userid, nodeid, addr, port, appid);
relaySession.onclose = function (sessionId) {
// Remove the relay session
delete relaySessions[sessionId];
// If there are not more relay sessions, clear the cleanup timer
if ((Object.keys(relaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(obj.cleanupTimer); obj.cleanupTimer = null; }
}
relaySession.sessionId = nextSessionId++;
// Set the multi-tunnel session
relaySessions[userid + '/' + relaySession.sessionId] = relaySession;
req.session.rid = relaySession.sessionId;
// Setup the cleanup timer if needed
if (obj.cleanupTimer == null) { obj.cleanupTimer = setInterval(checkTimeout, 10000); }
}
// Redirect to root
res.redirect('/');
});
}
// Check that everything is cleaned up
function checkTimeout() {
for (var i in relaySessions) { relaySessions[i].checkTimeout(); }
}
// Find a free port starting with the specified one and going up.
function CheckListenPort(port, addr, func) {
var s = obj.net.createServer(function (socket) { });
obj.webRelayServer = s.listen(port, addr, function () { s.close(function () { if (func) { func(port, addr); } }); }).on("error", function (err) {
if (args.exactports) { console.error("ERROR: MeshCentral HTTP relay server port " + port + " not available."); process.exit(); }
else { if (port < 65535) { CheckListenPort(port + 1, addr, func); } else { if (func) { func(0); } } }
});
}
// Start the ExpressJS web server, if the port is busy try the next one.
function StartWebRelayServer(port, addr) {
if (port == 0 || port == 65535) { return; }
if (obj.tlsServer != null) {
if (args.lanonly == true) {
obj.tcpServer = obj.tlsServer.listen(port, addr, function () { console.log('MeshCentral HTTPS relay server running on port ' + port + ((args.aliasport != null) ? (', alias port ' + args.aliasport) : '') + '.'); });
} else {
obj.tcpServer = obj.tlsServer.listen(port, addr, function () { console.log('MeshCentral HTTPS relay server running on ' + certificates.CommonName + ':' + port + ((args.aliasport != null) ? (', alias port ' + args.aliasport) : '') + '.'); });
obj.parent.updateServerState('servername', certificates.CommonName);
}
if (obj.parent.authlog) { obj.parent.authLog('https', 'Web relay server listening on ' + ((addr != null) ? addr : '0.0.0.0') + ' port ' + port + '.'); }
obj.parent.updateServerState('https-relay-port', port);
if (args.aliasport != null) { obj.parent.updateServerState('https-relay-aliasport', args.aliasport); }
} else {
obj.tcpServer = obj.app.listen(port, addr, function () { console.log('MeshCentral HTTP relay server running on port ' + port + ((args.aliasport != null) ? (', alias port ' + args.aliasport) : '') + '.'); });
obj.parent.updateServerState('http-relay-port', port);
if (args.aliasport != null) { obj.parent.updateServerState('http-relay-aliasport', args.aliasport); }
}
obj.port = port;
}
function getRandomPassword() { return Buffer.from(require('crypto').randomBytes(9), 'binary').toString('base64').split('/').join('@'); }
// Perform a IP match against a list
function isIPMatch(ip, matchList) {
const ipcheck = require('ipcheck');
for (var i in matchList) { if (ipcheck.match(ip, matchList[i]) == true) return true; }
return false;
}
// Start up the web relay server
serverStart();
CheckListenPort(args.relayport, args.relayportbind, StartWebRelayServer);
return obj;
};

View File

@ -2858,7 +2858,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
footer: (domain.footer == null) ? '' : domain.footer,
webstate: encodeURIComponent(webstate).replace(/'/g, '%27'),
amtscanoptions: amtscanoptions,
pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports()
pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports(),
webRelayPort: ((parent.webrelayserver != null) ? parent.webrelayserver.port : 0)
}, dbGetFunc.req, domain), user);
}
xdbGetFunc.req = req;
@ -5846,11 +5847,19 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
var selfurl = ' wss://' + req.headers.host;
if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { selfurl += ' wss://' + xforwardedhost; }
const extraScriptSrc = (parent.config.settings.extrascriptsrc != null) ? (' ' + parent.config.settings.extrascriptsrc) : '';
// If the web relay port is enabled, allow the web page to redirect to it
var extraFrameSrc = '';
if ((parent.webrelayserver != null) && (parent.webrelayserver.port != 0)) {
extraFrameSrc = ' https://' + req.headers.host + ':' + parent.webrelayserver.port;
if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { extraFrameSrc += ' https://' + xforwardedhost + ':' + parent.webrelayserver.port; }
}
const headers = {
'Referrer-Policy': 'no-referrer',
'X-XSS-Protection': '1; mode=block',
'X-Content-Type-Options': 'nosniff',
'Content-Security-Policy': "default-src 'none'; font-src 'self'; script-src 'self' 'unsafe-inline'" + extraScriptSrc + "; connect-src 'self'" + geourl + selfurl + "; img-src 'self' blob: data:" + geourl + " data:; style-src 'self' 'unsafe-inline'; frame-src 'self' mcrouter:; media-src 'self'; form-action 'self'"
'Content-Security-Policy': "default-src 'none'; font-src 'self'; script-src 'self' 'unsafe-inline'" + extraScriptSrc + "; connect-src 'self'" + geourl + selfurl + "; img-src 'self' blob: data:" + geourl + " data:; style-src 'self' 'unsafe-inline'; frame-src 'self' mcrouter:" + extraFrameSrc + "; media-src 'self'; form-action 'self'"
};
if (req.headers['user-agent'] && (req.headers['user-agent'].indexOf('Chrome') >= 0)) { headers['Permissions-Policy'] = 'interest-cohort=()'; } // Remove Google's FLoC Network, only send this if Chrome browser
if ((parent.config.settings.allowframing !== true) && (typeof parent.config.settings.allowframing !== 'string')) { headers['X-Frame-Options'] = 'sameorigin'; }
@ -6063,7 +6072,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
obj.app.ws(url + 'mstscrelay.ashx', function (ws, req) {
const domain = getDomain(req);
if (domain == null) { parent.debug('web', 'mstsc: failed checks.'); try { ws.close(); } catch (e) { } return; }
require('./apprelays.js').CreateMstscRelay(obj, obj.db, ws, req, obj.args, domain);
// If no user is logged in and we have a default user, set it now.
if ((req.session.userid == null) && (typeof obj.args.user == 'string') && (obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()])) { req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); }
try { require('./apprelays.js').CreateMstscRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
});
}
@ -6073,9 +6084,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
obj.app.ws(url + 'sshrelay.ashx', function (ws, req) {
const domain = getDomain(req);
if (domain == null) { parent.debug('web', 'ssh: failed checks.'); try { ws.close(); } catch (e) { } return; }
try {
require('./apprelays.js').CreateSshRelay(obj, obj.db, ws, req, obj.args, domain);
} catch (ex) { console.log(ex); }
// If no user is logged in and we have a default user, set it now.
if ((req.session.userid == null) && (typeof obj.args.user == 'string') && (obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()])) { req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); }
try { require('./apprelays.js').CreateSshRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
});
obj.app.ws(url + 'sshterminalrelay.ashx', function (ws, req) {
PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {