diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..b4978944
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -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
+ }
+}
+```
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..11fc491e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -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.
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..960df71d
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -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 }}
diff --git a/MeshCentralServer.njsproj b/MeshCentralServer.njsproj
index 7cbdaf20..0403d4b8 100644
--- a/MeshCentralServer.njsproj
+++ b/MeshCentralServer.njsproj
@@ -239,6 +239,7 @@
+
@@ -261,10 +262,8 @@
-
-
@@ -698,6 +697,7 @@
+
@@ -738,6 +738,7 @@
+
diff --git a/agents/MeshCentralRouter.exe b/agents/MeshCentralRouter.exe
index ef3cda87..4c0b9830 100644
Binary files a/agents/MeshCentralRouter.exe and b/agents/MeshCentralRouter.exe differ
diff --git a/agents/MeshCmd-signed.exe b/agents/MeshCmd-signed.exe
index ad7eb6a2..ad23a7c2 100644
Binary files a/agents/MeshCmd-signed.exe and b/agents/MeshCmd-signed.exe differ
diff --git a/agents/MeshCmd64-signed.exe b/agents/MeshCmd64-signed.exe
index 84211fdb..f1c1d3eb 100644
Binary files a/agents/MeshCmd64-signed.exe and b/agents/MeshCmd64-signed.exe differ
diff --git a/agents/compressModules.bat b/agents/compressModules.bat
index cc5af30b..dd9d8444 100644
--- a/agents/compressModules.bat
+++ b/agents/compressModules.bat
@@ -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
\ No newline at end of file
diff --git a/agents/hashagents.bat b/agents/hashagents.bat
index fd893be0..f8ca9582 100644
--- a/agents/hashagents.bat
+++ b/agents/hashagents.bat
@@ -1 +1 @@
-MeshService-signed.exe hashagents.js > hashagents.json
\ No newline at end of file
+MeshService.exe hashagents.js > hashagents.json
\ No newline at end of file
diff --git a/agents/hashagents.js b/agents/hashagents.js
index b5a5391b..52c0ee30 100644
--- a/agents/hashagents.js
+++ b/agents/hashagents.js
@@ -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,
diff --git a/agents/hashagents.json b/agents/hashagents.json
index 069fd7bc..14f83a82 100644
--- a/agents/hashagents.json
+++ b/agents/hashagents.json
@@ -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",
diff --git a/agents/meshcmd.js b/agents/meshcmd.js
index e63d0c86..5628303b 100644
--- a/agents/meshcmd.js
+++ b/agents/meshcmd.js
@@ -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
//
diff --git a/agents/meshcore.js b/agents/meshcore.js
index e8d3b723..54e12a23 100644
--- a/agents/meshcore.js
+++ b/agents/meshcore.js
@@ -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;
}
diff --git a/agents/recoverycore.js b/agents/recoverycore.js
index c0bbfc66..169b170c 100644
--- a/agents/recoverycore.js
+++ b/agents/recoverycore.js
@@ -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) {
diff --git a/amtmanager.js b/amtmanager.js
index 34d95781..23004792 100644
--- a/amtmanager.js
+++ b/amtmanager.js
@@ -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
}
diff --git a/apprelays.js b/apprelays.js
index ac742a2d..aa6e7bb2 100644
--- a/apprelays.js
+++ b/apprelays.js
@@ -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]); }
@@ -1623,4 +2227,4 @@ function checkRelayRights(parent, domain, user, relayNodeId, func) {
parent.GetNodeWithRights(domain, user, relayNodeId, function (node, rights, visible) {
func((node != null) && (rights == 0xFFFFFFFF));
});
-}
\ No newline at end of file
+}
diff --git a/authenticode.js b/authenticode.js
index faa5a016..213d3d4d 100644
--- a/authenticode.js
+++ b/authenticode.js
@@ -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;
+
diff --git a/db.js b/db.js
index b4939f5d..899d1e38 100644
--- a/db.js
+++ b/db.js
@@ -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); }
diff --git a/docs/docs/design/index.md b/docs/docs/design/index.md
index 3eda1e7c..6c90729a 100644
--- a/docs/docs/design/index.md
+++ b/docs/docs/design/index.md
@@ -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
+
+
+
+
+
## 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.
diff --git a/docs/docs/install/index.md b/docs/docs/install/index.md
index 34a428bf..f278b4b9 100644
--- a/docs/docs/install/index.md
+++ b/docs/docs/install/index.md
@@ -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
+
+
+
+
\ No newline at end of file
diff --git a/docs/docs/intelamt/index.md b/docs/docs/intelamt/index.md
index c7ba65ca..67b3f74d 100644
--- a/docs/docs/intelamt/index.md
+++ b/docs/docs/intelamt/index.md
@@ -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
+
+
+
+
+
## 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

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
+
+
+
+
+
+## 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.
+
+
+
+
diff --git a/docs/docs/meshcentral/codesigning.md b/docs/docs/meshcentral/codesigning.md
index f1482bb4..5cd7404a 100644
--- a/docs/docs/meshcentral/codesigning.md
+++ b/docs/docs/meshcentral/codesigning.md
@@ -6,6 +6,63 @@ Nodejs Code Signing module
+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
+
+!!!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`
\ No newline at end of file
diff --git a/docs/docs/meshcentral/debugging.md b/docs/docs/meshcentral/debugging.md
index b34360d5..ab6faa82 100644
--- a/docs/docs/meshcentral/debugging.md
+++ b/docs/docs/meshcentral/debugging.md
@@ -6,7 +6,22 @@ Make sure you understand how MeshCentral works with your browser using chrome de
-## Enabling trace in your browser Dev Tools
+## MeshCentral Server
+
+### Useful config.js settings
+
+
+
+```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
aka trying figure out what this is

+
+### 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
+
+
+
+```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.
+
diff --git a/docs/docs/meshcentral/devicetabs.md b/docs/docs/meshcentral/devicetabs.md
new file mode 100644
index 00000000..4373a399
--- /dev/null
+++ b/docs/docs/meshcentral/devicetabs.md
@@ -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
+
+
+
+## Desktop
+
+## Terminal
+
+## Files
+
+## Events
+
+## Details
+
+## Console
\ No newline at end of file
diff --git a/docs/docs/meshcentral/images/2022-06-17-15-56-14.png b/docs/docs/meshcentral/images/2022-06-17-15-56-14.png
new file mode 100644
index 00000000..3ad780dc
Binary files /dev/null and b/docs/docs/meshcentral/images/2022-06-17-15-56-14.png differ
diff --git a/docs/docs/meshcentral/images/2022-06-17-15-56-55.png b/docs/docs/meshcentral/images/2022-06-17-15-56-55.png
new file mode 100644
index 00000000..dd50e316
Binary files /dev/null and b/docs/docs/meshcentral/images/2022-06-17-15-56-55.png differ
diff --git a/docs/docs/meshcentral/images/2022-06-17-15-57-03.png b/docs/docs/meshcentral/images/2022-06-17-15-57-03.png
new file mode 100644
index 00000000..65111b19
Binary files /dev/null and b/docs/docs/meshcentral/images/2022-06-17-15-57-03.png differ
diff --git a/docs/docs/meshcentral/images/2022-06-17-15-57-15.png b/docs/docs/meshcentral/images/2022-06-17-15-57-15.png
new file mode 100644
index 00000000..60810b55
Binary files /dev/null and b/docs/docs/meshcentral/images/2022-06-17-15-57-15.png differ
diff --git a/docs/docs/meshcentral/images/2022-06-17-15-57-30.png b/docs/docs/meshcentral/images/2022-06-17-15-57-30.png
new file mode 100644
index 00000000..2a8337c8
Binary files /dev/null and b/docs/docs/meshcentral/images/2022-06-17-15-57-30.png differ
diff --git a/docs/docs/meshcentral/images/2022-06-17-15-57-52.png b/docs/docs/meshcentral/images/2022-06-17-15-57-52.png
new file mode 100644
index 00000000..11f97e89
Binary files /dev/null and b/docs/docs/meshcentral/images/2022-06-17-15-57-52.png differ
diff --git a/docs/docs/meshcentral/images/7daypowerstate.png b/docs/docs/meshcentral/images/7daypowerstate.png
new file mode 100644
index 00000000..de545544
Binary files /dev/null and b/docs/docs/meshcentral/images/7daypowerstate.png differ
diff --git a/docs/docs/meshcentral/index.md b/docs/docs/meshcentral/index.md
index e975f8dc..9a56067e 100644
--- a/docs/docs/meshcentral/index.md
+++ b/docs/docs/meshcentral/index.md
@@ -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
+
+
+
+
+
+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 Let’s Encrypt certificate in the database. Generally, one would use a reverse proxy with Let’s 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 Let’s Encrypt certificate using the Let’s Encryp
If Let’s 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, it’s 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

