diff --git a/MeshCentralServer.njsproj b/MeshCentralServer.njsproj index f88334e0..8639af10 100644 --- a/MeshCentralServer.njsproj +++ b/MeshCentralServer.njsproj @@ -116,6 +116,7 @@ + diff --git a/meshcentral.js b/meshcentral.js index b6a171d1..17f96280 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -824,8 +824,9 @@ function CreateMeshCentralServer(config, args) { // Create APF server to hook into webserver obj.apfserver = require('./apfserver.js').CreateApfServer(obj, obj.db, obj.args); + // Create MQTT Broker to hook into webserver and mpsserver - obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj,obj.db,obj.args); + if (obj.config.mqtt != null) { obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj, obj.db, obj.args); } // Start the web server and if needed, the redirection web server. obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates); @@ -1828,6 +1829,7 @@ function mainStart() { if (require('os').platform() == 'win32') { modules.push('node-windows'); if (sspi == true) { modules.push('node-sspi'); } } // Add Windows modules if (ldap == true) { modules.push('ldapauth-fork'); } if (config.letsencrypt != null) { modules.push('greenlock'); modules.push('le-store-certbot'); modules.push('le-challenge-fs'); modules.push('le-acme-core'); } // Add Greenlock Modules + if (config.mqtt != null) { modules.push('mqtt'); modules.push('aedes'); } // Add MQTT Modules if (config.settings.mongodb != null) { modules.push('mongodb'); } // Add MongoDB, official driver. else if (config.settings.xmongodb != null) { modules.push('mongojs'); } // Add MongoJS, old driver. if (config.smtp != null) { modules.push('nodemailer'); } // Add SMTP support diff --git a/mpsserver.js b/mpsserver.js index 9c77e3f1..0a6e8335 100644 --- a/mpsserver.js +++ b/mpsserver.js @@ -155,11 +155,11 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { disconnectCommandCount: disconnectCommandCount, socketClosedCount: socketClosedCount, socketErrorCount: socketErrorCount, - maxDomainDevicesReached : maxDomainDevicesReached + maxDomainDevicesReached: maxDomainDevicesReached }; } - // required for TLS piping to MQTT broker + // Required for TLS piping to MQTT broker function SerialTunnel(options) { var obj = new require('stream').Duplex(options); obj.forwardwrite = null; @@ -167,12 +167,13 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { 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; - } + } + // Return's the length of an MQTT packet function getMQTTPacketLength(chunk) { var packet_len = 0; - if (chunk.readUInt8(0)==16) { - if (chunk.readUInt8(1) < 128 ) { + if (chunk.readUInt8(0) == 16) { + if (chunk.readUInt8(1) < 128) { packet_len += chunk.readUInt8(1) + 2; } else { // continuation bit, get real value and do next @@ -188,15 +189,16 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { if (chunk.readUInt8(4) < 128) { packet_len += 1 + chunk.readUInt8(4) * 128 * 128 * 128; } else { - packet_len += 1 + (chunk.readUInt8(4) & 0x7F) * 128* 128 * 128; + packet_len += 1 + (chunk.readUInt8(4) & 0x7F) * 128 * 128 * 128; } - } + } } } } return packet_len; } + // Called when a new TLS/TCP connection is accepted function onConnection(socket) { connectionCount++; if (obj.args.mpstlsoffload) { @@ -220,31 +222,33 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { if (socket.tag.accumulator.length < 3) return; //if (!socket.tag.clientCert.subject) { console.log("MPS Connection, no client cert: " + socket.remoteAddress); socket.write('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nMeshCentral2 MPS server.\r\nNo client certificate given.'); socket.end(); return; } if (socket.tag.accumulator.substring(0, 3) == "GET") { if (args.mpsdebug) { console.log("MPS Connection, HTTP GET detected: " + socket.remoteAddress); } socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\nMeshCentral2 MPS server.
Intel® AMT computers should connect here."); socket.end(); return; } - - var chunk = Buffer.from(socket.tag.accumulator,"binary"); - var packet_len = 0; - if (chunk.readUInt8(0)==16) { - packet_len = getMQTTPacketLength(chunk); - } - - if (chunk.readUInt8(0)==16 && (socket.tag.accumulator.length < packet_len )) return;// minimum MQTT detection - - // check if it is MQTT, need more initial packet to probe - if (chunk.readUInt8(0) == 16 && ((chunk.slice(4, 8).toString() === "MQTT") || (chunk.slice(5, 9).toString() === "MQTT") - || (chunk.slice(6, 10).toString() === "MQTT") || (chunk.slice(7, 11).toString() === "MQTT"))) { - parent.debug("mps", "MQTT connection detected."); - socket.removeAllListeners("data"); - socket.removeAllListeners("close"); - socket.setNoDelay(true); - socket.serialtunnel = SerialTunnel(); - socket.on('data', function(b) { socket.serialtunnel.updateBuffer(Buffer.from(b,'binary'))}); - socket.serialtunnel.forwardwrite = function(b) { socket.write(b,"binary")} - socket.on("close", function() { socket.serialtunnel.emit('end');}); - //pass socket wrapper to mqtt broker - parent.mqttbroker.handle(socket.serialtunnel); - socket.unshift(socket.tag.accumulator); - return; + + // If the MQTT broker is active, look for inbound MQTT connections + if (parent.mqttbroker != null) { + var chunk = Buffer.from(socket.tag.accumulator, "binary"); + var packet_len = 0; + if (chunk.readUInt8(0) == 16) { packet_len = getMQTTPacketLength(chunk); } + if (chunk.readUInt8(0) == 16 && (socket.tag.accumulator.length < packet_len)) return; // Minimum MQTT detection + + // check if it is MQTT, need more initial packet to probe + if (chunk.readUInt8(0) == 16 && ((chunk.slice(4, 8).toString() === "MQTT") || (chunk.slice(5, 9).toString() === "MQTT") + || (chunk.slice(6, 10).toString() === "MQTT") || (chunk.slice(7, 11).toString() === "MQTT"))) { + parent.debug("mps", "MQTT connection detected."); + socket.removeAllListeners("data"); + socket.removeAllListeners("close"); + socket.setNoDelay(true); + socket.serialtunnel = SerialTunnel(); + socket.on('data', function (b) { socket.serialtunnel.updateBuffer(Buffer.from(b, 'binary')) }); + socket.serialtunnel.forwardwrite = function (b) { socket.write(b, "binary") } + socket.on("close", function () { socket.serialtunnel.emit('end'); }); + + // Pass socket wrapper to the MQTT broker + parent.mqttbroker.handle(socket.serialtunnel); + socket.unshift(socket.tag.accumulator); + return; + } } + socket.tag.first = false; // Setup this node with certificate authentication diff --git a/mqttbroker.js b/mqttbroker.js index 13f9de93..7836d0c3 100644 --- a/mqttbroker.js +++ b/mqttbroker.js @@ -6,49 +6,46 @@ * @version v0.0.1 */ - module.exports.CreateMQTTBroker = function (parent, db, args) { - - // internal objects container + + // Internal objects container var obj = {} obj.parent = parent; obj.db = db; obj.args = args; - obj.aedes = require("aedes")(); - - + // argument parsing -- tbd - + // event handling and filtering // authentication filter obj.aedes.authenticate = function (client, username, password, callback) { - // accept all user // TODO: add authentication handler - obj.parent.debug("mqtt","Authentication with "+username+":"+password); + obj.parent.debug("mqtt", "Authentication with " + username + ":" + password); callback(null, true); } - + // check if a client can publish a packet obj.aedes.authorizePublish = function (client, packet, callback) { - //TODO: add authorized publish control - obj.parent.debug("mqtt","AuthorizePublish"); + // TODO: add authorized publish control + obj.parent.debug("mqtt", "AuthorizePublish"); callback(null); } - - // check if a client can publish a packet + + // Check if a client can publish a packet obj.aedes.authorizeSubscribe = function (client, sub, callback) { - //TODO: add subscription control here - obj.parent.debug("mqtt","AuthorizeSubscribe"); + // TODO: add subscription control here + obj.parent.debug("mqtt", "AuthorizeSubscribe"); callback(null, sub); } - - // check if a client can publish a packet + + // Check if a client can publish a packet obj.aedes.authorizeForward = function (client, packet) { - //TODO: add forwarding control - obj.parent.debug("mqtt","AuthorizeForward"); + // TODO: add forwarding control + obj.parent.debug("mqtt", "AuthorizeForward"); return packet; } - obj.handle = obj.aedes.handle; - return obj; + + obj.handle = obj.aedes.handle; + return obj; } diff --git a/package.json b/package.json index 8ef2b428..dd3173b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "0.4.1-r", + "version": "0.4.1-s", "keywords": [ "Remote Management", "Intel AMT", @@ -27,7 +27,6 @@ "sample-config.json" ], "dependencies": { - "aedes": "^0.39.0", "archiver": "^3.0.0", "body-parser": "^1.19.0", "cbor": "4.1.5", @@ -40,11 +39,9 @@ "ipcheck": "^0.1.0", "meshcentral": "*", "minimist": "^1.2.0", - "mqtt": "^3.0.0", "multiparty": "^4.2.1", "nedb": "^1.8.0", "node-forge": "^0.8.4", - "otplib": "^11.0.1", "ws": "^6.2.1", "xmldom": "^0.1.27", "yauzl": "^2.10.0" diff --git a/views/default-mobile-min.handlebars b/views/default-mobile-min.handlebars index 4d32392f..ad9bf7e9 100644 --- a/views/default-mobile-min.handlebars +++ b/views/default-mobile-min.handlebars @@ -1 +1 @@ - {{{title}}}
{{{title}}}
{{{title2}}}
\ No newline at end of file + {{{title}}}
{{{title}}}
{{{title2}}}
\ No newline at end of file diff --git a/webserver.js b/webserver.js index 3aa50ffb..06a58dce 100644 --- a/webserver.js +++ b/webserver.js @@ -42,7 +42,7 @@ if (!String.prototype.endsWith) { String.prototype.endsWith = function (searchSt // Construct a HTTP server object module.exports.CreateWebServer = function (parent, db, args, certificates) { - var obj = {}, i = 0; + var obj = {}, i = 0; // Modules obj.fs = require('fs'); @@ -208,7 +208,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Fetch all meshes from the database, keep this in memory obj.db.GetAllType('mesh', function (err, docs) { obj.common.unEscapeAllLinksFieldName(docs); - for (var i in docs) {obj.meshes[docs[i]._id] = docs[i]; } // Get all meshes, including deleted ones. + for (var i in docs) { obj.meshes[docs[i]._id] = docs[i]; } // Get all meshes, including deleted ones. // We loaded the users and mesh state, start the server serverStart(); @@ -451,7 +451,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (ip) { for (var i = 0; i < ipList.length; i++) { if (require('ipcheck').match(ip, ipList[i])) { if (closeIfThis === true) { try { req.close(); } catch (e) { } } return true; } } } if (closeIfThis === false) { try { req.close(); } catch (e) { } } - } + } } catch (e) { console.log(e); } // Should never happen return false; } @@ -646,7 +646,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result) { if (result == false) { var randomWaitTime = 0; - + // 2-step auth is required, but the token is not present or not valid. if ((req.body.token != null) || (req.body.hwtoken != null)) { randomWaitTime = 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095); // This is a fail, wait a random time. 2 to 6 seconds. @@ -1251,7 +1251,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } // Check a user's password - obj.checkUserPassword = function(domain, user, password, func) { + obj.checkUserPassword = function (domain, user, password, func) { // Check the old password if (user.passtype != null) { // IIS default clear or weak password hashing (SHA-1) @@ -1259,7 +1259,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (err) { parent.debug('web', 'checkUserPassword: SHA-1 fail.'); return func(false); } if (hash == user.hash) { if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: SHA-1 locked.'); return func(false); } // Account is locked - parent.debug('web', 'checkUserPassword: SHA-1 ok.'); + parent.debug('web', 'checkUserPassword: SHA-1 ok.'); return func(true); // Allow password change } func(false); @@ -1270,7 +1270,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (err) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 fail.'); return func(false); } if (hash == user.hash) { if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 locked.'); return func(false); } // Account is locked - parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 ok.'); + parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 ok.'); return func(true); // Allow password change } func(false); @@ -1728,7 +1728,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } // Return the CIRA configuration script - obj.getCiraCleanupScript = function(func) { + obj.getCiraCleanupScript = function (func) { obj.fs.readFile(obj.parent.path.join(obj.parent.webPublicPath, 'scripts/cira_cleanup.mescript'), 'utf8', function (err, data) { if (err != null) { func(null); return; } func(Buffer.from(data)); @@ -1904,7 +1904,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } // Handle domain redirection - obj.handleDomainRedirect = function(req, res) { + obj.handleDomainRedirect = function (req, res) { const domain = checkUserIpAddress(req, res); if ((domain == null) || (domain.redirects == null)) { res.sendStatus(404); return; } var urlArgs = '', urlName = null, splitUrl = req.originalUrl.split("?"); @@ -2116,7 +2116,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { function handleRelayWebSocket(ws, req, domain, user, cookie) { if (!(req.query.host)) { console.log('ERR: No host target specified'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket parent.debug('web', 'Websocket relay connected from ' + user.name + ' for ' + req.query.host + '.'); - + try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive // Fetch information about the target @@ -2308,7 +2308,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { data = Buffer.from(data, 'binary'); recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (e) { } }); // TODO: Add TLS support } - } + } }; ws.forwardclient.onSendOk = function (ciraconn) { @@ -2468,7 +2468,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { const mesh = obj.meshes[ws.meshid]; if (mesh == null) { delete ws.meshid; ws.send(JSON.stringify({ errorText: 'Invalid device group' })); ws.close(); return; } if (mesh.mtype != 1) { ws.send(JSON.stringify({ errorText: 'Invalid device group type' })); ws.close(); return; } - + // Fetch the remote IP:Port for logging ws.remoteaddr = cleanRemoteAddr(req.ip); ws.remoteaddrport = ws.remoteaddr + ':' + ws._socket.remotePort; @@ -2526,7 +2526,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if ((activationMode == 4) && (cmd.modes.indexOf(2) == -1)) { ws.send(JSON.stringify({ messageText: 'ACM not allowed on this machine, activating in CCM instead...' })); activationMode = 2; } // We want to do ACM, but mode is not allowed. Change to CCM. // If we want to do CCM, but mode is not allowed. Error out. - if ((activationMode == 2) && (cmd.modes.indexOf(1) == -1)) { ws.send(JSON.stringify({ errorText: 'CCM is not an allowed activation mode' })); ws.close(); return; } + if ((activationMode == 2) && (cmd.modes.indexOf(1) == -1)) { ws.send(JSON.stringify({ errorText: 'CCM is not an allowed activation mode' })); ws.close(); return; } // Get the Intel AMT admin password, randomize if needed. var amtpassword = ((mesh.amt.password == '') ? getRandomAmtPassword() : mesh.amt.password); @@ -2992,7 +2992,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { }; // Get the web server hostname. This may change if using a domain with a DNS name. - obj.getWebServerName = function(domain) { + obj.getWebServerName = function (domain) { if (domain.dns != null) return domain.dns; return obj.certificates.CommonName; } @@ -3152,7 +3152,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Get the list of power events and send them res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'text/csv', 'Content-Disposition': 'attachment; filename="powerevents.csv"' }); obj.db.getPowerTimeline(node._id, function (err, docs) { - var xevents = [ 'Time, State, Previous State' ], prevState = 0; + var xevents = ['Time, State, Previous State'], prevState = 0; for (var i in docs) { if (docs[i].power != prevState) { prevState = docs[i].power; @@ -3220,7 +3220,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { "Referrer-Policy": "no-referrer", "X-XSS-Protection": "1; mode=block", "X-Content-Type-Options": "nosniff", - "Content-Security-Policy": "default-src 'none'; script-src 'self' 'unsafe-inline'; connect-src 'self' ws" + ((args.notls !== true)?'s':'') + "://" + req.headers.host + "; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-src 'self'; media-src 'self'" + "Content-Security-Policy": "default-src 'none'; script-src 'self' 'unsafe-inline'; connect-src 'self' ws" + ((args.notls !== true) ? 's' : '') + "://" + req.headers.host + "; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-src 'self'; media-src 'self'" }); } @@ -3277,7 +3277,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { obj.app.post(url + 'uploadmeshcorefile.ashx', handleUploadMeshCoreFile); obj.app.get(url + 'userfiles/*', handleDownloadUserFiles); obj.app.ws(url + 'echo.ashx', handleEchoWebSocket); - obj.app.ws(url+'apf.ashx', function (ws, req) { obj.parent.apfserver.onConnection(ws);}) + obj.app.ws(url + 'apf.ashx', function (ws, req) { obj.parent.apfserver.onConnection(ws); }) obj.app.ws(url + 'meshrelay.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie) { obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); }); }); obj.app.get(url + 'webrelay.ashx', function (req, res) { res.send('Websocket connection expected'); }); obj.app.get(url + 'health.ashx', function (req, res) { res.send('ok'); }); // TODO: Perform more server checking. @@ -3323,15 +3323,16 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (e) { console.log(e); } }); - // MQTT broker over websocket - obj.app.ws(url+'mqtt.ashx', function (ws, req) { - var ser = SerialTunnel(); - ws.on('message', function(b) { ser.updateBuffer(Buffer.from(b,'binary'))}); - ser.forwardwrite = function(b) { ws.send(b,"binary")} - ws.on("close", function() { ser.emit('end');}); - //pass socket wrapper to mqtt broker - obj.parent.mqttbroker.handle(ser); - }) + // Setup MQTT broker over websocket + if (obj.parent.mqttbroker != null) { + obj.app.ws(url + 'mqtt.ashx', function (ws, req) { + var ser = SerialTunnel(); + ws.on('message', function (b) { ser.updateBuffer(Buffer.from(b, 'binary')) }); + ser.forwardwrite = function (b) { ws.send(b, "binary") } + ws.on("close", function () { ser.emit('end'); }); + obj.parent.mqttbroker.handle(ser); // Pass socket wrapper to MQTT broker + }); + } // Memory Tracking if (typeof obj.args.memorytracking == 'number') { @@ -3384,7 +3385,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { res.status(404).render(getRenderPage('error404', req), { title: domain.title, title2: domain.title2 }); }); } - + // Start server on a free port CheckListenPort(obj.args.port, StartWebServer); }