Merge branch 'master_upstream' into Feature/docker
75
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Server Software (please complete the following information):**
|
||||
- OS: [e.g. Ubuntu]
|
||||
- Virtualization: [e.g. Docker]
|
||||
- Network: [e.g. LAN/WAN, reverse proxy, cloudflare, ssl offload, etc...]
|
||||
- Version: [e.g. 1.0.43]
|
||||
- Node: [e.g. 18.4.0]
|
||||
- Browser: [e.g. Google Chrome]
|
||||
|
||||
**Remote Device (please complete the following information):**
|
||||
- Device: [e.g. Laptop]
|
||||
- OS: [e.g. Windows 10]
|
||||
- Version: [e.g. 21H2]
|
||||
- Current Core Version (if known): [**HINT**: Go to a device then `console` Tab then type `info`]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
**Your config.json file**
|
||||
```
|
||||
{
|
||||
"$schema": "http://info.meshcentral.com/downloads/meshcentral-config-schema.json",
|
||||
"__comment1__": "This is a simple configuration file, all values and sections that start with underscore (_) are ignored. Edit a section and remove the _ in front of the name. Refer to the user's guide for details.",
|
||||
"__comment2__": "See node_modules/meshcentral/sample-config-advanced.json for a more advanced example.",
|
||||
"settings": {
|
||||
"_cert": "myserver.mydomain.com",
|
||||
"_WANonly": true,
|
||||
"_LANonly": true,
|
||||
"_sessionKey": "MyReallySecretPassword1",
|
||||
"_port": 443,
|
||||
"_aliasPort": 443,
|
||||
"_redirPort": 80,
|
||||
"_redirAliasPort": 80
|
||||
},
|
||||
"domains": {
|
||||
"": {
|
||||
"_title": "MyServer",
|
||||
"_title2": "Servername",
|
||||
"_minify": true,
|
||||
"_newAccounts": true,
|
||||
"_userNameIsEmail": true
|
||||
}
|
||||
},
|
||||
"_letsencrypt": {
|
||||
"__comment__": "Requires NodeJS 8.x or better, Go to https://letsdebug.net/ first before trying Let's Encrypt.",
|
||||
"email": "myemail@mydomain.com",
|
||||
"names": "myserver.mydomain.com",
|
||||
"production": false
|
||||
}
|
||||
}
|
||||
```
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
21
.github/workflows/release.yml
vendored
Normal file
@ -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 }}
|
@ -239,6 +239,7 @@
|
||||
<Compile Include="translate\translate.js" />
|
||||
<Compile Include="ua-parser.js" />
|
||||
<Compile Include="webauthn.js" />
|
||||
<Compile Include="webrelayserver.js" />
|
||||
<Compile Include="webserver.js" />
|
||||
<Compile Include="winservice.js" />
|
||||
<Content Include="agents\codesign.cer" />
|
||||
@ -261,10 +262,8 @@
|
||||
<Content Include="agents\MeshCommander-Small.gz" />
|
||||
<Content Include="agents\meshinstall-initd.sh" />
|
||||
<Content Include="agents\meshinstall-linux.sh" />
|
||||
<Content Include="agents\MeshService-signed.exe" />
|
||||
<Content Include="agents\MeshService.exe" />
|
||||
<Content Include="agents\MeshService.pdb" />
|
||||
<Content Include="agents\MeshService64-signed.exe" />
|
||||
<Content Include="agents\MeshService64.exe" />
|
||||
<Content Include="agents\MeshService64.pdb" />
|
||||
<Content Include="agents\modules_meshcore\coretranslations.json" />
|
||||
@ -698,6 +697,7 @@
|
||||
<Folder Include="typings\globals\localforage\" />
|
||||
<Folder Include="typings\globals\lru-cache\" />
|
||||
<Folder Include="typings\globals\marked\" />
|
||||
<Folder Include="typings\globals\moment-timezone\" />
|
||||
<Folder Include="typings\globals\moment\" />
|
||||
<Folder Include="typings\globals\node-forge\" />
|
||||
<Folder Include="typings\globals\nodemailer\" />
|
||||
@ -738,6 +738,7 @@
|
||||
<TypeScriptCompile Include="typings\globals\localforage\index.d.ts" />
|
||||
<TypeScriptCompile Include="typings\globals\lru-cache\index.d.ts" />
|
||||
<TypeScriptCompile Include="typings\globals\marked\index.d.ts" />
|
||||
<TypeScriptCompile Include="typings\globals\moment-timezone\index.d.ts" />
|
||||
<TypeScriptCompile Include="typings\globals\moment\index.d.ts" />
|
||||
<TypeScriptCompile Include="typings\globals\node-forge\index.d.ts" />
|
||||
<TypeScriptCompile Include="typings\globals\nodemailer\index.d.ts" />
|
||||
|
@ -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
|
@ -1 +1 @@
|
||||
MeshService-signed.exe hashagents.js > hashagents.json
|
||||
MeshService.exe hashagents.js > hashagents.json
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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,7 +534,8 @@ function run(argv) {
|
||||
return;
|
||||
});
|
||||
} else if (settings.action == 'amthashes') {
|
||||
// Display Intel AMT list of trusted hashes
|
||||
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; });
|
||||
@ -545,6 +552,10 @@ function run(argv) {
|
||||
});
|
||||
}
|
||||
});
|
||||
} 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
|
||||
//
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
require("clipboard")(data.data);
|
||||
} // Set the clipboard
|
||||
mesh.SendCommand({ action: 'msg', type: 'setclip', sessionid: data.sessionid, success: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
require("clipboard")(data.data);
|
||||
mesh.SendCommand({ action: 'msg', type: 'setclip', sessionid: data.sessionid, success: true });
|
||||
} // Set the clipboard
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
@ -707,10 +720,13 @@ function agentUpdate_Start(updateurl, updateoptions) {
|
||||
s.restart();
|
||||
}
|
||||
catch (zz)
|
||||
{
|
||||
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) {
|
||||
|
@ -1525,6 +1525,7 @@ 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.
|
||||
@ -1543,7 +1544,7 @@ module.exports.CreateAmtManager = function (parent) {
|
||||
}
|
||||
}
|
||||
|
||||
// Figure out is there are no changes to 802.1x wired configuration
|
||||
// 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
|
||||
@ -1574,7 +1575,6 @@ module.exports.CreateAmtManager = function (parent) {
|
||||
// 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,7 +1813,9 @@ 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;
|
||||
|
||||
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;
|
||||
@ -1840,6 +1842,7 @@ module.exports.CreateAmtManager = function (parent) {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Done
|
||||
devTaskCompleted(dev);
|
||||
@ -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
|
||||
}
|
||||
|
614
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]); }
|
||||
|
594
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,19 +293,33 @@ 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
|
||||
var pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(pkcs7raw));
|
||||
|
||||
// To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForjeJS adds support for it in the future
|
||||
// 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 PKCS7 message
|
||||
var pkcs7 = p7.messageFromAsn1(pkcs7der);
|
||||
var pkcs7content = pkcs7.rawCapture.content.value[0];
|
||||
var pkcs7 = null, pkcs7content = null;
|
||||
try {
|
||||
pkcs7 = p7.messageFromAsn1(pkcs7der);
|
||||
pkcs7content = pkcs7.rawCapture.content.value[0];
|
||||
} catch (ex) { }
|
||||
|
||||
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();
|
||||
@ -330,7 +349,7 @@ function createAuthenticodeHandler(path) {
|
||||
) {
|
||||
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.
|
||||
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);
|
||||
}
|
||||
@ -343,6 +362,9 @@ function createAuthenticodeHandler(path) {
|
||||
// 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) {
|
||||
@ -359,9 +381,130 @@ function createAuthenticodeHandler(path) {
|
||||
// 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
|
||||
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;
|
||||
//console.log('Write until signature', totalWrite);
|
||||
} 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,16 +1664,100 @@ 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'));
|
||||
|
||||
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; }
|
||||
|
||||
// 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; }
|
||||
|
||||
// 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;
|
||||
@ -1334,10 +1783,12 @@ function createAuthenticodeHandler(path) {
|
||||
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 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); }
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
|
38
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)); });
|
||||
}
|
||||
}
|
||||
};
|
||||
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); }
|
||||
|
@ -81,6 +81,12 @@ The main takeaway is that MeshCentral is mostly an ExpressJS application. This i
|
||||
|
||||
MeshCentral will run `npm install` automatically when any of these optional modules are needed but not currently available.
|
||||
|
||||
## Understanding the different modes: LAN, WAN and Hybrid
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/gx5Fh3pQOns" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
## Code files and folders
|
||||
|
||||
Someone would think the server is rather simple when taking a look at the MeshCentral server code files. At a high level, the entire server has 3 folders, 3 text files and a manageable number of .js files that are fairly self-descriptive. Here is a list of the source files and folders.
|
||||
|
@ -24,3 +24,9 @@ You can run the MeshCentral Server with --help to get options for background ins
|
||||
Once you get MeshCentral installed, the first user account that is created will be the server administrator. So, don't delay and navigate to the login page and create a new account. You can then start using your server right away. A lot of the fun with MeshCentral is the 100's of configuration options that are available in the config.json file. You can put your own branding on the web pages, setup a SMTP email server, SMS services and much more.
|
||||
|
||||
You can look [here for simple config.json](https://raw.githubusercontent.com/Ylianst/MeshCentral/master/sample-config.json), [here for a more advanced configuration](https://raw.githubusercontent.com/Ylianst/MeshCentral/master/sample-config-advanced.json) and [here for all possible configuration options](https://raw.githubusercontent.com/Ylianst/MeshCentral/master/meshcentral-config-schema.json). You can also take a look at the [MeshCentral User's Guide](https://meshcentral.com/info/docs/MeshCentral2InstallGuide.pdf) and [tutorial videos](https://meshcentral.com/info/tutorials.html) for additional help.
|
||||
|
||||
## Video Walkthru
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/LSiWuu71k_U" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
@ -13,6 +13,12 @@ Intel AMT Guide [as .odt](https://github.com/Ylianst/MeshCentral/blob/master/doc
|
||||
|
||||
This user guide contains all essential information for activating and using Intel® Active Management Technology (Intel® AMT) with MeshCentral. We will review how to activate, connect to and use Intel AMT features and how this benefit administrators that want to manage computers remotely. This document expect the reader to already be familiar with how to install and operate MeshCentral and have a basic understanding of how Intel® AMT works.
|
||||
|
||||
## History of AMT
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/_SXT0Gr4Mls" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
## Introduction
|
||||
|
||||
MeshCentral is a free open source web-based remote computer management software and it fully supports Intel® Active Management Technology (Intel® AMT). MeshCentral does not require that computers it manages support Intel AMT, but if a remote computer has this capability, MeshCentral will make use of it.
|
||||
@ -173,3 +179,19 @@ Once Intel AMT is in a situation where ACM activation can occur, the activation
|
||||
![](images/2022-05-16-23-16-05.png)
|
||||
|
||||
The best way to test this feature is to create an “Intel AMT only” device group and run the MeshCMD command on the remote system to perform activation. If there is a problem, this process should clearly display why ACM activation fails.
|
||||
|
||||
## Intel AMT MEI and LMS
|
||||
|
||||
Intel Active Management Technology (Intel AMT) can communicate to the local platform using the Management Engine Interface (MEI). We show how your can use that to get Intel AMT information. For more advanced usages, you need to connect using TCP and TLS which requires Intel Local Manageability Service (LMS). We show how MeshCentral's Mesh Agent and MeshCMD have a small version of LMS built-in and how it works
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/mStyhe-fSC0" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
## Intel AMT System Defense
|
||||
|
||||
As part of Intel AMT there are hardware filters in the network interface you can setup to match and perform actions on packets. This happens at Ethernet speeds with no slow down and independent of the OS.
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/q7RyboI4uew" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
@ -6,6 +6,63 @@ Nodejs Code Signing module
|
||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/xteKscs_Jgo" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
MeshCentral comes with authenticode.js, you can run it like this:
|
||||
|
||||
```bash
|
||||
node node_modules/meshcentral/authenticode-js
|
||||
```
|
||||
|
||||
and you will get
|
||||
|
||||
```
|
||||
MeshCentral Authenticode Tool.
|
||||
Usage:
|
||||
node authenticode.js [command] [options]
|
||||
Commands:
|
||||
info: Show information about an executable.
|
||||
--exe [file] Required executable to view information.
|
||||
--json Show information in JSON format.
|
||||
sign: Sign an executable.
|
||||
--exe [file] Required executable to sign.
|
||||
--out [file] Resulting signed executable.
|
||||
--pem [pemfile] Certificate & private key to sign the executable with.
|
||||
--desc [description] Description string to embbed into signature.
|
||||
--url [url] URL to embbed into signature.
|
||||
--hash [method] Default is SHA384, possible value: MD5, SHA224, SHA256, SHA384 or SHA512.
|
||||
--time [url] The time signing server URL.
|
||||
--proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://
|
||||
unsign: Remove the signature from the executable.
|
||||
--exe [file] Required executable to un-sign.
|
||||
--out [file] Resulting executable with signature removed.
|
||||
createcert: Create a code signging self-signed certificate and key.
|
||||
--out [pemfile] Required certificate file to create.
|
||||
--cn [value] Required certificate common name.
|
||||
--country [value] Certificate country name.
|
||||
--state [value] Certificate state name.
|
||||
--locality [value] Certificate locality name.
|
||||
--org [value] Certificate organization name.
|
||||
--ou [value] Certificate organization unit name.
|
||||
--serial [value] Certificate serial number.
|
||||
timestamp: Add a signed timestamp to an already signed executable.
|
||||
--exe [file] Required executable to sign.
|
||||
--out [file] Resulting signed executable.
|
||||
--time [url] The time signing server URL.
|
||||
--proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://
|
||||
|
||||
Note that certificate PEM files must first have the signing certificate,
|
||||
followed by all certificates that form the trust chain.
|
||||
|
||||
When doing sign/unsign, you can also change resource properties of the generated file.
|
||||
|
||||
--filedescription [value]
|
||||
--fileversion [value]
|
||||
--internalname [value]
|
||||
--legalcopyright [value]
|
||||
--originalfilename [value]
|
||||
--productname [value]
|
||||
--productversion [value]
|
||||
```
|
||||
|
||||
## Automatic Agent Code Signing
|
||||
|
||||
If you want to self-sign the mesh agent so you can whitelist the software in your AV, and lock it to your server and organization.
|
||||
@ -13,3 +70,6 @@ If you want to self-sign the mesh agent so you can whitelist the software in you
|
||||
<div class="video-wrapper">
|
||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/qMAestNgCwc" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
!!!note
|
||||
If you generate your private key on windows with use `BEGIN PRIVATE KEY` and openssl needs `BEGIN RSA PRIVATE KEY` you can convert your private key to rsa private key using `openssl rsa -in server.key -out server_new.key`
|
@ -6,7 +6,22 @@ Make sure you understand how MeshCentral works with your browser using chrome de
|
||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/3vI4URd3VzU" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
## Enabling trace in your browser Dev Tools
|
||||
## MeshCentral Server
|
||||
|
||||
### Useful config.js settings
|
||||
|
||||
<https://github.com/Ylianst/MeshCentral/blob/master/meshcentral-config-schema.json>
|
||||
|
||||
```json
|
||||
"AgentsInRAM": false,
|
||||
"AgentUpdateBlockSize": 2048,
|
||||
"agentUpdateSystem": 1,
|
||||
"noAgentUpdate": 1,
|
||||
"WsCompression": false,
|
||||
"AgentWsCompression": false,
|
||||
```
|
||||
|
||||
### Enabling trace in your browser Dev Tools
|
||||
|
||||
`Trace=1` as a parameter in chrome dev tools for debugging
|
||||
|
||||
@ -34,7 +49,7 @@ If you want to change node to meshcentral in journalctl, add this to /etc/system
|
||||
SyslogIdentifier=meshcentral
|
||||
```
|
||||
|
||||
## Server: Logging it all
|
||||
### Logging it all
|
||||
|
||||
To log everything that's possible, prepare the log directory.
|
||||
|
||||
@ -84,7 +99,7 @@ You'll then have 3 files:
|
||||
|
||||
`log.txt` will now log everything in the Trace tab
|
||||
|
||||
## Restricting server to specific IP(s)
|
||||
### Restricting server to specific IP(s)
|
||||
|
||||
When doing debugging on my development server, I use this line in the settings section to block all agent connections except the agent I want:
|
||||
|
||||
@ -94,8 +109,101 @@ When doing debugging on my development server, I use this line in the settings s
|
||||
|
||||
Of course, this is just for debugging.
|
||||
|
||||
## Finding system ID types
|
||||
### Finding system ID types
|
||||
|
||||
<https://serverurl/meshagents> aka trying figure out what this is
|
||||
|
||||
![ID](images/determine-id.png)
|
||||
|
||||
### Pull down cert .crt file from internet
|
||||
|
||||
[See #1662](https://github.com/Ylianst/MeshCentral/issues/1662#issuecomment-666559391) We have run into this challenge before, where our .crt file expired and then all our agents were unable to connect. In our case, the TLS cert was available on the internet, and thus, we were able to use these commands to update it:
|
||||
|
||||
```bash
|
||||
echo -n \| openssl s_client -connect yourdomain.com:443 2> /dev/null\| sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /opt/meshcentral/meshcentral-data/webserver-cert-public.crt
|
||||
service meshcentral restart
|
||||
```
|
||||
|
||||
## MeshAgent
|
||||
|
||||
### Agent Debug Logs to server
|
||||
|
||||
This automatically downloads all agent error logs into `meshcentral-data/agenterrorlogs.txt`
|
||||
|
||||
[Set](https://github.com/Ylianst/MeshCentral/blob/aa58afcc3a5d738177ab7a7b6d0228d72af82b85/meshcentral-config-schema.json#L100) in `config.json`
|
||||
|
||||
```json
|
||||
"agentLogDump": true
|
||||
```
|
||||
|
||||
### Determine Agent capabilities
|
||||
|
||||
On the server goto the agents console tab. Type:
|
||||
|
||||
```
|
||||
info
|
||||
```
|
||||
|
||||
### Useful MeshAgent.msh flags
|
||||
|
||||
<https://github.com/Ylianst/MeshAgent/blob/master/meshcore/agentcore.h#L190>
|
||||
|
||||
```json
|
||||
controlChannelDebug=1
|
||||
logUpdate=1
|
||||
```
|
||||
|
||||
### Obtain generated .msh File
|
||||
|
||||
If you need a trick to get the .msh file, you can add ?debug=1 to the URL and click "Add Agent", there will be an extra link to download it.
|
||||
|
||||
### MeshAgent Commands
|
||||
|
||||
```
|
||||
MeshAgent run
|
||||
MeshAgent dbTool.js list
|
||||
```
|
||||
|
||||
Forcing Core version from Cmdline
|
||||
|
||||
* Download meschore.js and rename to CoreModule.js and put it alongside MeshAgent.exe
|
||||
* Stop MeshAgent service
|
||||
* Run `MeshAgent.exe dbTool.js import CoreModule`
|
||||
|
||||
### On the fly Patching MeshAgent
|
||||
|
||||
[MeshAgent#89 (comment)](https://github.com/Ylianst/MeshAgent/issues/89#issuecomment-949901720)
|
||||
|
||||
There are two ways to do this... When debugging, and making changes, you can modify the .js file directly, and just save it in the same folder as the agent binary... The agent will use the .js file from disc if it's there, if it's newer than the one compiled in the binary. You don't even need to restart the agent. You can just clear the core, and reload the core.....
|
||||
|
||||
When you are satisfied with your changes to the .js file, you can use the clipboard, in the following fashion:
|
||||
|
||||
```bash
|
||||
meshagent -exec "require('clipboard').nativeAddCompressedModule('foo');process.exit();"
|
||||
```
|
||||
|
||||
if the file you modified isn't in the same folder as the agent binary, you can use the following command if you don't want to move the file, and edit it directly in the modules folder:
|
||||
|
||||
```bash
|
||||
meshagent -exec "setModulePath('pathToFolder');require('clipboard').nativeAddCompressedModule('foo');process.exit();"
|
||||
```
|
||||
|
||||
This command is just like the previous, except it searches for modules in the path specified.
|
||||
|
||||
Just substitute foo, with the name of the module that you modified. It will load the module from disc, compress it, and save it into the clipboard.. So you can just load up your editor for ILibDuktape_Polyfills.c, and find where that particular module is defined... and paste directly from the clipboard... The clipboard will contain all the necessary C code to uncompress and load the module.
|
||||
|
||||
If the compressed result is relatively long, it will auto break it up into multiple lines to work around an issue with visual studio's maximum string literal limitations.
|
||||
|
||||
### Agent Debugging using MeshCore JS Debugger
|
||||
|
||||
[(#119)](https://github.com/Ylianst/MeshAgent/issues/119) How to test changes to the meshagent and recompile them.
|
||||
|
||||
* Copy duktape-debugger.js to the mesh directory on the target machine.
|
||||
* From the console tab of the agent, enter this command, substituting the port number you want to use instead of 9999
|
||||
`eval "attachDebugger({ webport: 9999 })"`
|
||||
|
||||
Then open your browser to http://localhost:9999 or whatever port you used.
|
||||
|
||||
!!!note
|
||||
If you pause the debugger, and happen to forget about it, the agent will automatically kill itself and restart because it will think that a thread is stuck. Default debugger timeout is 10 minutes, you may find a log entry saved to disk saying "Microstack Thread STUCK", or something similar.
|
||||
|
||||
|
26
docs/docs/meshcentral/devicetabs.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Device Tabs
|
||||
|
||||
## General
|
||||
|
||||
### 7 Day Power State
|
||||
|
||||
Legend
|
||||
|
||||
1. Black color: device is powered om
|
||||
2. purple color: device is in sleep state
|
||||
3. blue/green color : device is connected trough amt/cira, but not powered on
|
||||
4. grey color: device is powered off
|
||||
|
||||
![](images/7daypowerstate.png)
|
||||
|
||||
## Desktop
|
||||
|
||||
## Terminal
|
||||
|
||||
## Files
|
||||
|
||||
## Events
|
||||
|
||||
## Details
|
||||
|
||||
## Console
|
BIN
docs/docs/meshcentral/images/2022-06-17-15-56-14.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
docs/docs/meshcentral/images/2022-06-17-15-56-55.png
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
docs/docs/meshcentral/images/2022-06-17-15-57-03.png
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
docs/docs/meshcentral/images/2022-06-17-15-57-15.png
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
docs/docs/meshcentral/images/2022-06-17-15-57-30.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
docs/docs/meshcentral/images/2022-06-17-15-57-52.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
docs/docs/meshcentral/images/7daypowerstate.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
@ -111,6 +111,16 @@ Click on any computer and go into the “Desktop” and “Files” tabs to remo
|
||||
|
||||
For advance users with console/command line interface experience, go into “Terminal” to perform scripting or quick tasks with CLI tools.
|
||||
|
||||
### Desktop Control
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/alknFiojQPM" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
Depending on how the agent is connected to the server, there are multiple methods to remote control. Mesh Agent, RDP, and AMT
|
||||
|
||||
For RDP connections, if you have previously saved the credentials that is usable by all users on the system. If you want to remove those saved credentials that's under the `General Tab` > `Credentials`. Click pen to clear them.
|
||||
|
||||
## Server Certificate
|
||||
|
||||
As seen in the previous chapter, MeshCentral is setup with a self-signed certificate by default and the web browser will issue a warning concerning the validity of the certificate.
|
||||
@ -448,6 +458,34 @@ This first line will load many of the “meshcentral-data” files into the data
|
||||
|
||||
Note that MeshCentral does not currently support placing a 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
|
||||
|
||||
![](images/2022-05-31-10-30-42.png)
|
||||
|
||||
To enable SSH support, add this line to the domain section of your config.json:
|
||||
|
||||
```json
|
||||
"ssh": true
|
||||
```
|
||||
|
||||
Video Walkthru
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/TtW5-g6SeZQ" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
|
||||
### Raritan and WebPowerSwitch with Relay
|
||||
|
||||
In addition to local device groups, the IP-KVM/Power switch device group was also improved to support a MeshAgent as a relay. This is big news for Raritan IP-KVM switch owners as you can now monitor your IP-KVM ports and access them remotely from the Internet. The same can be done with WebPowerSwitch allowing full out-of-band remote access to devices from anywhere in the world.
|
||||
@ -724,6 +767,12 @@ In addition to local device groups, the IP-KVM/Power switch device group was als
|
||||
|
||||
## NGINX Reverse-Proxy Setup
|
||||
|
||||
### Video Walkthru
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/YSmiLyKSX2I" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
Sometimes 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/
|
||||
|
||||
![](images/2022-05-19-00-23-11.png)
|
||||
@ -1180,6 +1229,38 @@ mongorestore --archive=backup.archive
|
||||
|
||||
This will re-import the database from the backup. You can then start MeshCentral again.
|
||||
|
||||
### Backup to Google Drive
|
||||
|
||||
```bash
|
||||
sudo systemctl stop meshcentral.service
|
||||
nano /opt/meshcentral/meshcentral-data/config.json
|
||||
```
|
||||
|
||||
Remove underscored items
|
||||
|
||||
![](images/2022-06-17-15-56-14.png)
|
||||
|
||||
```bash
|
||||
sudo systemctl start meshcentral.service
|
||||
sudo systemctl status meshcentral.service
|
||||
```
|
||||
|
||||
Log into your MC:
|
||||
|
||||
![](images/2022-06-17-15-56-55.png)
|
||||
|
||||
![](images/2022-06-17-15-57-03.png)
|
||||
|
||||
Create desktop app
|
||||
|
||||
![](images/2022-06-17-15-57-15.png)
|
||||
|
||||
Enter the Client ID and Client Secret into MC
|
||||
|
||||
![](images/2022-06-17-15-57-30.png)
|
||||
|
||||
![](images/2022-06-17-15-57-52.png)
|
||||
|
||||
## HashiCorp Vault support
|
||||
|
||||
MeshCentral has built-in support for HashiCorp Vault so that all configuration and certificates used by MeshCentral are retrieved from a Vault server. Vault is a secret store server and when used with MeshCentral, the MeshCentral server will not be storing any secrets locally. You can get started with Vault here: https://www.vaultproject.io/
|
||||
@ -1685,3 +1766,11 @@ su -c '/bin/bash -i' myOtherUser
|
||||
```
|
||||
|
||||
This will run bash in interactive mode and work correctly.
|
||||
|
||||
#### SSH and SFTP integration to the Terminal
|
||||
|
||||
MeshCentral has built-in web-based integration of SSH in the "Termina" tab and SFTP in the "Files" tab.
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/7qAbl2OuZEU" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
9
docs/docs/meshcentral/plugins.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Plugins
|
||||
|
||||
## Installation
|
||||
|
||||
1. Enable plugins in the configuration and restart MC as described.
|
||||
2. Log into MC as full administrator.
|
||||
3. Go my `My Server` -> `Plugins`, hit the Download plugin button.
|
||||
4. A dialog opens requesting an URL, put in: <https://github.com/ryanblenis/MeshCentral-ScriptTask>
|
||||
5. The plugin pops up in the plugin list below the download button, you can now configure and enable/disable it.
|
@ -416,6 +416,12 @@ In this example, the CIRA setup script was run on a remote computer. After the s
|
||||
|
||||
## IDE Redirection
|
||||
|
||||
## Video Walkthru
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe width="320" height="180" src="https://www.youtube.com/embed/iG73j7Toz1c" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
MeshCmd has all the code needed to perform Intel AMT IDE Redirection from the command line. This allows disk images on the administrator’s computer to be remotely mounted to an Intel AMT computer. You need to start with a floppy disk .img file and/or an .iso CDROM file.
|
||||
|
||||
![](images/2022-05-15-15-42-01.png)
|
||||
|
@ -9,6 +9,7 @@ nav:
|
||||
- MeshCentral2:
|
||||
- 'MeshCentral2 Guide': 'meshcentral/index.md'
|
||||
- 'All Configuration Options': 'meshcentral/config.md'
|
||||
- 'Device Tabs': 'meshcentral/devicetabs.md'
|
||||
- 'Tokens': 'meshcentral/tokens.md'
|
||||
- 'Assistant': 'meshcentral/assistant.md'
|
||||
- 'Code Signing': 'meshcentral/codesigning.md'
|
||||
|
1
docs/powerpoints/README.md
Normal file
@ -0,0 +1 @@
|
||||
Please place Powerpoints and slides here
|
@ -1889,7 +1889,10 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
|
||||
if (!device.intelamt) { device.intelamt = {}; }
|
||||
if ((command.intelamt.Versions != null) && (typeof command.intelamt.Versions == 'object')) {
|
||||
if ((command.intelamt.Versions.AMT != null) && (typeof command.intelamt.Versions.AMT == 'string') && (command.intelamt.Versions.AMT.length < 12) && (device.intelamt.ver != command.intelamt.Versions.AMT)) { changes.push('AMT version'); device.intelamt.ver = command.intelamt.Versions.AMT; change = 1; log = 1; }
|
||||
if ((command.intelamt.Versions.Sku != null) && (typeof command.intelamt.Versions.Sku == 'string')) { var sku = parseInt(command.intelamt.Versions.Sku); if (device.intelamt.sku !== command.intelamt.sku) { device.intelamt.sku = sku; change = 1; log = 1; } }
|
||||
if ((command.intelamt.Versions.Sku != null) && (typeof command.intelamt.Versions.Sku == 'string')) {
|
||||
const sku = parseInt(command.intelamt.Versions.Sku);
|
||||
if (device.intelamt.sku !== sku) { device.intelamt.sku = sku; change = 1; log = 1; }
|
||||
}
|
||||
}
|
||||
if ((command.intelamt.ProvisioningState != null) && (typeof command.intelamt.ProvisioningState == 'number') && (device.intelamt.state != command.intelamt.ProvisioningState)) { changes.push('AMT state'); device.intelamt.state = command.intelamt.ProvisioningState; change = 1; log = 1; }
|
||||
if ((command.intelamt.Flags != null) && (typeof command.intelamt.Flags == 'number') && (device.intelamt.flags != command.intelamt.Flags)) {
|
||||
|
@ -101,6 +101,8 @@
|
||||
"agentCoreDump": { "type": "boolean", "default": false, "description": "Automatically activates and transfers any agent crash dump files to the server in meshcentral-data/coredumps." },
|
||||
"agentCoreDumpUsers": { "type": "array", "description": "List of non-administrator users that have access to mesh agent crash dumps." },
|
||||
"agentSignLock": { "type": "boolean", "default": false, "description": "When code signing an agent using authenticode, lock the agent to only allow connection to this server. (This is in testing, the default value will change to true in the future)." },
|
||||
"agentTimeStampServer": { "type": [ "boolean", "string" ], "default": "http://timestamp.comodoca.com/authenticode", "description": "The time stamping server to use when code signing Windows executables. When set to false, the executables are not time stamped." },
|
||||
"agentTimeStampProxy": { "type": [ "boolean", "string" ], "description": "The HTTP proxy to use when contacting the time stamping server, if false, no proxy is used. By default, the npmproxy value is used." },
|
||||
"ignoreAgentHashCheck": { "type": [ "boolean", "string" ], "default": false, "description": "When true, the agent no longer checked the TLS certificate of the server. This should be used for debugging only. You can also set this to a comma seperated list of IP addresses to ignore, for example: \"192.168.2.100,192.168.1.0/24\"." },
|
||||
"exactPorts": { "type": "boolean", "default": false, "description": "When set to true, MeshCentral will only grab the required TCP listening ports or fail. It will not try to use the next available port of it's busy." },
|
||||
"allowLoginToken": { "type": "boolean", "default": false },
|
||||
@ -542,6 +544,20 @@
|
||||
"backgroundColor": { "type": "string", "default": null, "description": "Background color, valid values are RBG in format 0,0,0 to 255,255,255 or format #000000 to #FFFFFF." }
|
||||
}
|
||||
},
|
||||
"agentFileInfo": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Use this section to set resource metadata of the Windows agents prior to signing. In Windows, you can right-click and select properties to view these values.",
|
||||
"properties": {
|
||||
"fileDescription": { "type": "string", "description": "Executable file description." },
|
||||
"fileVersion": { "type": "string", "description": "Executable file version, generally in the form of 1.2.3.4." },
|
||||
"internalName": { "type": "string", "description": "Executable internal name." },
|
||||
"legalCopyright": { "type": "string", "description": "Executable legal copyright." },
|
||||
"originalFilename": { "type": "string", "description": "Executable original file name." },
|
||||
"productName": { "type": "string", "description": "Executable product name." },
|
||||
"productVersion": { "type": "string", "description": "Executable product version, generally in the form of 1.2.3.4." }
|
||||
}
|
||||
},
|
||||
"assistantCustomization": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
178
meshcentral.js
@ -145,7 +145,7 @@ function CreateMeshCentralServer(config, args) {
|
||||
if ((obj.args.help == true) || (obj.args['?'] == true)) {
|
||||
console.log('MeshCentral v' + getCurrentVersion() + ', remote computer management web portal.');
|
||||
console.log('This software is open source under Apache 2.0 license.');
|
||||
console.log('Details at: https://www.meshcommander.com/meshcentral2\r\n');
|
||||
console.log('Details at: https://www.meshcentral.com\r\n');
|
||||
if ((obj.platform == 'win32') || (obj.platform == 'linux')) {
|
||||
console.log('Run as a background service');
|
||||
console.log(' --install/uninstall Install MeshCentral as a background service.');
|
||||
@ -1617,6 +1617,7 @@ function CreateMeshCentralServer(config, args) {
|
||||
|
||||
// Load the list of mesh agents and install scripts
|
||||
if ((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true)) { for (i in obj.meshAgentsArchitectureNumbers) { obj.meshAgentsArchitectureNumbers[i].update = false; } }
|
||||
obj.signMeshAgents(obj.config.domains[''], function () {
|
||||
obj.updateMeshAgentsTable(obj.config.domains[''], function () {
|
||||
obj.updateMeshAgentInstallScripts();
|
||||
|
||||
@ -1645,6 +1646,11 @@ function CreateMeshCentralServer(config, args) {
|
||||
obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates, obj.StartEx5);
|
||||
if (obj.redirserver != null) { obj.redirserver.hookMainWebServer(obj.certificates); }
|
||||
|
||||
// Start the HTTP relay web server if needed
|
||||
if ((obj.args.relayport != null) && (obj.args.relayport != 0)) {
|
||||
obj.webrelayserver = require('./webrelayserver.js').CreateWebRelayServer(obj, obj.db, obj.args, obj.certificates, function () { });
|
||||
}
|
||||
|
||||
// Update proxy certificates
|
||||
if (obj.supportsProxyCertificatesRequest == true) { obj.updateProxyCertificates(true); }
|
||||
|
||||
@ -1888,6 +1894,7 @@ function CreateMeshCentralServer(config, args) {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Called when the web server finished loading
|
||||
@ -2841,31 +2848,36 @@ function CreateMeshCentralServer(config, args) {
|
||||
10006: { id: 10006, localname: 'MeshCentralAssistant.exe', rname: 'MeshCentralAssistant.exe', desc: 'MeshCentral Assistant for Windows', update: false, amt: false, platform: 'win32' } // MeshCentral Assistant
|
||||
};
|
||||
|
||||
// Update the list of available mesh agents
|
||||
obj.updateMeshAgentsTable = function (domain, func) {
|
||||
// Sign windows agents
|
||||
obj.signMeshAgents = function (domain, func) {
|
||||
// Setup the domain is specified
|
||||
var objx = domain, suffix = '';
|
||||
if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
|
||||
|
||||
// Check if a custom agent signing certificate is available
|
||||
var agentSignCertInfo = require('./authenticode.js').loadCertificates([ obj.path.join(obj.datapath, 'agentsigningcert.pem') ]);
|
||||
var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
|
||||
|
||||
// If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
|
||||
if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
|
||||
agentSignCertInfo = {
|
||||
cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
|
||||
key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
|
||||
extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert) ]
|
||||
extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
|
||||
}
|
||||
}
|
||||
if (agentSignCertInfo == null) { func(); return; } // No code signing certificate, nothing to do.
|
||||
|
||||
// Setup the domain is specified
|
||||
var objx = domain, suffix = '';
|
||||
if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
|
||||
|
||||
// Generate the agent signature description and URL
|
||||
var serverSignedAgentsPath, signDesc, signUrl;
|
||||
if (agentSignCertInfo != null) {
|
||||
serverSignedAgentsPath = obj.path.join(obj.datapath, 'signedagents' + suffix);
|
||||
signDesc = (domain.title ? domain.title : agentSignCertInfo.cert.subject.hash);
|
||||
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
|
||||
signUrl = 'https://' + ((domain.dns != null) ? domain.dns : obj.certificates.CommonName);
|
||||
const serverSignedAgentsPath = obj.path.join(obj.datapath, 'signedagents' + suffix);
|
||||
const signDesc = (domain.title ? domain.title : agentSignCertInfo.cert.subject.hash);
|
||||
const httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
|
||||
var httpsHost = ((domain.dns != null) ? domain.dns : obj.certificates.CommonName);
|
||||
if (obj.args.agentaliasdns != null) { httpsHost = obj.args.agentaliasdns; }
|
||||
var signUrl = 'https://' + httpsHost;
|
||||
if (httpsPort != 443) { signUrl += ':' + httpsPort; }
|
||||
var xdomain = (domain.dns == null) ? domain.id : '';
|
||||
if (xdomain != '') xdomain += '/';
|
||||
@ -2873,14 +2885,23 @@ function CreateMeshCentralServer(config, args) {
|
||||
|
||||
// If requested, lock the agent to this server
|
||||
if (obj.config.settings.agentsignlock) { signUrl += '?ServerID=' + obj.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert).toUpperCase(); }
|
||||
}
|
||||
|
||||
// Load agent information file. This includes the data & time of the agent.
|
||||
const agentInfo = [];
|
||||
try { agentInfo = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'hashagents.json'), 'utf8')); } catch (ex) { }
|
||||
// Setup the time server
|
||||
var timeStampUrl = 'http://timestamp.comodoca.com/authenticode';
|
||||
if (args.agenttimestampserver === false) { timeStampUrl = null; }
|
||||
else if (typeof args.agenttimestampserver == 'string') { timeStampUrl = args.agenttimestampserver; }
|
||||
|
||||
// Setup the time server proxy
|
||||
var timeStampProxy = null;
|
||||
if (typeof args.agenttimestampproxy == 'string') { timeStampProxy = args.agenttimestampproxy; }
|
||||
else if ((args.agenttimestampproxy !== false) && (typeof args.npmproxy == 'string')) { timeStampProxy = args.npmproxy; }
|
||||
|
||||
// Setup the pending operations counter
|
||||
var pendingOperations = 1;
|
||||
|
||||
var archcount = 0;
|
||||
for (var archid in obj.meshAgentsArchitectureNumbers) {
|
||||
if (obj.meshAgentsArchitectureNumbers[archid].codesign !== true) continue;
|
||||
|
||||
var agentpath;
|
||||
if (domain.id == '') {
|
||||
// Load all agents when processing the default domain
|
||||
@ -2893,15 +2914,8 @@ function CreateMeshCentralServer(config, args) {
|
||||
if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
|
||||
}
|
||||
|
||||
// Fetch agent binary information
|
||||
var stats = null;
|
||||
try { stats = obj.fs.statSync(agentpath); } catch (ex) { }
|
||||
if ((stats == null)) continue; // If this agent does not exist, skip it.
|
||||
|
||||
// Check if we need to sign this agent, if so, check if it's already been signed
|
||||
if ((obj.meshAgentsArchitectureNumbers[archid].codesign === true) && (agentSignCertInfo != null)) {
|
||||
// Open the original agent with authenticode
|
||||
var signeedagentpath = obj.path.join(serverSignedAgentsPath, obj.meshAgentsArchitectureNumbers[archid].localname);
|
||||
const signeedagentpath = obj.path.join(serverSignedAgentsPath, obj.meshAgentsArchitectureNumbers[archid].localname);
|
||||
const originalAgent = require('./authenticode.js').createAuthenticodeHandler(agentpath);
|
||||
if (originalAgent != null) {
|
||||
// Check if the agent is already signed correctly
|
||||
@ -2910,28 +2924,126 @@ function CreateMeshCentralServer(config, args) {
|
||||
(destinationAgent != null) &&
|
||||
(destinationAgent.fileHashSigned != null) &&
|
||||
(Buffer.compare(destinationAgent.fileHashSigned, destinationAgent.fileHashActual) == 0) &&
|
||||
((Buffer.compare(destinationAgent.fileHashSigned, originalAgent.getHash(destinationAgent.fileHashAlgo))) == 0) &&
|
||||
(destinationAgent.signingAttribs.indexOf(signUrl) >= 0) &&
|
||||
(destinationAgent.signingAttribs.indexOf(signDesc) >= 0)
|
||||
);
|
||||
if (destinationAgent != null) { destinationAgent.close(); }
|
||||
|
||||
if (destinationAgent != null) {
|
||||
// If the agent is signed correctly, look to see if the resources in the destination agent are correct
|
||||
var orgVersionStrings = originalAgent.getVersionInfo();
|
||||
if (destinationAgentOk == true) {
|
||||
var versionStrings = destinationAgent.getVersionInfo();
|
||||
var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
|
||||
for (var i in versionProperties) {
|
||||
const prop = versionProperties[i], propl = prop.toLowerCase();
|
||||
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo[propl] == 'string')) {
|
||||
if (domain.agentfileinfo[propl] != versionStrings[prop]) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
|
||||
} else {
|
||||
if (orgVersionStrings[prop] != versionStrings[prop]) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If everything looks ok, runs a hash of the original and destination agent skipping the CRC, resource and signature blocks. If different, sign the agent again.
|
||||
if ((destinationAgentOk == true) && (originalAgent.getHashNoResources('sha384').compare(destinationAgent.getHashNoResources('sha384')) != 0)) { destinationAgentOk = false; }
|
||||
|
||||
// We are done comparing the destination agent, close it.
|
||||
destinationAgent.close();
|
||||
}
|
||||
|
||||
if (destinationAgentOk == false) {
|
||||
// If not signed correctly, sign it. First, create the server signed agent folder if needed
|
||||
try { obj.fs.mkdirSync(serverSignedAgentsPath); } catch (ex) { }
|
||||
if (originalAgent.sign(agentSignCertInfo, { out: signeedagentpath, desc: signDesc, url: signUrl }) == true) {
|
||||
const xagentSignedFunc = function agentSignedFunc(err, size) {
|
||||
if (err == null) {
|
||||
// Agent was signed succesfuly
|
||||
agentpath = signeedagentpath;
|
||||
console.log(obj.common.format('Code signed agent {0}.', obj.meshAgentsArchitectureNumbers[archid].localname));
|
||||
console.log(obj.common.format('Code signed agent {0}.', agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname));
|
||||
} else {
|
||||
console.log(obj.common.format('Failed to sign agent {0}.', obj.meshAgentsArchitectureNumbers[archid].localname));
|
||||
// Failed to sign agent
|
||||
addServerWarning('Failed to sign agent \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [ agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err ]);
|
||||
}
|
||||
if (--pendingOperations === 0) { agentSignedFunc.func(); }
|
||||
}
|
||||
pendingOperations++;
|
||||
xagentSignedFunc.func = func;
|
||||
xagentSignedFunc.objx = objx;
|
||||
xagentSignedFunc.archid = archid;
|
||||
xagentSignedFunc.signeedagentpath = signeedagentpath;
|
||||
|
||||
// Parse the resources in the executable and make any required changes
|
||||
var resChanges = false, versionStrings = null;
|
||||
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object')) {
|
||||
versionStrings = originalAgent.getVersionInfo();
|
||||
var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
|
||||
for (var i in versionProperties) {
|
||||
const prop = versionProperties[i], propl = prop.toLowerCase();
|
||||
if (domain.agentfileinfo[propl] && (domain.agentfileinfo[propl] != versionStrings[prop])) { versionStrings[prop] = domain.agentfileinfo[propl]; resChanges = true; }
|
||||
}
|
||||
if (resChanges == true) { originalAgent.setVersionInfo(versionStrings); }
|
||||
}
|
||||
|
||||
const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone
|
||||
obj.debug('main', "Code signing agent with arguments: " + JSON.stringify(signingArguments));
|
||||
if (resChanges == false) {
|
||||
// Sign the agent the simple way, without changing any resources.
|
||||
originalAgent.sign(agentSignCertInfo, signingArguments, xagentSignedFunc);
|
||||
} else {
|
||||
// Change the agent resources and sign the agent, this is a much more involved process.
|
||||
// NOTE: This is experimental and could corupt the agent.
|
||||
originalAgent.writeExecutable(signingArguments, agentSignCertInfo, xagentSignedFunc);
|
||||
}
|
||||
} else {
|
||||
// Signed agent is already ok, use it.
|
||||
agentpath = signeedagentpath;
|
||||
}
|
||||
originalAgent.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (--pendingOperations === 0) { func(); }
|
||||
}
|
||||
|
||||
// Update the list of available mesh agents
|
||||
obj.updateMeshAgentsTable = function (domain, func) {
|
||||
// Check if a custom agent signing certificate is available
|
||||
var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
|
||||
|
||||
// If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
|
||||
if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
|
||||
agentSignCertInfo = {
|
||||
cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
|
||||
key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
|
||||
extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
|
||||
}
|
||||
}
|
||||
|
||||
// Setup the domain is specified
|
||||
var objx = domain, suffix = '';
|
||||
if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
|
||||
|
||||
// Load agent information file. This includes the data & time of the agent.
|
||||
const agentInfo = [];
|
||||
try { agentInfo = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'hashagents.json'), 'utf8')); } catch (ex) { }
|
||||
|
||||
var archcount = 0;
|
||||
for (var archid in obj.meshAgentsArchitectureNumbers) {
|
||||
var agentpath;
|
||||
if (domain.id == '') {
|
||||
// Load all agents when processing the default domain
|
||||
agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
|
||||
const agentpath2 = obj.path.join(obj.datapath, 'signedagents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
|
||||
if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; } // If the agent is present in "meshcentral-data/signedagents", use that one instead.
|
||||
const agentpath3 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
|
||||
if (obj.fs.existsSync(agentpath3)) { agentpath = agentpath3; } // If the agent is present in "meshcentral-data/agents", use that one instead.
|
||||
} else {
|
||||
// When processing an extra domain, only load agents that are specific to that domain
|
||||
var agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
|
||||
if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
|
||||
}
|
||||
|
||||
// Fetch agent binary information
|
||||
var stats = null;
|
||||
try { stats = obj.fs.statSync(agentpath); } catch (ex) { }
|
||||
if ((stats == null)) continue; // If this agent does not exist, skip it.
|
||||
|
||||
// Setup agent information
|
||||
archcount++;
|
||||
@ -2952,7 +3064,7 @@ function CreateMeshCentralServer(config, args) {
|
||||
// Load the agent with a random msh added to it.
|
||||
const outStream = new require('stream').Duplex();
|
||||
outStream.meshAgentBinary = objx.meshAgentBinaries[archid];
|
||||
outStream.meshAgentBinary.randomMsh = agentSignCertInfo.cert.subject.hash;
|
||||
if (agentSignCertInfo) { outStream.meshAgentBinary.randomMsh = agentSignCertInfo.cert.subject.hash; } else { outStream.meshAgentBinary.randomMsh = obj.crypto.randomBytes(16).toString('hex'); }
|
||||
outStream.bufferList = [];
|
||||
outStream._write = function (chunk, encoding, callback) { this.bufferList.push(chunk); if (callback) callback(); }; // Append the chuck.
|
||||
outStream._read = function (size) { }; // Do nothing, this is not going to be called.
|
||||
|
@ -43,6 +43,8 @@ const MESHRIGHT_ADMIN = 0xFFFFFFFF;
|
||||
// 10 = Web-RDP
|
||||
// 11 = Web-SSH
|
||||
// 12 = Web-VNC
|
||||
// 13 = Web-SSH-Files
|
||||
// 14 = Web-TCP
|
||||
// 100 = Intel AMT WSMAN
|
||||
// 101 = Intel AMT Redirection
|
||||
// 200 = Messenger
|
||||
|
30
meshuser.js
@ -696,7 +696,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||
// Request a list of all nodes
|
||||
db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', command.id, function (err, docs) {
|
||||
|
||||
//console.log(docs);
|
||||
//console.log(err, docs);
|
||||
|
||||
if (docs == null) { docs = []; }
|
||||
parent.common.unEscapeAllLinksFieldName(docs);
|
||||
@ -2867,8 +2867,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||
if ((command.actiontype == 400) && common.validateInt(command.time, 1, 30000)) { routeCommandToNode({ action: 'msg', type: 'console', nodeid: node._id, value: 'flash ' + command.time }, MESHRIGHT_ADMIN, 0); }
|
||||
if ((command.actiontype == 401) && common.validateInt(command.time, 1, 30000)) { routeCommandToNode({ action: 'msg', type: 'console', nodeid: node._id, value: 'vibrate ' + command.time }, MESHRIGHT_ADMIN, 0); }
|
||||
} else {
|
||||
// Check we have the rights to delete this device
|
||||
if ((rights & MESHRIGHT_RESETOFF) == 0) return;
|
||||
// Check we have the rights to perform this operation
|
||||
if ((command.actiontype == 302) && ((rights & MESHRIGHT_WAKEDEVICE) == 0)) return; // This is a Intel AMT power on operation, check if we have WAKE rights
|
||||
if ((command.actiontype != 302) && ((rights & MESHRIGHT_RESETOFF) == 0)) return; // For all other operations, check that we have RESET/OFF rights
|
||||
|
||||
// If this device is connected on MQTT, send a power action.
|
||||
if ((parent.parent.mqttbroker != null) && (command.actiontype >= 0) && (command.actiontype <= 4)) { parent.parent.mqttbroker.publish(node._id, 'powerAction', ['', '', 'poweroff', 'reset', 'sleep'][command.actiontype]); }
|
||||
@ -2913,7 +2914,6 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||
// Perform input validation
|
||||
try {
|
||||
if (common.validateStrArray(command.nodeids, 1, 256) == false) { err = "Invalid nodeids"; } // Check nodeids
|
||||
else if (common.validateString(command.title, 1, 512) == false) { err = "Invalid title"; } // Check title
|
||||
else if (common.validateString(command.msg, 1, 4096) == false) { err = "Invalid message"; } // Check message
|
||||
else {
|
||||
var nodeids = [];
|
||||
@ -2928,6 +2928,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||
break;
|
||||
}
|
||||
|
||||
// Check the title, if needed, use a default one
|
||||
if (common.validateString(command.title, 1, 512) == false) { delete command.title } // Check title
|
||||
if ((command.title == null) && (typeof domain.notificationmessages == 'object') && (typeof domain.notificationmessages.title == 'string')) { command.title = domain.notificationmessages.title; }
|
||||
if ((command.title == null) && (typeof domain.title == 'string')) { command.title = domain.title; }
|
||||
if (command.title == null) { command.title = "MeshCentral"; }
|
||||
|
||||
for (i in command.nodeids) {
|
||||
// Get the node and the rights for this node
|
||||
parent.GetNodeWithRights(domain, user, command.nodeids[i], function (node, rights, visible) {
|
||||
@ -3020,6 +3026,22 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||
}
|
||||
}
|
||||
|
||||
if ((typeof command.httpport == 'number') && (command.httpport > 0) && (command.httpport < 65536)) {
|
||||
if ((command.httpport == 80) && (node.httpport != null)) {
|
||||
delete node.httpport; change = 1; changes.push('httpport'); // Delete the HTTP port
|
||||
} else {
|
||||
node.httpport = command.httpport; change = 1; changes.push('httpport'); // Set the HTTP port
|
||||
}
|
||||
}
|
||||
|
||||
if ((typeof command.httpsport == 'number') && (command.httpsport > 0) && (command.httpsport < 65536)) {
|
||||
if ((command.httpsport == 443) && (node.httpsport != null)) {
|
||||
delete node.httpsport; change = 1; changes.push('httpsport'); // Delete the HTTPS port
|
||||
} else {
|
||||
node.httpsport = command.httpsport; change = 1; changes.push('httpsport'); // Set the HTTPS port
|
||||
}
|
||||
}
|
||||
|
||||
if ((typeof command.ssh == 'number') && (command.ssh == 0)) {
|
||||
if ((node.ssh != null) && (node.ssh[user._id] != null)) { delete node.ssh[user._id]; change = 1; changes.push('ssh'); } // Delete the SSH cendentials
|
||||
}
|
||||
|
25
mpsserver.js
@ -484,7 +484,10 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
|
||||
// We are under the limit, create the new device.
|
||||
// Node is not in the database, add it. Credentials will be empty until added by the user.
|
||||
var device = { type: 'node', mtype: 1, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: socket.tag.name, icon: (socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: domainid, intelamt: { user: (typeof socket.tag.meiState.amtuser == 'string') ? socket.tag.meiState.amtuser : '', pass: (typeof socket.tag.meiState.amtpass == 'string') ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
|
||||
if (socket.tag.meiState != null) {
|
||||
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
|
||||
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
|
||||
}
|
||||
obj.db.Set(device);
|
||||
|
||||
// Event the new node
|
||||
@ -506,7 +509,10 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
|
||||
|
||||
// Node is not in the database, add it. Credentials will be empty until added by the user.
|
||||
var device = { type: 'node', mtype: 1, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: socket.tag.name, icon: (socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: domainid, intelamt: { user: (typeof socket.tag.meiState.amtuser == 'string') ? socket.tag.meiState.amtuser : '', pass: (typeof socket.tag.meiState.amtpass == 'string') ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
|
||||
if (socket.tag.meiState != null) {
|
||||
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
|
||||
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
|
||||
}
|
||||
obj.db.Set(device);
|
||||
|
||||
// Event the new node
|
||||
@ -707,7 +713,10 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
|
||||
// We are under the limit, create the new device.
|
||||
// Node is not in the database, add it. Credentials will be empty until added by the user.
|
||||
var device = { type: 'node', mtype: 1, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: socket.tag.name, icon: (socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: initialMesh.domain, intelamt: { user: (typeof socket.tag.meiState.amtuser == 'string') ? socket.tag.meiState.amtuser : '', pass: (typeof socket.tag.meiState.amtpass == 'string') ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
|
||||
if (socket.tag.meiState != null) {
|
||||
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
|
||||
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
|
||||
}
|
||||
obj.db.Set(device);
|
||||
|
||||
// Event the new node
|
||||
@ -733,7 +742,10 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
|
||||
|
||||
// Node is not in the database, add it. Credentials will be empty until added by the user.
|
||||
var device = { type: 'node', mtype: 1, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: socket.tag.name, icon: (socket.tag.meiState && socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: initialMesh.domain, intelamt: { user: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtuser == 'string')) ? socket.tag.meiState.amtuser : '', pass: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtpass == 'string')) ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
|
||||
if ((socket.tag.meiState != null) && (typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
|
||||
if (socket.tag.meiState != null) {
|
||||
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
|
||||
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
|
||||
}
|
||||
obj.db.Set(device);
|
||||
|
||||
// Event the new node
|
||||
@ -793,7 +805,10 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
|
||||
|
||||
// Node is not in the database, add it. Credentials will be empty until added by the user.
|
||||
var device = { type: 'node', mtype: 2, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: hostname, icon: (socket.tag.meiState && socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: initialMesh.domain, intelamt: { user: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtuser == 'string')) ? socket.tag.meiState.amtuser : '', pass: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtpass == 'string')) ? socket.tag.meiState.amtpass : '', tls: 0, state: 2, agent: { id: 0, caps: 0 } } };
|
||||
if ((socket.tag.meiState != null) && (typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
|
||||
if (socket.tag.meiState != null) {
|
||||
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
|
||||
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
|
||||
}
|
||||
obj.db.Set(device);
|
||||
|
||||
// Event the new node
|
||||
@ -827,9 +842,11 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
|
||||
|
||||
// Node is not in the database, add it. Credentials will be empty until added by the user.
|
||||
var device = { type: 'node', mtype: 2, _id: socket.tag.nodeid, meshid: socket.tag.meshid, name: hostname, icon: (socket.tag.meiState && socket.tag.meiState.isBatteryPowered) ? 2 : 1, host: hostname, domain: initialMesh.domain, agent: { ver: 0, id: 0, caps: 0 }, intelamt: { uuid: socket.tag.SystemId, user: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtuser == 'string')) ? socket.tag.meiState.amtuser : '', pass: ((socket.tag.meiState) && (typeof socket.tag.meiState.amtpass == 'string')) ? socket.tag.meiState.amtpass : '', tls: 0, state: 2 } };
|
||||
if ((socket.tag.meiState != null) && (typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
|
||||
if (socket.tag.meiState != null) {
|
||||
if ((typeof socket.tag.meiState.desc == 'string') && (socket.tag.meiState.desc.length > 0) && (socket.tag.meiState.desc.length < 1024)) { device.desc = socket.tag.meiState.desc; }
|
||||
if ((typeof socket.tag.meiState.Versions == 'object') && (typeof socket.tag.meiState.Versions.Sku == 'string')) { device.intelamt.sku = parseInt(socket.tag.meiState.Versions.Sku); }
|
||||
}
|
||||
obj.db.Set(device);
|
||||
console.log('ADDED', device);
|
||||
|
||||
// Event the new node
|
||||
addedDeviceCount++;
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "meshcentral",
|
||||
"version": "1.0.36",
|
||||
"version": "1.0.45",
|
||||
"keywords": [
|
||||
"Remote Device Management",
|
||||
"Remote Device Monitoring",
|
||||
|
2
public/scripts/agent-desktop-0.0.2-min.js
vendored
@ -533,6 +533,9 @@ var CreateAgentRemoteDesktop = function (canvasid, scrolldiv) {
|
||||
else if (Button == obj.MouseButton.RIGHT) { Button = obj.MouseButton.LEFT; }
|
||||
}
|
||||
|
||||
// Reverse mouse wheel if needed
|
||||
if (obj.ReverseMouseWheel) { Delta = -1 * Delta; }
|
||||
|
||||
var MouseMsg = "";
|
||||
if (Action == obj.KeyAction.DBLCLICK) {
|
||||
MouseMsg = String.fromCharCode(0x00, obj.InputType.MOUSE, 0x00, 0x0A, 0x00, 0x88, ((X / 256) & 0xFF), (X & 0xFF), ((Y / 256) & 0xFF), (Y & 0xFF));
|
||||
|
2
public/scripts/agent-rdp-0.0.1-min.js
vendored
@ -18,6 +18,10 @@ var CreateRDPDesktop = function (canvasid) {
|
||||
obj.m.onClipboardChanged = null;
|
||||
obj.onConsoleMessageChange = null;
|
||||
|
||||
var xMouseCursorActive = true;
|
||||
var xMouseCursorCurrent = 'default';
|
||||
obj.mouseCursorActive = function (x) { if (xMouseCursorActive == x) return; xMouseCursorActive = x; obj.CanvasId.style.cursor = ((x == true) ? xMouseCursorCurrent : 'default'); }
|
||||
|
||||
function mouseButtonMap(button) {
|
||||
// Swap mouse buttons if needed
|
||||
if (obj.m.SwapMouse === true) return [2, 0, 1, 0, 0][button];
|
||||
@ -75,6 +79,12 @@ var CreateRDPDesktop = function (canvasid) {
|
||||
obj.render.update(bitmap);
|
||||
break;
|
||||
}
|
||||
case 'rdp-pointer': {
|
||||
var pointer = msg[1];
|
||||
xMouseCursorCurrent = pointer;
|
||||
if (xMouseCursorActive) { obj.CanvasId.style.cursor = pointer; }
|
||||
break;
|
||||
}
|
||||
case 'rdp-close': {
|
||||
obj.Stop();
|
||||
break;
|
||||
@ -198,6 +208,7 @@ var CreateRDPDesktop = function (canvasid) {
|
||||
if (obj.mouseNagleTimer != null) { clearTimeout(obj.mouseNagleTimer); obj.mouseNagleTimer = null; }
|
||||
var delta = 0;
|
||||
if (e.detail) { delta = (e.detail * 120); } else if (e.wheelDelta) { delta = (e.wheelDelta * 3); }
|
||||
if (obj.m.ReverseMouseWheel) { delta = -1 * delta; } // Reverse the mouse wheel
|
||||
if (delta != 0) { obj.socket.send(JSON.stringify(['wheel', m.x, m.y, delta, false, false])); }
|
||||
e.preventDefault();
|
||||
return false;
|
||||
|
2
public/scripts/amt-desktop-0.0.2-min.js
vendored
@ -931,6 +931,10 @@ var CreateAmtRemoteDesktop = function (divid, scrolldiv) {
|
||||
else if (typeof e.detail == 'number') { v = -1 * e.detail; }
|
||||
else if (typeof e.wheelDelta == 'number') { v = e.wheelDelta; }
|
||||
if (v == 0) return;
|
||||
|
||||
// Reverse mouse wheel if needed
|
||||
if (obj.ReverseMouseWheel) { v = -1 * v; }
|
||||
|
||||
var tmpmask = obj.buttonmask;
|
||||
obj.buttonmask |= (1 << ((v > 0) ? 3 : 4));
|
||||
obj.mousemove(e, 1);
|
||||
|
@ -1,7 +1,7 @@
|
||||
@ECHO OFF
|
||||
CD ..\translate
|
||||
%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js minifyall
|
||||
%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js translateall
|
||||
%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js extractall
|
||||
%LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node translate.js minifyall
|
||||
%LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node translate.js translateall
|
||||
%LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node translate.js extractall
|
||||
DEL ..\emails\translations\*-min_*
|
||||
Pause
|
@ -22,7 +22,7 @@ var fs = require('fs');
|
||||
var type = require('./type');
|
||||
var log = require('./log');
|
||||
var tls = require('tls');
|
||||
var crypto = require('crypto');
|
||||
//var crypto = require('crypto');
|
||||
var events = require('events');
|
||||
|
||||
/**
|
||||
|
@ -186,7 +186,7 @@ var FastPathUpdateType = {
|
||||
FASTPATH_UPDATETYPE_PTR_NULL : 0x5,
|
||||
FASTPATH_UPDATETYPE_PTR_DEFAULT : 0x6,
|
||||
FASTPATH_UPDATETYPE_PTR_POSITION : 0x8,
|
||||
FASTPATH_UPDATETYPE_COLOR : 0x9,
|
||||
FASTPATH_UPDATETYPE_COLOR : 0x9, // Mouse cursor
|
||||
FASTPATH_UPDATETYPE_CACHED : 0xA,
|
||||
FASTPATH_UPDATETYPE_POINTER : 0xB
|
||||
};
|
||||
@ -1075,7 +1075,7 @@ function clipPDU() {
|
||||
* @param opt {object} type option
|
||||
* @returns {type.Component}
|
||||
*/
|
||||
function fastPathBitmapUpdateDataPDU (opt) {
|
||||
function fastPathBitmapUpdateDataPDU(opt) {
|
||||
var self = {
|
||||
__FASTPATH_UPDATE_TYPE__ : FastPathUpdateType.FASTPATH_UPDATETYPE_BITMAP,
|
||||
header : new type.UInt16Le(FastPathUpdateType.FASTPATH_UPDATETYPE_BITMAP, { constant : true }),
|
||||
@ -1093,13 +1093,67 @@ function fastPathBitmapUpdateDataPDU (opt) {
|
||||
return new type.Component(self, opt);
|
||||
}
|
||||
|
||||
// This is a table of cursorid to cursor name.
|
||||
// Created by movering the mouse over this page: https://www.w3schools.com/csSref/tryit.asp?filename=trycss_cursor
|
||||
const cursorIdTable = {
|
||||
// Normal style mouse cursor
|
||||
903013897: 'alias',
|
||||
370524792: 'all-scroll',
|
||||
853046751: 'cell',
|
||||
2101250798: 'col-resize',
|
||||
703681364: 'copy',
|
||||
992638936: 'crosshair',
|
||||
1539083673: 'ew-resize',
|
||||
1919796298: 'grab',
|
||||
1010243511: 'grabbing',
|
||||
1247283057: 'help',
|
||||
1390892051: 'none',
|
||||
885751489: 'not-allowed',
|
||||
1732952247: 'row-resize',
|
||||
747144997: 'url',
|
||||
2018345610: 'zoom-in',
|
||||
347367048: 'zoom-out',
|
||||
1872942890: 'default',
|
||||
1737852989: 'text',
|
||||
1932827019: 'ns-resize',
|
||||
1884471290: 'nesw-resize',
|
||||
1204065391: 'nwse-resize',
|
||||
2030531519: 'progress',
|
||||
1050842114: 'pointer',
|
||||
|
||||
// Black style cursors
|
||||
1258195498: 'default',
|
||||
219484254: 'all-scroll',
|
||||
399295089: 'text',
|
||||
1912613597: 'wait',
|
||||
864127801: 'ew-resize',
|
||||
23245044: 'nesw-resize',
|
||||
1966995494: 'not-allowed',
|
||||
1873216615: 'help',
|
||||
255126408: 'nesw-resize',
|
||||
157191894: 'ns-resize',
|
||||
1768446509: 'pointer',
|
||||
1032011501: 'crosshair'
|
||||
}
|
||||
|
||||
function fastPathPointerUpdateDataPDU(opt, cursorId, cursorStr) {
|
||||
var self = {
|
||||
__FASTPATH_UPDATE_TYPE__: FastPathUpdateType.FASTPATH_UPDATETYPE_COLOR,
|
||||
header: new type.UInt16Le(FastPathUpdateType.FASTPATH_UPDATETYPE_COLOR, { constant: true }),
|
||||
cursorId: cursorId,
|
||||
cursorStr: cursorStr
|
||||
};
|
||||
|
||||
return new type.Component(self, opt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see http://msdn.microsoft.com/en-us/library/cc240622.aspx
|
||||
* @param updateData {type.Component}
|
||||
* @param opt {object} type option
|
||||
* @returns {type.Component}
|
||||
*/
|
||||
function fastPathUpdatePDU (updateData, opt) {
|
||||
function fastPathUpdatePDU(updateData, opt) {
|
||||
var self = {
|
||||
updateHeader : new type.UInt8( function () {
|
||||
return self.updateData.obj.__FASTPATH_UPDATE_TYPE__;
|
||||
@ -1116,13 +1170,28 @@ function fastPathUpdatePDU (updateData, opt) {
|
||||
}) };
|
||||
|
||||
switch (self.updateHeader.value & 0xf) {
|
||||
case FastPathUpdateType.FASTPATH_UPDATETYPE_BITMAP:
|
||||
case FastPathUpdateType.FASTPATH_UPDATETYPE_BITMAP: {
|
||||
self.updateData = fastPathBitmapUpdateDataPDU(options).read(s);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
case FastPathUpdateType.FASTPATH_UPDATETYPE_COLOR: {
|
||||
var data = new type.BinaryString(null, options).read(s);
|
||||
|
||||
// Hash the data to get a cursor id.
|
||||
// This is a hack since the cursor bitmap is sent but we can't use that, we has hash the bitmap and use that as a hint as to what cursor we need to display
|
||||
const hasher = require('crypto').createHash('sha384');
|
||||
hasher.update(data.value);
|
||||
const cursorid = Math.abs(hasher.digest().readInt32BE(0));
|
||||
const cursorStr = cursorIdTable[cursorid];
|
||||
//if (cursorStr == null) { console.log('Unknown cursorId: ' + cursorid); }
|
||||
self.updateData = fastPathPointerUpdateDataPDU(options, cursorid, cursorStr);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
self.updateData = new type.BinaryString(null, options).read(s);
|
||||
log.debug('unknown fast path pdu type ' + (self.updateHeader.value & 0xf));
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
|
@ -270,9 +270,14 @@ Client.prototype.recvFastPath = function (secFlag, s) {
|
||||
while (s.availableLength() > 0) {
|
||||
var pdu = data.fastPathUpdatePDU().read(s);
|
||||
switch (pdu.obj.updateHeader.value & 0xf) {
|
||||
case data.FastPathUpdateType.FASTPATH_UPDATETYPE_BITMAP:
|
||||
case data.FastPathUpdateType.FASTPATH_UPDATETYPE_BITMAP: {
|
||||
this.emit('bitmap', pdu.obj.updateData.obj.rectangles.obj);
|
||||
break;
|
||||
}
|
||||
case data.FastPathUpdateType.FASTPATH_UPDATETYPE_COLOR: {
|
||||
this.emit('pointer', pdu.obj.updateData.obj.cursorId, pdu.obj.updateData.obj.cursorStr);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
@ -160,6 +160,8 @@ function RdpClient(config) {
|
||||
}).on('close', function () {
|
||||
self.connected = false;
|
||||
self.emit('close');
|
||||
}).on('pointer', function (cursorId, cursorStr) {
|
||||
self.emit('pointer', cursorId, cursorStr);
|
||||
}).on('bitmap', function (bitmaps) {
|
||||
for (var bitmap in bitmaps) {
|
||||
var bitmapData = bitmaps[bitmap].obj.bitmapDataStream.value;
|
||||
|
@ -142,7 +142,7 @@ module.exports.CreateRedirServer = function (parent, db, args, func) {
|
||||
obj.parent.updateServerState('redirect-port', port);
|
||||
func(obj.port);
|
||||
}).on('error', function (err) {
|
||||
if ((err.code == 'EACCES') && (port < 65535)) { StartRedirServer(port + 1); } else { console.log(err); func(obj.port); }
|
||||
if ((err.code == 'EACCES') && (port < 65535)) { StartRedirServer(port + 1, addr); } else { console.log(err); func(obj.port); }
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
"_WANonly": true,
|
||||
"_LANonly": true,
|
||||
"_maintenanceMode": true,
|
||||
"_certificatePrivateKeyPassword": [ "password1", "password2" ],
|
||||
"_certificatePrivateKeyPassword": ["password1", "password2"],
|
||||
"_sessionTime": 60,
|
||||
"_sessionKey": "MyReallySecretPassword1",
|
||||
"_sessionSameSite": "strict",
|
||||
@ -38,6 +38,8 @@
|
||||
"_agentCoreDump": true,
|
||||
"_agentCoreDumpUsers": "user1,user2",
|
||||
"_agentSignLock": true,
|
||||
"_agentTimeStampServer": "http://timestamp.digicert.com",
|
||||
"_agentTimeStampProxy": "http://1.2.3.4:80",
|
||||
"_ignoreAgentHashCheck": true,
|
||||
"_exactPorts": true,
|
||||
"_allowLoginToken": true,
|
||||
@ -72,14 +74,15 @@
|
||||
"_webPush": { "email": "xxxxx@xxxxx.com" },
|
||||
"_publicPushNotifications": true,
|
||||
"_desktopMultiplex": true,
|
||||
"_ipBlockedUserRedirect": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"_userAllowedIP": "127.0.0.1,192.168.1.0/24",
|
||||
"_userBlockedIP": "127.0.0.1,::1,192.168.0.100",
|
||||
"_agentAllowedIP": "192.168.0.100/24",
|
||||
"_agentBlockedIP": "127.0.0.1,::1",
|
||||
"_authLog": "c:\\temp\\auth.log",
|
||||
"_InterUserMessaging": [ "user//admin" ],
|
||||
"_manageAllDeviceGroups": [ "user//admin" ],
|
||||
"_manageCrossDomain": [ "user//admin" ],
|
||||
"_InterUserMessaging": ["user//admin"],
|
||||
"_manageAllDeviceGroups": ["user//admin"],
|
||||
"_manageCrossDomain": ["user//admin"],
|
||||
"_localDiscovery": {
|
||||
"name": "Local server name",
|
||||
"info": "Information about this server"
|
||||
@ -91,6 +94,7 @@
|
||||
"_mpsAliasPort": 4433,
|
||||
"_mpsAliasHost": "mps.mydomain.com",
|
||||
"_mpsTlsOffload": true,
|
||||
"_mpsHighSecurity": true,
|
||||
"_no2FactorAuth": true,
|
||||
"_runOnServerStarted": "c:\\tmp\\mcstart.bat",
|
||||
"_runOnServerUpdated": "c:\\tmp\\mcupdate.bat",
|
||||
@ -162,19 +166,21 @@
|
||||
"title2": "Servername",
|
||||
"_titlePicture": "title-sample.png",
|
||||
"_loginPicture": "title-sample.png",
|
||||
"_rootRedirect": "https://www.youtube.com/watch?v=Gs069dndIYk",
|
||||
"_mobileSite": false,
|
||||
"_unknownUserRootRedirect": "https://www.youtube.com/watch?v=2Q_ZzBGPdqE",
|
||||
"_nightMode": 1,
|
||||
"_userQuota": 1048576,
|
||||
"_meshQuota": 248576,
|
||||
"_loginKey": ["abc", "123"],
|
||||
"_agentKey": ["abc", "123"],
|
||||
"_ipkvm": false,
|
||||
"minify": true,
|
||||
"_guestDeviceSharing" : false,
|
||||
"_AutoRemoveInactiveDevices": 37,
|
||||
"_DeviceSearchBarServerAndClientName": false,
|
||||
"_loginKey": [ "abc", "123" ],
|
||||
"_agentKey": [ "abc", "123" ],
|
||||
"_newAccounts": true,
|
||||
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
|
||||
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
|
||||
"_userNameIsEmail": true,
|
||||
"_newAccountEmailDomains": [ "sample.com" ],
|
||||
"_newAccountsRights": [ "nonewgroups", "notools" ],
|
||||
"_newAccountEmailDomains": ["sample.com"],
|
||||
"_newAccountsRights": ["nonewgroups", "notools"],
|
||||
"_welcomeText": "Sample Text on Login Page.",
|
||||
"_welcomePicture": "mainwelcome.jpg",
|
||||
"_welcomePictureFullScreen": false,
|
||||
@ -184,6 +190,13 @@
|
||||
"_hide": 4,
|
||||
"_footer": "<a href='https://twitter.com/mytwitter'>Twitter</a>",
|
||||
"_loginfooter": "This is a private server.",
|
||||
"_allowSavingDeviceCredentials": false,
|
||||
"_guestDeviceSharing": false,
|
||||
"_AutoRemoveInactiveDevices": 37,
|
||||
"_DeviceSearchBarServerAndClientName": false,
|
||||
"_agentSelfGuestSharing": {
|
||||
"expire": 120
|
||||
},
|
||||
"_certUrl": "https://192.168.2.106:443/",
|
||||
"_altMessenging": {
|
||||
"name": "Jitsi",
|
||||
@ -199,7 +212,7 @@
|
||||
"protocol": "http",
|
||||
"port": 80,
|
||||
"_ip": "192.168.1.100",
|
||||
"_filter": [ "mesh/(domainid)/(meshid)", "node/(domainid)/(nodeid)" ]
|
||||
"_filter": ["mesh/(domainid)/(meshid)", "node/(domainid)/(nodeid)"]
|
||||
},
|
||||
{
|
||||
"name": "HTTPS",
|
||||
@ -210,7 +223,7 @@
|
||||
},
|
||||
"PreconfiguredRemoteInput": [
|
||||
{
|
||||
"name": "CompagnyUrl",
|
||||
"name": "CompanyUrl",
|
||||
"value": "https://help.mycompany.com/"
|
||||
},
|
||||
{
|
||||
@ -274,6 +287,15 @@
|
||||
"image": "agent-logo.png",
|
||||
"fileName": "compagnyagent"
|
||||
},
|
||||
"_agentFileInfo": {
|
||||
"_filedescription": "sample_filedescription",
|
||||
"_fileversion": "0.1.2.3",
|
||||
"_internalname": "sample_internalname",
|
||||
"_legalcopyright": "sample_legalcopyright",
|
||||
"_originalfilename": "sample_originalfilename",
|
||||
"_productname": "sample_productname",
|
||||
"_productversion": "0.1.2.3"
|
||||
},
|
||||
"_assistantCustomization": {
|
||||
"title": "Company® Product™",
|
||||
"image": "assistant-logo.png",
|
||||
@ -324,14 +346,24 @@
|
||||
"log": "amtactivation.log",
|
||||
"certs": {
|
||||
"mycertname": {
|
||||
"certfiles": [ "amtacm-leafcert.crt", "amtacm-intermediate1.crt", "amtacm-intermediate2.crt", "amtacm-rootcert.crt" ],
|
||||
"certfiles": [
|
||||
"amtacm-leafcert.crt",
|
||||
"amtacm-intermediate1.crt",
|
||||
"amtacm-intermediate2.crt",
|
||||
"amtacm-rootcert.crt"
|
||||
],
|
||||
"keyfile": "amtacm-leafcert.key"
|
||||
}
|
||||
}
|
||||
},
|
||||
"_amtManager": {
|
||||
"adminAccounts": [{ "user": "admin", "pass": "MyP@ssw0rd" }],
|
||||
"environmentDetection": [ "domain1.com", "domain2.com", "domain3.com", "domain4.com" ],
|
||||
"environmentDetection": [
|
||||
"domain1.com",
|
||||
"domain2.com",
|
||||
"domain3.com",
|
||||
"domain4.com"
|
||||
],
|
||||
"wifiProfiles": [
|
||||
{
|
||||
"name": "Profile1",
|
||||
@ -354,8 +386,8 @@
|
||||
"Strict-Transport-Security": "max-age=360000",
|
||||
"x-frame-options": "SAMEORIGIN"
|
||||
},
|
||||
"_agentConfig": [ "webSocketMaskOverride=1", "coreDumpEnabled=1" ],
|
||||
"_assistantConfig": [ "disableUpdate=1" ],
|
||||
"_agentConfig": ["webSocketMaskOverride=1", "coreDumpEnabled=1"],
|
||||
"_assistantConfig": ["disableUpdate=1"],
|
||||
"_sessionRecording": {
|
||||
"_onlySelectedUsers": true,
|
||||
"_onlySelectedUserGroups": true,
|
||||
@ -366,42 +398,42 @@
|
||||
"_maxRecordingDays": 15,
|
||||
"_maxRecordingSizeMegabytes": 3,
|
||||
"__protocols__": "Is an array: 1 = Terminal, 2 = Desktop, 5 = Files, 100 = Intel AMT WSMAN, 101 = Intel AMT Redirection, 200 = Messenger",
|
||||
"protocols": [ 1, 2, 101 ]
|
||||
"protocols": [1, 2, 101]
|
||||
},
|
||||
"_authStrategies": {
|
||||
"__comment__": "This section is used to allow users to login using other accounts. You will need to get an API key from the services and register callback URL's",
|
||||
"twitter": {
|
||||
"_callbackurl": "https://server/auth-twitter-callback",
|
||||
"newAccounts": true,
|
||||
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
|
||||
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
|
||||
"clientid": "xxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
"google": {
|
||||
"_callbackurl": "https://server/auth-google-callback",
|
||||
"newAccounts": true,
|
||||
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
|
||||
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
|
||||
"clientid": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
|
||||
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
"github": {
|
||||
"_callbackurl": "https://server/auth-github-callback",
|
||||
"newAccounts": true,
|
||||
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
|
||||
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
|
||||
"clientid": "xxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
"reddit": {
|
||||
"_callbackurl": "https://server/auth-reddit-callback",
|
||||
"newAccounts": true,
|
||||
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
|
||||
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
|
||||
"clientid": "xxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
"azure": {
|
||||
"_callbackurl": "https://server/auth-azure-callback",
|
||||
"newAccounts": true,
|
||||
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
|
||||
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
|
||||
"clientid": "00000000-0000-0000-0000-000000000000",
|
||||
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"tenantid": "00000000-0000-0000-0000-000000000000"
|
||||
@ -409,7 +441,7 @@
|
||||
"jumpcloud": {
|
||||
"_callbackurl": "https://server/auth-jumpcloud-callback",
|
||||
"newAccounts": true,
|
||||
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
|
||||
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
|
||||
"entityid": "meshcentral",
|
||||
"idpurl": "https://sso.jumpcloud.com/saml2/saml2",
|
||||
"cert": "jumpcloud-saml.pem"
|
||||
@ -418,8 +450,8 @@
|
||||
"_callbackurl": "https://server/auth-saml-callback",
|
||||
"_disableRequestedAuthnContext": true,
|
||||
"newAccounts": true,
|
||||
"_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ],
|
||||
"_newAccountsRights": [ "nonewgroups", "notools" ],
|
||||
"_newAccountsUserGroups": ["ugrp//xxxxxxxxxxxxxxxxx"],
|
||||
"_newAccountsRights": ["nonewgroups", "notools"],
|
||||
"entityid": "meshcentral",
|
||||
"idpurl": "https://server/saml2",
|
||||
"cert": "saml.pem"
|
||||
@ -455,7 +487,7 @@
|
||||
"uid": "anneonyme",
|
||||
"mail": "anneonyme@example.com",
|
||||
"email": "anneonyme@example.com",
|
||||
"otherMail": [ "other.anneonyme@example.com", "anneonyme@example.com" ]
|
||||
"otherMail": ["other.anneonyme@example.com", "anneonyme@example.com"]
|
||||
},
|
||||
"so": {
|
||||
"displayName": "Sticker Sophie",
|
||||
@ -463,7 +495,7 @@
|
||||
"uid": "ssticker",
|
||||
"mail": "ssticker@example.com",
|
||||
"email": "ssticker@example.com",
|
||||
"otherMail": [ "other.ssticker@example.com", "ssticker@example.com" ]
|
||||
"otherMail": ["other.ssticker@example.com", "ssticker@example.com"]
|
||||
}
|
||||
},
|
||||
"__LDAPOptions": {
|
||||
@ -512,7 +544,7 @@
|
||||
"_sendmail": {
|
||||
"newline": "unix",
|
||||
"path": "/usr/sbin/sendmail",
|
||||
"_args": [ "-f", "foo@example.com" ]
|
||||
"_args": ["-f", "foo@example.com"]
|
||||
},
|
||||
"_sms": {
|
||||
"provider": "twilio",
|
||||
|
@ -1,4 +0,0 @@
|
||||
@ECHO OFF
|
||||
%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js minifyall
|
||||
%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js translateall
|
||||
%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js extractall
|
4
translate/translate-node14.bat
Normal file
@ -0,0 +1,4 @@
|
||||
@ECHO OFF
|
||||
%LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node translate.js minifyall
|
||||
%LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node translate.js translateall
|
||||
%LOCALAPPDATA%\..\Roaming\nvm\v14.16.0\node translate.js extractall
|
@ -3541,7 +3541,7 @@
|
||||
|
||||
x += '</table><br />';
|
||||
// Show action button, only show if we have permissions 4, 8, 64
|
||||
if (((meshrights & (4 + 8 + 64)) != 0) && (node.mtype < 3)) { x += '<input type=button value="' + "Actions" + '" onclick=deviceActionFunction() />'; }
|
||||
if (((meshrights & (4 + 8 + 64 + 262144)) != 0) && (node.mtype < 3)) { x += '<input type=button value="' + "Actions" + '" onclick=deviceActionFunction() />'; }
|
||||
x += '<input type=button value="' + "Notes" + '" onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponent(node._id) + '") />';
|
||||
//if ((connectivity & 1) && (meshrights & 8) && (node.agent.id < 5)) { x += '<input type=button value=Toast onclick=deviceToastFunction() />'; }
|
||||
|
||||
@ -3676,35 +3676,6 @@
|
||||
if (currentDevicePanel == 1) { deskAdjust(); }
|
||||
}
|
||||
|
||||
/*
|
||||
function deviceActionFunction() {
|
||||
if (xxdialogMode) return;
|
||||
var rights = GetNodeRights(currentNode), count = 0;
|
||||
var x = "Select an operation to perform on this device." + '<br /><br />';
|
||||
var y = '<select id=d2deviceop style=float:right;width:170px>';
|
||||
if ((rights & 64) != 0) { count++; y += '<option value=100>' + "Wake-up" + '</option>'; } // Wake-up permission
|
||||
//if (((currentNode.conn & 1) != 0) && ((rights & 131072) != 0)) { count++; y += '<option value=106>' + "Run Commands" + '</option>'; } // Remote command permission
|
||||
if ((currentNode.conn != 0) && ((rights & 262144) != 0)) { count++; y += '<option value=4>' + "Sleep" + '</option><option value=3>' + "Reset" + '</option><option value=2>' + "Power off" + '</option>'; }
|
||||
//if ((currentNode.conn & 16) != 0) { count++; y += '<option value=103>' + "Send MQTT Message" + '</option>'; }
|
||||
//if (((currentNode.conn & 1) != 0) && ((rights & 32768) != 0)) { count++; y += '<option value=104>' + "Uninstall Agent" + '</option>'; }
|
||||
y += '</select>';
|
||||
x += addHtmlValue("Operation", y);
|
||||
if (count == 0) { x = "No actions currently available for this device."; }
|
||||
setDialogMode(2, "Device Action", (count == 0) ? 1 : 3, deviceActionFunctionEx, x);
|
||||
}
|
||||
|
||||
function deviceActionFunctionEx() {
|
||||
var op = Q('d2deviceop').value;
|
||||
if (op == 100) {
|
||||
// Device wake
|
||||
meshserver.send({ action: 'wakedevices', nodeids: [currentNode._id] });
|
||||
} else {
|
||||
// Power operation
|
||||
meshserver.send({ action: 'poweraction', nodeids: [currentNode._id], actiontype: op });
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function deviceActionFunction() {
|
||||
if (xxdialogMode) return;
|
||||
var rights = GetNodeRights(currentNode), count = 0;
|
||||
@ -3723,12 +3694,15 @@
|
||||
//if (((currentNode.conn & 1) != 0) && ((rights & 131072) != 0)) { count++; y += '<option value=106>' + "Run Commands" + '</option>'; } // Remote command permission
|
||||
if ((currentNode.conn != 0) && ((rights & 262144) != 0)) { count++; y += '<option value=4>' + "Sleep" + '</option><option value=3>' + "Reset" + '</option><option value=2>' + "Power off" + '</option>'; }
|
||||
//if ((currentNode.conn & 16) != 0) { count++; y += '<option value=103>' + "Send MQTT Message" + '</option>'; }
|
||||
if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && (rights == 0xFFFFFFFF)) {
|
||||
if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 262144) != 0)) {
|
||||
count++;
|
||||
y += '<option value=310>' + "Intel® AMT Reset" + '</option>';
|
||||
y += '<option value=302>' + "Intel® AMT Power on" + '</option>';
|
||||
y += '<option value=308>' + "Intel® AMT Power off" + '</option>';
|
||||
}
|
||||
if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 64) != 0)) {
|
||||
count++;
|
||||
y += '<option value=302>' + "Intel® AMT Power on" + '</option>';
|
||||
}
|
||||
//if ((getNodeAmtVersion(currentNode) >= 15) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && (rights == 0xFFFFFFFF) && ((features & 0x00000400) == 0)) { count++; y += '<option value=107>' + "Intel® AMT One Click Recovery" + '</option>'; } // CIRA (2) or AMT (4) connected
|
||||
//if (((currentNode.conn & 1) != 0) && ((rights & 32768) != 0)) { count++; y += '<option value=104>' + "Uninstall Agent" + '</option>'; }
|
||||
}
|
||||
@ -4070,7 +4044,7 @@
|
||||
);
|
||||
|
||||
// Show the right settings
|
||||
QV('d7amtkvm', (currentNode.intelamt != null && ((currentNode.intelamt.ver != null) || (currentNode.agent == null))) && ((deskState == 0) || (desktop.contype == 2)));
|
||||
QV('d7amtkvm', (currentNode.intelamt != null && ((typeof currentNode.intelamt.sku != 'number') || ((currentNode.intelamt.sku & 16) == 0)) && ((currentNode.intelamt.ver != null) || (currentNode.agent == null))) && ((deskState == 0) || (desktop.contype == 2)));
|
||||
QV('d7meshkvm', ((currentNode.agent != null) && (currentNode.agent.caps & 1) && ((deskState == false) || (desktop.contype == 1))));
|
||||
|
||||
// Enable buttons
|
||||
@ -4241,7 +4215,7 @@
|
||||
}
|
||||
|
||||
function applyDesktopSettings() {
|
||||
var r = '', ops = (features & 512) ? [90, 70, 50, 40, 30, 20, 10, 5, 1] : [50, 40, 30, 20, 10, 5, 1];
|
||||
var r = '', ops = (features & 512) ? [100, 90, 70, 50, 40, 30, 20, 10, 5, 1] : [50, 40, 30, 20, 10, 5, 1];
|
||||
for (var i in ops) { r += '<option value=' + ops[i] + '>' + ops[i] + '%</option>'; }
|
||||
QH('d7bitmapquality', r);
|
||||
d7desktopmode.value = desktopsettings.encoding;
|
||||
@ -4695,7 +4669,7 @@
|
||||
|
||||
// Enable action button if mesh type is not "local devices"
|
||||
QV('termActionsBtn', terminalNode.mtype != 3);
|
||||
if (((termState == true) && (terminal.contype != 3)) || (terminalNode.agent.id == 3) || (terminalNode.agent.id == 4)) {
|
||||
if (((termState == true) && (terminal.contype != 3)) || (terminalNode.agent == null) || (terminalNode.agent.id == 3) || (terminalNode.agent.id == 4)) {
|
||||
QH('terminalCustomUpperRight', '');
|
||||
} else {
|
||||
QH('terminalCustomUpperRight', '<a style=cursor:pointer onclick=cmsshportaction(1,event)>' + format("SSH Port {0}", (terminalNode.sshport ? terminalNode.sshport : 22)) + '</a>');
|
||||
@ -5321,7 +5295,7 @@
|
||||
QE('p13PasteButton', advancedFeatures && (currentNode.mtype != 3) && ((p13filetreelocation.length > 0) || (winAgent == false)) && ((p13clipboard != null) && (p13clipboard.length > 0)));
|
||||
}
|
||||
var filesState = ((files != null) && (files.state != 0));
|
||||
if (((filesState == true) && (files.contype != 2)) || (filesNode.agent.id == 3) || (filesNode.agent.id == 4)) {
|
||||
if (((filesState == true) && (files.contype != 2)) || (filesNode.agent == null) || (filesNode.agent.id == 3) || (filesNode.agent.id == 4)) {
|
||||
QH('filesCustomUpperRight', '');
|
||||
} else {
|
||||
QH('filesCustomUpperRight', '<a style=cursor:pointer onclick=cmsshportaction(1,event)>' + format("SSH Port {0}", (filesNode.sshport ? filesNode.sshport : 22)) + '</a>');
|
||||
@ -5798,20 +5772,13 @@
|
||||
}
|
||||
}
|
||||
for (var j = 0; j < m.length; j++) {
|
||||
var iplayer = m[j];
|
||||
if (iplayer.family == 'IPv4') {
|
||||
if (iplayer.gateway && iplayer.netmask) {
|
||||
x += addDetailItem("IPv4 Layer", format("{0}, Mask: {1}, Gateway: {2}", EscapeHtml(iplayer.address), EscapeHtml(iplayer.netmask), EscapeHtml(iplayer.gateway)));
|
||||
} else {
|
||||
if (iplayer.address) { x += addDetailItem("IPv4 Layer", format("{0}", EscapeHtml(iplayer.address))); }
|
||||
}
|
||||
}
|
||||
if (iplayer.family == 'IPv6') {
|
||||
if (iplayer.gateway && iplayer.netmask) {
|
||||
x += addDetailItem("IPv6 Layer", format("{0}, Mask: {1}, Gateway: {2}", EscapeHtml(iplayer.address), EscapeHtml(iplayer.netmask), EscapeHtml(iplayer.gateway)));
|
||||
} else {
|
||||
if (iplayer.address) { x += addDetailItem("IPv6 Layer", format("{0}", EscapeHtml(iplayer.address))); }
|
||||
}
|
||||
var iplayer = m[j], items = [];
|
||||
if (iplayer.address) { items.push(format("IP: {0}", EscapeHtml(iplayer.address))); }
|
||||
if (iplayer.netmask) { items.push(format("Mask: {0}", EscapeHtml(iplayer.netmask))); }
|
||||
if (iplayer.gateway) { items.push(format("Gateway: {0}", EscapeHtml(iplayer.gateway))); }
|
||||
if (items.length > 0) {
|
||||
if (iplayer.family == 'IPv4') { x += addDetailItem("IPv4 Layer", items.join(", ")); }
|
||||
if (iplayer.family == 'IPv6') { x += addDetailItem("IPv6 Layer", items.join(", ")); }
|
||||
}
|
||||
}
|
||||
x += '</div>';
|
||||
@ -5831,7 +5798,13 @@
|
||||
x += addDetailItem("Security", (node.intelamt.tls == 1) ? "Secured using TLS" : "TLS is not setup", s);
|
||||
// Check that the Intel AMT user is setup and there is no warnings (1 = invalid credentials, 8 = trying)
|
||||
x += addDetailItem("Admin Credentials", ((node.intelamt.user) == null || (node.intelamt.user == '') || ((node.intelamt.warn != null) && ((node.intelamt.warn & 9) != 0))) ? "Not Known" : "Known", s);
|
||||
if (x != '') { sections.push({ name: "Intel® 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) {
|
||||
|
@ -112,6 +112,12 @@
|
||||
<div id="rfbPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
|
||||
<div class="cmtext" onclick="cmrfbportaction(1,event)">Alternate Port</div>
|
||||
</div>
|
||||
<div id="httpPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
|
||||
<div class="cmtext" onclick="cmhttpportaction(1,event)">Alternate Port</div>
|
||||
</div>
|
||||
<div id="httpsPortContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
|
||||
<div class="cmtext" onclick="cmhttpsportaction(1,event)">Alternate Port</div>
|
||||
</div>
|
||||
<div id="filesContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
|
||||
<div class="cmtext" onclick="cmfilesaction(1,event)">Rename</div>
|
||||
<div class="cmtext" onclick="cmfilesaction(2,event)">Edit</div>
|
||||
@ -619,7 +625,7 @@
|
||||
</div>
|
||||
<div id="p11warning" onclick="showFeaturesDlg()">
|
||||
<div class="icon2"></div>
|
||||
<div class="warningbox">Intel® AMT Redirection port or KVM feature is disabled<span id="p11warninga">, click here to enable it.</span></div>
|
||||
<div class="warningbox">Redirection port or KVM feature is disabled<span id="p11warninga">, click here to enable it.</span></div>
|
||||
</div>
|
||||
<div id="p11warning2" onclick="showPowerActionDlg()">
|
||||
<div class="icon2"></div>
|
||||
@ -655,7 +661,7 @@
|
||||
<input type="button" id="autoconnectbutton1" value="AutoConnect" onclick=autoConnectDesktop(event) onkeypress="return false" onkeydown="return false" style="display:none;margin-right:4px" />
|
||||
<span id=connectbutton1span><input type=button id=connectbutton1 cmenu="deskConnectButton" title="Connect using MeshAgent remote desktop" value="Connect" onclick=connectDesktop(event,3) onkeypress="return false" onkeydown="return false" disabled="disabled" /></span>
|
||||
<span id=connectbutton1rspan><input type=button id=connectbutton1r cmenu="altPortContextMenu" value="RDP Connect" title="Connect using RDP" onclick=askRdpCredentials() onkeypress="return false" onkeydown="return false" disabled="disabled" /></span>
|
||||
<span id=connectbutton1hspan><input type=button id=connectbutton1h value="HW Connect" title="Connect using Intel® AMT hardware KVM" onclick=connectDesktop(event,2) onkeypress="return false" onkeydown="return false" disabled="disabled" /></span>
|
||||
<span id=connectbutton1hspan><input type=button id=connectbutton1h value="HW Connect" title="Connect using hardware KVM" onclick=connectDesktop(event,2) onkeypress="return false" onkeydown="return false" disabled="disabled" /></span>
|
||||
<span id=disconnectbutton1span><input type=button id=disconnectbutton1 cmenu="deskDisconnectButton" value="Disconnect" onclick=connectDesktop(event,0) onkeypress="return false" onkeydown="return false" /></span>
|
||||
<span id="deskstatus" style="line-height:22px">Disconnected</span><span id="deskmetadata"></span>
|
||||
</div>
|
||||
@ -696,7 +702,7 @@
|
||||
<span id="DeskLatency" title="Desktop Session Latency"></span>
|
||||
<span id="DeskTimer" style="line-height:22px" title="Session time"></span>
|
||||
<input id=DeskToolsButton type=button value=Tools title="Toggle tools view" onkeypress="return false" onkeydown="return false" onclick="toggleDeskTools()" />
|
||||
<span> </span>
|
||||
<span> </span>
|
||||
<span id=DeskChatButton class="deskarea" title="Open chat window to this computer"><img class="desktopButtons" src='images/icon-chat.png' onclick=deviceChat(event) height=16 width=16 style=padding-top:2px /></span>
|
||||
<span id=DeskNotifyButton title="Display a notification on the remote computer"><img class="desktopButtons" src='images/icon-notify.png' onclick=deviceToastFunction() height=16 width=16 style=padding-top:2px /></span>
|
||||
<span id=DeskLockButton title="Lock the remote computer"><img src='images/icon-lock.png' class="desktopButtons" onclick=deviceLockFunction() height=16 width=16 /></span>
|
||||
@ -731,7 +737,7 @@
|
||||
</div>
|
||||
<div id="p12warning" onclick=showFeaturesDlg()>
|
||||
<div class="icon2"></div>
|
||||
<div class="warningbox">Intel® AMT Redirection port or KVM feature is disabled<span id="p12warninga">, click here to enable it.</span></div>
|
||||
<div class="warningbox">Redirection port or KVM feature is disabled<span id="p12warninga">, click here to enable it.</span></div>
|
||||
</div>
|
||||
<div id="p12warning2" onclick=showPowerActionDlg()>
|
||||
<div class="icon2"></div>
|
||||
@ -864,7 +870,7 @@
|
||||
<div id="p14title">
|
||||
<div id="p14BackButton" style="float:left"><div class="backButton" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"><div class="backButtonEx"></div></div></div>
|
||||
<div id="devListToolbarViewIcons"><div class="viewSelector" onclick=deskToggleFull(event) title="Full Screen. Hold shift to browser full screen."><div class="viewSelector5"></div></div></div>
|
||||
<h1>Intel® AMT - <span id=p14deviceName></span></h1>
|
||||
<h1><span id=p14deviceNamePrefix>Intel® AMT</span> - <span id=p14deviceName></span></h1>
|
||||
</div>
|
||||
<iframe id=p14iframe src="{{{domainurl}}}commander.htm"></iframe>
|
||||
</div>
|
||||
@ -1323,6 +1329,7 @@
|
||||
<div>Other Settings</div>
|
||||
<div id="d7otherset2" style="display:block">
|
||||
<label style="display:block"><input type="checkbox" id="d7deskSwapMouse" />Swap Mouse Buttons</label>
|
||||
<label style="display:block"><input type="checkbox" id="d7deskrmw" />Reverse Mouse Wheel</label>
|
||||
<label style="display:block"><input type="checkbox" id="d7deskRemoteKeyMap" />Use Remote Keyboard Map</label>
|
||||
<label style="display:block" id="d7deskAutoClipboardLabel"><input type="checkbox" id="d7deskAutoClipboard" />Automatic Clipboard</label>
|
||||
<label style="display:block" id="d7deskAutoLockLabel"><input type="checkbox" id="d7deskAutoLock" />Lock on Disconnect</label>
|
||||
@ -1346,6 +1353,7 @@
|
||||
<label style="display:block"><input type="checkbox" id="d7showfocus" />Show Focus Tool</label>
|
||||
<label style="display:block"><input type="checkbox" id="d7showcursor" />Show Local Mouse Cursor</label>
|
||||
<label style="display:block"><input type="checkbox" id="d7localKeyMap" />Local Keyboard Map</label>
|
||||
<label style="display:block"><input type="checkbox" id="d7kvmrmw" />Reverse Mouse Wheel</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1379,6 +1387,7 @@
|
||||
<label style="display:block"><input type="checkbox" id="d7rdp9" />Enable Desktop Composision</label>
|
||||
<label style="display:block"><input type="checkbox" id="d7rdpclip" />Automatic Clipboard</label>
|
||||
<label style="display:block"><input type="checkbox" id="d7rdpsmb" />Swap Mouse Buttons</label>
|
||||
<label style="display:block"><input type="checkbox" id="d7rdprmw" />Reverse Mouse Wheel</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1446,6 +1455,7 @@
|
||||
var features = parseInt('{{{features}}}');
|
||||
var features2 = parseInt('{{{features2}}}');
|
||||
var sessionTime = parseInt('{{{sessiontime}}}');
|
||||
var webRelayPort = parseInt('{{{webRelayPort}}}');
|
||||
var sessionRefreshTimer = null;
|
||||
var domain = '{{{domain}}}';
|
||||
var domainUrl = '{{{domainurl}}}';
|
||||
@ -2316,7 +2326,8 @@
|
||||
18: "SMTP server has limited use in LAN mode.",
|
||||
19: "SMS gateway has limited use in LAN mode.",
|
||||
20: "Invalid \"LoginCookieEncryptionKey\" in config.json.",
|
||||
21: "Backup path can't be set within meshcentral-data folder, backup settings ignored."
|
||||
21: "Backup path can't be set within meshcentral-data folder, backup settings ignored.",
|
||||
22: "Failed to sign agent {0}: {1}"
|
||||
};
|
||||
var x = '';
|
||||
for (var i in message.warnings) {
|
||||
@ -2325,7 +2336,7 @@
|
||||
x += '<div style=color:red;padding-bottom:6px><b>' + "WARNING: " + y + '</b></div>';
|
||||
} else {
|
||||
var z = ServerWarnings[y.id];
|
||||
if (z == null) { z = y.msg; } else { z = format(z, y.args); }
|
||||
if (z == null) { z = y.msg; } else { z = format(z, ...y.args); }
|
||||
x += '<div style=color:red;padding-bottom:6px><b>' + "WARNING: " + z + '</b></div>';
|
||||
}
|
||||
}
|
||||
@ -2733,7 +2744,7 @@
|
||||
if (message.name != null) { url += ('&name=' + encodeURIComponentEx(message.name)); }
|
||||
if (message.ip != null) { url += ('&remoteip=' + message.ip); }
|
||||
url += ('&appid=' + message.protocol + '&autoexit=1'); // Protocol: 0 = Custom, 1 = HTTP, 2 = HTTPS, 3 = RDP, 4 = PuTTY, 5 = WinSCP, 6 = MCRDesktop, 7 = MCRFiles
|
||||
console.log(url);
|
||||
//console.log(url);
|
||||
downloadFile(url, '');
|
||||
} else if (message.tag == 'novnc') {
|
||||
var vncurl = window.location.origin + domainUrl + 'novnc/vnc.html?ws=wss%3A%2F%2F' + window.location.host + encodeURIComponentEx(domainUrl) + (message.localRelay?'local':'mesh') + 'relay.ashx%3Fauth%3D' + message.cookie + '&show_dot=1' + (urlargs.key?('&key=' + urlargs.key):'') + '&l={{{lang}}}';
|
||||
@ -3235,6 +3246,8 @@
|
||||
node.rdpport = message.event.node.rdpport;
|
||||
node.rfbport = message.event.node.rfbport;
|
||||
node.sshport = message.event.node.sshport;
|
||||
node.httpport = message.event.node.httpport;
|
||||
node.httpsport = message.event.node.httpsport;
|
||||
node.consent = message.event.node.consent;
|
||||
node.pmt = message.event.node.pmt;
|
||||
if (message.event.node.links != null) { node.links = message.event.node.links; } else { delete node.links; }
|
||||
@ -4569,6 +4582,10 @@
|
||||
|
||||
// RDP link, show this link only of the remote machine is Windows.
|
||||
if ((((node.conn & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0) && (node.agent.id != 14)) {
|
||||
if (webRelayPort != 0) {
|
||||
x += '<a href=# onclick=p10WebRouter("' + node._id + '",1,' + (node.httpport ? node.httpport : 80) + ')>' + "HTTP" + ((node.httpport && (node.httpport != 80)) ? '/' + node.httpport : '') + '</a> ';
|
||||
x += '<a href=# onclick=p10WebRouter("' + node._id + '",2,' + (node.httpsport ? node.httpsport : 443) + ')>' + "HTTPS" + ((node.httspport && (node.httpsport != 443)) ? '/' + node.httpsport : '') + '</a> ';
|
||||
}
|
||||
if ((node.agent.id > 0) && (node.agent.id < 5)) {
|
||||
if (navigator.platform.toLowerCase() == 'win32') {
|
||||
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.rdp != false)) {
|
||||
@ -4579,12 +4596,12 @@
|
||||
if (node.agent.id > 4) {
|
||||
if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) {
|
||||
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.ssh != false)) {
|
||||
x += '<a href=# onclick=p10MCRouter("' + node._id + '",4,22)>' + "SSH" + '</a> ';
|
||||
x += '<a href=# onclick=p10MCRouter("' + node._id + '",4,' + (node.sshport ? node.sshport : 22) + ')>' + "SSH" + '</a> ';
|
||||
}
|
||||
}
|
||||
if (navigator.platform.toLowerCase() == 'win32') {
|
||||
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.scp != false)) {
|
||||
x += '<a href=# onclick=p10MCRouter("' + node._id + '",5,22)>' + "SCP" + '</a> ';
|
||||
x += '<a href=# onclick=p10MCRouter("' + node._id + '",5,' + (node.sshport ? node.sshport : 22) + ')>' + "SCP" + '</a> ';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4851,6 +4868,7 @@
|
||||
desk.m.ScalingLevel = multidesktopsettings.scaling;
|
||||
if (multidesktopsettings.framerate) { desk.m.FrameRateTimer = multidesktopsettings.framerate; }
|
||||
if (multidesktopsettings.swapmouse) { desk.m.SwapMouse = multidesktopsettings.swapmouse; }
|
||||
if (multidesktopsettings.rmw) { desk.m.ReverseMouseWheel = multidesktopsettings.rmw; }
|
||||
if (multidesktopsettings.remotekeymap == true) { desk.m.remoteKeyMap = multidesktopsettings.remotekeymap; }
|
||||
//desk.m.onDisplayinfo = deskDisplayInfo;
|
||||
desk.m.onScreenSizeChange = mdeskAdjust; // Multi-Desktop Adjust
|
||||
@ -5546,6 +5564,7 @@
|
||||
var op = Q('d2deviceop').value, title = Q('dp2notifyTitle').value, msg = Q('d2notifyMsg').value, chkNodeIds = getCheckedDevices();
|
||||
if (msg.length == 0) return;
|
||||
if (title == '') { title = decodeURIComponent('{{{extitle}}}'); }
|
||||
if (title == '') { title = "MeshCentral"; }
|
||||
if (op == 1) { // MessageBox
|
||||
for (var i = 0; i < chkNodeIds.length; i++) { meshserver.send({ action: 'msg', type: 'messagebox', nodeid: chkNodeIds[i], title: title, msg: msg }); }
|
||||
} else if (op == 2) { // Toast
|
||||
@ -6055,6 +6074,32 @@
|
||||
if (currentNode.rfbport != null) { Q('d10rfbport').value = currentNode.rfbport; }
|
||||
}
|
||||
|
||||
function cmhttpportaction(action) {
|
||||
if (xxdialogMode) return;
|
||||
var x = "HTTP remote connection port:" + '<br /><br /><input type=text placeholder="80" inputmode="numeric" pattern="[0-9]*" onkeypress="return (event.keyCode == 8) || (event.charCode >= 48 && event.charCode <= 57)" maxlength=5 id=d10httpport type=text>';
|
||||
setDialogMode(2, "HTTP Connection", 3, function() {
|
||||
// Save the new HTTP port to the server
|
||||
var httpport = ((Q('d10httpport').value.length > 0) ? parseInt(Q('d10httpport').value) : 80);
|
||||
meshserver.send({ action: 'changedevice', nodeid: currentNode._id, httpport: httpport });
|
||||
//if (currentNode != null) { p10rfb(currentNode._id, httpport); }
|
||||
}, x, currentNode);
|
||||
Q('d10httpport').focus();
|
||||
if (currentNode.httpport != null) { Q('d10httpport').value = currentNode.httpport; }
|
||||
}
|
||||
|
||||
function cmhttpsportaction(action) {
|
||||
if (xxdialogMode) return;
|
||||
var x = "HTTPS remote connection port:" + '<br /><br /><input type=text placeholder="443" inputmode="numeric" pattern="[0-9]*" onkeypress="return (event.keyCode == 8) || (event.charCode >= 48 && event.charCode <= 57)" maxlength=5 id=d10httpsport type=text>';
|
||||
setDialogMode(2, "HTTPS Connection", 3, function() {
|
||||
// Save the new HTTP port to the server
|
||||
var httpsport = ((Q('d10httpsport').value.length > 0) ? parseInt(Q('d10httpsport').value) : 443);
|
||||
meshserver.send({ action: 'changedevice', nodeid: currentNode._id, httpsport: httpsport });
|
||||
//if (currentNode != null) { p10rfb(currentNode._id, httpsport); }
|
||||
}, x, currentNode);
|
||||
Q('d10httpsport').focus();
|
||||
if (currentNode.httpsport != null) { Q('d10httpsport').value = currentNode.httpsport; }
|
||||
}
|
||||
|
||||
function cmfilesaction(action) {
|
||||
if (xxdialogMode) return;
|
||||
var filetreexx = p13sort_files(p13filetree.dir);
|
||||
@ -6144,6 +6189,8 @@
|
||||
QV('altPortContextMenu', false);
|
||||
QV('rfbPortContextMenu', false);
|
||||
QV('sshPortContextMenu', false);
|
||||
QV('httpPortContextMenu', false);
|
||||
QV('httpsPortContextMenu', false);
|
||||
QV('filesContextMenu', false);
|
||||
QV('deskPlayerContextMenu', false);
|
||||
QV('deskKeyShortcutContextMenu', false);
|
||||
@ -7066,7 +7113,7 @@
|
||||
|
||||
x += '</table><br />';
|
||||
// Show action button, only show if we have permissions 4, 8, 64
|
||||
if (((meshrights & (4 + 8 + 64)) != 0) && (node.mtype < 3) && ((node.agent == null) || (node.agent.id != 34))) { x += '<input type=button value="' + "Actions" + '" title="' + "Perform power actions on the device" + '" onclick=deviceActionFunction() />'; }
|
||||
if (((meshrights & (4 + 8 + 64 + 262144)) != 0) && (node.mtype < 3) && ((node.agent == null) || (node.agent.id != 34))) { x += '<input type=button value="' + "Actions" + '" title="' + "Perform power actions on the device" + '" onclick=deviceActionFunction() />'; }
|
||||
x += '<input type=button value="' + "Notes" + '" title="' + "View notes about this device" + '" onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponentEx(node._id) + '") />';
|
||||
x += '<input type=button value="' + "Log Event" + '" title="' + "Write an event for this device" + '" onclick=writeDeviceEvent("' + encodeURIComponentEx(node._id) + '") />';
|
||||
if (node.mtype != 4) {
|
||||
@ -7135,22 +7182,26 @@
|
||||
|
||||
// RDP link, show this link only of the remote machine is Windows.
|
||||
if ((((connectivity & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0)) {
|
||||
if (webRelayPort != 0) {
|
||||
x += '<a href=# cmenu=httpPortContextMenu onclick=p10WebRouter("' + node._id + '",1,' + (node.httpport ? node.httpport : 80) + ')>' + "HTTP" + ((node.httpport && (node.httpport != 80)) ? '/' + node.httpport : '') + '</a> ';
|
||||
x += '<a href=# cmenu=httpsPortContextMenu onclick=p10WebRouter("' + node._id + '",2,' + (node.httpsport ? node.httpsport : 443) + ')>' + "HTTPS" + ((node.httpsport && (node.httpsport != 443)) ? '/' + node.httpsport : '') + '</a> ';
|
||||
}
|
||||
if ((node.agent.id > 0) && (node.agent.id < 5)) {
|
||||
if (navigator.platform.toLowerCase() == 'win32') {
|
||||
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.rdp != false)) {
|
||||
x += '<a href=# cmenu=altPortContextMenu id=rdpMCRouterLink onclick=p10MCRouter("' + node._id + '",3) title="' + "Requires installation of MeshCentral Router" + '.">' + "RDP" + '</a> ';
|
||||
x += '<a href=# cmenu=altPortContextMenu id=rdpMCRouterLink onclick=p10MCRouter("' + node._id + '",3) title="' + "Requires installation of MeshCentral Router" + '.">' + "RDP" + ((node.rdpport && (node.rdpport != 3389)) ? '/' + node.rdpport : '') + '</a> ';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node.agent.id > 4) {
|
||||
if ((navigator.platform.toLowerCase() == 'win32') || (navigator.platform.toLowerCase() == 'macintel')) {
|
||||
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.ssh != false)) {
|
||||
x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",4,22) title="' + "Requires installation of MeshCentral Router." + '">' + "SSH" + '</a> ';
|
||||
x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",4,' + (node.sshport ? node.sshport : 22) + ') title="' + "Requires installation of MeshCentral Router." + '">' + "SSH" + ((node.sshport && (node.sshport != 22)) ? '/' + node.sshport : '') + '</a> ';
|
||||
}
|
||||
}
|
||||
if (navigator.platform.toLowerCase() == 'win32') {
|
||||
if ((serverinfo.devicemeshrouterlinks == null) || (serverinfo.devicemeshrouterlinks.scp != false)) {
|
||||
x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",5,22) title="' + "Requires installation of MeshCentral Router." + '">' + "SCP" + '</a> ';
|
||||
x += '<a href=# cmenu=sshPortContextMenu onclick=p10MCRouter("' + node._id + '",5,' + (node.sshport ? node.sshport : 22) + ') title="' + "Requires installation of MeshCentral Router." + '">' + "SCP" + ((node.sshport && (node.sshport != 22)) ? '/' + node.sshport : '') + '</a> ';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 += '<option value=106>' + "Run Commands" + '</option>'; } // Remote command permission
|
||||
if ((currentNode.conn != 0) && ((rights & 262144) != 0)) { count++; y += '<option value=4>' + "Sleep" + '</option><option value=3>' + "Reset" + '</option><option value=2>' + "Power off" + '</option>'; }
|
||||
if ((currentNode.conn & 16) != 0) { count++; y += '<option value=103>' + "Send MQTT Message" + '</option>'; }
|
||||
if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && (rights == 0xFFFFFFFF)) {
|
||||
if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 262144) != 0)) {
|
||||
count++;
|
||||
y += '<option value=310>' + "Intel® AMT Reset" + '</option>';
|
||||
y += '<option value=302>' + "Intel® AMT Power on" + '</option>';
|
||||
y += '<option value=308>' + "Intel® AMT Power off" + '</option>';
|
||||
}
|
||||
if ((currentNode.intelamt != null) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && ((rights & 64) != 0)) {
|
||||
count++;
|
||||
y += '<option value=302>' + "Intel® AMT Power on" + '</option>';
|
||||
}
|
||||
if ((getNodeAmtVersion(currentNode) >= 15) && (currentNode.intelamt.state == 2) && ((currentNode.conn & 6) != 0) && (rights == 0xFFFFFFFF) && ((features & 0x00000400) == 0)) { count++; y += '<option value=107>' + "Intel® AMT One Click Recovery" + '</option>'; } // CIRA (2) or AMT (4) connected
|
||||
if (((currentNode.conn & 1) != 0) && ((rights & 32768) != 0)) { count++; y += '<option value=104>' + "Uninstall Agent" + '</option>'; }
|
||||
}
|
||||
@ -8043,6 +8108,22 @@
|
||||
meshserver.send({ action: 'removedevices', nodeids: [ nodeid ] });
|
||||
}
|
||||
|
||||
function p10WebRouter(nodeid, protocol, port, addr) {
|
||||
var relayid = null;
|
||||
var node = getNodeFromId(nodeid);
|
||||
if (node.mtype == 3) { // Setup device relay if needed
|
||||
var mesh = meshes[node.meshid];
|
||||
if (mesh && mesh.relayid) { relayid = mesh.relayid; addr = node.host; }
|
||||
}
|
||||
var servername = serverinfo.name;
|
||||
if ((servername.indexOf('.') == -1) || ((features & 2) != 0)) { servername = window.location.hostname; } // If the server name is not set or it's in LAN-only mode, use the URL hostname as server name.
|
||||
var url = 'https://' + servername + ':' + webRelayPort + '/control-redirect.ashx?n=' + nodeid + '&p=' + port + '&appid=' + protocol; // Protocol: 1 = HTTP, 2 = HTTPS
|
||||
if (addr != null) { url += '&addr=' + addr; }
|
||||
if (relayid != null) { url += '&relayid=' + relayid; }
|
||||
safeNewWindow(url, 'WebRelay');
|
||||
return false;
|
||||
}
|
||||
|
||||
function p10MCRouter(nodeid, protocol, port, addr, localport) {
|
||||
var node = getNodeFromId(nodeid);
|
||||
var mesh = meshes[node.meshid];
|
||||
@ -8370,7 +8451,7 @@
|
||||
);
|
||||
}
|
||||
// Show the right settings
|
||||
QV('td7amtkvm', (currentNode.intelamt != null && ((currentNode.intelamt.ver != null) || (currentNode.agent == null))) && ((deskState == 0) || (desktop.contype == 2)));
|
||||
QV('td7amtkvm', ((currentNode.intelamt != null) && ((typeof currentNode.intelamt.sku != 'number') || ((currentNode.intelamt.sku & 16) == 0)) && ((currentNode.intelamt.ver != null) || (currentNode.agent == null))) && ((deskState == 0) || (desktop.contype == 2)));
|
||||
QV('td7meshkvm', (webRtcDesktop) || ((currentNode.agent != null) && (currentNode.agent.caps & 1) && ((deskState == 0) || (desktop.contype == 1))));
|
||||
QV('td7rdpkvm', ((currentNode.agent != null) && ((currentNode.agent.id == 3) || (currentNode.agent.id == 4)) && ((deskState == 0) || (desktop.contype == 4))));
|
||||
|
||||
@ -8456,6 +8537,7 @@
|
||||
desktop.m.bpp = (desktopsettings.encoding == 1 || desktopsettings.encoding == 3) ? 1 : 2;
|
||||
desktop.m.useZRLE = (desktopsettings.encoding < 3);
|
||||
desktop.m.localKeyMap = desktopsettings.localkeymap;
|
||||
desktop.m.ReverseMouseWheel = desktopsettings.kvmrmw;
|
||||
desktop.m.showmouse = desktopsettings.showmouse;
|
||||
desktop.m.onScreenSizeChange = deskAdjust;
|
||||
desktop.m.onKvmData = function (x) {
|
||||
@ -8568,6 +8650,7 @@
|
||||
desktop.m.ScalingLevel = desktopsettings.scaling;
|
||||
if (desktopsettings.framerate) { desktop.m.FrameRateTimer = desktopsettings.framerate; }
|
||||
if (desktopsettings.swapmouse) { desktop.m.SwapMouse = desktopsettings.swapmouse; }
|
||||
if (desktopsettings.rmw) { desktop.m.ReverseMouseWheel = desktopsettings.rmw; }
|
||||
if (desktopsettings.remotekeymap == true) { desktop.m.remoteKeyMap = desktopsettings.remotekeymap; }
|
||||
desktop.m.onDisplayinfo = deskDisplayInfo;
|
||||
desktop.m.onScreenSizeChange = deskAdjust;
|
||||
@ -8584,6 +8667,7 @@
|
||||
desktop.m.onScreenSizeChange = mdeskAdjust;
|
||||
desktop.m.onClipboardChanged = function(text) { if ((text != null) && (desktopsettings.rdpautoclipboard) && (navigator.clipboard != null)) { navigator.clipboard.writeText(text).then(function() { }).catch(function(err) { console.log(err); }) } } // Put remote clipboard data into our clipboard
|
||||
if (desktopsettings.rdpsmb) { desktop.m.SwapMouse = desktopsettings.rdpsmb; }
|
||||
if (desktopsettings.rdprmw) { desktop.m.ReverseMouseWheel = desktopsettings.rdprmw; }
|
||||
desktop.Start(desktopNode._id, currentNode.rdpport ? currentNode.rdpport : 3389, tsid);
|
||||
desktop.contype = 4;
|
||||
desktop.onConsoleMessageChange = function () {
|
||||
@ -8866,12 +8950,15 @@
|
||||
desktopsettings.scaling = d7bitmapscaling.value;
|
||||
desktopsettings.framerate = d7framelimiter.value;
|
||||
desktopsettings.swapmouse = d7deskSwapMouse.checked;
|
||||
desktopsettings.rmw = d7deskrmw.checked;
|
||||
desktopsettings.remotekeymap = d7deskRemoteKeyMap.checked;
|
||||
desktopsettings.autoclipboard = d7deskAutoClipboard.checked;
|
||||
desktopsettings.autolock = d7deskAutoLock.checked;
|
||||
desktopsettings.localkeymap = d7localKeyMap.checked;
|
||||
desktopsettings.kvmrmw = d7kvmrmw.checked;
|
||||
desktopsettings.rdpsize = d7rdpsize.value;
|
||||
desktopsettings.rdpsmb = d7rdpsmb.checked;
|
||||
desktopsettings.rdprmw = d7rdprmw.checked;
|
||||
desktopsettings.rdpautoclipboard = d7rdpclip.checked;
|
||||
var rdpflags = 0;
|
||||
for (var i = 1; i < 10; i++) { if ((i != 5) && (Q('d7rdp' + i).checked)) { rdpflags |= (1 << (i - 1)); } }
|
||||
@ -8880,23 +8967,29 @@
|
||||
applyDesktopSettings();
|
||||
updateDesktopButtons();
|
||||
if (desktop) {
|
||||
if (desktop.contype == 1) {
|
||||
if (desktop.contype == 1) { // Intel AMT KVM
|
||||
desktop.m.SwapMouse = desktopsettings.swapmouse;
|
||||
desktop.m.ReverseMouseWheel = desktopsettings.rmw;
|
||||
desktop.m.remoteKeyMap = desktopsettings.remotekeymap;
|
||||
if (desktop.State != 0) {
|
||||
desktop.m.SendCompressionLevel(webpSupport?4:1, desktopsettings.quality, desktopsettings.scaling, desktopsettings.framerate);
|
||||
desktop.sendCtrlMsg('{"ctrlChannel":"102938","type":"autolock","value":' + desktopsettings.autolock + '}');
|
||||
}
|
||||
}
|
||||
if (desktop.contype == 2) {
|
||||
if (desktop.contype == 2) { // Mesh Agent Remote Desktop
|
||||
desktop.m.ReverseMouseWheel = desktopsettings.kvmrmw;
|
||||
if (desktopsettings.showfocus == false) { desktop.m.focusmode = 0; deskFocusBtn.value = "All Focus"; }
|
||||
if (desktop.State != 0) { desktop.Stop(); setTimeout(function () { connectDesktop(null, 2); }, 50); }
|
||||
}
|
||||
if (desktop.contype == 4) { // Web-RDP
|
||||
desktop.m.SwapMouse = desktopsettings.rdpsmb;
|
||||
desktop.m.ReverseMouseWheel = desktopsettings.rdprmw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyDesktopSettings() {
|
||||
var r = '', ops = (features & 512)?[90,80,70,60,50,40,30,20,10,5,1]:[60,50,40,30,20,10,5,1];
|
||||
var r = '', ops = (features & 512)?[100,90,80,70,60,50,40,30,20,10,5,1]:[60,50,40,30,20,10,5,1];
|
||||
for (var i in ops) { r += '<option value=' + ops[i] + '>' + ops[i] + '%</option>'; }
|
||||
QH('d7bitmapquality', r);
|
||||
d7desktopmode.value = desktopsettings.encoding;
|
||||
@ -8907,14 +9000,17 @@
|
||||
d7bitmapscaling.value = desktopsettings.scaling;
|
||||
if (desktopsettings.framerate) { d7framelimiter.value = desktopsettings.framerate; } else { d7framelimiter.value = 100; }
|
||||
if (desktopsettings.swapmouse != null) { d7deskSwapMouse.checked = desktopsettings.swapmouse; }
|
||||
if (desktopsettings.rmw != null) { d7deskrmw.checked = desktopsettings.rmw; }
|
||||
if (desktopsettings.remotekeymap != null) { d7deskRemoteKeyMap.checked = desktopsettings.remotekeymap; }
|
||||
if (desktopsettings.autoclipboard != null) { d7deskAutoClipboard.checked = desktopsettings.autoclipboard; }
|
||||
if (desktopsettings.autolock != null) { d7deskAutoLock.checked = desktopsettings.autolock; }
|
||||
if (desktopsettings.localkeymap) { d7localKeyMap.checked = desktopsettings.localkeymap; }
|
||||
if (desktopsettings.kvmrmw) { d7kvmrmw.checked = desktopsettings.kvmrmw; }
|
||||
QV('deskFocusBtn', (desktop != null) && (desktop.contype == 2) && (desktop.state != 0) && (desktopsettings.showfocus));
|
||||
if (desktopsettings.rdpsize != null) { d7rdpsize.value = desktopsettings.rdpsize; }
|
||||
if (desktopsettings.rdpflags == null) { desktopsettings.rdpflags = 0x2F; }
|
||||
if (desktopsettings.rdpsmb != null) { d7rdpsmb.checked = desktopsettings.rdpsmb; }
|
||||
if (desktopsettings.rdprmw != null) { d7rdprmw.checked = desktopsettings.rdprmw; }
|
||||
if (desktopsettings.rdpautoclipboard != null) { d7rdpclip.checked = desktopsettings.rdpautoclipboard; }
|
||||
for (var i = 1; i < 10; i++) { if (i != 5) { Q('d7rdp' + i).checked = ((desktopsettings.rdpflags & (1 << (i - 1))) != 0); } }
|
||||
}
|
||||
@ -9639,7 +9735,7 @@
|
||||
|
||||
// Enable action button if mesh type is not "local devices"
|
||||
QV('termActionsBtn', terminalNode.mtype != 3);
|
||||
if (((termState == true) && (terminal.contype != 3)) || (terminalNode.agent.id == 3) || (terminalNode.agent.id == 4)) {
|
||||
if (((termState == true) && (terminal.contype != 3)) || (terminalNode.agent == null) || (terminalNode.agent.id == 3) || (terminalNode.agent.id == 4)) {
|
||||
QH('terminalCustomUpperRight', '');
|
||||
} else {
|
||||
QH('terminalCustomUpperRight', '<a style=cursor:pointer onclick=cmsshportaction(1,event)>' + format("SSH Port {0}", (terminalNode.sshport?terminalNode.sshport:22)) + '</a>');
|
||||
@ -10431,7 +10527,7 @@
|
||||
QE('p13PasteButton', advancedFeatures && ((p13filetreelocation.length > 0) || (winAgent == false)) && ((p13clipboard != null) && (p13clipboard.length > 0)));
|
||||
}
|
||||
var filesState = ((files != null) && (files.state != 0));
|
||||
if (((filesState == true) && (files.contype != 2)) || (filesNode.agent.id == 3) || (filesNode.agent.id == 4)) {
|
||||
if (((filesState == true) && (files.contype != 2)) || (filesNode.agent == null) || (filesNode.agent.id == 3) || (filesNode.agent.id == 4)) {
|
||||
QH('filesCustomUpperRight', '');
|
||||
} else {
|
||||
QH('filesCustomUpperRight', '<a style=cursor:pointer onclick=cmsshportaction(1,event)>' + format("SSH Port {0}", (filesNode.sshport?filesNode.sshport:22)) + '</a>');
|
||||
@ -11130,20 +11226,13 @@
|
||||
}
|
||||
}
|
||||
for (var j = 0; j < m.length; j++) {
|
||||
var iplayer = m[j];
|
||||
if (iplayer.family == 'IPv4') {
|
||||
if (iplayer.gateway && iplayer.netmask) {
|
||||
x += addDetailItem("IPv4 Layer", format("IP: {0}, Mask: {1}, Gateway: {2}", EscapeHtml(iplayer.address), EscapeHtml(iplayer.netmask), EscapeHtml(iplayer.gateway)));
|
||||
} else {
|
||||
if (iplayer.address) { x += addDetailItem("IPv4 Layer", format("IP: {0}", EscapeHtml(iplayer.address))); }
|
||||
}
|
||||
}
|
||||
if (iplayer.family == 'IPv6') {
|
||||
if (iplayer.gateway && iplayer.netmask) {
|
||||
x += addDetailItem("IPv6 Layer", format("IP: {0}, Mask: {1}, Gateway: {2}", EscapeHtml(iplayer.address), EscapeHtml(iplayer.netmask), EscapeHtml(iplayer.gateway)));
|
||||
} else {
|
||||
if (iplayer.address) { x += addDetailItem("IPv6 Layer", format("IP: {0}", EscapeHtml(iplayer.address))); }
|
||||
}
|
||||
var iplayer = m[j], items = [];
|
||||
if (iplayer.address) { items.push(format("IP: {0}", EscapeHtml(iplayer.address))); }
|
||||
if (iplayer.netmask) { items.push(format("Mask: {0}", EscapeHtml(iplayer.netmask))); }
|
||||
if (iplayer.gateway) { items.push(format("Gateway: {0}", EscapeHtml(iplayer.gateway))); }
|
||||
if (items.length > 0) {
|
||||
if (iplayer.family == 'IPv4') { x += addDetailItem("IPv4 Layer", items.join(", ")); }
|
||||
if (iplayer.family == 'IPv6') { x += addDetailItem("IPv6 Layer", items.join(", ")); }
|
||||
}
|
||||
}
|
||||
x += '</div>';
|
||||
@ -11163,7 +11252,13 @@
|
||||
x += addDetailItem("Security", (node.intelamt.tls == 1)?"Secured using TLS":"TLS is not setup", s);
|
||||
// Check that the Intel AMT user is setup and there is no warnings (1 = invalid credentials, 8 = trying)
|
||||
x += addDetailItem("Admin Credentials", ((node.intelamt.user) == null || (node.intelamt.user == '') || ((node.intelamt.warn != null) && ((node.intelamt.warn & 9) != 0)))?"Not Known":"Known", s);
|
||||
if (x != '') { sections.push({ name: "Intel® 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) {
|
||||
|
@ -1188,7 +1188,7 @@
|
||||
}
|
||||
|
||||
function applyDesktopSettings() {
|
||||
var r = '', ops = (features & 512) ? [90, 70, 50, 40, 30, 20, 10, 5, 1] : [50, 40, 30, 20, 10, 5, 1];
|
||||
var r = '', ops = (features & 512) ? [100, 90, 70, 50, 40, 30, 20, 10, 5, 1] : [50, 40, 30, 20, 10, 5, 1];
|
||||
for (var i in ops) { r += '<option value=' + ops[i] + '>' + ops[i] + '%</option>'; }
|
||||
QH('d7bitmapquality', r);
|
||||
d7desktopmode.value = desktopsettings.encoding;
|
||||
|
@ -780,7 +780,7 @@
|
||||
}
|
||||
|
||||
function applyDesktopSettings() {
|
||||
var r = '', ops = (features2 & 1) ? [90, 80, 70, 60, 50, 40, 30, 20, 10, 5, 1] : [60, 50, 40, 30, 20, 10, 5, 1]
|
||||
var r = '', ops = (features2 & 1) ? [100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 5, 1] : [60, 50, 40, 30, 20, 10, 5, 1]
|
||||
for (var i in ops) { r += '<option value=' + ops[i] + '>' + ops[i] + '%</option>'; }
|
||||
QH('d7bitmapquality', r);
|
||||
d7desktopmode.value = desktopsettings.encoding;
|
||||
|
272
webrelayserver.js
Normal file
@ -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;
|
||||
};
|
23
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) {
|
||||
|