diff --git a/agents/agentrecoverycore.js b/agents/agentrecoverycore.js index 203b16e981..603083c026 100644 --- a/agents/agentrecoverycore.js +++ b/agents/agentrecoverycore.js @@ -154,6 +154,8 @@ require('MeshAgent').AddCommandHandler(function (data) if (xurl != null) { var woptions = http.parseUri(xurl); woptions.rejectUnauthorized = 0; + // Add custom relay headers if provided by the server + if (data.relayHeaders != null) { woptions.headers = data.relayHeaders; } //sendConsoleText(JSON.stringify(woptions)); var tunnel = http.request(woptions); tunnel.on('upgrade', function (response, s, head) diff --git a/agents/meshcore.js b/agents/meshcore.js index e0c31907b4..d8af32dd59 100644 --- a/agents/meshcore.js +++ b/agents/meshcore.js @@ -1150,6 +1150,9 @@ function handleServerCommand(data) { woptions.checkServerIdentity = tunnel_checkServerIdentity; woptions.checkServerIdentity.servertlshash = data.servertlshash; + // Add custom relay headers if provided by the server + if (data.relayHeaders != null) { woptions.headers = data.relayHeaders; } + //sendConsoleText(JSON.stringify(woptions)); //sendConsoleText('TUNNEL: ' + JSON.stringify(data, null, 2)); diff --git a/agents/recoverycore.js b/agents/recoverycore.js index 5d9f13bc29..9b3eb91a56 100644 --- a/agents/recoverycore.js +++ b/agents/recoverycore.js @@ -961,6 +961,8 @@ require('MeshAgent').AddCommandHandler(function (data) } woptions.checkServerIdentity.servertlshash = data.servertlshash; + // Add custom relay headers if provided by the server + if (data.relayHeaders != null) { woptions.headers = data.relayHeaders; } //sendConsoleText(JSON.stringify(woptions)); var tunnel = http.request(woptions); diff --git a/meshagent.js b/meshagent.js index 5ea181d97f..868bf3ec72 100644 --- a/meshagent.js +++ b/meshagent.js @@ -2152,7 +2152,9 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { obj.RequestCoreDump = function (agenthashhex, corehashhex) { if (agenthashhex.length > 16) { agenthashhex = agenthashhex.substring(0, 16); } const cookie = parent.parent.encodeCookie({ a: 'aft', b: 'coredump', c: obj.agentInfo.agentId + '-' + agenthashhex + '-' + obj.nodeid + '.dmp' }, parent.parent.loginCookieEncryptionKey); - obj.send('{"action":"msg","type":"tunnel","value":"*/' + (((domain.dns == null) && (domain.id != '')) ? (domain.id + '/') : '') + 'agenttransfer.ashx?c=' + cookie + '","rights":"4294967295"}'); + const cmd = { action: 'msg', type: 'tunnel', value: '*/' + (((domain.dns == null) && (domain.id != '')) ? (domain.id + '/') : '') + 'agenttransfer.ashx?c=' + cookie, rights: '4294967295' }; + if (typeof domain.agentrelayheaders == 'object') { cmd.relayHeaders = domain.agentrelayheaders; } + obj.send(JSON.stringify(cmd)); } // Return true if we need to ignore the agent hash check diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index 80ecc43ac1..b456299b54 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -2807,6 +2807,13 @@ }, "description": "This is used to add custom HTTP headers to all responses. For example setting \"httpHeaders\": { \"X-Frame-Options\":\"DENY\" } will add the X-Frame-Options header to all responses." }, + "agentRelayHeaders": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers added to agent relay/tunnel WebSocket connections. Useful when MeshCentral is behind a gateway that requires specific headers on agent relay paths." + }, "agentConfig": { "type": "array", "uniqueItems": true, diff --git a/meshcentral.js b/meshcentral.js index 5665b72734..e5d1047331 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -838,7 +838,7 @@ function CreateMeshCentralServer(config, args) { obj.args = args = config2.settings; // Lower case all keys in the config file - obj.common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']); + obj.common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'agentrelayheaders', 'telegram/proxy']); // Grad some of the values from the original config.json file if present. if ((config.settings.vault != null) && (config2.settings != null)) { config2.settings.vault = config.settings.vault; } @@ -1375,7 +1375,7 @@ function CreateMeshCentralServer(config, args) { for (i in args) { config2.settings[i] = args[i]; } // Lower case all keys in the config file - common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']); + common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'agentrelayheaders', 'telegram/proxy']); // Grab some of the values from the original config.json file if present. config2['mysql'] = config['mysql']; @@ -4062,7 +4062,7 @@ function getConfig(createSampleConfig) { // Lower case all keys in the config file try { - require('./common.js').objKeysToLower(config, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']); + require('./common.js').objKeysToLower(config, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'agentrelayheaders', 'telegram/proxy']); } catch (ex) { console.log('CRITICAL ERROR: Unable to access the file \"./common.js\".\r\nCheck folder & file permissions.'); process.exit(); diff --git a/meshdesktopmultiplex.js b/meshdesktopmultiplex.js index 9b6d1e1911..9bcd52c4ae 100644 --- a/meshdesktopmultiplex.js +++ b/meshdesktopmultiplex.js @@ -1291,6 +1291,7 @@ function CreateMeshRelayEx2(parent, ws, req, domain, user, cookie) { const rcookie = parent.parent.encodeCookie(rcookieData, parent.parent.loginCookieEncryptionKey); if (obj.id == undefined) { obj.id = ('' + Math.random()).substring(2); } // If there is no connection id, generate one. const command = { nodeid: node._id, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?id=' + obj.id + '&rauth=' + rcookie, tcpport: cookie.tcpport, tcpaddr: cookie.tcpaddr }; + if (typeof domain.agentrelayheaders == 'object') { command.relayHeaders = domain.agentrelayheaders; } parent.parent.debug('relay', 'Relay: Sending agent tunnel command: ' + JSON.stringify(command)); if (obj.sendAgentMessage(command, user ? user._id : null, cookie.domainid) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + obj.req.clientIp + ')'); } performRelay(0); @@ -1314,10 +1315,12 @@ function CreateMeshRelayEx2(parent, ws, req, domain, user, cookie) { if (obj.req.query.tcpport != null) { const command = { nodeid: node._id, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?id=' + obj.id + '&rauth=' + rcookie, tcpport: obj.req.query.tcpport, tcpaddr: ((obj.req.query.tcpaddr == null) ? '127.0.0.1' : obj.req.query.tcpaddr) }; + if (typeof domain.agentrelayheaders == 'object') { command.relayHeaders = domain.agentrelayheaders; } parent.parent.debug('relay', 'Relay: Sending agent TCP tunnel command: ' + JSON.stringify(command)); if (obj.sendAgentMessage(command, user ? user._id : null, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + obj.req.clientIp + ')'); } } else if (obj.req.query.udpport != null) { const command = { nodeid: node._id, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?id=' + obj.id + '&rauth=' + rcookie, udpport: obj.req.query.udpport, udpaddr: ((obj.req.query.udpaddr == null) ? '127.0.0.1' : obj.req.query.udpaddr) }; + if (typeof domain.agentrelayheaders == 'object') { command.relayHeaders = domain.agentrelayheaders; } parent.parent.debug('relay', 'Relay: Sending agent UDP tunnel command: ' + JSON.stringify(command)); if (obj.sendAgentMessage(command, user ? user._id : null, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + obj.req.clientIp + ')'); } } @@ -1340,6 +1343,7 @@ function CreateMeshRelayEx2(parent, ws, req, domain, user, cookie) { if (user != null) { rcookieData.ruserid = user._id; } else if (obj.nouser === true) { rcookieData.nouser = 1; } const rcookie = parent.parent.encodeCookie(rcookieData, parent.parent.loginCookieEncryptionKey); const command = { nodeid: node._id, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?p=2&id=' + obj.id + '&rauth=' + rcookie + '&nodeid=' + node._id, soptions: {}, usage: 2, rights: cookie.r, guestuserid: user._id, guestname: cookie.gn, consent: cookie.cf, remoteaddr: cleanRemoteAddr(obj.req.clientIp) }; + if (typeof domain.agentrelayheaders == 'object') { command.relayHeaders = domain.agentrelayheaders; } if (typeof domain.consentmessages == 'object') { if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; } if (typeof domain.consentmessages.desktop == 'string') { command.soptions.consentMsgDesktop = domain.consentmessages.desktop; } diff --git a/meshdevicefile.js b/meshdevicefile.js index 695a0f65b9..61a1ff40c4 100644 --- a/meshdevicefile.js +++ b/meshdevicefile.js @@ -287,6 +287,7 @@ module.exports.CreateMeshDeviceFile = function (parent, ws, res, req, domain, us // Send connection request to agent const rcookie = parent.parent.encodeCookie({ ruserid: user._id }, parent.parent.loginCookieEncryptionKey); const command = { nodeid: nodeid, action: 'msg', type: 'tunnel', userid: user._id, value: '*/devicefile.ashx?id=' + obj.id + '&rauth=' + rcookie, soptions: {} }; + if (typeof domain.agentrelayheaders == 'object') { command.relayHeaders = domain.agentrelayheaders; } parent.parent.debug('relay', 'FileRelay: Sending agent tunnel command: ' + JSON.stringify(command)); if (obj.sendAgentMessage(command, user, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'FileRelay: Unable to contact this agent (' + obj.req.clientIp + ')'); } } diff --git a/meshrelay.js b/meshrelay.js index 0f4fcbfa95..8fc510df1b 100644 --- a/meshrelay.js +++ b/meshrelay.js @@ -894,6 +894,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { const rcookie = parent.parent.encodeCookie(rcookieData, parent.parent.loginCookieEncryptionKey); if (obj.id == null) { obj.id = parent.crypto.randomBytes(9).toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); } // If there is no connection id, generate one. const command = { nodeid: cookie.nodeid, action: 'msg', type: 'tunnel', value: '*/' + xdomain + 'meshrelay.ashx?' + (obj.req.query.p != null ? ('p=' + obj.req.query.p + '&') : '') + 'id=' + obj.id + '&rauth=' + rcookie, tcpport: cookie.tcpport, tcpaddr: cookie.tcpaddr, soptions: {} }; + if (typeof domain.agentrelayheaders == 'object') { command.relayHeaders = domain.agentrelayheaders; } if (user) { command.userid = user._id; } if (typeof domain.consentmessages == 'object') { if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; } @@ -934,6 +935,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { const rcookie = parent.parent.encodeCookie({ ruserid: user._id }, parent.parent.loginCookieEncryptionKey); if (obj.req.query.tcpport != null) { const command = { nodeid: obj.req.query.nodeid, action: 'msg', type: 'tunnel', userid: user._id, value: '*/' + xdomain + 'meshrelay.ashx?' + (obj.req.query.p != null ? ('p=' + obj.req.query.p + '&') : '') + 'id=' + obj.id + '&rauth=' + rcookie, tcpport: obj.req.query.tcpport, tcpaddr: ((obj.req.query.tcpaddr == null) ? '127.0.0.1' : obj.req.query.tcpaddr), soptions: {} }; + if (typeof domain.agentrelayheaders == 'object') { command.relayHeaders = domain.agentrelayheaders; } if (typeof domain.consentmessages == 'object') { if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; } if (typeof domain.consentmessages.desktop == 'string') { command.soptions.consentMsgDesktop = domain.consentmessages.desktop; } @@ -953,7 +955,9 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { parent.parent.debug('relay', 'Relay: Sending agent TCP tunnel command: ' + JSON.stringify(command)); if (obj.sendAgentMessage(command, user._id, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + obj.req.clientIp + ')'); } } else if (obj.req.query.udpport != null) { - const command = { nodeid: obj.req.query.nodeid, action: 'msg', type: 'tunnel', userid: user._id, value: '*/' + xdomain + 'meshrelay.ashx?' + (obj.req.query.p != null ? ('p=' + obj.req.query.p + '&') : '') + 'id=' + obj.id + '&rauth=' + rcookie, udpport: obj.req.query.udpport, udpaddr: ((obj.req.query.udpaddr == null) ? '127.0.0.1' : obj.req.query.udpaddr), soptions: {} }; if (typeof domain.consentmessages == 'object') { + const command = { nodeid: obj.req.query.nodeid, action: 'msg', type: 'tunnel', userid: user._id, value: '*/' + xdomain + 'meshrelay.ashx?' + (obj.req.query.p != null ? ('p=' + obj.req.query.p + '&') : '') + 'id=' + obj.id + '&rauth=' + rcookie, udpport: obj.req.query.udpport, udpaddr: ((obj.req.query.udpaddr == null) ? '127.0.0.1' : obj.req.query.udpaddr), soptions: {} }; + if (typeof domain.agentrelayheaders == 'object') { command.relayHeaders = domain.agentrelayheaders; } + if (typeof domain.consentmessages == 'object') { if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; } if (typeof domain.consentmessages.desktop == 'string') { command.soptions.consentMsgDesktop = domain.consentmessages.desktop; } if (typeof domain.consentmessages.terminal == 'string') { command.soptions.consentMsgTerminal = domain.consentmessages.terminal; } @@ -994,6 +998,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { if (user != null) { rcookieData.ruserid = user._id; } else if (obj.nouser === true) { rcookieData.nouser = 1; } const rcookie = parent.parent.encodeCookie(rcookieData, parent.parent.loginCookieEncryptionKey); const command = { nodeid: node._id, action: 'msg', type: 'tunnel', value: '*/' + xdomain + 'meshrelay.ashx?p=' + obj.req.query.p + '&id=' + obj.id + '&rauth=' + rcookie + '&nodeid=' + node._id, soptions: {}, rights: cookie.r, guestuserid: user._id, guestname: cookie.gn, consent: cookie.cf, remoteaddr: cleanRemoteAddr(obj.req.clientIp) }; + if (typeof domain.agentrelayheaders == 'object') { command.relayHeaders = domain.agentrelayheaders; } obj.guestname = cookie.gn; // Limit what this relay connection can do diff --git a/meshuser.js b/meshuser.js index 46f78f834d..52bbc88248 100644 --- a/meshuser.js +++ b/meshuser.js @@ -1023,6 +1023,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Add tunnel pre-message deflate if (typeof parent.parent.config.settings.agentwscompression == 'boolean') { command.perMessageDeflate = parent.parent.config.settings.agentwscompression; } + + // Add agent relay headers + if (typeof domain.agentrelayheaders == 'object') { command.relayHeaders = domain.agentrelayheaders; } } // If a response is needed, set a callback function