2022-06-19 00:25:21 +00:00
|
|
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
|
|
import path from "path";
|
|
|
|
import fs, {Stats} from "fs";
|
|
|
|
import os from "os";
|
|
|
|
import _ from "lodash";
|
|
|
|
import colors from "chalk";
|
|
|
|
|
|
|
|
import log from "./log";
|
|
|
|
import Helper from "./helper";
|
|
|
|
import Utils from "./command-line/utils";
|
|
|
|
import Network from "./models/network";
|
|
|
|
|
|
|
|
// TODO: Type this
|
|
|
|
export type WebIRC = {
|
|
|
|
[key: string]: any;
|
|
|
|
};
|
|
|
|
|
|
|
|
type Https = {
|
|
|
|
enable: boolean;
|
|
|
|
key: string;
|
|
|
|
certificate: string;
|
|
|
|
ca: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
type FileUpload = {
|
|
|
|
enable: boolean;
|
|
|
|
maxFileSize: number;
|
|
|
|
baseUrl?: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type Defaults = Pick<
|
|
|
|
Network,
|
|
|
|
| "name"
|
|
|
|
| "host"
|
|
|
|
| "port"
|
|
|
|
| "password"
|
|
|
|
| "tls"
|
|
|
|
| "rejectUnauthorized"
|
|
|
|
| "nick"
|
|
|
|
| "username"
|
|
|
|
| "realname"
|
|
|
|
| "leaveMessage"
|
|
|
|
| "sasl"
|
|
|
|
| "saslAccount"
|
|
|
|
| "saslPassword"
|
|
|
|
> & {
|
|
|
|
join?: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
type Identd = {
|
|
|
|
enable: boolean;
|
|
|
|
port: number;
|
|
|
|
};
|
|
|
|
|
|
|
|
type SearchDN = {
|
|
|
|
rootDN: string;
|
|
|
|
rootPassword: string;
|
|
|
|
filter: string;
|
|
|
|
base: string;
|
|
|
|
scope: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
type Ldap = {
|
|
|
|
enable: boolean;
|
|
|
|
url: string;
|
|
|
|
tlsOptions: any;
|
|
|
|
primaryKey: string;
|
|
|
|
searchDN: SearchDN;
|
|
|
|
baseDN?: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
type TlsOptions = any;
|
|
|
|
|
|
|
|
type Debug = {
|
|
|
|
ircFramework: boolean;
|
|
|
|
raw: boolean;
|
|
|
|
};
|
|
|
|
|
2024-02-02 03:05:47 +00:00
|
|
|
type StoragePolicy = {
|
|
|
|
enabled: boolean;
|
|
|
|
maxAgeDays: number;
|
|
|
|
deletionPolicy: "statusOnly" | "everything";
|
|
|
|
};
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
export type ConfigType = {
|
|
|
|
public: boolean;
|
|
|
|
host: string | undefined;
|
|
|
|
port: number;
|
|
|
|
bind: string | undefined;
|
|
|
|
reverseProxy: boolean;
|
|
|
|
maxHistory: number;
|
|
|
|
https: Https;
|
|
|
|
theme: string;
|
|
|
|
prefetch: boolean;
|
|
|
|
disableMediaPreview: boolean;
|
|
|
|
prefetchStorage: boolean;
|
|
|
|
prefetchMaxImageSize: number;
|
|
|
|
prefetchMaxSearchSize: number;
|
|
|
|
prefetchTimeout: number;
|
|
|
|
fileUpload: FileUpload;
|
|
|
|
transports: string[];
|
|
|
|
leaveMessage: string;
|
|
|
|
defaults: Defaults;
|
|
|
|
lockNetwork: boolean;
|
|
|
|
messageStorage: string[];
|
2024-02-02 03:05:47 +00:00
|
|
|
storagePolicy: StoragePolicy;
|
2022-06-19 00:25:21 +00:00
|
|
|
useHexIp: boolean;
|
|
|
|
webirc?: WebIRC;
|
|
|
|
identd: Identd;
|
|
|
|
oidentd?: string;
|
|
|
|
ldap: Ldap;
|
|
|
|
debug: Debug;
|
|
|
|
themeColor: string;
|
|
|
|
};
|
2022-05-01 19:12:39 +00:00
|
|
|
|
|
|
|
class Config {
|
2022-06-19 00:25:21 +00:00
|
|
|
values = require(path.resolve(
|
|
|
|
path.join(__dirname, "..", "defaults", "config.js")
|
|
|
|
)) as ConfigType;
|
|
|
|
#homePath = "";
|
2022-05-01 19:12:39 +00:00
|
|
|
|
|
|
|
getHomePath() {
|
|
|
|
return this.#homePath;
|
|
|
|
}
|
|
|
|
|
|
|
|
getConfigPath() {
|
|
|
|
return path.join(this.#homePath, "config.js");
|
|
|
|
}
|
|
|
|
|
|
|
|
getUserLogsPath() {
|
|
|
|
return path.join(this.#homePath, "logs");
|
|
|
|
}
|
|
|
|
|
|
|
|
getStoragePath() {
|
|
|
|
return path.join(this.#homePath, "storage");
|
|
|
|
}
|
|
|
|
|
|
|
|
getFileUploadPath() {
|
|
|
|
return path.join(this.#homePath, "uploads");
|
|
|
|
}
|
|
|
|
|
|
|
|
getUsersPath() {
|
|
|
|
return path.join(this.#homePath, "users");
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
getUserConfigPath(name: string) {
|
2022-05-01 19:12:39 +00:00
|
|
|
return path.join(this.getUsersPath(), `${name}.json`);
|
|
|
|
}
|
|
|
|
|
|
|
|
getClientCertificatesPath() {
|
|
|
|
return path.join(this.#homePath, "certificates");
|
|
|
|
}
|
|
|
|
|
|
|
|
getPackagesPath() {
|
|
|
|
return path.join(this.#homePath, "packages");
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
getPackageModulePath(packageName: string) {
|
2022-05-01 19:12:39 +00:00
|
|
|
return path.join(this.getPackagesPath(), "node_modules", packageName);
|
|
|
|
}
|
|
|
|
|
|
|
|
getDefaultNick() {
|
|
|
|
if (!this.values.defaults.nick) {
|
|
|
|
return "thelounge";
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
return this.values.defaults.nick.replace(/%/g, () =>
|
|
|
|
Math.floor(Math.random() * 10).toString()
|
|
|
|
);
|
2022-05-01 19:12:39 +00:00
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
merge(newConfig: ConfigType) {
|
2022-05-01 19:12:39 +00:00
|
|
|
this._merge_config_objects(this.values, newConfig);
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
_merge_config_objects(oldConfig: ConfigType, newConfig: ConfigType) {
|
2022-05-01 19:12:39 +00:00
|
|
|
// semi exposed function so that we can test it
|
|
|
|
// it mutates the oldConfig, but returns it as a convenience for testing
|
|
|
|
|
|
|
|
for (const key in newConfig) {
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(oldConfig, key)) {
|
|
|
|
log.warn(`Unknown key "${colors.bold(key)}", please verify your config.`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return _.mergeWith(oldConfig, newConfig, (objValue, srcValue, key) => {
|
|
|
|
// Do not override config variables if the type is incorrect (e.g. object changed into a string)
|
|
|
|
if (
|
|
|
|
typeof objValue !== "undefined" &&
|
|
|
|
objValue !== null &&
|
|
|
|
typeof objValue !== typeof srcValue
|
|
|
|
) {
|
|
|
|
log.warn(`Incorrect type for "${colors.bold(key)}", please verify your config.`);
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
2022-05-01 19:12:39 +00:00
|
|
|
return objValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// For arrays, simply override the value with user provided one.
|
|
|
|
if (_.isArray(objValue)) {
|
2022-06-19 00:25:21 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
2022-05-01 19:12:39 +00:00
|
|
|
return srcValue;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
setHome(newPath: string) {
|
2022-05-01 19:12:39 +00:00
|
|
|
this.#homePath = Helper.expandHome(newPath);
|
|
|
|
|
|
|
|
// Reload config from new home location
|
|
|
|
const configPath = this.getConfigPath();
|
|
|
|
|
|
|
|
if (fs.existsSync(configPath)) {
|
|
|
|
const userConfig = require(configPath);
|
|
|
|
|
|
|
|
if (_.isEmpty(userConfig)) {
|
|
|
|
log.warn(
|
|
|
|
`The file located at ${colors.green(
|
|
|
|
configPath
|
|
|
|
)} does not appear to expose anything.`
|
|
|
|
);
|
|
|
|
log.warn(
|
|
|
|
`Make sure it is non-empty and the configuration is exported using ${colors.bold(
|
|
|
|
"module.exports = { ... }"
|
|
|
|
)}.`
|
|
|
|
);
|
|
|
|
log.warn("Using default configuration...");
|
|
|
|
}
|
|
|
|
|
|
|
|
this.merge(userConfig);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.values.fileUpload.baseUrl) {
|
|
|
|
try {
|
|
|
|
new URL("test/file.png", this.values.fileUpload.baseUrl);
|
2022-06-19 00:25:21 +00:00
|
|
|
} catch (e: any) {
|
|
|
|
this.values.fileUpload.baseUrl = undefined;
|
2022-05-01 19:12:39 +00:00
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
log.warn(
|
|
|
|
`The ${colors.bold("fileUpload.baseUrl")} you specified is invalid: ${String(
|
|
|
|
e
|
|
|
|
)}`
|
|
|
|
);
|
2022-05-01 19:12:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
const manifestPath = Utils.getFileFromRelativeToRoot("public", "thelounge.webmanifest");
|
2022-05-01 19:12:39 +00:00
|
|
|
|
|
|
|
// Check if manifest exists, if not, the app most likely was not built
|
|
|
|
if (!fs.existsSync(manifestPath)) {
|
|
|
|
log.error(
|
|
|
|
`The client application was not built. Run ${colors.bold(
|
|
|
|
"NODE_ENV=production yarn build"
|
|
|
|
)} to resolve this.`
|
|
|
|
);
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load theme color from the web manifest
|
|
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
|
|
this.values.themeColor = manifest.theme_color;
|
|
|
|
|
|
|
|
// log dir probably shouldn't be world accessible.
|
|
|
|
// Create it with the desired permission bits if it doesn't exist yet.
|
2022-06-19 00:25:21 +00:00
|
|
|
let logsStat: Stats | undefined = undefined;
|
2022-05-01 19:12:39 +00:00
|
|
|
|
|
|
|
const userLogsPath = this.getUserLogsPath();
|
|
|
|
|
|
|
|
try {
|
|
|
|
logsStat = fs.statSync(userLogsPath);
|
|
|
|
} catch {
|
|
|
|
// ignored on purpose, node v14.17.0 will give us {throwIfNoEntry: false}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!logsStat) {
|
|
|
|
try {
|
|
|
|
fs.mkdirSync(userLogsPath, {recursive: true, mode: 0o750});
|
2022-06-19 00:25:21 +00:00
|
|
|
} catch (e: any) {
|
2022-05-01 19:12:39 +00:00
|
|
|
log.error("Unable to create logs directory", e);
|
|
|
|
}
|
|
|
|
} else if (logsStat && logsStat.mode & 0o001) {
|
|
|
|
log.warn(
|
|
|
|
userLogsPath,
|
2022-06-19 00:25:21 +00:00
|
|
|
"is world readable.",
|
|
|
|
"The log files may be exposed. Please fix the permissions."
|
2022-05-01 19:12:39 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
if (os.platform() !== "win32") {
|
2022-06-19 00:25:21 +00:00
|
|
|
log.warn(`run \`chmod o-x "${userLogsPath}"\` to correct it.`);
|
2022-05-01 19:12:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
export default new Config();
|