mirror of
https://github.com/thelounge/thelounge
synced 2024-11-25 13:30:21 +00:00
Merge pull request #3844 from thelounge/xpaw/certfp
CertFP support; separate SASL configuration
This commit is contained in:
commit
0642ae58ce
17 changed files with 548 additions and 146 deletions
|
@ -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:realname">Real name</label>
|
||||
<input
|
||||
id="connect:realname"
|
||||
v-model="defaults.realname"
|
||||
class="input"
|
||||
name="realname"
|
||||
maxlength="300"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="defaults.uuid && !$store.state.serverConfiguration.public">
|
||||
<div class="connect-row">
|
||||
<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"
|
||||
@input="resizeCommandsInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="!defaults.uuid">
|
||||
<div class="connect-row">
|
||||
<label for="connect:channels">Channels</label>
|
||||
<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">
|
||||
<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>
|
||||
<div class="connect-row">
|
||||
<label for="connect:realname">Real name</label>
|
||||
<input
|
||||
id="connect:realname"
|
||||
class="input"
|
||||
name="realname"
|
||||
:value="defaults.realname"
|
||||
maxlength="300"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="defaults.uuid">
|
||||
<div class="connect-row">
|
||||
<label for="connect:commands">Commands</label>
|
||||
<textarea
|
||||
id="connect:commands"
|
||||
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') : ''"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn" :disabled="disabled ? true : false">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="connect-row">
|
||||
<label for="connect:channels">Channels</label>
|
||||
<input id="connect:channels" class="input" name="join" :value="defaults.join" />
|
||||
<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>
|
||||
<div>
|
||||
<button type="submit" class="btn" :disabled="disabled ? true : false">
|
||||
Connect
|
||||
</button>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -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(",")
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 : [],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
if (Helper.config.displayNetwork) {
|
||||
// Return fields required to edit a network
|
||||
fieldsToReturn = [
|
||||
const fieldsToReturn = [
|
||||
"uuid",
|
||||
"nick",
|
||||
"name",
|
||||
"host",
|
||||
"port",
|
||||
"tls",
|
||||
"rejectUnauthorized",
|
||||
"nick",
|
||||
"password",
|
||||
"username",
|
||||
"realname",
|
||||
"sasl",
|
||||
"saslAccount",
|
||||
"saslPassword",
|
||||
"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",
|
||||
]);
|
||||
|
|
134
src/plugins/clientCertificate.js
Normal file
134
src/plugins/clientCertificate.js
Normal 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`),
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
1
test/fixtures/.gitignore
vendored
1
test/fixtures/.gitignore
vendored
|
@ -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/
|
||||
|
|
|
@ -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",
|
||||
|
|
53
test/plugins/clientCertificate.js
Normal file
53
test/plugins/clientCertificate.js
Normal 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;
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue