Added multi-agent upload feature.

This commit is contained in:
Ylian Saint-Hilaire 2020-11-08 01:13:23 -08:00
parent c2f0336146
commit 7aef3cda0c
6 changed files with 1862 additions and 1681 deletions

View File

@ -43,6 +43,8 @@ var MESHRIGHT_NODESKTOP = 65536;
function createMeshCore(agent) {
var obj = {};
var agentFileHttpRequests = {}; // Currently active agent HTTPS GET requests from the server.
if (process.platform == 'win32' && require('user-sessions').isRoot()) {
// Check the Agent Uninstall MetaData for correctness, as the installer may have written an incorrect value
try {
@ -1140,6 +1142,33 @@ function createMeshCore(agent) {
case 'meshToolInfo':
if (data.pipe == true) { delete data.pipe; delete data.action; data.cmd = 'meshToolInfo'; broadcastToRegisteredApps(data); }
break;
case 'wget': // Server uses this command to tell the agent to download a file using HTTPS/GET and place it in a given path. This is used for one-to-many file uploads.
if ((data.overwrite !== true) && fs.existsSync(data.path)) break; // Don't overwrite an existing file.
data.url = 'http' + getServerTargetUrlEx('*/').substring(2);
var agentFileHttpOptions = http.parseUri(data.url);
agentFileHttpOptions.path = data.urlpath;
agentFileHttpOptions.rejectUnauthorized = 0; // TODO: Check TLS cert
if (agentFileHttpOptions == null) break;
var agentFileHttpRequest = http.request(agentFileHttpOptions,
function (response) {
response.xparent = this;
try {
response.xfile = fs.createWriteStream(this.xpath, { flags: 'wbN' })
response.pipe(response.xfile);
response.end = function () { delete agentFileHttpRequests[this.xparent.xurlpath]; delete this.xparent; }
} catch (ex) {
delete agentFileHttpRequests[this.xurlpath];
delete response.xparent;
return;
}
}
);
agentFileHttpRequest.on('error', function (ex) { delete agentFileHttpRequests[this.xurlpath]; });
agentFileHttpRequest.end();
agentFileHttpRequest.xurlpath = data.urlpath;
agentFileHttpRequest.xpath = data.path;
agentFileHttpRequests[data.urlpath] = agentFileHttpRequest;
break;
default:
// Unknown action, ignore it.
break;

View File

@ -107,6 +107,13 @@ function CreateMeshCentralServer(config, args) {
if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/emails'))) { obj.webEmailsOverridePath = obj.path.join(__dirname, '../meshcentral-web/emails'); }
}
// Clean up any temporary files
var removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
var dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
if (err != null) return;
for (var i in files) { try { const filepath = obj.path.join(obj.filespath, 'tmp', files[i]); if (obj.fs.statSync(filepath).mtime.getTime() < removeTime) { obj.fs.unlink(filepath, function () { }); } } catch (ex) { } }
});
// Look to see if data and/or file path is specified
if (obj.config.settings && (typeof obj.config.settings.datapath == 'string')) { obj.datapath = obj.config.settings.datapath; }
if (obj.config.settings && (typeof obj.config.settings.filespath == 'string')) { obj.filespath = obj.config.settings.filespath; }
@ -1550,6 +1557,13 @@ function CreateMeshCentralServer(config, args) {
// Perform database maintenance
obj.db.maintenance();
// Clean up any temporary files
var removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
var dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
if (err != null) return;
for (var i in files) { try { const filepath = obj.path.join(obj.filespath, 'tmp', files[i]); if (obj.fs.statSync(filepath).mtime.getTime() < removeTime) { obj.fs.unlink(filepath, function () { }); } } catch (ex) { } }
});
// Check for self-update that targets a specific version
if ((typeof obj.args.selfupdate == 'string') && (getCurrentVerion() === obj.args.selfupdate)) { obj.args.selfupdate = false; }

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -4440,6 +4440,7 @@
if ((rights & 131072) && ((added & 16) == 0)) { added |= 16; addedOptions += '<option value=106>' + "Run commands" + '</option>'; }
if ((rights & 16384) && ((added & 32) == 0)) { added |= 32; addedOptions += '<option value=108>' + "Device notification" + '</option>'; }
if ((rights & 4) && ((added & 64) == 0)) { added |= 64; addedOptions += '<option value=107>' + "Edit tags" + '</option>'; }
if ((rights & 8) && ((added & 256) == 0)) { added |= 256; addedOptions += '<option value=109>' + "Upload files" + '</option>'; }
if ((rights & 32768) && ((added & 128) == 0)) { added |= 128; addedOptions += '<option value=101>' + "Delete devices" + '</option>'; }
}
@ -4519,6 +4520,19 @@
x += '<textarea id=d2notifyMsg style=background-color:#fcf3cf;width:100%;height:140px;resize:none;overflow-y:scroll></textarea>';
setDialogMode(2, "Device Notification", 3, d2groupActionFunctionNotifyExec, x);
Q('d2notifyMsg').focus();
} else if (op == 109) {
// Upload files
var wintype = false, linuxtype = false, chkNodeIds = getCheckedDevices();
for (var i in chkNodeIds) { var n = getNodeFromId(chkNodeIds[i]); if (n.agent) { if ((n.agent.id > 0) && (n.agent.id < 5)) { wintype = true; } else { linuxtype = true; } } }
var x = "Upload selected files to all selected devices" + '<br /><br />';
x += '<form method=post enctype=multipart/form-data action=uploadfilebatch.ashx target=fileUploadFrame>';
x += '<input type=hidden name=authCookie value=' + authCookie + ' /><input type=hidden name=nodeIds value=' + chkNodeIds.join(',') + ' /><input type=submit id=d2batchUploadSubmit style=display:none />';
x += '<input type=file name=files id=d2uploadinput style=width:100% multiple=multiple onchange="d2batchUploadValidate()" /><br /><br />';
if (wintype) { x += addHtmlValue("Windows Path", '<input style=width:250px type=text onchange="d2batchUploadValidate()" onkeyup="d2batchUploadValidate()" name=winpath id=d2winuploadpath placeholder="C:\\temp" value="" />'); }
if (linuxtype) { x += addHtmlValue("Linux Path", '<input style=width:250px type=text onchange="d2batchUploadValidate()" onkeyup="d2batchUploadValidate()" name=linuxpath id=d2linuxuploadpath placeholder="/tmp" value="" />'); }
x += '<br /><label><input type=checkbox name=overwriteFiles />' + "Overwrite if file exists?" + '</label></form>';
setDialogMode(2, "Batch File Upload", 3, d2batchUploadValidateOk, x);
d2batchUploadValidate();
} else {
// Power operation
meshserver.send({ action: 'poweraction', nodeids: getCheckedDevices(), actiontype: parseInt(op) });
@ -4526,6 +4540,9 @@
}
}
function d2batchUploadValidate() { QE('idx_dlgOkButton', (Q('d2uploadinput').files.length != 0) && ((Q('d2winuploadpath') == null) || (Q('d2winuploadpath').value != '')) && ((Q('d2linuxuploadpath') == null) || (Q('d2linuxuploadpath').value != ''))); }
function d2batchUploadValidateOk() { Q('d2batchUploadSubmit').click(); }
function d2groupActionFunctionNotifyExec() {
var op = Q('d2deviceop').value, title = Q('dp2notifyTitle').value, msg = Q('d2notifyMsg').value, chkNodeIds = getCheckedDevices();
if (msg.length == 0) return;
@ -10735,10 +10752,10 @@
if (overWriteCount == 0) {
// If no overwrite, go ahead with upload
p5PerformUpload(1, files);
p5PerformUpload(1, e.dataTransfer.files);
} else {
// Otherwise, prompt for confirmation
setDialogMode(2, "Upload File", 3, p5PerformUpload, format((overWriteCount == 1)?"Upload will overwrite 1 file. Continue?":"Upload will overwrite {0} files. Continue?", overWriteCount), files);
setDialogMode(2, "Upload File", 3, p5PerformUpload, format((overWriteCount == 1)?"Upload will overwrite 1 file. Continue?":"Upload will overwrite {0} files. Continue?", overWriteCount), e.dataTransfer.files);
}
}

