mirror of
https://github.com/thelounge/thelounge
synced 2024-11-21 19:43:07 +00:00
first pass
This commit is contained in:
parent
106937dae5
commit
8e4246129d
11 changed files with 187 additions and 495 deletions
|
@ -52,7 +52,8 @@
|
|||
"./client/index.html.tpl",
|
||||
"./dist/package.json",
|
||||
"./dist/**/*.js",
|
||||
"./public/**"
|
||||
"./public/**",
|
||||
"./packages/**"
|
||||
],
|
||||
"dependencies": {
|
||||
"@fastify/busboy": "1.0.0",
|
||||
|
|
15
packages/auth/local/helper.js
Normal file
15
packages/auth/local/helper.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const bcrypt = require("bcryptjs");
|
||||
|
||||
module.exports = {
|
||||
password: {
|
||||
hash(password) {
|
||||
return bcrypt.hashSync(password, bcrypt.genSaltSync(11));
|
||||
},
|
||||
compare(password, expected) {
|
||||
return bcrypt.compare(password, expected);
|
||||
},
|
||||
requiresUpdate(password) {
|
||||
return bcrypt.getRounds(password) !== 11;
|
||||
},
|
||||
},
|
||||
};
|
48
packages/auth/local/index.js
Normal file
48
packages/auth/local/index.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
const Helper = require("./helper");
|
||||
|
||||
async function localAuth(api, client, providerData) {
|
||||
const user = providerData && providerData.user;
|
||||
const password = providerData && providerData.password;
|
||||
|
||||
// If no user is found, or if the client has not provided a password,
|
||||
// fail the authentication straight away
|
||||
if (!password) {
|
||||
api.Logger.error(`No password specified!`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If this user has no password set, fail the authentication
|
||||
if (!client.config.password) {
|
||||
api.Logger.error(`User ${user} with no password tried to sign in`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return await Helper.password
|
||||
.compare(password, client.config.password)
|
||||
.then((matching) => {
|
||||
if (matching && Helper.password.requiresUpdate(client.config.password)) {
|
||||
const hash = Helper.password.hash(password);
|
||||
|
||||
client.setPassword(hash, (success) => {
|
||||
if (success) {
|
||||
api.Logger.info(
|
||||
`User ${user} logged in and their hashed password has been updated to match new security requirements`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return matching;
|
||||
})
|
||||
.catch((error) => {
|
||||
api.Logger.error(`Error while checking users password. Error: ${error}`);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
onServerStart(api) {
|
||||
api.Logger.info("LOADED!");
|
||||
api.Auth.register((client, data) => localAuth(api, client, data));
|
||||
},
|
||||
};
|
34
packages/auth/local/package.json
Normal file
34
packages/auth/local/package.json
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "thelounge-auth-plugin-local",
|
||||
"description": "Local Auth for thelounge",
|
||||
"version": "1.0.0",
|
||||
"preferGlobal": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thelounge/thelounge.git"
|
||||
},
|
||||
"homepage": "https://thelounge.chat/",
|
||||
"scripts": {},
|
||||
"keywords": [
|
||||
"lounge",
|
||||
"browser",
|
||||
"web",
|
||||
"chat",
|
||||
"client",
|
||||
"irc",
|
||||
"server",
|
||||
"thelounge",
|
||||
"auth"
|
||||
],
|
||||
"thelounge": {
|
||||
"supports": ">=4.4.3"
|
||||
},
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"./index.js",
|
||||
"./package.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"bcryptjs": "2.4.3"
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ import crypto from "crypto";
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import Auth from "./plugins/auth";
|
||||
import Client, {UserConfig} from "./client";
|
||||
import Config from "./config";
|
||||
import WebPush from "./plugins/webpush";
|
||||
|
@ -28,20 +27,9 @@ class ClientManager {
|
|||
|
||||
if (!Config.values.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 (!Config.values.ldap.enable) {
|
||||
this.autoloadUsers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findClient(name: string) {
|
||||
name = name.toLowerCase();
|
||||
return this.clients.find((u) => u.name.toLowerCase() === name);
|
||||
}
|
||||
|
||||
loadUsers() {
|
||||
let users = this.getUsers();
|
||||
|
||||
|
@ -74,75 +62,22 @@ class ClientManager {
|
|||
return true;
|
||||
});
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
autoloadUsers() {
|
||||
fs.watch(
|
||||
Config.getUsersPath(),
|
||||
_.debounce(
|
||||
() => {
|
||||
const loaded = this.clients.map((c) => c.name);
|
||||
const updatedUsers = this.getUsers();
|
||||
|
||||
if (updatedUsers.length === 0) {
|
||||
log.info(
|
||||
`There are currently no users. Create one with ${colors.bold(
|
||||
"thelounge add <name>"
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
// 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}
|
||||
)
|
||||
);
|
||||
// Fallback to loading all users
|
||||
users.forEach((name) => this.loadUser(name));
|
||||
}
|
||||
|
||||
loadUser(name: string) {
|
||||
const userConfig = this.readUserConfig(name);
|
||||
let userConfig = this.readUserConfig(name) ?? {
|
||||
clientSettings: {},
|
||||
log: true,
|
||||
password: "",
|
||||
sessions: {},
|
||||
};
|
||||
|
||||
if (!userConfig) {
|
||||
return;
|
||||
}
|
||||
name = name.toLowerCase();
|
||||
let client = this.clients.find((u) => u.name.toLowerCase() === name);
|
||||
|
||||
let client = this.findClient(name);
|
||||
|
||||
if (client) {
|
||||
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.
|
||||
*
|
||||
* @see https://github.com/thelounge/thelounge/issues/598
|
||||
*/
|
||||
client.config.password = userConfig.password;
|
||||
log.info(`Password for user ${colors.bold(name)} was reset.`);
|
||||
}
|
||||
} else {
|
||||
if (!client) {
|
||||
client = new Client(this, name, userConfig);
|
||||
client.connect();
|
||||
this.clients.push(client);
|
||||
|
@ -188,6 +123,14 @@ class ClientManager {
|
|||
throw e;
|
||||
}
|
||||
|
||||
this.fixUserPerms(name);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private fixUserPerms(name: string) {
|
||||
const userPath = Config.getUserConfigPath(name);
|
||||
|
||||
try {
|
||||
const userFolderStat = fs.statSync(Config.getUsersPath());
|
||||
const userFileStat = fs.statSync(userPath);
|
||||
|
@ -216,8 +159,6 @@ class ClientManager {
|
|||
// 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
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getDataToSave(client: Client) {
|
||||
|
@ -249,6 +190,8 @@ class ClientManager {
|
|||
});
|
||||
fs.renameSync(pathTemp, pathReal);
|
||||
|
||||
this.fixUserPerms(client.name);
|
||||
|
||||
return callback ? callback() : true;
|
||||
} catch (e: any) {
|
||||
log.error(`Failed to update user ${colors.green(client.name)} (${e})`);
|
||||
|
@ -277,7 +220,7 @@ class ClientManager {
|
|||
|
||||
if (!fs.existsSync(userPath)) {
|
||||
log.error(`Tried to read non-existing user ${colors.green(name)}`);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -287,7 +230,7 @@ class ClientManager {
|
|||
log.error(`Failed to read user ${colors.bold(name)}: ${e}`);
|
||||
}
|
||||
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,15 @@ program
|
|||
// our yarn invocation sets $HOME to the cachedir, so we must expand ~ now
|
||||
// else the path will be invalid when npm expands it.
|
||||
packageName = expandTildeInLocalPath(packageName);
|
||||
readFile = fspromises
|
||||
.readFile(path.join(packageName.substring("file:".length), "package.json"), "utf-8")
|
||||
.then((data) => JSON.parse(data) as typeof packageJson);
|
||||
} else if (packageName.startsWith("internal:")) {
|
||||
isLocalFile = true;
|
||||
// make it easy to install the internal packages
|
||||
packageName = packageName.substring("internal:".length);
|
||||
packageName = "file:" + path.resolve(__dirname, "../../../packages", packageName);
|
||||
|
||||
readFile = fspromises
|
||||
.readFile(path.join(packageName.substring("file:".length), "package.json"), "utf-8")
|
||||
.then((data) => JSON.parse(data) as typeof packageJson);
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
import colors from "chalk";
|
||||
import Client from "../client";
|
||||
import ClientManager from "../clientManager";
|
||||
import log from "../log";
|
||||
|
||||
export type AuthHandler = (
|
||||
manager: ClientManager,
|
||||
client: Client,
|
||||
user: string,
|
||||
password: string,
|
||||
callback: (success: boolean) => void
|
||||
) => void;
|
||||
|
||||
// The order defines priority: the first available plugin is used.
|
||||
// Always keep 'local' auth plugin at the end of the list; it should always be enabled.
|
||||
const plugins = [import("./auth/ldap"), import("./auth/local")];
|
||||
|
||||
const toExport = {
|
||||
moduleName: "<module with no name>",
|
||||
|
||||
// Must override: implements authentication mechanism
|
||||
auth: () => unimplemented("auth"),
|
||||
|
||||
// Optional to override: implements filter for loading users at start up
|
||||
// This allows an auth plugin to check if a user is still acceptable, if the plugin
|
||||
// can do so without access to the user's unhashed password.
|
||||
// Returning 'false' triggers fallback to default behaviour of loading all users
|
||||
loadUsers: () => false,
|
||||
// local auth should always be enabled, but check here to verify
|
||||
initialized: false,
|
||||
// TODO: fix typing
|
||||
async initialize() {
|
||||
if (toExport.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Override default API stubs with exports from first enabled plugin found
|
||||
const resolvedPlugins = await Promise.all(plugins);
|
||||
|
||||
for (const {default: plugin} of resolvedPlugins) {
|
||||
if (plugin.isEnabled()) {
|
||||
toExport.initialized = true;
|
||||
|
||||
for (const name in plugin) {
|
||||
toExport[name] = plugin[name];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!toExport.initialized) {
|
||||
log.error("None of the auth plugins is enabled");
|
||||
}
|
||||
},
|
||||
} as any;
|
||||
|
||||
function unimplemented(funcName: string) {
|
||||
log.debug(
|
||||
`Auth module ${colors.bold(toExport.moduleName)} doesn't implement function ${colors.bold(
|
||||
funcName
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
// Default API implementations
|
||||
export default toExport;
|
|
@ -1,244 +0,0 @@
|
|||
import ldap, {SearchOptions} from "ldapjs";
|
||||
import colors from "chalk";
|
||||
|
||||
import log from "../../log";
|
||||
import Config from "../../config";
|
||||
import type {AuthHandler} from "../auth";
|
||||
|
||||
function ldapAuthCommon(
|
||||
user: string,
|
||||
bindDN: string,
|
||||
password: string,
|
||||
callback: (success: boolean) => void
|
||||
) {
|
||||
const config = Config.values;
|
||||
|
||||
const ldapclient = ldap.createClient({
|
||||
url: config.ldap.url,
|
||||
tlsOptions: config.ldap.tlsOptions,
|
||||
});
|
||||
|
||||
ldapclient.on("error", function (err: Error) {
|
||||
log.error(`Unable to connect to LDAP server: ${err.toString()}`);
|
||||
callback(false);
|
||||
});
|
||||
|
||||
ldapclient.bind(bindDN, password, function (err) {
|
||||
ldapclient.unbind();
|
||||
|
||||
if (err) {
|
||||
log.error(`LDAP bind failed: ${err.toString()}`);
|
||||
callback(false);
|
||||
} else {
|
||||
callback(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function simpleLdapAuth(user: string, password: string, callback: (success: boolean) => void) {
|
||||
if (!user || !password) {
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
const config = Config.values;
|
||||
|
||||
const userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1");
|
||||
const bindDN = `${config.ldap.primaryKey}=${userDN},${config.ldap.baseDN || ""}`;
|
||||
|
||||
log.info(`Auth against LDAP ${config.ldap.url} with provided bindDN ${bindDN}`);
|
||||
|
||||
ldapAuthCommon(user, bindDN, password, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* LDAP auth using initial DN search (see config comment for ldap.searchDN)
|
||||
*/
|
||||
function advancedLdapAuth(user: string, password: string, callback: (success: boolean) => void) {
|
||||
if (!user || !password) {
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
const config = Config.values;
|
||||
const userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1");
|
||||
|
||||
const ldapclient = ldap.createClient({
|
||||
url: config.ldap.url,
|
||||
tlsOptions: config.ldap.tlsOptions,
|
||||
});
|
||||
|
||||
const base = config.ldap.searchDN.base;
|
||||
const searchOptions: SearchOptions = {
|
||||
scope: config.ldap.searchDN.scope,
|
||||
filter: `(&(${config.ldap.primaryKey}=${userDN})${config.ldap.searchDN.filter})`,
|
||||
attributes: ["dn"],
|
||||
};
|
||||
|
||||
ldapclient.on("error", function (err: Error) {
|
||||
log.error(`Unable to connect to LDAP server: ${err.toString()}`);
|
||||
callback(false);
|
||||
});
|
||||
|
||||
ldapclient.bind(config.ldap.searchDN.rootDN, config.ldap.searchDN.rootPassword, function (err) {
|
||||
if (err) {
|
||||
log.error("Invalid LDAP root credentials");
|
||||
ldapclient.unbind();
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ldapclient.search(base, searchOptions, function (err2, res) {
|
||||
if (err2) {
|
||||
log.warn(`LDAP User not found: ${userDN}`);
|
||||
ldapclient.unbind();
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let found = false;
|
||||
|
||||
res.on("searchEntry", function (entry) {
|
||||
found = true;
|
||||
const bindDN = entry.objectName;
|
||||
log.info(`Auth against LDAP ${config.ldap.url} with found bindDN ${bindDN || ""}`);
|
||||
ldapclient.unbind();
|
||||
|
||||
// TODO: Fix type !
|
||||
ldapAuthCommon(user, bindDN!, password, callback);
|
||||
});
|
||||
|
||||
res.on("error", function (err3: Error) {
|
||||
log.error(`LDAP error: ${err3.toString()}`);
|
||||
callback(false);
|
||||
});
|
||||
|
||||
res.on("end", function (result) {
|
||||
ldapclient.unbind();
|
||||
|
||||
if (!found) {
|
||||
log.warn(
|
||||
`LDAP Search did not find anything for: ${userDN} (${
|
||||
result?.status.toString() || "unknown"
|
||||
})`
|
||||
);
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const ldapAuth: AuthHandler = (manager, client, user, password, callback) => {
|
||||
// TODO: Enable the use of starttls() as an alternative to ldaps
|
||||
|
||||
// TODO: move this out of here and get rid of `manager` and `client` in
|
||||
// auth plugin API
|
||||
function callbackWrapper(valid: boolean) {
|
||||
if (valid && !client) {
|
||||
manager.addUser(user, null, true);
|
||||
}
|
||||
|
||||
callback(valid);
|
||||
}
|
||||
|
||||
let auth: typeof simpleLdapAuth | typeof advancedLdapAuth;
|
||||
|
||||
if ("baseDN" in Config.values.ldap) {
|
||||
auth = simpleLdapAuth;
|
||||
} else {
|
||||
auth = advancedLdapAuth;
|
||||
}
|
||||
|
||||
return auth(user, password, callbackWrapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Use the LDAP filter from config to check that users still exist before loading them
|
||||
* via the supplied callback function.
|
||||
*/
|
||||
|
||||
function advancedLdapLoadUsers(users: string[], callbackLoadUser) {
|
||||
const config = Config.values;
|
||||
|
||||
const ldapclient = ldap.createClient({
|
||||
url: config.ldap.url,
|
||||
tlsOptions: config.ldap.tlsOptions,
|
||||
});
|
||||
|
||||
const base = config.ldap.searchDN.base;
|
||||
|
||||
ldapclient.on("error", function (err: Error) {
|
||||
log.error(`Unable to connect to LDAP server: ${err.toString()}`);
|
||||
});
|
||||
|
||||
ldapclient.bind(config.ldap.searchDN.rootDN, config.ldap.searchDN.rootPassword, function (err) {
|
||||
if (err) {
|
||||
log.error("Invalid LDAP root credentials");
|
||||
return true;
|
||||
}
|
||||
|
||||
const remainingUsers = new Set(users);
|
||||
|
||||
const searchOptions: SearchOptions = {
|
||||
scope: config.ldap.searchDN.scope,
|
||||
filter: `${config.ldap.searchDN.filter}`,
|
||||
attributes: [config.ldap.primaryKey],
|
||||
paged: true,
|
||||
};
|
||||
|
||||
ldapclient.search(base, searchOptions, function (err2, res) {
|
||||
if (err2) {
|
||||
log.error(`LDAP search error: ${err2?.toString()}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
res.on("searchEntry", function (entry) {
|
||||
const user = entry.attributes[0].vals[0].toString();
|
||||
|
||||
if (remainingUsers.has(user)) {
|
||||
remainingUsers.delete(user);
|
||||
callbackLoadUser(user);
|
||||
}
|
||||
});
|
||||
|
||||
res.on("error", function (err3) {
|
||||
log.error(`LDAP error: ${err3.toString()}`);
|
||||
});
|
||||
|
||||
res.on("end", function () {
|
||||
remainingUsers.forEach((user) => {
|
||||
log.warn(
|
||||
`No account info in LDAP for ${colors.bold(
|
||||
user
|
||||
)} but user config file exists`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ldapclient.unbind();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function ldapLoadUsers(users: string[], callbackLoadUser) {
|
||||
if ("baseDN" in Config.values.ldap) {
|
||||
// simple LDAP case can't test for user existence without access to the
|
||||
// user's unhashed password, so indicate need to fallback to default
|
||||
// loadUser behaviour by returning false
|
||||
return false;
|
||||
}
|
||||
|
||||
return advancedLdapLoadUsers(users, callbackLoadUser);
|
||||
}
|
||||
|
||||
function isLdapEnabled() {
|
||||
return !Config.values.public && Config.values.ldap.enable;
|
||||
}
|
||||
|
||||
export default {
|
||||
moduleName: "ldap",
|
||||
auth: ldapAuth,
|
||||
isEnabled: isLdapEnabled,
|
||||
loadUsers: ldapLoadUsers,
|
||||
};
|
|
@ -1,51 +0,0 @@
|
|||
import colors from "chalk";
|
||||
import log from "../../log";
|
||||
import Helper from "../../helper";
|
||||
import type {AuthHandler} from "../auth";
|
||||
|
||||
const localAuth: AuthHandler = (_manager, client, user, password, callback) => {
|
||||
// If no user is found, or if the client has not provided a password,
|
||||
// fail the authentication straight away
|
||||
if (!client || !password) {
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
// If this user has no password set, fail the authentication
|
||||
if (!client.config.password) {
|
||||
log.error(
|
||||
`User ${colors.bold(
|
||||
user
|
||||
)} with no local password set tried to sign in. (Probably a LDAP user)`
|
||||
);
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
Helper.password
|
||||
.compare(password, client.config.password)
|
||||
.then((matching) => {
|
||||
if (matching && Helper.password.requiresUpdate(client.config.password)) {
|
||||
const hash = Helper.password.hash(password);
|
||||
|
||||
client.setPassword(hash, (success) => {
|
||||
if (success) {
|
||||
log.info(
|
||||
`User ${colors.bold(
|
||||
client.name
|
||||
)} logged in and their hashed password has been updated to match new security requirements`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
callback(matching);
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error(`Error while checking users password. Error: ${error}`);
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
moduleName: "local",
|
||||
auth: localAuth,
|
||||
isEnabled: () => true,
|
||||
};
|
|
@ -11,8 +11,11 @@ import fs from "fs";
|
|||
import Utils from "../../command-line/utils";
|
||||
import Client from "../../client";
|
||||
|
||||
type Package = {
|
||||
onServerStart: (packageApis: any) => void;
|
||||
export type AuthHandler = (client: Client, providerData: any) => Promise<boolean>;
|
||||
|
||||
export type PackageApis = ReturnType<typeof packageApis>;
|
||||
export type Package = {
|
||||
onServerStart: (packageApis: PackageApis) => void;
|
||||
};
|
||||
|
||||
const packageMap = new Map<string, Package>();
|
||||
|
@ -29,6 +32,7 @@ export type PackageInfo = {
|
|||
|
||||
const stylesheets: string[] = [];
|
||||
const files: string[] = [];
|
||||
const authProviders: [string, AuthHandler][] = [];
|
||||
|
||||
const TIME_TO_LIVE = 15 * 60 * 1000; // 15 minutes, in milliseconds
|
||||
|
||||
|
@ -42,6 +46,7 @@ export default {
|
|||
getFiles,
|
||||
getStylesheets,
|
||||
getPackage,
|
||||
getAuthProviders,
|
||||
loadPackages,
|
||||
outdated,
|
||||
};
|
||||
|
@ -49,6 +54,9 @@ export default {
|
|||
// TODO: verify binds worked. Used to be 'this' instead of 'packageApis'
|
||||
const packageApis = function (packageInfo: PackageInfo) {
|
||||
return {
|
||||
Auth: {
|
||||
register: addAuthProvider.bind(packageApis, packageInfo.packageName),
|
||||
},
|
||||
Stylesheets: {
|
||||
addFile: addStylesheet.bind(packageApis, packageInfo.packageName),
|
||||
},
|
||||
|
@ -107,6 +115,14 @@ function getEnabledPackages(packageJson: string) {
|
|||
return [];
|
||||
}
|
||||
|
||||
function addAuthProvider(packageName: string, handler: AuthHandler) {
|
||||
authProviders.push([packageName, handler]);
|
||||
}
|
||||
|
||||
function getAuthProviders() {
|
||||
return authProviders;
|
||||
}
|
||||
|
||||
function getPersistentStorageDir(packageName: string) {
|
||||
const dir = path.join(Config.getPackagesPath(), packageName);
|
||||
fs.mkdirSync(dir, {recursive: true}); // we don't care if it already exists or not
|
||||
|
|
|
@ -17,7 +17,6 @@ import Config, {ConfigType} from "./config";
|
|||
import Identification from "./identification";
|
||||
import changelog from "./plugins/changelog";
|
||||
import inputs from "./plugins/inputs";
|
||||
import Auth from "./plugins/auth";
|
||||
|
||||
import themes from "./plugins/packages/themes";
|
||||
themes.loadLocalThemes();
|
||||
|
@ -232,12 +231,8 @@ export default async function (
|
|||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
socket.on("error", (err) => log.error(`io socket error: ${err}`));
|
||||
|
||||
if (Config.values.public) {
|
||||
performAuthentication.call(socket, {});
|
||||
} else {
|
||||
socket.on("auth:perform", performAuthentication);
|
||||
socket.emit("auth:start", serverHash);
|
||||
}
|
||||
socket.on("auth:perform", performAuthentication);
|
||||
socket.emit("auth:start", serverHash);
|
||||
});
|
||||
|
||||
manager = new ClientManager();
|
||||
|
@ -917,14 +912,12 @@ function getServerConfiguration(): ServerConfiguration {
|
|||
return {...Config.values, ...{stylesheets: packages.getStylesheets()}};
|
||||
}
|
||||
|
||||
function performAuthentication(this: Socket, data: AuthPerformData) {
|
||||
async function performAuthentication(this: Socket, data: any) {
|
||||
if (!_.isPlainObject(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const socket = this;
|
||||
let client: Client | undefined;
|
||||
let token: string;
|
||||
|
||||
const finalInit = () => {
|
||||
let lastMessage = -1;
|
||||
|
@ -986,26 +979,6 @@ function performAuthentication(this: Socket, data: AuthPerformData) {
|
|||
});
|
||||
};
|
||||
|
||||
if (Config.values.public) {
|
||||
client = new Client(manager!);
|
||||
client.connect();
|
||||
manager!.clients.push(client);
|
||||
|
||||
const cb_client = client; // ensure TS can see we never have a nil client
|
||||
socket.on("disconnect", function () {
|
||||
manager!.clients = _.without(manager!.clients, cb_client);
|
||||
cb_client.quit();
|
||||
});
|
||||
|
||||
initClient();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.user !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const authCallback = (success: boolean) => {
|
||||
// Authorization failed
|
||||
if (!success) {
|
||||
|
@ -1027,23 +1000,32 @@ function performAuthentication(this: Socket, data: AuthPerformData) {
|
|||
return;
|
||||
}
|
||||
|
||||
// If authorization succeeded but there is no loaded user,
|
||||
// load it and find the user again (this happens with LDAP)
|
||||
if (!client) {
|
||||
client = manager!.loadUser(data.user);
|
||||
|
||||
if (!client) {
|
||||
throw new Error(`authCallback: ${data.user} not found after second lookup`);
|
||||
}
|
||||
}
|
||||
|
||||
initClient();
|
||||
};
|
||||
|
||||
client = manager!.findClient(data.user);
|
||||
let client: Client | undefined;
|
||||
let token: string;
|
||||
|
||||
if (Config.values.public) {
|
||||
client = new Client(manager!);
|
||||
client.connect();
|
||||
manager!.clients.push(client);
|
||||
|
||||
const cb_client = client; // ensure TS can see we never have a nil client
|
||||
socket.on("disconnect", function () {
|
||||
manager!.clients = _.without(manager!.clients, cb_client);
|
||||
cb_client.quit();
|
||||
});
|
||||
|
||||
initClient();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
client = manager!.loadUser(data.user);
|
||||
|
||||
// We have found an existing user and client has provided a token
|
||||
if (client && "token" in data && data.token) {
|
||||
if ("token" in data && data.token) {
|
||||
const providedToken = client.calculateTokenHash(data.token);
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(client.config.sessions, providedToken)) {
|
||||
|
@ -1054,16 +1036,22 @@ function performAuthentication(this: Socket, data: AuthPerformData) {
|
|||
}
|
||||
}
|
||||
|
||||
if (!("user" in data && "password" in data)) {
|
||||
log.warn("performAuthentication: callback data has no user or no password");
|
||||
authCallback(false);
|
||||
return;
|
||||
log.warn("auth provider count: " + packages.getAuthProviders().length);
|
||||
|
||||
for (const [providerName, handler] of packages.getAuthProviders()) {
|
||||
log.warn(`calling handler for ${providerName}`);
|
||||
|
||||
if (
|
||||
await handler(client, {user: data.user, password: data.password, ...data[providerName]})
|
||||
) {
|
||||
manager!.saveUser(client);
|
||||
authCallback(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Auth.initialize().then(() => {
|
||||
// Perform password checking
|
||||
Auth.auth(manager, client, data.user, data.password, authCallback);
|
||||
});
|
||||
log.warn("no auth providers succeeded!");
|
||||
authCallback(false);
|
||||
}
|
||||
|
||||
function reverseDnsLookup(ip: string, callback: (hostname: string) => void) {
|
||||
|
|
Loading…
Reference in a new issue