+To enable SSH support, add this line to the domain section of your config.json:
+
+```json
+"ssh": true
+```
+
Video Walkthru
+
### 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
+
+
+
+
+
Sometimes it’s 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/

@@ -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
+
+
+
+```bash
+sudo systemctl start meshcentral.service
+sudo systemctl status meshcentral.service
+```
+
+Log into your MC:
+
+
+
+
+
+Create desktop app
+
+
+
+Enter the Client ID and Client Secret into MC
+
+
+
+
+
## 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.
+
+
+
+
\ No newline at end of file
diff --git a/docs/docs/meshcentral/plugins.md b/docs/docs/meshcentral/plugins.md
new file mode 100644
index 00000000..e9d664ff
--- /dev/null
+++ b/docs/docs/meshcentral/plugins.md
@@ -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:
+5. The plugin pops up in the plugin list below the download button, you can now configure and enable/disable it.
diff --git a/docs/docs/meshcmd/index.md b/docs/docs/meshcmd/index.md
index 1032d51b..38528f91 100644
--- a/docs/docs/meshcmd/index.md
+++ b/docs/docs/meshcmd/index.md
@@ -416,6 +416,12 @@ In this example, the CIRA setup script was run on a remote computer. After the s
## IDE Redirection
+## Video Walkthru
+
+
';
- var y = '';
- 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 += ''; } // Remote command permission
if ((currentNode.conn != 0) && ((rights & 262144) != 0)) { count++; y += ''; }
//if ((currentNode.conn & 16) != 0) { count++; y += ''; }
- 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 += '';
- y += '';
y += '';
}
+ if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 64) != 0)) {
+ count++;
+ y += '';
+ }
//if ((getNodeAmtVersion(currentNode) >= 15) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && (rights == 0xFFFFFFFF) && ((features & 0x00000400) == 0)) { count++; y += ''; } // CIRA (2) or AMT (4) connected
//if (((currentNode.conn & 1) != 0) && ((rights & 32768) != 0)) { count++; y += ''; }
}
@@ -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 += ''; }
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', '' + format("SSH Port {0}", (terminalNode.sshport ? terminalNode.sshport : 22)) + '');
@@ -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', '' + format("SSH Port {0}", (filesNode.sshport ? filesNode.sshport : 22)) + '');
@@ -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 += '';
@@ -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® Active Management Technology (Intel® AMT)", html: x, img: 'amt' }); }
+ if (x != '') {
+ if ((typeof node.intelamt.sku == 'number') && ((node.intelamt.sku & 16) != 0)) {
+ sections.push({ name: "Intel® Standard Manageability (Intel® SM)", html: x, img: 'amt' });
+ } else {
+ sections.push({ name: "Intel® Active Management Technology (Intel® AMT)", html: x, img: 'amt' });
+ }
+ }
}
if (hardware.identifiers) {
diff --git a/views/default.handlebars b/views/default.handlebars
index b5000e5a..7bfaf286 100644
--- a/views/default.handlebars
+++ b/views/default.handlebars
@@ -112,6 +112,12 @@
Alternate Port
+
+
Alternate Port
+
+
+
Alternate Port
+
Rename
Edit
@@ -619,7 +625,7 @@
-
Intel® AMT Redirection port or KVM feature is disabled, click here to enable it.
+
Redirection port or KVM feature is disabled, click here to enable it.
@@ -655,7 +661,7 @@
-
+
Disconnected
@@ -696,7 +702,7 @@
-  
+
@@ -731,7 +737,7 @@
-
Intel® AMT Redirection port or KVM feature is disabled, click here to enable it.
+
Redirection port or KVM feature is disabled, click here to enable it.
@@ -864,7 +870,7 @@
-
Intel® AMT -
+
Intel® AMT -
@@ -1323,6 +1329,7 @@
Other Settings
+
@@ -1346,6 +1353,7 @@
+
@@ -1379,6 +1387,7 @@
+
@@ -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 += '
' + "WARNING: " + y + '
';
} 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 += '
' + "WARNING: " + z + '
';
}
}
@@ -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 += '' + "HTTP" + ((node.httpport && (node.httpport != 80)) ? '/' + node.httpport : '') + ' ';
+ x += '' + "HTTPS" + ((node.httspport && (node.httpsport != 443)) ? '/' + node.httpsport : '') + ' ';
+ }
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 += '' + "SSH" + ' ';
+ x += '' + "SSH" + ' ';
}
}
if (navigator.platform.toLowerCase() == 'win32') {
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.scp != false)) {
- x += '' + "SCP" + ' ';
+ x += '' + "SCP" + ' ';
}
}
}
@@ -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:" + '
';
+ 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:" + '
';
+ 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 += ' ';
// 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 += ''; }
+ if (((meshrights & (4 + 8 + 64 + 262144)) != 0) && (node.mtype < 3) && ((node.agent == null) || (node.agent.id != 34))) { x += ''; }
x += '';
x += '';
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 += '' + "HTTP" + ((node.httpport && (node.httpport != 80)) ? '/' + node.httpport : '') + ' ';
+ x += '' + "HTTPS" + ((node.httpsport && (node.httpsport != 443)) ? '/' + node.httpsport : '') + ' ';
+ }
if ((node.agent.id > 0) && (node.agent.id < 5)) {
if (navigator.platform.toLowerCase() == 'win32') {
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.rdp != false)) {
- x += '' + "RDP" + ' ';
+ x += '' + "RDP" + ((node.rdpport && (node.rdpport != 3389)) ? '/' + node.rdpport : '') + ' ';
}
}
}
if (node.agent.id > 4) {
if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) {
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.ssh != false)) {
- x += '' + "SSH" + ' ';
+ x += '' + "SSH" + ((node.sshport && (node.sshport != 22)) ? '/' + node.sshport : '') + ' ';
}
}
if (navigator.platform.toLowerCase() == 'win32') {
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.scp != false)) {
- x += '' + "SCP" + ' ';
+ x += '' + "SCP" + ((node.sshport && (node.sshport != 22)) ? '/' + node.sshport : '') + ' ';
}
}
}
@@ -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®SM");
+ QH('p14deviceNamePrefix', "Intel® SM");
+ } else {
+ QH('MainDevAmt', "Intel®AMT");
+ QH('p14deviceNamePrefix', "Intel® 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 += ''; } // Remote command permission
if ((currentNode.conn != 0) && ((rights & 262144) != 0)) { count++; y += ''; }
if ((currentNode.conn & 16) != 0) { count++; y += ''; }
- 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 += '';
- y += '';
y += '';
}
+ if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 64) != 0)) {
+ count++;
+ y += '';
+ }
if ((getNodeAmtVersion(currentNode) >= 15) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && (rights == 0xFFFFFFFF) && ((features & 0x00000400) == 0)) { count++; y += ''; } // CIRA (2) or AMT (4) connected
if (((currentNode.conn & 1) != 0) && ((rights & 32768) != 0)) { count++; y += ''; }
}
@@ -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 += ''; }
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', '' + format("SSH Port {0}", (terminalNode.sshport?terminalNode.sshport:22)) + '');
@@ -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', '' + format("SSH Port {0}", (filesNode.sshport?filesNode.sshport:22)) + '');
@@ -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 += '';
@@ -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® Active Management Technology (Intel® AMT)", html: x, img: 'amt64.png' }); }
+ if (x != '') {
+ if ((typeof node.intelamt.sku == 'number') && ((node.intelamt.sku & 16) != 0)) {
+ sections.push({ name: "Intel® Standard Manageability (Intel® SM)", html: x, img: 'amt64.png' });
+ } else {
+ sections.push({ name: "Intel® Active Management Technology (Intel® AMT)", html: x, img: 'amt64.png' });
+ }
+ }
}
if (hardware.identifiers) {
diff --git a/views/sharing-mobile.handlebars b/views/sharing-mobile.handlebars
index 87bb00ce..e2ad0125 100644
--- a/views/sharing-mobile.handlebars
+++ b/views/sharing-mobile.handlebars
@@ -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 += ''; }
QH('d7bitmapquality', r);
d7desktopmode.value = desktopsettings.encoding;
diff --git a/views/sharing.handlebars b/views/sharing.handlebars
index ca10d11a..4208d680 100644
--- a/views/sharing.handlebars
+++ b/views/sharing.handlebars
@@ -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 += ''; }
QH('d7bitmapquality', r);
d7desktopmode.value = desktopsettings.encoding;
diff --git a/webrelayserver.js b/webrelayserver.js
new file mode 100644
index 00000000..15d7334c
--- /dev/null
+++ b/webrelayserver.js
@@ -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;
+};
diff --git a/webserver.js b/webserver.js
index b4b6be3e..70b57ff6 100644
--- a/webserver.js
+++ b/webserver.js
@@ -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) {