Merge pull request #3844 from thelounge/xpaw/certfp

CertFP support; separate SASL configuration
This commit is contained in:
Pavel Djundik 2020-04-22 14:05:34 +03:00 committed by GitHub
commit 0642ae58ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 548 additions and 146 deletions

View File

@ -6,25 +6,23 @@
<form class="container" method="post" action="" @submit.prevent="onSubmit">
<h1 class="title">
<template v-if="defaults.uuid">
<input type="hidden" name="uuid" :value="defaults.uuid" />
<input v-model="defaults.uuid" type="hidden" name="uuid" />
Edit {{ defaults.name }}
</template>
<template v-else>
Connect
<template v-if="!config.displayNetwork && config.lockNetwork">
to {{ defaults.name }}
</template>
<template v-if="config.lockNetwork">to {{ defaults.name }}</template>
</template>
</h1>
<template v-if="config.displayNetwork">
<template v-if="!config.lockNetwork">
<h2>Network settings</h2>
<div class="connect-row">
<label for="connect:name">Name</label>
<input
id="connect:name"
v-model="defaults.name"
class="input"
name="name"
:value="defaults.name"
maxlength="100"
/>
</div>
@ -33,41 +31,52 @@
<div class="input-wrap">
<input
id="connect:host"
v-model="defaults.host"
class="input"
name="host"
:value="defaults.host"
aria-label="Server address"
maxlength="255"
required
:disabled="config.lockNetwork ? true : false"
/>
<span id="connect:portseparator">:</span>
<input
id="connect:port"
ref="serverPort"
v-model="defaults.port"
class="input"
type="number"
min="1"
max="65535"
name="port"
:value="defaults.port"
aria-label="Server port"
:disabled="config.lockNetwork ? true : false"
/>
</div>
</div>
<div class="connect-row">
<label for="connect:password">Password</label>
<RevealPassword
v-slot:default="slotProps"
class="input-wrap password-container"
>
<input
id="connect:password"
v-model="defaults.password"
class="input"
:type="slotProps.isVisible ? 'text' : 'password'"
placeholder="Server password (optional)"
name="password"
maxlength="300"
/>
</RevealPassword>
</div>
<div class="connect-row">
<label></label>
<div class="input-wrap">
<label class="tls">
<input
v-model="defaults.tls"
type="checkbox"
name="tls"
:checked="defaults.tls ? true : false"
:disabled="
config.lockNetwork || defaults.hasSTSPolicy ? true : false
"
@change="onSecureChanged"
:disabled="defaults.hasSTSPolicy"
/>
Use secure connection (TLS)
<span
@ -79,10 +88,9 @@
</label>
<label class="tls">
<input
v-model="defaults.rejectUnauthorized"
type="checkbox"
name="rejectUnauthorized"
:checked="defaults.rejectUnauthorized ? true : false"
:disabled="config.lockNetwork ? true : false"
/>
Only allow trusted certificates
</label>
@ -95,10 +103,10 @@
<label for="connect:nick">Nick</label>
<input
id="connect:nick"
v-model="defaults.nick"
class="input nick"
name="nick"
pattern="[^\s:!@]+"
:value="defaults.nick"
maxlength="100"
required
@input="onNickChanged"
@ -110,68 +118,207 @@
<input
id="connect:username"
ref="usernameInput"
v-model="defaults.username"
class="input username"
name="username"
:value="defaults.username"
maxlength="100"
/>
</div>
</template>
<div class="connect-row">
<label for="connect:password">Password</label>
<RevealPassword v-slot:default="slotProps" class="input-wrap password-container">
<input
id="connect:password"
v-model="defaults.password"
class="input"
:type="slotProps.isVisible ? 'text' : 'password'"
name="password"
maxlength="300"
/>
</RevealPassword>
</div>
<div class="connect-row">
<label for="connect:realname">Real name</label>
<input
id="connect:realname"
v-model="defaults.realname"
class="input"
name="realname"
:value="defaults.realname"
maxlength="300"
/>
</div>
<template v-if="defaults.uuid">
<template v-if="defaults.uuid && !$store.state.serverConfiguration.public">
<div class="connect-row">
<label for="connect:commands">Commands</label>
<label for="connect:commands">
Commands
<span
class="tooltipped tooltipped-ne tooltipped-no-delay"
aria-label="One /command per line.
Each command will be executed in
the server tab on new connection"
>
<button class="extra-help" />
</span>
</label>
<textarea
id="connect:commands"
ref="commandsInput"
:value="defaults.commands ? defaults.commands.join('\n') : ''"
class="input"
name="commands"
placeholder="One /command per line, each command will be executed in the server tab on new connection"
:value="defaults.commands ? defaults.commands.join('\n') : ''"
@input="resizeCommandsInput"
/>
</div>
<div>
<button type="submit" class="btn" :disabled="disabled ? true : false">
Save
</button>
</div>
</template>
<template v-else>
<template v-else-if="!defaults.uuid">
<div class="connect-row">
<label for="connect:channels">Channels</label>
<input id="connect:channels" class="input" name="join" :value="defaults.join" />
</div>
<div>
<button type="submit" class="btn" :disabled="disabled ? true : false">
Connect
</button>
<input
id="connect:channels"
v-model="defaults.join"
class="input"
name="join"
/>
</div>
</template>
<template v-if="$store.state.serverConfiguration.public">
<template v-if="config.lockNetwork">
<div class="connect-row">
<label></label>
<div class="input-wrap">
<label class="tls">
<input v-model="displayPasswordField" type="checkbox" />
I have a password
</label>
</div>
</div>
<div v-if="displayPasswordField" class="connect-row">
<label for="connect:password">Password</label>
<RevealPassword
v-slot:default="slotProps"
class="input-wrap password-container"
>
<input
id="connect:password"
ref="publicPassword"
v-model="defaults.password"
class="input"
:type="slotProps.isVisible ? 'text' : 'password'"
placeholder="Server password (optional)"
name="password"
maxlength="300"
/>
</RevealPassword>
</div>
</template>
</template>
<template v-else>
<h2 id="label-auth">Authentication</h2>
<div class="connect-row connect-auth" role="group" aria-labelledby="label-auth">
<label class="opt">
<input
:checked="!defaults.sasl"
type="radio"
name="sasl"
value=""
@change="setSaslAuth('')"
/>
No authentication
</label>
<label class="opt">
<input
:checked="defaults.sasl === 'plain'"
type="radio"
name="sasl"
value="plain"
@change="setSaslAuth('plain')"
/>
Username + password (SASL PLAIN)
</label>
<label
v-if="!$store.state.serverConfiguration.public && defaults.tls"
class="opt"
>
<input
:checked="defaults.sasl === 'external'"
type="radio"
name="sasl"
value="external"
@change="setSaslAuth('external')"
/>
Client certificate (SASL EXTERNAL)
</label>
</div>
<template v-if="defaults.sasl === 'plain'">
<div class="connect-row">
<label for="connect:username">Account</label>
<input
id="connect:saslAccount"
v-model="defaults.saslAccount"
class="input"
name="saslAccount"
maxlength="100"
required
/>
</div>
<div class="connect-row">
<label for="connect:password">Password</label>
<RevealPassword
v-slot:default="slotProps"
class="input-wrap password-container"
>
<input
id="connect:saslPassword"
v-model="defaults.saslPassword"
class="input"
:type="slotProps.isVisible ? 'text' : 'password'"
name="saslPassword"
maxlength="300"
required
/>
</RevealPassword>
</div>
</template>
<div v-else-if="defaults.sasl === 'external'" class="connect-sasl-external">
<p>
The Lounge automatically generates and manages the client certificate.
</p>
<p>
On the IRC server, you will need to tell the services to attach the
certificate fingerprint (certfp) to your account, for example:
</p>
<pre><code>/msg NickServ CERT ADD</code></pre>
</div>
</template>
<div>
<button type="submit" class="btn" :disabled="disabled ? true : false">
<template v-if="defaults.uuid">Save network</template>
<template v-else>Connect</template>
</button>
</div>
</form>
</div>
</template>
<style>
#connect .connect-auth {
display: block;
margin-bottom: 10px;
}
#connect .connect-auth .opt {
display: block;
width: 100%;
}
#connect .connect-auth input {
margin: 3px 10px 0 0;
}
#connect .connect-sasl-external {
padding: 10px;
border-radius: 2px;
background-color: #d9edf7;
color: #31708f;
}
#connect .connect-sasl-external pre {
margin: 0;
user-select: text;
}
</style>
<script>
import RevealPassword from "./RevealPassword.vue";
import SidebarToggle from "./SidebarToggle.vue";
@ -191,9 +338,33 @@ export default {
return {
config: this.$store.state.serverConfiguration,
previousUsername: this.defaults.username,
displayPasswordField: false,
};
},
watch: {
displayPasswordField(value) {
if (value) {
this.$nextTick(() => this.$refs.publicPassword.focus());
}
},
"defaults.commands"() {
this.$nextTick(this.resizeCommandsInput);
},
"defaults.tls"(isSecureChecked) {
const ports = [6667, 6697];
const newPort = isSecureChecked ? 0 : 1;
// If you disable TLS and current port is 6697,
// set it to 6667, and vice versa
if (this.defaults.port === ports[newPort]) {
this.defaults.port = ports[1 - newPort];
}
},
},
methods: {
setSaslAuth(type) {
this.defaults.sasl = type;
},
onNickChanged(event) {
// Username input is not available when useHexIp is set
if (!this.$refs.usernameInput) {
@ -209,16 +380,6 @@ export default {
this.previousUsername = event.target.value;
},
onSecureChanged(event) {
const ports = ["6667", "6697"];
const newPort = event.target.checked ? 0 : 1;
// If you disable TLS and current port is 6697,
// set it to 6667, and vice versa
if (this.$refs.serverPort.value === ports[newPort]) {
this.$refs.serverPort.value = ports[1 - newPort];
}
},
onSubmit(event) {
const formData = new FormData(event.target);
const data = {};
@ -229,6 +390,18 @@ export default {
this.handleSubmit(data);
},
resizeCommandsInput() {
if (!this.$refs.commandsInput) {
return;
}
// Reset height first so it can down size
this.$refs.commandsInput.style.height = "";
// 2 pixels to account for the border
this.$refs.commandsInput.style.height =
Math.ceil(this.$refs.commandsInput.scrollHeight + 2) + "px";
},
},
};
</script>

View File

@ -59,16 +59,11 @@ export default {
// When the network is locked, URL overrides should not affect disabled fields
if (
this.$store.state.serverConfiguration.lockNetwork &&
["host", "port", "tls", "rejectUnauthorized"].includes(key)
["name", "host", "port", "tls", "rejectUnauthorized"].includes(key)
) {
continue;
}
// When the network is not displayed, its name in the UI is not customizable
if (!this.$store.state.serverConfiguration.displayNetwork && key === "name") {
continue;
}
if (key === "join") {
value = value
.split(",")

View File

@ -481,6 +481,12 @@ This may break orientation if your browser does not support that."
</div>
</template>
<style>
textarea#user-specified-css-input {
height: 100px;
}
</style>
<script>
import socket from "../../js/socket";
import webpush from "../../js/webpush";

View File

@ -288,6 +288,7 @@ p {
.channel-list-item::before,
#footer .icon,
#chat .count::before,
#connect .extra-help,
#settings .extra-help,
#settings #play::before,
#form #upload::before,
@ -507,6 +508,7 @@ p {
line-height: 45px;
}
#connect .extra-help::before,
#settings .extra-help::before {
content: "\f059"; /* http://fontawesome.io/icon/question-circle/ */
}
@ -938,7 +940,6 @@ background on hover (unless active) */
textarea.input {
resize: vertical;
height: 100px;
min-height: 35px;
padding: 6px 10px;
line-height: 1.5;
@ -1826,8 +1827,8 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
}
#connect .btn {
margin-left: 25%;
margin-top: 15px;
width: 100%;
}
#settings .apple-push-unsupported,
@ -1874,6 +1875,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
margin-right: 6px;
}
#connect .extra-help,
#settings .extra-help {
cursor: help;
}
@ -2621,11 +2623,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
flex-grow: 1;
}
#connect .btn {
margin-left: 0;
width: 100%;
}
#help .help-version-title {
flex-direction: column;
}

