Repository: thelounge/thelounge
Pull request #4477 (2022-02-13 13:29:25)

Preserve client certificate when TLS is indirectly enabled by a STS policy
Closes GH-4152

Only the last commit directly addresses the issue; the first four commits are only loosely related test refactoring (needed to test the fix) and additions (which test the basics this fix's tests rely on)


Patch:
diff --git a/src/models/network.js b/src/models/network.js
index 17aaff60..bfd14c06 100644
--- a/src/models/network.js
+++ b/src/models/network.js
@@ -122,10 +122,6 @@ Network.prototype.validate = function (client) {
 		this.sasl = "";
 	}
 
-	if (!this.tls) {
-		ClientCertificate.remove(this.uuid);
-	}
-
 	if (Helper.config.lockNetwork) {
 		// This check is needed to prevent invalid user configurations
 		if (
diff --git a/src/plugins/irc-events/link.js b/src/plugins/irc-events/link.js
index 00236def..0d7d6a45 100644
--- a/src/plugins/irc-events/link.js
+++ b/src/plugins/irc-events/link.js
@@ -251,8 +251,6 @@ function parse(msg, chan, preview, res, client) {
 		case "audio/x-mpeg":
 		case "audio/x-mpeg-3":
 		case "audio/flac":
-		case "audio/x-flac":
-		case "audio/mp4":
 		case "audio/x-m4a":
 			if (!preview.link.startsWith("https://")) {
 				break;
diff --git a/src/plugins/uploader.js b/src/plugins/uploader.js
index 44a1629a..426f717f 100644
--- a/src/plugins/uploader.js
+++ b/src/plugins/uploader.js
@@ -117,8 +117,6 @@ class Uploader {
 			detectedMimeType = "audio/wav";
 		} else if (detectedMimeType === "audio/x-flac") {
 			detectedMimeType = "audio/flac";
-		} else if (detectedMimeType === "audio/x-m4a") {
-			detectedMimeType = "audio/mp4";
 		}
 
 		res.setHeader("Content-Disposition", disposition);


Test patch:
diff --git a/test/fixtures/env.js b/test/fixtures/env.js
index 67f22edb..57adf7ad 100644
--- a/test/fixtures/env.js
+++ b/test/fixtures/env.js
@@ -1,4 +1,13 @@
 "use strict";
 
+const fs = require("fs");
+
 const home = require("path").join(__dirname, ".thelounge");
 require("../../src/helper").setHome(home);
+
+const STSPolicies = require("../../src/plugins/sts"); // Must be imported *after* setHome
+
+exports.mochaGlobalTeardown = async function () {
+	STSPolicies.refresh.cancel(); // Cancel debounced function, so it does not write later
+	fs.unlinkSync(STSPolicies.stsFile);
+};
diff --git a/test/models/network.js b/test/models/network.js
index fe8699d9..1b5a1ed1 100644
--- a/test/models/network.js
+++ b/test/models/network.js
@@ -6,8 +6,69 @@ const Msg = require("../../src/models/msg");
 const User = require("../../src/models/user");
 const Network = require("../../src/models/network");
 const Helper = require("../../src/helper");
+const STSPolicies = require("../../src/plugins/sts");
+const ClientCertificate = require("../../src/plugins/clientCertificate");
 
 describe("Network", function () {
+	describe("Network(attr)", function () {
+		it("should generate uuid (v4) for each network", function () {
+			const network1 = new Network();
+			const network2 = new Network();
+
+			expect(network1.uuid).to.have.lengthOf(36);
+			expect(network2.uuid).to.have.lengthOf(36);
+			expect(network1.uuid).to.not.equal(network2.uuid);
+		});
+
+		it("lobby should be at the top", function () {
+			const network = new Network({
+				name: "Super Nice Network",
+				channels: [
+					new Chan({name: "AAAA!", type: Chan.Type.QUERY}),
+					new Chan({name: "#thelounge"}),
+					new Chan({name: "&foobar"}),
+				],
+			});
+			network.channels.push(new Chan({name: "#swag"}));
+
+			expect(network.channels[0].name).to.equal("Super Nice Network");
+			expect(network.channels[0].type).to.equal(Chan.Type.LOBBY);
+		});
+
+		it("should maintain channel reference", function () {
+			const chan = new Chan({
+				name: "#506-bug-fix",
+				messages: [
+					new Msg({
+						text: "message in constructor",
+					}),
+				],
+			});
+
+			const network = new Network({
+				name: "networkName",
+				channels: [chan],
+			});
+
+			chan.messages.push(
+				new Msg({
+					text: "message in original instance",
+				})
+			);
+
+			network.channels[1].messages.push(
+				new Msg({
+					text: "message after network creation",
+				})
+			);
+
+			expect(network.channels[1].messages).to.have.lengthOf(3);
+			expect(network.channels[1].messages[0].text).to.equal("message in constructor");
+			expect(network.channels[1].messages[1].text).to.equal("message in original instance");
+			expect(network.channels[1].messages[2].text).to.equal("message after network creation");
+		});
+	});
+
 	describe("#export()", function () {
 		it("should produce an valid object", function () {
 			const network = new Network({
@@ -61,8 +122,10 @@ describe("Network", function () {
 				ignoreList: [],
 			});
 		});
+	});
 
-		it("validate should set correct defaults", function () {
+	describe("#validate()", function () {
+		it("should set correct defaults", function () {
 			Helper.config.defaults.nick = "";
 
 			const network = new Network({
@@ -83,7 +146,7 @@ describe("Network", function () {
 			expect(network2.username).to.equal("InvalidNick");
 		});
 
-		it("lockNetwork should be enforced when validating", function () {
+		it("should enforce lockNetwork", function () {
 			Helper.config.lockNetwork = true;
 
 			// Make sure we lock in private mode
@@ -113,7 +176,101 @@ describe("Network", function () {
 			Helper.config.lockNetwork = false;
 		});
 
-		it("editing a network should enforce correct types", function () {
+		it("should apply STS policies iff they match", function () {
+			const client = {idMsg: 1, emit() {}};
+			STSPolicies.update("irc.example.com", 7000, 3600);
+
+			let network = new Network({
+				host: "irc.example.com",
+				port: 1337,
+				tls: false,
+			});
+
+			expect(network.validate(client)).to.be.true;
+			expect(network.port).to.equal(7000);
+			expect(network.tls).to.be.true;
+
+			network = new Network({
+				host: "irc2.example.com",
+				port: 1337,
+				tls: false,
+			});
+
+			expect(network.validate(client)).to.be.true;
+			expect(network.port).to.equal(1337);
+			expect(network.tls).to.be.false;
+
+			STSPolicies.update("irc.example.com", 7000, 0); // Cleanup
+		});
+
+		it("should not remove client certs if TLS is disabled", function () {
+			Helper.config.public = false;
+
+			const client = {idMsg: 1, emit() {}, messageStorage: []};
+
+			const network = new Network({host: "irc.example.com", sasl: "external"});
+			network.createIrcFramework(client);
+			expect(network.irc).to.not.be.null;
+
+			const client_cert = network.irc.options.client_certificate;
+			expect(client_cert).to.not.be.null;
+			expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert);
+
+			expect(network.validate(client)).to.be.true;
+
+			expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert); // Should be unchanged
+
+			ClientCertificate.remove(network.uuid);
+			Helper.config.public = true;
+		});
+
+		it("should not remove client certs if there is a STS policy", function () {
+			Helper.config.public = false;
+
+			const client = {idMsg: 1, emit() {}, messageStorage: []};
+			STSPolicies.update("irc.example.com", 7000, 3600);
+
+			const network = new Network({host: "irc.example.com", sasl: "external"});
+			network.createIrcFramework(client);
+			expect(network.irc).to.not.be.null;
+
+			const client_cert = network.irc.options.client_certificate;
+			expect(client_cert).to.not.be.null;
+			expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert);
+
+			expect(network.validate(client)).to.be.true;
+
+			expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert); // Should be unchanged
+
+			ClientCertificate.remove(network.uuid);
+			Helper.config.public = true;
+		});
+	});
+
+	describe("#createIrcFramework(client)", function () {
+		it("should generate and use a client certificate when using SASL external", function () {
+			Helper.config.public = false;
+
+			const client = {idMsg: 1, emit() {}};
+			STSPolicies.update("irc.example.com", 7000, 3600);
+
+			let network = new Network({host: "irc.example.com"});
+			network.createIrcFramework(client);
+			expect(network.irc).to.not.be.null;
+			expect(network.irc.options.client_certificate).to.be.null;
+
+			network = new Network({host: "irc.example.com", sasl: "external"});
+			network.createIrcFramework(client);
+			expect(network.irc).to.not.be.null;
+			expect(network.irc.options.client_certificate).to.not.be.null;
+
+			ClientCertificate.remove(network.uuid);
+			Helper.config.public = true;
+		});
+	});
+
+	describe("#edit(client, args)", function () {
+		it("should enforce correct types", function () {
 			let saveCalled = false;
 			let nameEmitCalled = false;
 
@@ -177,63 +334,6 @@ describe("Network", function () {
 				"/whois test",
 			]);
 		});
-
-		it("should generate uuid (v4) for each network", function () {
-			const network1 = new Network();
-			const network2 = new Network();
-
-			expect(network1.uuid).to.have.lengthOf(36);
-			expect(network2.uuid).to.have.lengthOf(36);
-			expect(network1.uuid).to.not.equal(network2.uuid);
-		});
-
-		it("lobby should be at the top", function () {
-			const network = new Network({
-				name: "Super Nice Network",
-				channels: [
-					new Chan({name: "AAAA!", type: Chan.Type.QUERY}),
-					new Chan({name: "#thelounge"}),
-					new Chan({name: "&foobar"}),
-				],
-			});
-			network.channels.push(new Chan({name: "#swag"}));
-
-			expect(network.channels[0].name).to.equal("Super Nice Network");
-			expect(network.channels[0].type).to.equal(Chan.Type.LOBBY);
-		});
-
-		it("should maintain channel reference", function () {
-			const chan = new Chan({
-				name: "#506-bug-fix",
-				messages: [
-					new Msg({
-						text: "message in constructor",
-					}),
-				],
-			});
-
-			const network = new Network({
-				name: "networkName",
-				channels: [chan],
-			});
-
-			chan.messages.push(
-				new Msg({
-					text: "message in original instance",
-				})
-			);
-
-			network.channels[1].messages.push(
-				new Msg({
-					text: "message after network creation",
-				})
-			);
-
-			expect(network.channels[1].messages).to.have.lengthOf(3);
-			expect(network.channels[1].messages[0].text).to.equal("message in constructor");
-			expect(network.channels[1].messages[1].text).to.equal("message in original instance");
-			expect(network.channels[1].messages[2].text).to.equal("message after network creation");
-		});
 	});
 
 	describe("#getFilteredClone(lastActiveChannel, lastMessage)", function () {