From 9b9db35e3ce5af06bc145ea7d891b570ab04c054 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 19 Feb 2020 12:18:47 +0200 Subject: [PATCH 1/5] Implement basic STS reconnection --- src/client.js | 1 + src/plugins/irc-events/cap.js | 65 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/plugins/irc-events/cap.js diff --git a/src/client.js b/src/client.js index c126aba7..6cb313ff 100644 --- a/src/client.js +++ b/src/client.js @@ -22,6 +22,7 @@ module.exports = Client; const events = [ "away", + "cap", "connection", "unhandled", "ctcp", diff --git a/src/plugins/irc-events/cap.js b/src/plugins/irc-events/cap.js new file mode 100644 index 00000000..b477b1ab --- /dev/null +++ b/src/plugins/irc-events/cap.js @@ -0,0 +1,65 @@ +"use strict"; + +const Msg = require("../../models/msg"); + +module.exports = function(irc, network) { + const client = this; + + irc.on("cap ls", (data) => { + handleSTS(data); + }); + + irc.on("cap new", (data) => { + handleSTS(data); + }); + + function handleSTS(data) { + if (!Object.prototype.hasOwnProperty.call(data.capabilities, "sts")) { + return; + } + + const isSecure = irc.connection.transport.socket.encrypted; + const values = {}; + + data.capabilities.sts.split(",").map((value) => { + value = value.split("=", 2); + values[value[0]] = value[1]; + }); + + if (isSecure) { + // TODO: store and update duration + } else { + const port = parseInt(values.port, 10); + + if (isNaN(port)) { + return; + } + + network.channels[0].pushMessage( + client, + new Msg({ + text: `Server sent a strict transport security policy, reconnecting to port ${port}…`, + }), + true + ); + + // Forcefully end the connection + irc.connection.end(); + + // Update the port + network.port = port; + irc.options.port = port; + + // Enable TLS + network.tls = true; + network.rejectUnauthorized = true; + irc.options.tls = true; + irc.options.rejectUnauthorized = true; + + // Start a new connection + irc.connect(); + + client.save(); + } + } +}; From d9985e731858726b41a96e9619730410bb20121e Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 19 Feb 2020 13:20:22 +0200 Subject: [PATCH 2/5] Enforce STS policies --- src/models/network.js | 20 ++++++++- src/plugins/irc-events/cap.js | 11 ++++- src/plugins/sts.js | 82 +++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/plugins/sts.js diff --git a/src/models/network.js b/src/models/network.js index 41cab75b..c6004090 100644 --- a/src/models/network.js +++ b/src/models/network.js @@ -6,6 +6,7 @@ const IrcFramework = require("irc-framework"); const Chan = require("./chan"); const Msg = require("./msg"); const Helper = require("../helper"); +const STSPolicies = require("../plugins/sts"); module.exports = Network; @@ -78,7 +79,7 @@ Network.prototype.validate = function(client) { this.username = cleanString(this.username) || "thelounge"; this.realname = cleanString(this.realname) || "The Lounge User"; this.password = cleanString(this.password); - this.host = cleanString(this.host); + this.host = cleanString(this.host).toLowerCase(); this.name = cleanString(this.name); if (!this.port) { @@ -124,6 +125,23 @@ Network.prototype.validate = function(client) { return false; } + const stsPolicy = STSPolicies.get(this.host); + + if (stsPolicy && !this.tls) { + this.channels[0].pushMessage( + client, + new Msg({ + type: Msg.Type.ERROR, + text: `${this.host} has an active strict transport security policy, will connect to port ${stsPolicy.port} over a secure connection.`, + }), + true + ); + + this.port = stsPolicy.port; + this.tls = true; + this.rejectUnauthorized = true; + } + return true; }; diff --git a/src/plugins/irc-events/cap.js b/src/plugins/irc-events/cap.js index b477b1ab..3b1d7c8b 100644 --- a/src/plugins/irc-events/cap.js +++ b/src/plugins/irc-events/cap.js @@ -1,6 +1,7 @@ "use strict"; const Msg = require("../../models/msg"); +const STSPolicies = require("../sts"); module.exports = function(irc, network) { const client = this; @@ -27,7 +28,13 @@ module.exports = function(irc, network) { }); if (isSecure) { - // TODO: store and update duration + const duration = parseInt(values.duration, 10); + + if (isNaN(duration)) { + return; + } + + STSPolicies.update(network.host, network.port, duration); } else { const port = parseInt(values.port, 10); @@ -38,7 +45,7 @@ module.exports = function(irc, network) { network.channels[0].pushMessage( client, new Msg({ - text: `Server sent a strict transport security policy, reconnecting to port ${port}…`, + text: `Server sent a strict transport security policy, reconnecting to ${network.host}:${port}…`, }), true ); diff --git a/src/plugins/sts.js b/src/plugins/sts.js new file mode 100644 index 00000000..6dadcc59 --- /dev/null +++ b/src/plugins/sts.js @@ -0,0 +1,82 @@ +"use strict"; + +const _ = require("lodash"); +const fs = require("fs"); +const path = require("path"); +const log = require("../log"); +const Helper = require("../helper"); + +class STSPolicies { + constructor() { + this.stsFile = path.join(Helper.getHomePath(), "sts-policies.json"); + this.policies = new Map(); + this.refresh = _.debounce(this.saveFile, 10000, {maxWait: 60000}); + + if (!fs.existsSync(this.stsFile)) { + return; + } + + const storedPolicies = JSON.parse(fs.readFileSync(this.stsFile, "utf-8")); + const now = Date.now(); + + storedPolicies.forEach((value) => { + if (value.expires > now) { + this.policies.set(value.host, { + port: value.port, + expires: value.expires, + }); + } + }); + } + + get(host) { + const policy = this.policies.get(host); + + if (typeof policy === "undefined") { + return null; + } + + if (policy.expires <= Date.now()) { + this.policies.delete(host); + this.refresh(); + return null; + } + + return policy; + } + + update(host, port, duration) { + if (duration > 0) { + this.policies.set(host, { + port: port, + expires: Date.now() + duration * 1000, + }); + } else { + this.policies.delete(host); + } + + this.refresh(); + } + + saveFile() { + const policiesToStore = []; + + this.policies.forEach((value, key) => { + policiesToStore.push({ + host: key, + port: value.port, + expires: value.expires, + }); + }); + + const file = JSON.stringify(policiesToStore, null, "\t"); + + fs.writeFile(this.stsFile, file, {flag: "w+"}, (err) => { + if (err) { + log.error("Failed to update STS policies file!", err); + } + }); + } +} + +module.exports = new STSPolicies(); From 568427ca98f22e2c89178d4a9d85b31bc93d5c86 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 19 Feb 2020 13:26:43 +0200 Subject: [PATCH 3/5] Disable changing TLS if STS is enforced --- client/components/NetworkForm.vue | 10 +++++++++- src/models/network.js | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/client/components/NetworkForm.vue b/client/components/NetworkForm.vue index 2c893314..1f295e0c 100644 --- a/client/components/NetworkForm.vue +++ b/client/components/NetworkForm.vue @@ -65,10 +65,18 @@ type="checkbox" name="tls" :checked="defaults.tls ? true : false" - :disabled="config.lockNetwork ? true : false" + :disabled=" + config.lockNetwork || defaults.hasSTSPolicy ? true : false + " @change="onSecureChanged" /> Use secure connection (TLS) + 🔒 STS