View File

@ -238,25 +238,11 @@ module.exports = {
join: "#thelounge",
},
// ### `displayNetwork`
//
// When set to `false`, network fields will not be shown in the "Connect"
// window.
//
// Note that even though users cannot access and set these fields, they can
// still connect to other networks using the `/connect` command. See the
// `lockNetwork` setting to restrict users from connecting to other networks.
//
// This value is set to `true` by default.
displayNetwork: true,
// ### `lockNetwork`
//
// When set to `true`, users will not be able to modify host, port and TLS
// settings and will be limited to the configured network.
//
// It is often useful to use it with `displayNetwork` when setting The
// Lounge as a public web client for a specific IRC network.
// These fields will also be hidden from the UI.
//
// This value is set to `false` by default.
lockNetwork: false,

View File

@ -55,6 +55,7 @@
"linkify-it": "2.2.0",
"lodash": "4.17.15",
"mime-types": "2.1.26",
"node-forge": "0.9.1",
"package-json": "6.5.0",
"read": "1.0.7",
"read-chunk": "3.2.0",

View File

@ -227,7 +227,7 @@ Client.prototype.connect = function (args, isStartup = false) {
const network = new Network({
uuid: args.uuid,
name: String(
args.name || (Helper.config.displayNetwork ? "" : Helper.config.defaults.name) || ""
args.name || (Helper.config.lockNetwork ? Helper.config.defaults.name : "") || ""
),
host: String(args.host || ""),
port: parseInt(args.port, 10),
@ -238,6 +238,9 @@ Client.prototype.connect = function (args, isStartup = false) {
nick: String(args.nick || ""),
username: String(args.username || ""),
realname: String(args.realname || ""),
sasl: String(args.sasl || ""),
saslAccount: String(args.saslAccount || ""),
saslPassword: String(args.saslPassword || ""),
commands: args.commands || [],
channels: channels,
ignoreList: args.ignoreList ? args.ignoreList : [],

View File

@ -18,6 +18,7 @@ let storagePath;
let packagesPath;
let fileUploadPath;
let userLogsPath;
let clientCertificatesPath;
const Helper = {
config: null,
@ -31,6 +32,7 @@ const Helper = {
getUsersPath,
getUserConfigPath,
getUserLogsPath,
getClientCertificatesPath,
setHome,
getVersion,
getVersionCacheBust,
@ -100,6 +102,7 @@ function setHome(newPath) {
fileUploadPath = path.join(homePath, "uploads");
packagesPath = path.join(homePath, "packages");
userLogsPath = path.join(homePath, "logs");
clientCertificatesPath = path.join(homePath, "certificates");
// Reload config from new home location
if (fs.existsSync(configPath)) {
@ -122,16 +125,6 @@ function setHome(newPath) {
mergeConfig(this.config, userConfig);
}
if (!this.config.displayNetwork && !this.config.lockNetwork) {
this.config.lockNetwork = true;
log.warn(
`${colors.bold("displayNetwork")} and ${colors.bold(
"lockNetwork"
)} are false, setting ${colors.bold("lockNetwork")} to true.`
);
}
if (this.config.fileUpload.baseUrl) {
try {
new URL("test/file.png", this.config.fileUpload.baseUrl);
@ -185,6 +178,10 @@ function getUserLogsPath() {
return userLogsPath;
}
function getClientCertificatesPath() {
return clientCertificatesPath;
}
function getStoragePath() {
return storagePath;
}

View File

@ -7,6 +7,7 @@ const Chan = require("./chan");
const Msg = require("./msg");
const Helper = require("../helper");
const STSPolicies = require("../plugins/sts");
const ClientCertificate = require("../plugins/clientCertificate");
module.exports = Network;
@ -34,6 +35,9 @@ function Network(attr) {
commands: [],
username: "",
realname: "",
sasl: "",
saslAccount: "",
saslPassword: "",
channels: [],
irc: null,
serverOptions: {
@ -81,11 +85,21 @@ Network.prototype.validate = function (client) {
this.password = cleanString(this.password);
this.host = cleanString(this.host).toLowerCase();
this.name = cleanString(this.name);
this.saslAccount = cleanString(this.saslAccount);
this.saslPassword = cleanString(this.saslPassword);
if (!this.port) {
this.port = this.tls ? 6697 : 6667;
}
if (!["", "plain", "external"].includes(this.sasl)) {
this.sasl = "";
}
if (!this.tls) {
ClientCertificate.remove(this.uuid);
}
if (Helper.config.lockNetwork) {
// This check is needed to prevent invalid user configurations
if (
@ -106,6 +120,7 @@ Network.prototype.validate = function (client) {
return false;
}
this.name = Helper.config.defaults.name;
this.host = Helper.config.defaults.host;
this.port = Helper.config.defaults.port;
this.tls = Helper.config.defaults.tls;
@ -148,24 +163,17 @@ Network.prototype.validate = function (client) {
Network.prototype.createIrcFramework = function (client) {
this.irc = new IrcFramework.Client({
version: false, // We handle it ourselves
host: this.host,
port: this.port,
nick: this.nick,
username: Helper.config.useHexIp ? Helper.ip2hex(client.config.browser.ip) : this.username,
gecos: this.realname,
password: this.password,
tls: this.tls,
outgoing_addr: Helper.config.bind,
rejectUnauthorized: this.rejectUnauthorized,
enable_chghost: true,
enable_echomessage: true,
enable_setname: true,
auto_reconnect: true,
auto_reconnect_wait: 10000 + Math.floor(Math.random() * 1000), // If multiple users are connected to the same network, randomize their reconnections a little
auto_reconnect_max_retries: 360, // At least one hour (plus timeouts) worth of reconnections
webirc: this.createWebIrc(client),
});
this.setIrcFrameworkOptions(client);
this.irc.requestCap([
"znc.in/self-message", // Legacy echo-message for ZNC
]);
@ -177,6 +185,36 @@ Network.prototype.createIrcFramework = function (client) {
}
};
Network.prototype.setIrcFrameworkOptions = function (client) {
this.irc.options.host = this.host;
this.irc.options.port = this.port;
this.irc.options.password = this.password;
this.irc.options.nick = this.nick;
this.irc.options.username = Helper.config.useHexIp
? Helper.ip2hex(client.config.browser.ip)
: this.username;
this.irc.options.gecos = this.realname;
this.irc.options.tls = this.tls;
this.irc.options.rejectUnauthorized = this.rejectUnauthorized;
this.irc.options.webirc = this.createWebIrc(client);
this.irc.options.client_certificate = this.tls ? ClientCertificate.get(this.uuid) : null;
if (!this.sasl) {
delete this.irc.options.sasl_mechanism;
delete this.irc.options.account;
} else if (this.sasl === "external") {
this.irc.options.sasl_mechanism = "EXTERNAL";
this.irc.options.account = {};
} else if (this.sasl === "plain") {
delete this.irc.options.sasl_mechanism;
this.irc.options.account = {
account: this.saslAccount,
password: this.saslPassword,
};
}
};
Network.prototype.createWebIrc = function (client) {
if (
!Helper.config.webirc ||
@ -221,6 +259,9 @@ Network.prototype.edit = function (client, args) {
this.password = String(args.password || "");
this.username = String(args.username || "");
this.realname = String(args.realname || "");
this.sasl = String(args.sasl || "");
this.saslAccount = String(args.saslAccount || "");
this.saslPassword = String(args.saslPassword || "");
// Split commands into an array
this.commands = String(args.commands || "")
@ -243,7 +284,7 @@ Network.prototype.edit = function (client, args) {
// Send new nick straight away
this.irc.changeNick(this.nick);
} else {
this.irc.options.nick = this.irc.user.nick = this.nick;
this.irc.user.nick = this.nick;
// Update UI nick straight away if IRC is not connected
client.emit("nick", {
@ -261,16 +302,10 @@ Network.prototype.edit = function (client, args) {
this.irc.raw("SETNAME", this.realname);
}
this.irc.options.host = this.host;
this.irc.options.port = this.port;
this.irc.options.password = this.password;
this.irc.options.gecos = this.irc.user.gecos = this.realname;
this.irc.options.tls = this.tls;
this.irc.options.rejectUnauthorized = this.rejectUnauthorized;
this.setIrcFrameworkOptions(client);
if (!Helper.config.useHexIp) {
this.irc.options.username = this.irc.user.username = this.username;
}
this.irc.user.username = this.irc.options.username;
this.irc.user.gecos = this.irc.options.gecos;
}
client.save();
@ -297,6 +332,10 @@ Network.prototype.setNick = function (nick) {
if (this.keepNick === nick) {
this.keepNick = null;
}
if (this.irc) {
this.irc.options.nick = nick;
}
};
/**
@ -385,26 +424,24 @@ Network.prototype.quit = function (quitMessage) {
};
Network.prototype.exportForEdit = function () {
let fieldsToReturn;
const fieldsToReturn = [
"uuid",
"name",
"nick",
"password",
"username",
"realname",
"sasl",
"saslAccount",
"saslPassword",
"commands",
];
if (Helper.config.displayNetwork) {
// Return fields required to edit a network
fieldsToReturn = [
"uuid",
"nick",
"name",
"host",
"port",
"tls",
"rejectUnauthorized",
"password",
"username",
"realname",
"commands",
];
} else {
// Same fields as in getClientConfiguration when network is hidden
fieldsToReturn = ["name", "nick", "username", "password", "realname"];
if (!Helper.config.lockNetwork) {
fieldsToReturn.push("host");
fieldsToReturn.push("port");
fieldsToReturn.push("tls");
fieldsToReturn.push("rejectUnauthorized");
}
const data = _.pick(this, fieldsToReturn);
@ -428,6 +465,9 @@ Network.prototype.export = function () {
"password",
"username",
"realname",
"sasl",
"saslAccount",
"saslPassword",
"commands",
"ignoreList",
]);

View File

@ -0,0 +1,134 @@
"use strict";
const path = require("path");
const fs = require("fs");
const crypto = require("crypto");
const {md, pki} = require("node-forge");
const log = require("../log");
const Helper = require("../helper");
module.exports = {
get,
remove,
};
function get(uuid) {
if (Helper.config.public) {
return null;
}
const folderPath = Helper.getClientCertificatesPath();
const paths = getPaths(folderPath, uuid);
if (!fs.existsSync(paths.privateKeyPath) || !fs.existsSync(paths.certificatePath)) {
return generateAndWrite(folderPath, paths);
}
try {
return {
private_key: fs.readFileSync(paths.privateKeyPath, "utf-8"),
certificate: fs.readFileSync(paths.certificatePath, "utf-8"),
};
} catch (e) {
log.error("Unable to remove certificate", e);
}
return null;
}
function remove(uuid) {
if (Helper.config.public) {
return null;
}
const paths = getPaths(Helper.getClientCertificatesPath(), uuid);
try {
if (fs.existsSync(paths.privateKeyPath)) {
fs.unlinkSync(paths.privateKeyPath);
}
if (fs.existsSync(paths.certificatePath)) {
fs.unlinkSync(paths.certificatePath);
}
} catch (e) {
log.error("Unable to remove certificate", e);
}
}
function generateAndWrite(folderPath, paths) {
const certificate = generate();
try {
fs.mkdirSync(folderPath, {recursive: true});
fs.writeFileSync(paths.privateKeyPath, certificate.private_key, {
mode: 0o600,
});
fs.writeFileSync(paths.certificatePath, certificate.certificate, {
mode: 0o600,
});
return certificate;
} catch (e) {
log.error("Unable to write certificate", e);
}
return null;
}
function generate() {
const keys = pki.rsa.generateKeyPair(2048);
const cert = pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = crypto.randomBytes(16).toString("hex").toUpperCase();
// Set notBefore a day earlier just in case the time between
// the client and server is not perfectly in sync
cert.validity.notBefore = new Date();
cert.validity.notBefore.setDate(cert.validity.notBefore.getDate() - 1);
// Set notAfter 100 years into the future just in case
// the server actually validates this field
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 100);
const attrs = [
{
name: "commonName",
value: "The Lounge IRC Client",
},
];
cert.setSubject(attrs);
cert.setIssuer(attrs);
// Set extensions that indicate this is a client authentication certificate
cert.setExtensions([
{
name: "extKeyUsage",
clientAuth: true,
},
{
name: "nsCertType",
client: true,
},
]);
// Sign this certificate with a SHA256 signature
cert.sign(keys.privateKey, md.sha256.create());
const pem = {
private_key: pki.privateKeyToPem(keys.privateKey),
certificate: pki.certificateToPem(cert),
};
return pem;
}
function getPaths(folderPath, uuid) {
return {
privateKeyPath: path.join(folderPath, `${uuid}.pem`),
certificatePath: path.join(folderPath, `${uuid}.crt`),
};
}

View File

@ -1,6 +1,7 @@
"use strict";
const _ = require("lodash");
const ClientCertificate = require("../clientCertificate");
exports.commands = ["quit"];
exports.allowDisconnected = true;
@ -18,5 +19,7 @@ exports.input = function (network, chan, cmd, args) {
const quitMessage = args[0] ? args.join(" ") : null;
network.quit(quitMessage);
ClientCertificate.remove(network.uuid);
return true;
};

View File

@ -706,18 +706,12 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
}
function getClientConfiguration() {
const config = _.pick(Helper.config, [
"public",
"lockNetwork",
"displayNetwork",
"useHexIp",
"prefetch",
]);
const config = _.pick(Helper.config, ["public", "lockNetwork", "useHexIp", "prefetch"]);
config.fileUpload = Helper.config.fileUpload.enable;
config.ldapEnabled = Helper.config.ldap.enable;
if (config.displayNetwork) {
if (!config.lockNetwork) {
config.defaults = _.clone(Helper.config.defaults);
} else {
// Only send defaults that are visible on the client
@ -738,6 +732,9 @@ function getClientConfiguration() {
config.themes = themes.getAll();
config.defaultTheme = Helper.config.theme;
config.defaults.nick = Helper.getDefaultNick();
config.defaults.sasl = "";
config.defaults.saslAccount = "";
config.defaults.saslPassword = "";
if (Uploader) {
config.fileUploadMaxFileSize = Uploader.getMaxFileSize();

View File

@ -1,6 +1,7 @@
# Files that may be generated by tests
.thelounge/storage/
.thelounge/logs/
.thelounge/certificates/
# Fixtures contain fake packages, stored in a fake node_modules folder
!.thelounge/packages/node_modules/

View File

@ -14,6 +14,9 @@ describe("Network", function () {
uuid: "hello world",
awayMessage: "I am away",
name: "networkName",
sasl: "plain",
saslAccount: "testaccount",
saslPassword: "testpassword",
channels: [
new Chan({name: "#thelounge", key: ""}),
new Chan({name: "&foobar", key: ""}),
@ -37,6 +40,9 @@ describe("Network", function () {
password: "",
username: "",
realname: "",
sasl: "plain",
saslAccount: "testaccount",
saslPassword: "testpassword",
commands: [],
nick: "chillin`",
channels: [
@ -121,6 +127,9 @@ describe("Network", function () {
username: 1234,
password: 4567,
realname: 8901,
sasl: "something",
saslAccount: 1337,
saslPassword: 1337,
commands: "/command 1 2 3\r\n/ping HELLO\r\r\r\r/whois test\r\n\r\n",
ip: "newIp",
hostname: "newHostname",
@ -144,6 +153,9 @@ describe("Network", function () {
expect(network.username).to.equal("1234");
expect(network.password).to.equal("4567");
expect(network.realname).to.equal("8901");
expect(network.sasl).to.equal("");
expect(network.saslAccount).to.equal("1337");
expect(network.saslPassword).to.equal("1337");
expect(network.commands).to.deep.equal([
"/command 1 2 3",
"/ping HELLO",

View File

@ -0,0 +1,53 @@
"use strict";
const fs = require("fs");
const path = require("path");
const {expect} = require("chai");
const ClientCertificate = require("../../src/plugins/clientCertificate");
const Helper = require("../../src/helper");
describe("ClientCertificate", function () {
it("should not generate a client certificate in public mode", function () {
Helper.config.public = true;
const certificate = ClientCertificate.get("this-is-test-uuid");
expect(certificate).to.be.null;
});
it("should generate a client certificate", function () {
Helper.config.public = false;
const certificate = ClientCertificate.get("this-is-test-uuid");
expect(certificate.certificate).to.match(/^-----BEGIN CERTIFICATE-----/);
expect(certificate.private_key).to.match(/^-----BEGIN RSA PRIVATE KEY-----/);
const certificate2 = ClientCertificate.get("this-is-test-uuid");
expect(certificate2.certificate).to.equal(certificate.certificate);
expect(certificate2.private_key).to.equal(certificate.private_key);
Helper.config.public = true;
});
it("should remove the client certificate files", function () {
Helper.config.public = false;
const privateKeyPath = path.join(
Helper.getClientCertificatesPath(),
`this-is-test-uuid.pem`
);
const certificatePath = path.join(
Helper.getClientCertificatesPath(),
`this-is-test-uuid.crt`
);
expect(fs.existsSync(privateKeyPath)).to.be.true;
expect(fs.existsSync(certificatePath)).to.be.true;
ClientCertificate.remove("this-is-test-uuid");
expect(fs.existsSync(privateKeyPath)).to.be.false;
expect(fs.existsSync(certificatePath)).to.be.false;
Helper.config.public = true;
});
});

View File

@ -113,7 +113,6 @@ describe("Server", function () {
expect(data.defaultTheme).to.equal("default");
expect(data.themes).to.be.an("array");
expect(data.lockNetwork).to.equal(false);
expect(data.displayNetwork).to.equal(true);
expect(data.useHexIp).to.equal(false);
done();

View File

@ -5848,6 +5848,11 @@ node-fetch@2.1.2:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5"
integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=
node-forge@0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==
node-libs-browser@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"