View File

@ -2718,6 +2718,20 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, null, res, req, domain, user, meshid, nodeid);
}
// Handle download of a server file by an agent
function handleAgentDownloadFile(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if (req.query.c == null) { res.sendStatus(404); return; }
// Check the inbound desktop sharing cookie
var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 5); // 5 minute timeout
if ((c == null) || (c.a != 'tmpdl') || (c.d != domain.id) || (c.nid == null) || (c.f == null) || (obj.common.IsFilenameValid(c.f) == false)) { res.sendStatus(404); return; }
// Send the file back
try { res.sendFile(obj.path.join(obj.filespath, 'tmp', c.f)); return; } catch (ex) { res.sendStatus(404); }
}
// Handle logo request
function handleLogoRequest(req, res) {
const domain = checkUserIpAddress(req, res);
@ -3154,6 +3168,75 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
});
}
// Upload a file to the server and then batch upload to many agents
function handleUploadFileBatch(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if (domain.userQuota == -1) { res.sendStatus(401); return; }
var authUserid = null;
if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
const multiparty = require('multiparty');
const form = new multiparty.Form();
form.parse(req, function (err, fields, files) {
// If an authentication cookie is embedded in the form, use that.
if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
}
if (authUserid == null) { res.sendStatus(401); return; }
// Get the user
const user = obj.users[authUserid];
if ((user == null) || (user.siteadmin & 8) == 0) { res.sendStatus(401); return; } // Check if we have file rights
// Get fields
if ((fields == null) || (fields.nodeIds == null) || (fields.nodeIds.length != 1)) { res.sendStatus(404); return; }
var cmd = { nodeids: fields.nodeIds[0].split(','), files: [], user: user, domain: domain };
if ((fields.winpath != null) && (fields.winpath.length == 1)) { cmd.windowsPath = fields.winpath[0]; }
if ((fields.linuxpath != null) && (fields.linuxpath.length == 1)) { cmd.linuxPath = fields.linuxpath[0]; }
if ((fields.overwriteFiles != null) && (fields.overwriteFiles.length == 1) && (fields.overwriteFiles[0] == 'on')) { cmd.overwrite = true; }
// Get server temporary path
var serverpath = obj.path.join(obj.filespath, 'tmp')
try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
try { obj.fs.mkdirSync(serverpath); } catch (ex) { }
// More typical upload method, the file data is in a multipart mime post.
for (var i in files.files) {
var file = files.files[i], ftarget = getRandomPassword() + '-' + file.originalFilename, fpath = obj.path.join(serverpath, ftarget);
cmd.files.push({ name: file.originalFilename, target: ftarget });
// Rename the file
obj.fs.rename(file.path, fpath, function (err) {
if (err && (err.code === 'EXDEV')) {
// On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
obj.common.copyFile(file.path, fpath, function (err) { obj.fs.unlink(file.path, function (err) { handleUploadFileBatchEx(cmd); }); });
} else {
handleUploadFileBatchEx(cmd);
}
});
}
res.send('');
});
}
// Instruct one of more agents to download a URL to a given local drive location.
function handleUploadFileBatchEx(cmd) {
for (var i in cmd.nodeids) {
obj.GetNodeWithRights(cmd.domain, cmd.user, cmd.nodeids[i], function (node, rights, visible) {
if ((node == null) || ((rights & 8) == 0) || (visible == false)) return; // We don't have remote control rights to this device
var agentPath = ((node.agent.id > 0) && (node.agent.id < 5)) ? cmd.windowsPath : cmd.linuxPath;
for (var f in cmd.files) {
const acmd = { action: 'wget', overwrite: cmd.overwrite, urlpath: '/agentdownload.ashx?c=' + obj.parent.encodeCookie({ a: 'tmpdl', d: cmd.domain.id, nid: node._id, f: cmd.files[f].target }, obj.parent.loginCookieEncryptionKey), path: obj.path.join(agentPath, cmd.files[f].name) };
var agent = obj.wsagents[node._id];
if (agent != null) { try { agent.send(JSON.stringify(acmd)); } catch (ex) { } }
// TODO: Add support for peer servers.
}
});
}
}
// Subscribe to all events we are allowed to receive
obj.subscribe = function (userid, target) {
const user = obj.users[userid];
@ -4793,6 +4876,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
obj.app.get(url + 'devicepowerevents.ashx', obj.handleDevicePowerEvents);
obj.app.get(url + 'downloadfile.ashx', handleDownloadFile);
obj.app.post(url + 'uploadfile.ashx', handleUploadFile);
obj.app.post(url + 'uploadfilebatch.ashx', handleUploadFileBatch);
obj.app.post(url + 'uploadmeshcorefile.ashx', handleUploadMeshCoreFile);
obj.app.get(url + 'userfiles/*', handleDownloadUserFiles);
obj.app.ws(url + 'echo.ashx', handleEchoWebSocket);
@ -4808,6 +4892,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
});
obj.app.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
obj.app.get(url + 'devicefile.ashx', handleDeviceFile);
obj.app.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
obj.app.get(url + 'logo.png', handleLogoRequest);
obj.app.get(url + 'loginlogo.png', handleLoginLogoRequest);
obj.app.post(url + 'translations', handleTranslationsRequest);