2016-10-09 19:14:02 +00:00
|
|
|
"use strict";
|
|
|
|
|
2017-10-15 16:05:19 +00:00
|
|
|
const _ = require("lodash");
|
2018-06-15 20:31:06 +00:00
|
|
|
const log = require("./log");
|
2018-03-02 18:28:54 +00:00
|
|
|
const colors = require("chalk");
|
2019-12-18 09:06:20 +00:00
|
|
|
const crypto = require("crypto");
|
2017-10-15 16:05:19 +00:00
|
|
|
const fs = require("fs");
|
|
|
|
const path = require("path");
|
2020-04-22 13:33:09 +00:00
|
|
|
const Auth = require("./plugins/auth");
|
2017-10-15 16:05:19 +00:00
|
|
|
const Client = require("./client");
|
|
|
|
const Helper = require("./helper");
|
2017-07-10 19:47:03 +00:00
|
|
|
const WebPush = require("./plugins/webpush");
|
2014-08-13 23:43:11 +00:00
|
|
|
|
|
|
|
module.exports = ClientManager;
|
|
|
|
|
|
|
|
function ClientManager() {
|
2014-08-14 16:35:37 +00:00
|
|
|
this.clients = [];
|
2016-12-17 09:51:33 +00:00
|
|
|
}
|
|
|
|
|
2020-03-21 20:55:36 +00:00
|
|
|
ClientManager.prototype.init = function (identHandler, sockets) {
|
2016-12-17 09:51:33 +00:00
|
|
|
this.sockets = sockets;
|
|
|
|
this.identHandler = identHandler;
|
2017-07-10 19:47:03 +00:00
|
|
|
this.webPush = new WebPush();
|
2016-12-17 09:51:33 +00:00
|
|
|
|
2019-10-31 09:01:44 +00:00
|
|
|
if (!Helper.config.public) {
|
|
|
|
this.loadUsers();
|
|
|
|
|
|
|
|
// LDAP does not have user commands, and users are dynamically
|
|
|
|
// created upon logon, so we don't need to watch for new files
|
|
|
|
if (!Helper.config.ldap.enable) {
|
|
|
|
this.autoloadUsers();
|
|
|
|
}
|
2016-04-26 20:41:08 +00:00
|
|
|
}
|
2016-12-17 09:51:33 +00:00
|
|
|
};
|
2014-08-13 23:43:11 +00:00
|
|
|
|
2020-03-21 20:55:36 +00:00
|
|
|
ClientManager.prototype.findClient = function (name) {
|
2020-05-15 09:51:01 +00:00
|
|
|
name = name.toLowerCase();
|
|
|
|
return this.clients.find((u) => u.name.toLowerCase() === name);
|
2014-08-14 16:35:37 +00:00
|
|
|
};
|
|
|
|
|
2020-03-21 20:55:36 +00:00
|
|
|
ClientManager.prototype.loadUsers = function () {
|
2020-05-15 09:51:01 +00:00
|
|
|
let users = this.getUsers();
|
2017-08-23 05:11:28 +00:00
|
|
|
|
2017-08-26 04:55:49 +00:00
|
|
|
if (users.length === 0) {
|
2019-10-31 09:01:44 +00:00
|
|
|
log.info(
|
|
|
|
`There are currently no users. Create one with ${colors.bold("thelounge add <name>")}.`
|
|
|
|
);
|
2020-05-15 09:51:01 +00:00
|
|
|
|
|
|
|
return;
|
2017-08-23 05:11:28 +00:00
|
|
|
}
|
|
|
|
|
2020-05-15 09:51:01 +00:00
|
|
|
const alreadySeenUsers = new Set();
|
|
|
|
users = users.filter((user) => {
|
|
|
|
user = user.toLowerCase();
|
|
|
|
|
|
|
|
if (alreadySeenUsers.has(user)) {
|
|
|
|
log.error(
|
|
|
|
`There is more than one user named "${colors.bold(
|
|
|
|
user
|
|
|
|
)}". Usernames are now case insensitive, duplicate users will not load.`
|
|
|
|
);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
alreadySeenUsers.add(user);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
|
2020-04-22 13:33:09 +00:00
|
|
|
// This callback is used by Auth plugins to load users they deem acceptable
|
|
|
|
const callbackLoadUser = (user) => {
|
|
|
|
this.loadUser(user);
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!Auth.loadUsers(users, callbackLoadUser)) {
|
|
|
|
// Fallback to loading all users
|
|
|
|
users.forEach((name) => this.loadUser(name));
|
|
|
|
}
|
2019-10-31 09:01:44 +00:00
|
|
|
};
|
2016-12-07 05:50:11 +00:00
|
|
|
|
2020-03-21 20:55:36 +00:00
|
|
|
ClientManager.prototype.autoloadUsers = function () {
|
2019-07-17 09:33:59 +00:00
|
|
|
fs.watch(
|
|
|
|
Helper.getUsersPath(),
|
|
|
|
_.debounce(
|
|
|
|
() => {
|
|
|
|
const loaded = this.clients.map((c) => c.name);
|
|
|
|
const updatedUsers = this.getUsers();
|
|
|
|
|
|
|
|
if (updatedUsers.length === 0) {
|
2019-10-31 09:01:44 +00:00
|
|
|
log.info(
|
|
|
|
`There are currently no users. Create one with ${colors.bold(
|
|
|
|
"thelounge add <name>"
|
|
|
|
)}.`
|
|
|
|
);
|
2019-07-17 09:33:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Reload all users. Existing users will only have their passwords reloaded.
|
|
|
|
updatedUsers.forEach((name) => this.loadUser(name));
|
|
|
|
|
|
|
|
// Existing users removed since last time users were loaded
|
|
|
|
_.difference(loaded, updatedUsers).forEach((name) => {
|
|
|
|
const client = _.find(this.clients, {name});
|
|
|
|
|
|
|
|
if (client) {
|
|
|
|
client.quit(true);
|
|
|
|
this.clients = _.without(this.clients, client);
|
|
|
|
log.info(`User ${colors.bold(name)} disconnected and removed.`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
1000,
|
|
|
|
{maxWait: 10000}
|
|
|
|
)
|
|
|
|
);
|
2014-08-14 01:51:54 +00:00
|
|
|
};
|
|
|
|
|
2020-03-21 20:55:36 +00:00
|
|
|
ClientManager.prototype.loadUser = function (name) {
|
2017-09-30 22:19:53 +00:00
|
|
|
const userConfig = readUserConfig(name);
|
2017-10-15 16:05:19 +00:00
|
|
|
|
2017-09-30 22:19:53 +00:00
|
|
|
if (!userConfig) {
|
2014-08-14 01:51:54 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-10-15 16:05:19 +00:00
|
|
|
|
|
|
|
let client = this.findClient(name);
|
|
|
|
|
|
|
|
if (client) {
|
2017-09-30 22:19:53 +00:00
|
|
|
if (userConfig.password !== client.config.password) {
|
|
|
|
/**
|
|
|
|
* If we happen to reload an existing client, make super duper sure we
|
|
|
|
* have their latest password. We're not replacing the entire config
|
|
|
|
* object, because that could have undesired consequences.
|
|
|
|
*
|
2018-02-21 17:48:22 +00:00
|
|
|
* @see https://github.com/thelounge/thelounge/issues/598
|
2017-09-30 22:19:53 +00:00
|
|
|
*/
|
|
|
|
client.config.password = userConfig.password;
|
|
|
|
log.info(`Password for user ${colors.bold(name)} was reset.`);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
client = new Client(this, name, userConfig);
|
|
|
|
this.clients.push(client);
|
2014-09-24 22:23:54 +00:00
|
|
|
}
|
2018-02-20 07:28:04 +00:00
|
|
|
|
2017-10-15 16:05:19 +00:00
|
|
|
return client;
|
2014-08-13 23:43:11 +00:00
|
|
|
};
|
|
|
|
|
2020-03-21 20:55:36 +00:00
|
|
|
ClientManager.prototype.getUsers = function () {
|
2017-10-15 16:05:19 +00:00
|
|
|
return fs
|
2017-12-07 03:54:54 +00:00
|
|
|
.readdirSync(Helper.getUsersPath())
|
2017-10-15 16:05:19 +00:00
|
|
|
.filter((file) => file.endsWith(".json"))
|
|
|
|
.map((file) => file.slice(0, -5));
|
2014-08-13 23:43:11 +00:00
|
|
|
};
|
|
|
|
|
2020-03-21 20:55:36 +00:00
|
|
|
ClientManager.prototype.addUser = function (name, password, enableLog) {
|
2017-10-15 16:05:19 +00:00
|
|
|
if (path.basename(name) !== name) {
|
|
|
|
throw new Error(`${name} is an invalid username.`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const userPath = Helper.getUserConfigPath(name);
|
|
|
|
|
|
|
|
if (fs.existsSync(userPath)) {
|
|
|
|
log.error(`User ${colors.green(name)} already exists.`);
|
2014-08-14 17:25:22 +00:00
|
|
|
return false;
|
2014-08-13 23:43:11 +00:00
|
|
|
}
|
2016-04-02 21:19:57 +00:00
|
|
|
|
2017-10-15 16:05:19 +00:00
|
|
|
const user = {
|
|
|
|
password: password || "",
|
2018-02-24 01:07:08 +00:00
|
|
|
log: enableLog,
|
2017-10-15 16:05:19 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
|
|
|
fs.writeFileSync(userPath, JSON.stringify(user, null, "\t"));
|
2015-09-30 22:39:57 +00:00
|
|
|
} catch (e) {
|
2017-10-15 16:05:19 +00:00
|
|
|
log.error(`Failed to create user ${colors.green(name)} (${e})`);
|
2014-08-13 23:43:11 +00:00
|
|
|
throw e;
|
|
|
|
}
|
2017-10-15 16:05:19 +00:00
|
|
|
|
2019-09-24 10:56:12 +00:00
|
|
|
try {
|
|
|
|
const userFolderStat = fs.statSync(Helper.getUsersPath());
|
|
|
|
const userFileStat = fs.statSync(userPath);
|
|
|
|
|
|
|
|
if (
|
|
|
|
userFolderStat &&
|
|
|
|
userFileStat &&
|
|
|
|
(userFolderStat.uid !== userFileStat.uid || userFolderStat.gid !== userFileStat.gid)
|
|
|
|
) {
|
|
|
|
log.warn(
|
|
|
|
`User ${colors.green(
|
|
|
|
name
|
|
|
|
)} has been created, but with a different uid (or gid) than expected.`
|
|
|
|
);
|
|
|
|
log.warn(
|
|
|
|
"The file owner has been changed to the expected user. " +
|
|
|
|
"To prevent any issues, please run thelounge commands " +
|
|
|
|
"as the correct user that owns the config folder."
|
|
|
|
);
|
|
|
|
log.warn(
|
|
|
|
"See https://thelounge.chat/docs/usage#using-the-correct-system-user for more information."
|
|
|
|
);
|
|
|
|
fs.chownSync(userPath, userFolderStat.uid, userFolderStat.gid);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
// We're simply verifying file owner as a safe guard for users
|
|
|
|
// that run `thelounge add` as root, so we don't care if it fails
|
|
|
|
}
|
|
|
|
|
2014-08-14 17:25:22 +00:00
|
|
|
return true;
|
2014-08-13 23:43:11 +00:00
|
|
|
};
|
|
|
|
|
2020-03-21 20:55:36 +00:00
|
|
|
ClientManager.prototype.getDataToSave = function (client) {
|
2019-12-15 15:26:18 +00:00
|
|
|
const json = Object.assign({}, client.config, {
|
|
|
|
networks: client.networks.map((n) => n.export()),
|
|
|
|
});
|
|
|
|
const newUser = JSON.stringify(json, null, "\t");
|
2020-03-21 20:55:36 +00:00
|
|
|
const newHash = crypto.createHash("sha256").update(newUser).digest("hex");
|
2019-12-18 09:06:20 +00:00
|
|
|
|
|
|
|
return {newUser, newHash};
|
|
|
|
};
|
|
|
|
|
2020-03-21 20:55:36 +00:00
|
|
|
ClientManager.prototype.saveUser = function (client, callback) {
|
2019-12-18 09:06:20 +00:00
|
|
|
const {newUser, newHash} = this.getDataToSave(client);
|
|
|
|
|
|
|
|
// Do not write to disk if the exported data hasn't actually changed
|
|
|
|
if (client.fileHash === newHash) {
|
|
|
|
return;
|
|
|
|
}
|
2017-10-15 16:05:19 +00:00
|
|
|
|
2019-12-15 15:26:18 +00:00
|
|
|
const pathReal = Helper.getUserConfigPath(client.name);
|
2019-12-15 14:56:17 +00:00
|
|
|
const pathTemp = pathReal + ".tmp";
|
|
|
|
|
2017-11-11 18:44:09 +00:00
|
|
|
try {
|
2019-12-15 14:56:17 +00:00
|
|
|
// Write to a temp file first, in case the write fails
|
|
|
|
// we do not lose the original file (for example when disk is full)
|
|
|
|
fs.writeFileSync(pathTemp, newUser);
|
|
|
|
fs.renameSync(pathTemp, pathReal);
|
|
|
|
|
2018-01-13 23:05:23 +00:00
|
|
|
return callback ? callback() : true;
|
2017-11-11 18:44:09 +00:00
|
|
|
} catch (e) {
|
2019-12-15 15:26:18 +00:00
|
|
|
log.error(`Failed to update user ${colors.green(client.name)} (${e})`);
|
2018-02-20 07:28:04 +00:00
|
|
|
|
2018-01-13 23:05:23 +00:00
|
|
|
if (callback) {
|
|
|
|
callback(e);
|
|
|
|
}
|
2017-11-11 18:44:09 +00:00
|
|
|
}
|
2016-02-17 00:14:43 +00:00
|
|
|
};
|
|
|
|
|
2020-03-21 20:55:36 +00:00
|
|
|
ClientManager.prototype.removeUser = function (name) {
|
2017-10-15 16:05:19 +00:00
|
|
|
const userPath = Helper.getUserConfigPath(name);
|
|
|
|
|
|
|
|
if (!fs.existsSync(userPath)) {
|
|
|
|
log.error(`Tried to remove non-existing user ${colors.green(name)}.`);
|
2016-02-17 00:14:43 +00:00
|
|
|
return false;
|
|
|
|
}
|
2017-10-15 16:05:19 +00:00
|
|
|
|
|
|
|
fs.unlinkSync(userPath);
|
|
|
|
|
|
|
|
return true;
|
2016-02-17 00:14:43 +00:00
|
|
|
};
|
|
|
|
|
2017-10-15 16:05:19 +00:00
|
|
|
function readUserConfig(name) {
|
|
|
|
const userPath = Helper.getUserConfigPath(name);
|
|
|
|
|
|
|
|
if (!fs.existsSync(userPath)) {
|
|
|
|
log.error(`Tried to read non-existing user ${colors.green(name)}`);
|
2014-08-14 17:25:22 +00:00
|
|
|
return false;
|
2014-08-13 23:43:11 +00:00
|
|
|
}
|
2017-10-15 16:05:19 +00:00
|
|
|
|
2018-03-02 10:37:36 +00:00
|
|
|
try {
|
|
|
|
const data = fs.readFileSync(userPath, "utf-8");
|
|
|
|
return JSON.parse(data);
|
|
|
|
} catch (e) {
|
|
|
|
log.error(`Failed to read user ${colors.bold(name)}: ${e}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2017-10-15 16:05:19 +00:00
|
|
|
}
|