2022-06-19 00:25:21 +00:00
|
|
|
import _ from "lodash";
|
|
|
|
import {Server as wsServer} from "ws";
|
|
|
|
import express, {NextFunction, Request, Response} from "express";
|
|
|
|
import fs from "fs";
|
|
|
|
import path from "path";
|
2024-04-07 12:17:32 +00:00
|
|
|
import {Server as ioServer, Socket as ioSocket} from "socket.io";
|
2022-06-19 00:25:21 +00:00
|
|
|
import dns from "dns";
|
|
|
|
import colors from "chalk";
|
|
|
|
import net from "net";
|
|
|
|
|
|
|
|
import log from "./log";
|
|
|
|
import Client from "./client";
|
|
|
|
import ClientManager from "./clientManager";
|
|
|
|
import Uploader from "./plugins/uploader";
|
|
|
|
import Helper from "./helper";
|
|
|
|
import Config, {ConfigType, Defaults} from "./config";
|
|
|
|
import Identification from "./identification";
|
|
|
|
import changelog from "./plugins/changelog";
|
|
|
|
import inputs from "./plugins/inputs";
|
|
|
|
import Auth from "./plugins/auth";
|
|
|
|
|
|
|
|
import themes, {ThemeForClient} from "./plugins/packages/themes";
|
2018-01-05 17:40:34 +00:00
|
|
|
themes.loadLocalThemes();
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
import packages from "./plugins/packages/index";
|
|
|
|
import {NetworkWithIrcFramework} from "./models/network";
|
|
|
|
import Utils from "./command-line/utils";
|
|
|
|
import type {
|
|
|
|
ClientToServerEvents,
|
|
|
|
ServerToClientEvents,
|
|
|
|
InterServerEvents,
|
|
|
|
SocketData,
|
2024-02-15 22:01:22 +00:00
|
|
|
} from "../shared/types/socket-events";
|
2024-02-24 10:13:11 +00:00
|
|
|
import {ChanType} from "../shared/types/chan";
|
2024-02-29 06:53:23 +00:00
|
|
|
import {
|
|
|
|
LockedSharedConfiguration,
|
|
|
|
SharedConfiguration,
|
|
|
|
ConfigNetDefaults,
|
|
|
|
LockedConfigNetDefaults,
|
|
|
|
} from "../shared/types/config";
|
2022-06-19 00:25:21 +00:00
|
|
|
|
|
|
|
type ServerOptions = {
|
|
|
|
dev: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
type ServerConfiguration = ConfigType & {
|
|
|
|
stylesheets: string[];
|
|
|
|
};
|
|
|
|
|
|
|
|
type IndexTemplateConfiguration = ServerConfiguration & {
|
|
|
|
cacheBust: string;
|
|
|
|
};
|
|
|
|
|
2024-03-24 13:23:07 +00:00
|
|
|
type Socket = ioSocket<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
|
2024-04-07 12:17:32 +00:00
|
|
|
export type Server = ioServer<
|
|
|
|
ClientToServerEvents,
|
|
|
|
ServerToClientEvents,
|
|
|
|
InterServerEvents,
|
|
|
|
SocketData
|
|
|
|
>;
|
2024-03-24 13:23:07 +00:00
|
|
|
|
2017-08-28 09:18:31 +00:00
|
|
|
// A random number that will force clients to reload the page if it differs
|
|
|
|
const serverHash = Math.floor(Date.now() * Math.random());
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
let manager: ClientManager | null = null;
|
2014-08-14 16:35:37 +00:00
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
export default async function (
|
|
|
|
options: ServerOptions = {
|
|
|
|
dev: false,
|
|
|
|
}
|
|
|
|
) {
|
2016-12-17 09:51:33 +00:00
|
|
|
log.info(`The Lounge ${colors.green(Helper.getVersion())} \
|
2019-07-17 09:33:59 +00:00
|
|
|
(Node.js ${colors.green(process.versions.node)} on ${colors.green(process.platform)} ${
|
|
|
|
process.arch
|
|
|
|
})`);
|
2022-05-01 19:12:39 +00:00
|
|
|
log.info(`Configuration file: ${colors.green(Config.getConfigPath())}`);
|
2014-08-14 16:35:37 +00:00
|
|
|
|
2018-06-06 09:19:51 +00:00
|
|
|
const staticOptions = {
|
|
|
|
redirect: false,
|
|
|
|
maxAge: 86400 * 1000,
|
|
|
|
};
|
|
|
|
|
2019-11-07 20:19:54 +00:00
|
|
|
const app = express();
|
|
|
|
|
|
|
|
if (options.dev) {
|
2022-06-19 00:25:21 +00:00
|
|
|
(await import("./plugins/dev-server")).default(app);
|
2019-11-07 20:19:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
app.set("env", "production")
|
2017-11-12 18:24:21 +00:00
|
|
|
.disable("x-powered-by")
|
2016-05-01 17:27:10 +00:00
|
|
|
.use(allRequests)
|
2021-10-26 20:20:06 +00:00
|
|
|
.use(addSecurityHeaders)
|
2019-03-21 09:43:13 +00:00
|
|
|
.get("/", indexRequest)
|
|
|
|
.get("/service-worker.js", forceNoCacheRequest)
|
|
|
|
.get("/js/bundle.js.map", forceNoCacheRequest)
|
|
|
|
.get("/css/style.css.map", forceNoCacheRequest)
|
2022-06-19 00:25:21 +00:00
|
|
|
.use(express.static(Utils.getFileFromRelativeToRoot("public"), staticOptions))
|
2022-05-01 19:12:39 +00:00
|
|
|
.use("/storage/", express.static(Config.getStoragePath(), staticOptions));
|
2014-11-01 20:06:01 +00:00
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (Config.values.fileUpload.enable) {
|
2018-09-03 07:30:05 +00:00
|
|
|
Uploader.router(app);
|
|
|
|
}
|
|
|
|
|
2018-01-12 05:46:55 +00:00
|
|
|
// This route serves *installed themes only*. Local themes are served directly
|
|
|
|
// from the `public/themes/` folder as static assets, without entering this
|
2018-09-03 07:30:05 +00:00
|
|
|
// handler. Remember this if you make changes to this function, serving of
|
2018-01-12 05:46:55 +00:00
|
|
|
// local themes will not get those changes.
|
2017-06-22 21:09:55 +00:00
|
|
|
app.get("/themes/:theme.css", (req, res) => {
|
|
|
|
const themeName = req.params.theme;
|
2019-07-22 16:50:04 +00:00
|
|
|
const theme = themes.getByName(themeName);
|
2018-02-20 07:28:04 +00:00
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
if (theme === undefined || theme.filename === undefined) {
|
2017-06-22 21:09:55 +00:00
|
|
|
return res.status(404).send("Not found");
|
|
|
|
}
|
2018-02-20 07:28:04 +00:00
|
|
|
|
2019-07-22 16:50:04 +00:00
|
|
|
return res.sendFile(theme.filename);
|
2017-06-22 21:09:55 +00:00
|
|
|
});
|
|
|
|
|
2018-01-05 17:40:34 +00:00
|
|
|
app.get("/packages/:package/:filename", (req, res) => {
|
|
|
|
const packageName = req.params.package;
|
|
|
|
const fileName = req.params.filename;
|
|
|
|
const packageFile = packages.getPackage(packageName);
|
2018-02-20 07:28:04 +00:00
|
|
|
|
2019-10-02 09:28:08 +00:00
|
|
|
if (!packageFile || !packages.getFiles().includes(`${packageName}/${fileName}`)) {
|
2018-01-05 17:40:34 +00:00
|
|
|
return res.status(404).send("Not found");
|
|
|
|
}
|
2018-02-20 07:28:04 +00:00
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
const packagePath = Config.getPackageModulePath(packageName);
|
2018-01-05 17:40:34 +00:00
|
|
|
return res.sendFile(path.join(packagePath, fileName));
|
|
|
|
});
|
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (Config.values.public && (Config.values.ldap || {}).enable) {
|
2019-07-17 09:33:59 +00:00
|
|
|
log.warn(
|
|
|
|
"Server is public and set to use LDAP. Set to private mode if trying to use LDAP authentication."
|
|
|
|
);
|
2016-07-30 01:20:38 +00:00
|
|
|
}
|
|
|
|
|
2022-11-22 01:21:27 +00:00
|
|
|
let server: import("http").Server | import("https").Server;
|
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (!Config.values.https.enable) {
|
2022-06-19 00:25:21 +00:00
|
|
|
const createServer = (await import("http")).createServer;
|
|
|
|
server = createServer(app);
|
2014-09-26 23:26:21 +00:00
|
|
|
} else {
|
2022-05-01 19:12:39 +00:00
|
|
|
const keyPath = Helper.expandHome(Config.values.https.key);
|
|
|
|
const certPath = Helper.expandHome(Config.values.https.certificate);
|
|
|
|
const caPath = Helper.expandHome(Config.values.https.ca);
|
2017-04-13 21:05:28 +00:00
|
|
|
|
|
|
|
if (!keyPath.length || !fs.existsSync(keyPath)) {
|
2016-10-04 22:35:04 +00:00
|
|
|
log.error("Path to SSL key is invalid. Stopping server...");
|
2021-12-02 06:55:06 +00:00
|
|
|
process.exit(1);
|
2016-10-04 22:35:04 +00:00
|
|
|
}
|
2017-04-13 21:05:28 +00:00
|
|
|
|
|
|
|
if (!certPath.length || !fs.existsSync(certPath)) {
|
2016-10-04 22:35:04 +00:00
|
|
|
log.error("Path to SSL certificate is invalid. Stopping server...");
|
2021-12-02 06:55:06 +00:00
|
|
|
process.exit(1);
|
2016-10-04 22:35:04 +00:00
|
|
|
}
|
2017-04-13 21:05:28 +00:00
|
|
|
|
2017-04-10 18:49:58 +00:00
|
|
|
if (caPath.length && !fs.existsSync(caPath)) {
|
|
|
|
log.error("Path to SSL ca bundle is invalid. Stopping server...");
|
2021-12-02 06:55:06 +00:00
|
|
|
process.exit(1);
|
2017-04-10 18:49:58 +00:00
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
const createServer = (await import("https")).createServer;
|
|
|
|
server = createServer(
|
2019-07-17 09:33:59 +00:00
|
|
|
{
|
|
|
|
key: fs.readFileSync(keyPath),
|
|
|
|
cert: fs.readFileSync(certPath),
|
|
|
|
ca: caPath ? fs.readFileSync(caPath) : undefined,
|
|
|
|
},
|
|
|
|
app
|
|
|
|
);
|
2014-09-26 23:26:21 +00:00
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
let listenParams:
|
|
|
|
| string
|
|
|
|
| {
|
|
|
|
port: number;
|
|
|
|
host: string | undefined;
|
|
|
|
};
|
2017-08-31 18:56:20 +00:00
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (typeof Config.values.host === "string" && Config.values.host.startsWith("unix:")) {
|
|
|
|
listenParams = Config.values.host.replace(/^unix:/, "");
|
2017-08-31 18:56:20 +00:00
|
|
|
} else {
|
|
|
|
listenParams = {
|
2022-05-01 19:12:39 +00:00
|
|
|
port: Config.values.port,
|
|
|
|
host: Config.values.host,
|
2017-08-31 18:56:20 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
2017-09-03 12:13:56 +00:00
|
|
|
server.on("error", (err) => log.error(`${err}`));
|
|
|
|
|
2017-08-31 18:56:20 +00:00
|
|
|
server.listen(listenParams, () => {
|
|
|
|
if (typeof listenParams === "string") {
|
|
|
|
log.info("Available on socket " + colors.green(listenParams));
|
|
|
|
} else {
|
2022-05-01 19:12:39 +00:00
|
|
|
const protocol = Config.values.https.enable ? "https" : "http";
|
2022-06-19 00:25:21 +00:00
|
|
|
const address = server?.address();
|
|
|
|
|
|
|
|
if (address && typeof address !== "string") {
|
|
|
|
// TODO: Node may revert the Node 18 family string --> number change
|
|
|
|
// @ts-expect-error This condition will always return 'false' since the types 'string' and 'number' have no overlap.
|
|
|
|
if (address.family === "IPv6" || address.family === 6) {
|
|
|
|
address.address = "[" + address.address + "]";
|
|
|
|
}
|
2017-08-31 18:56:20 +00:00
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
log.info(
|
|
|
|
"Available at " +
|
|
|
|
colors.green(`${protocol}://${address.address}:${address.port}/`) +
|
|
|
|
` in ${colors.bold(Config.values.public ? "public" : "private")} mode`
|
|
|
|
);
|
2019-12-16 17:24:30 +00:00
|
|
|
}
|
2022-06-19 00:25:21 +00:00
|
|
|
}
|
2019-12-16 17:24:30 +00:00
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
// This should never happen
|
|
|
|
if (!server) {
|
|
|
|
return;
|
2017-08-31 18:56:20 +00:00
|
|
|
}
|
2017-08-23 05:19:50 +00:00
|
|
|
|
2024-04-07 12:17:32 +00:00
|
|
|
const sockets: Server = new ioServer(server, {
|
2022-06-19 00:25:21 +00:00
|
|
|
wsEngine: wsServer,
|
2019-08-03 09:03:02 +00:00
|
|
|
cookie: false,
|
2017-08-23 05:19:50 +00:00
|
|
|
serveClient: false,
|
2022-06-19 00:25:21 +00:00
|
|
|
|
|
|
|
// TODO: type as Server.Transport[]
|
|
|
|
transports: Config.values.transports as any,
|
2021-03-01 02:53:36 +00:00
|
|
|
pingTimeout: 60000,
|
2017-08-23 05:19:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
sockets.on("connect", (socket) => {
|
2022-06-19 00:25:21 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
2019-06-10 10:13:27 +00:00
|
|
|
socket.on("error", (err) => log.error(`io socket error: ${err}`));
|
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (Config.values.public) {
|
2017-08-23 05:19:50 +00:00
|
|
|
performAuthentication.call(socket, {});
|
|
|
|
} else {
|
2019-11-05 19:29:51 +00:00
|
|
|
socket.on("auth:perform", performAuthentication);
|
|
|
|
socket.emit("auth:start", serverHash);
|
2017-08-23 05:19:50 +00:00
|
|
|
}
|
|
|
|
});
|
2014-08-14 16:35:37 +00:00
|
|
|
|
2017-08-23 05:19:50 +00:00
|
|
|
manager = new ClientManager();
|
2019-07-05 14:02:57 +00:00
|
|
|
packages.loadPackages();
|
2016-12-07 05:50:11 +00:00
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
const defaultTheme = themes.getByName(Config.values.theme);
|
2019-07-22 16:50:04 +00:00
|
|
|
|
|
|
|
if (defaultTheme === undefined) {
|
|
|
|
log.warn(
|
|
|
|
`The specified default theme "${colors.red(
|
2022-05-01 19:12:39 +00:00
|
|
|
Config.values.theme
|
2019-07-22 16:50:04 +00:00
|
|
|
)}" does not exist, verify your config.`
|
|
|
|
);
|
2022-05-01 19:12:39 +00:00
|
|
|
Config.values.theme = "default";
|
2019-07-22 16:50:04 +00:00
|
|
|
} else if (defaultTheme.themeColor) {
|
2022-05-01 19:12:39 +00:00
|
|
|
Config.values.themeColor = defaultTheme.themeColor;
|
2019-07-22 16:50:04 +00:00
|
|
|
}
|
|
|
|
|
2022-04-12 00:45:36 +00:00
|
|
|
new Identification((identHandler, err) => {
|
|
|
|
if (err) {
|
|
|
|
log.error(`Could not start identd server, ${err.message}`);
|
|
|
|
process.exit(1);
|
2022-06-19 00:25:21 +00:00
|
|
|
} else if (!manager) {
|
|
|
|
log.error("Could not start identd server, ClientManager is undefined");
|
|
|
|
process.exit(1);
|
2022-04-12 00:45:36 +00:00
|
|
|
}
|
|
|
|
|
2017-08-23 05:19:50 +00:00
|
|
|
manager.init(identHandler, sockets);
|
|
|
|
});
|
2017-08-30 17:26:45 +00:00
|
|
|
|
|
|
|
// Handle ctrl+c and kill gracefully
|
2022-06-19 00:25:21 +00:00
|
|
|
let suicideTimeout: NodeJS.Timeout | null = null;
|
2018-02-20 07:28:04 +00:00
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
const exitGracefully = async function () {
|
2017-08-30 17:26:45 +00:00
|
|
|
if (suicideTimeout !== null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-12-18 11:08:18 +00:00
|
|
|
log.info("Exiting...");
|
|
|
|
|
|
|
|
// Close all client and IRC connections
|
2022-06-19 00:25:21 +00:00
|
|
|
if (manager) {
|
|
|
|
manager.clients.forEach((client) => client.quit());
|
|
|
|
}
|
2018-12-18 11:08:18 +00:00
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (Config.values.prefetchStorage) {
|
2017-12-18 14:58:43 +00:00
|
|
|
log.info("Clearing prefetch storage folder, this might take a while...");
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
(await import("./plugins/storage")).default.emptyDir();
|
2017-12-18 14:58:43 +00:00
|
|
|
}
|
|
|
|
|
2017-08-30 17:26:45 +00:00
|
|
|
// Forcefully exit after 3 seconds
|
|
|
|
suicideTimeout = setTimeout(() => process.exit(1), 3000);
|
|
|
|
|
|
|
|
// Close http server
|
2022-06-19 00:25:21 +00:00
|
|
|
server?.close(() => {
|
|
|
|
if (suicideTimeout !== null) {
|
|
|
|
clearTimeout(suicideTimeout);
|
|
|
|
}
|
|
|
|
|
2017-08-30 17:26:45 +00:00
|
|
|
process.exit(0);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
/* eslint-disable @typescript-eslint/no-misused-promises */
|
2017-08-30 17:26:45 +00:00
|
|
|
process.on("SIGINT", exitGracefully);
|
|
|
|
process.on("SIGTERM", exitGracefully);
|
2022-06-19 00:25:21 +00:00
|
|
|
/* eslint-enable @typescript-eslint/no-misused-promises */
|
2017-12-18 14:58:43 +00:00
|
|
|
|
|
|
|
// Clear storage folder after server starts successfully
|
2022-05-01 19:12:39 +00:00
|
|
|
if (Config.values.prefetchStorage) {
|
2022-06-19 00:25:21 +00:00
|
|
|
import("./plugins/storage")
|
|
|
|
.then(({default: storage}) => {
|
|
|
|
storage.emptyDir();
|
|
|
|
})
|
|
|
|
.catch((err: Error) => {
|
|
|
|
log.error(`Could not clear storage folder, ${err.message}`);
|
|
|
|
});
|
2017-12-18 14:58:43 +00:00
|
|
|
}
|
2017-10-06 09:53:08 +00:00
|
|
|
|
2020-01-17 10:17:37 +00:00
|
|
|
changelog.checkForUpdates(manager);
|
2016-12-17 09:51:33 +00:00
|
|
|
});
|
2017-10-06 09:53:08 +00:00
|
|
|
|
|
|
|
return server;
|
2022-06-19 00:25:21 +00:00
|
|
|
}
|
2014-08-14 16:35:37 +00:00
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
function getClientLanguage(socket: Socket): string | null {
|
2017-12-28 13:34:49 +00:00
|
|
|
const acceptLanguage = socket.handshake.headers["accept-language"];
|
|
|
|
|
|
|
|
if (typeof acceptLanguage === "string" && /^[\x00-\x7F]{1,50}$/.test(acceptLanguage)) {
|
|
|
|
// only allow ASCII strings between 1-50 characters in length
|
|
|
|
return acceptLanguage;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
function getClientIp(socket: Socket) {
|
2019-01-09 09:38:21 +00:00
|
|
|
let ip = socket.handshake.address || "127.0.0.1";
|
2016-07-31 00:54:09 +00:00
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (Config.values.reverseProxy) {
|
2022-06-19 00:25:21 +00:00
|
|
|
const forwarded = String(socket.handshake.headers["x-forwarded-for"])
|
2019-07-17 09:33:59 +00:00
|
|
|
.split(/\s*,\s*/)
|
|
|
|
.filter(Boolean);
|
2017-06-11 19:33:04 +00:00
|
|
|
|
|
|
|
if (forwarded.length && net.isIP(forwarded[0])) {
|
|
|
|
ip = forwarded[0];
|
|
|
|
}
|
2016-04-03 05:12:49 +00:00
|
|
|
}
|
2016-07-31 00:54:09 +00:00
|
|
|
|
|
|
|
return ip.replace(/^::ffff:/, "");
|
2016-04-03 05:12:49 +00:00
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
function getClientSecure(socket: Socket) {
|
2019-07-16 09:51:22 +00:00
|
|
|
let secure = socket.handshake.secure;
|
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (Config.values.reverseProxy && socket.handshake.headers["x-forwarded-proto"] === "https") {
|
2019-07-16 09:51:22 +00:00
|
|
|
secure = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return secure;
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
function allRequests(req: Request, res: Response, next: NextFunction) {
|
2016-05-01 17:27:10 +00:00
|
|
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
function addSecurityHeaders(req: Request, res: Response, next: NextFunction) {
|
2017-07-06 15:33:09 +00:00
|
|
|
const policies = [
|
2017-12-07 18:45:45 +00:00
|
|
|
"default-src 'none'", // default to nothing
|
2019-12-22 19:24:46 +00:00
|
|
|
"base-uri 'none'", // disallow <base>, has no fallback to default-src
|
2018-09-02 17:34:47 +00:00
|
|
|
"form-action 'self'", // 'self' to fix saving passwords in Firefox, even though login is handled in javascript
|
2017-12-07 18:45:45 +00:00
|
|
|
"connect-src 'self' ws: wss:", // allow self for polling; websockets
|
2018-01-30 09:23:34 +00:00
|
|
|
"style-src 'self' https: 'unsafe-inline'", // allow inline due to use in irc hex colors
|
2017-12-07 18:45:45 +00:00
|
|
|
"script-src 'self'", // javascript
|
|
|
|
"worker-src 'self'", // service worker
|
|
|
|
"manifest-src 'self'", // manifest.json
|
|
|
|
"font-src 'self' https:", // allow loading fonts from secure sites (e.g. google fonts)
|
|
|
|
"media-src 'self' https:", // self for notification sound; allow https media (audio previews)
|
2017-07-06 15:33:09 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
// If prefetch is enabled, but storage is not, we have to allow mixed content
|
2017-12-27 18:56:38 +00:00
|
|
|
// - https://user-images.githubusercontent.com is where we currently push our changelog screenshots
|
|
|
|
// - data: is required for the HTML5 video player
|
2022-05-01 19:12:39 +00:00
|
|
|
if (Config.values.prefetchStorage || !Config.values.prefetch) {
|
2017-12-27 18:56:38 +00:00
|
|
|
policies.push("img-src 'self' data: https://user-images.githubusercontent.com");
|
2017-07-06 15:33:09 +00:00
|
|
|
policies.unshift("block-all-mixed-content");
|
2017-12-07 18:45:45 +00:00
|
|
|
} else {
|
2017-12-27 18:56:38 +00:00
|
|
|
policies.push("img-src http: https: data:");
|
2017-07-06 15:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
res.setHeader("Content-Security-Policy", policies.join("; "));
|
2017-04-20 10:17:25 +00:00
|
|
|
res.setHeader("Referrer-Policy", "no-referrer");
|
2017-11-12 18:24:21 +00:00
|
|
|
|
2021-10-26 20:20:06 +00:00
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
function forceNoCacheRequest(req: Request, res: Response, next: NextFunction) {
|
2021-10-26 20:20:06 +00:00
|
|
|
// Intermittent proxies must not cache the following requests,
|
|
|
|
// browsers must fetch the latest version of these files (service worker, source maps)
|
|
|
|
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
function indexRequest(req: Request, res: Response) {
|
2021-10-26 20:20:06 +00:00
|
|
|
res.setHeader("Content-Type", "text/html");
|
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
return fs.readFile(
|
2022-06-19 00:25:21 +00:00
|
|
|
Utils.getFileFromRelativeToRoot("client/index.html.tpl"),
|
2019-07-17 09:33:59 +00:00
|
|
|
"utf-8",
|
|
|
|
(err, file) => {
|
|
|
|
if (err) {
|
|
|
|
throw err;
|
|
|
|
}
|
2017-11-12 18:24:21 +00:00
|
|
|
|
2022-11-22 02:07:29 +00:00
|
|
|
const config: IndexTemplateConfiguration = {
|
|
|
|
...getServerConfiguration(),
|
|
|
|
...{cacheBust: Helper.getVersionCacheBust()},
|
|
|
|
};
|
2019-03-08 10:29:49 +00:00
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
res.send(_.template(file)(config));
|
|
|
|
}
|
|
|
|
);
|
2014-08-14 16:35:37 +00:00
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
function initializeClient(
|
|
|
|
socket: Socket,
|
|
|
|
client: Client,
|
|
|
|
token: string,
|
|
|
|
lastMessage: number,
|
|
|
|
openChannel: number
|
|
|
|
) {
|
2019-11-05 19:29:51 +00:00
|
|
|
socket.off("auth:perform", performAuthentication);
|
|
|
|
socket.emit("auth:success");
|
2016-09-25 06:52:16 +00:00
|
|
|
|
2017-10-17 09:45:18 +00:00
|
|
|
client.clientAttach(socket.id, token);
|
|
|
|
|
2019-10-17 10:53:29 +00:00
|
|
|
// Client sends currently active channel on reconnect,
|
|
|
|
// pass it into `open` directly so it is verified and updated if necessary
|
|
|
|
if (openChannel) {
|
|
|
|
client.open(socket.id, openChannel);
|
|
|
|
|
|
|
|
// If client provided channel passes checks, use it. if client has invalid
|
|
|
|
// channel open (or windows like settings) then use last known server active channel
|
|
|
|
openChannel = client.attachedClients[socket.id].openChannel || client.lastActiveChannel;
|
|
|
|
} else {
|
|
|
|
openChannel = client.lastActiveChannel;
|
|
|
|
}
|
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (Config.values.fileUpload.enable) {
|
2018-09-04 09:08:30 +00:00
|
|
|
new Uploader(socket);
|
2018-09-03 07:30:05 +00:00
|
|
|
}
|
|
|
|
|
2020-03-21 20:55:36 +00:00
|
|
|
socket.on("disconnect", function () {
|
2018-10-21 08:05:05 +00:00
|
|
|
process.nextTick(() => client.clientDetach(socket.id));
|
2017-08-13 18:37:12 +00:00
|
|
|
});
|
2016-11-19 20:34:05 +00:00
|
|
|
|
2018-02-21 11:17:56 +00:00
|
|
|
socket.on("input", (data) => {
|
2020-08-07 16:52:50 +00:00
|
|
|
if (_.isPlainObject(data)) {
|
2017-08-13 18:37:12 +00:00
|
|
|
client.input(data);
|
|
|
|
}
|
2018-02-21 11:17:56 +00:00
|
|
|
});
|
2016-09-25 06:41:10 +00:00
|
|
|
|
2018-02-21 11:17:56 +00:00
|
|
|
socket.on("more", (data) => {
|
2020-08-07 16:52:50 +00:00
|
|
|
if (_.isPlainObject(data)) {
|
2018-01-07 13:04:37 +00:00
|
|
|
const history = client.more(data);
|
|
|
|
|
|
|
|
if (history !== null) {
|
|
|
|
socket.emit("more", history);
|
|
|
|
}
|
2017-08-13 18:37:12 +00:00
|
|
|
}
|
2018-02-21 11:17:56 +00:00
|
|
|
});
|
2017-08-13 18:37:12 +00:00
|
|
|
|
2018-03-15 08:37:32 +00:00
|
|
|
socket.on("network:new", (data) => {
|
2020-08-07 16:52:50 +00:00
|
|
|
if (_.isPlainObject(data)) {
|
2017-08-13 18:37:12 +00:00
|
|
|
// prevent people from overriding webirc settings
|
2017-11-28 17:25:15 +00:00
|
|
|
data.uuid = null;
|
2018-03-15 08:37:05 +00:00
|
|
|
data.commands = null;
|
2018-07-11 07:57:02 +00:00
|
|
|
data.ignoreList = null;
|
2017-08-13 18:37:12 +00:00
|
|
|
|
2023-03-14 20:24:06 +00:00
|
|
|
client.connectToNetwork(data);
|
2017-08-13 18:37:12 +00:00
|
|
|
}
|
2018-02-21 11:17:56 +00:00
|
|
|
});
|
2017-08-13 18:37:12 +00:00
|
|
|
|
2018-03-15 08:37:05 +00:00
|
|
|
socket.on("network:get", (data) => {
|
|
|
|
if (typeof data !== "string") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const network = _.find(client.networks, {uuid: data});
|
|
|
|
|
|
|
|
if (!network) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-11-02 18:45:00 +00:00
|
|
|
socket.emit("network:info", network.exportForEdit());
|
2018-03-15 08:37:05 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
socket.on("network:edit", (data) => {
|
2020-08-07 16:52:50 +00:00
|
|
|
if (!_.isPlainObject(data)) {
|
2018-03-15 08:37:05 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const network = _.find(client.networks, {uuid: data.uuid});
|
|
|
|
|
|
|
|
if (!network) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
(network as NetworkWithIrcFramework).edit(client, data);
|
2018-03-15 08:37:05 +00:00
|
|
|
});
|
|
|
|
|
2020-01-30 08:52:29 +00:00
|
|
|
socket.on("history:clear", (data) => {
|
2020-08-07 16:52:50 +00:00
|
|
|
if (_.isPlainObject(data)) {
|
2020-01-30 08:52:29 +00:00
|
|
|
client.clearHistory(data);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (!Config.values.public && !Config.values.ldap.enable) {
|
2018-02-21 11:17:56 +00:00
|
|
|
socket.on("change-password", (data) => {
|
2020-08-07 16:52:50 +00:00
|
|
|
if (_.isPlainObject(data)) {
|
2018-01-11 11:33:36 +00:00
|
|
|
const old = data.old_password;
|
|
|
|
const p1 = data.new_password;
|
|
|
|
const p2 = data.verify_password;
|
2018-02-20 07:28:04 +00:00
|
|
|
|
2019-03-03 15:19:48 +00:00
|
|
|
if (typeof p1 === "undefined" || p1 === "" || p1 !== p2) {
|
2017-08-13 18:37:12 +00:00
|
|
|
socket.emit("change-password", {
|
2019-03-03 15:19:48 +00:00
|
|
|
error: "",
|
2019-03-03 15:27:57 +00:00
|
|
|
success: false,
|
2017-08-13 18:37:12 +00:00
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2016-05-31 21:28:31 +00:00
|
|
|
|
2017-08-13 18:37:12 +00:00
|
|
|
Helper.password
|
|
|
|
.compare(old || "", client.config.password)
|
|
|
|
.then((matching) => {
|
|
|
|
if (!matching) {
|
|
|
|
socket.emit("change-password", {
|
2019-03-03 15:19:48 +00:00
|
|
|
error: "password_incorrect",
|
2019-03-03 15:27:57 +00:00
|
|
|
success: false,
|
2017-08-13 18:37:12 +00:00
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2018-02-20 07:28:04 +00:00
|
|
|
|
2017-08-13 18:37:12 +00:00
|
|
|
const hash = Helper.password.hash(p1);
|
2017-03-23 07:47:51 +00:00
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
client.setPassword(hash, (success: boolean) => {
|
|
|
|
const obj = {success: false, error: undefined} as {
|
|
|
|
success: boolean;
|
|
|
|
error: string | undefined;
|
|
|
|
};
|
2017-03-23 07:47:51 +00:00
|
|
|
|
2017-08-13 18:37:12 +00:00
|
|
|
if (success) {
|
2019-03-03 15:19:48 +00:00
|
|
|
obj.success = true;
|
2017-08-13 18:37:12 +00:00
|
|
|
} else {
|
2019-03-03 15:19:48 +00:00
|
|
|
obj.error = "update_failed";
|
2017-08-13 18:37:12 +00:00
|
|
|
}
|
2017-03-23 07:47:51 +00:00
|
|
|
|
2017-08-13 18:37:12 +00:00
|
|
|
socket.emit("change-password", obj);
|
2017-03-23 07:47:51 +00:00
|
|
|
});
|
2019-07-17 09:33:59 +00:00
|
|
|
})
|
2022-06-19 00:25:21 +00:00
|
|
|
.catch((error: Error) => {
|
|
|
|
log.error(`Error while checking users password. Error: ${error.message}`);
|
2017-08-13 18:37:12 +00:00
|
|
|
});
|
2016-02-17 02:29:44 +00:00
|
|
|
}
|
2018-02-21 11:17:56 +00:00
|
|
|
});
|
2017-08-13 18:37:12 +00:00
|
|
|
}
|
2017-07-24 06:01:25 +00:00
|
|
|
|
2018-02-21 11:17:56 +00:00
|
|
|
socket.on("open", (data) => {
|
|
|
|
client.open(socket.id, data);
|
|
|
|
});
|
2017-07-24 06:01:25 +00:00
|
|
|
|
2024-03-24 15:39:26 +00:00
|
|
|
socket.on("sort:networks", (data) => {
|
|
|
|
if (!_.isPlainObject(data)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!Array.isArray(data.order)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
client.sortNetworks(data.order);
|
|
|
|
});
|
|
|
|
|
|
|
|
socket.on("sort:channels", (data) => {
|
|
|
|
if (!_.isPlainObject(data)) {
|
|
|
|
return;
|
2017-08-13 18:37:12 +00:00
|
|
|
}
|
2024-03-24 15:39:26 +00:00
|
|
|
|
|
|
|
if (!Array.isArray(data.order) || typeof data.network !== "string") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
client.sortChannels(data.network, data.order);
|
2018-02-21 11:17:56 +00:00
|
|
|
});
|
2017-07-24 06:01:25 +00:00
|
|
|
|
2018-02-21 11:17:56 +00:00
|
|
|
socket.on("names", (data) => {
|
2020-08-07 16:52:50 +00:00
|
|
|
if (_.isPlainObject(data)) {
|
2017-08-13 18:37:12 +00:00
|
|
|
client.names(data);
|
|
|
|
}
|
2018-02-21 11:17:56 +00:00
|
|
|
});
|
2017-08-13 18:37:12 +00:00
|
|
|
|
2019-07-04 07:41:09 +00:00
|
|
|
socket.on("changelog", () => {
|
2022-06-19 00:25:21 +00:00
|
|
|
Promise.all([changelog.fetch(), packages.outdated()])
|
|
|
|
.then(([changelogData, packageUpdate]) => {
|
2019-07-04 07:41:09 +00:00
|
|
|
changelogData.packages = packageUpdate;
|
|
|
|
socket.emit("changelog", changelogData);
|
2022-06-19 00:25:21 +00:00
|
|
|
})
|
|
|
|
.catch((error: Error) => {
|
|
|
|
log.error(`Error while fetching changelog. Error: ${error.message}`);
|
|
|
|
});
|
Improve UI of the About section and changelog viewer
- Keep consistent width between the Help page and Changelog (which is already different from other windows 😠)
- Add icons to the About links
- Make sure `li` elements (i.e. all the lists in changelogs) are consistent in size with rest of the client
- Display version and release notes link on the "About The Lounge" header line, smaller, pushed to the right
- Check new releases when opening the Help window in order to display it without having to open the release notes. Release notes are being fed to the Changelog page at that moment to avoid fetching twice.
- Re-check version/fetch release notes after 24h. Since The Lounge can now run 24/7, reconnect when losing the network, we have to assume an "always-on" usage.
- Change icon, animate background color when getting response from GitHub to avoid flashing.
- Combine click handlers with our wonderful window management. These were the same handler, even with similar checks (`target` exists, etc.), just in 2 different places. This is necessary for the next item.
- Combine "Open release notes" and "Go back to Help" button behaviors with window management handlers. The window management code is gross as ever, and is in desperate need of a refactor, but at least there is no duplicated code for the same behavior + history management. This fixes the "Next" history behavior (however reloading the app while viewing the notes does not load on the notes, but this is a bug for a different PR!).
- Added a rule in the history management thingy: if a link we want to add history handling to has an `id`, store that in the state
- Added a button to go back to the Help window
- Fixed links to releases
- Send user to the GitHub issues *list* instead of *new issue form* because if they do not have a GitHub account, they will be redirected to the login page, which is a rather unpleasant experience when you are already confused...
- Fixed a bug that would return data about a new release in `latest` even though it is already the `current`. It was showing the current version as "The Lounge v... is now available".
- Added https://user-images.githubusercontent.com to the CSP rule when prefetch storage is enabled, because that is where we have stored screenshots in the changelog so far. Meh (we can improve that later if we decide to have a dedicated place for screenshots).
- Fetch changelog info even in public mode because users in public mode can access the release notes. They do not see the result of the version checker however.
2017-12-23 03:40:41 +00:00
|
|
|
});
|
2017-07-13 18:32:18 +00:00
|
|
|
|
2022-02-18 20:21:17 +00:00
|
|
|
// In public mode only one client can be connected,
|
|
|
|
// so there's no need to handle msg:preview:toggle
|
2022-05-01 19:12:39 +00:00
|
|
|
if (!Config.values.public) {
|
2022-02-18 20:21:17 +00:00
|
|
|
socket.on("msg:preview:toggle", (data) => {
|
|
|
|
if (_.isPlainObject(data)) {
|
|
|
|
return;
|
|
|
|
}
|
2018-02-21 11:17:56 +00:00
|
|
|
|
2022-02-18 20:21:17 +00:00
|
|
|
const networkAndChan = client.find(data.target);
|
|
|
|
const newState = Boolean(data.shown);
|
2018-02-20 07:28:04 +00:00
|
|
|
|
2022-02-18 20:21:17 +00:00
|
|
|
if (!networkAndChan) {
|
|
|
|
return;
|
|
|
|
}
|
2017-08-13 18:37:12 +00:00
|
|
|
|
2022-02-18 20:21:17 +00:00
|
|
|
// Process multiple message at once for /collapse and /expand commands
|
|
|
|
if (Array.isArray(data.messageIds)) {
|
|
|
|
for (const msgId of data.messageIds) {
|
|
|
|
const message = networkAndChan.chan.findMessage(msgId);
|
2019-11-04 10:41:04 +00:00
|
|
|
|
2022-02-18 20:21:17 +00:00
|
|
|
if (message) {
|
|
|
|
for (const preview of message.previews) {
|
|
|
|
preview.shown = newState;
|
|
|
|
}
|
|
|
|
}
|
2019-11-04 10:41:04 +00:00
|
|
|
}
|
|
|
|
|
2022-02-18 20:21:17 +00:00
|
|
|
return;
|
|
|
|
}
|
2019-11-04 10:41:04 +00:00
|
|
|
|
2024-04-01 20:45:58 +00:00
|
|
|
const message = data.msgId ? networkAndChan.chan.findMessage(data.msgId) : null;
|
2017-08-13 18:37:12 +00:00
|
|
|
|
2022-02-18 20:21:17 +00:00
|
|
|
if (!message) {
|
|
|
|
return;
|
|
|
|
}
|
2017-08-13 18:37:12 +00:00
|
|
|
|
2024-04-01 20:45:58 +00:00
|
|
|
const preview = data.link ? message.findPreview(data.link) : null;
|
2017-08-13 18:37:12 +00:00
|
|
|
|
2022-02-18 20:21:17 +00:00
|
|
|
if (preview) {
|
|
|
|
preview.shown = newState;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2017-07-24 06:01:25 +00:00
|
|
|
|
2019-11-04 09:21:05 +00:00
|
|
|
socket.on("mentions:get", () => {
|
|
|
|
socket.emit("mentions:list", client.mentions);
|
|
|
|
});
|
|
|
|
|
2021-10-12 22:56:39 +00:00
|
|
|
socket.on("mentions:dismiss", (msgId) => {
|
2019-11-04 09:21:05 +00:00
|
|
|
if (typeof msgId !== "number") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
client.mentions.splice(
|
|
|
|
client.mentions.findIndex((m) => m.msgId === msgId),
|
|
|
|
1
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2021-10-12 22:56:39 +00:00
|
|
|
socket.on("mentions:dismiss_all", () => {
|
2020-07-19 14:29:52 +00:00
|
|
|
client.mentions = [];
|
2019-11-04 09:21:05 +00:00
|
|
|
});
|
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (!Config.values.public) {
|
2018-02-20 11:24:46 +00:00
|
|
|
socket.on("push:register", (subscription) => {
|
2019-06-25 08:51:47 +00:00
|
|
|
if (!Object.prototype.hasOwnProperty.call(client.config.sessions, token)) {
|
2018-02-20 11:24:46 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-07-10 19:47:03 +00:00
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
const registration = client.registerPushSubscription(
|
|
|
|
client.config.sessions[token],
|
|
|
|
subscription
|
|
|
|
);
|
2017-07-10 19:47:03 +00:00
|
|
|
|
2018-02-20 11:24:46 +00:00
|
|
|
if (registration) {
|
|
|
|
client.manager.webPush.pushSingle(client, registration, {
|
|
|
|
type: "notification",
|
|
|
|
timestamp: Date.now(),
|
|
|
|
title: "The Lounge",
|
|
|
|
body: "🚀 Push notifications have been enabled",
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2017-07-10 19:47:03 +00:00
|
|
|
|
2018-02-20 11:24:46 +00:00
|
|
|
socket.on("push:unregister", () => client.unregisterPushSubscription(token));
|
|
|
|
}
|
2017-07-10 19:47:03 +00:00
|
|
|
|
2017-08-15 09:44:29 +00:00
|
|
|
const sendSessionList = () => {
|
2022-06-19 00:25:21 +00:00
|
|
|
// TODO: this should use the ClientSession type currently in client
|
|
|
|
const sessions = _.map(client.config.sessions, (session, sessionToken) => {
|
|
|
|
return {
|
|
|
|
current: sessionToken === token,
|
|
|
|
active: _.reduce(
|
|
|
|
client.attachedClients,
|
|
|
|
(count, attachedClient) =>
|
|
|
|
count + (attachedClient.token === sessionToken ? 1 : 0),
|
|
|
|
0
|
|
|
|
),
|
|
|
|
lastUse: session.lastUse,
|
|
|
|
ip: session.ip,
|
|
|
|
agent: session.agent,
|
|
|
|
token: sessionToken, // TODO: Ideally don't expose actual tokens to the client
|
|
|
|
};
|
|
|
|
});
|
2017-08-15 09:44:29 +00:00
|
|
|
|
|
|
|
socket.emit("sessions:list", sessions);
|
|
|
|
};
|
|
|
|
|
|
|
|
socket.on("sessions:get", sendSessionList);
|
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (!Config.values.public) {
|
2017-12-11 19:01:15 +00:00
|
|
|
socket.on("setting:set", (newSetting) => {
|
2020-08-07 16:52:50 +00:00
|
|
|
if (!_.isPlainObject(newSetting)) {
|
2017-12-11 19:01:15 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
if (
|
|
|
|
typeof newSetting.value === "object" ||
|
|
|
|
typeof newSetting.name !== "string" ||
|
|
|
|
newSetting.name[0] === "_"
|
|
|
|
) {
|
2019-01-16 08:52:09 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-12-11 19:01:15 +00:00
|
|
|
// We do not need to do write operations and emit events if nothing changed.
|
|
|
|
if (client.config.clientSettings[newSetting.name] !== newSetting.value) {
|
|
|
|
client.config.clientSettings[newSetting.name] = newSetting.value;
|
|
|
|
|
|
|
|
// Pass the setting to all clients.
|
|
|
|
client.emit("setting:new", {
|
|
|
|
name: newSetting.name,
|
|
|
|
value: newSetting.value,
|
|
|
|
});
|
|
|
|
|
2019-12-15 15:26:18 +00:00
|
|
|
client.save();
|
2019-01-16 09:23:12 +00:00
|
|
|
|
2020-07-22 15:28:12 +00:00
|
|
|
if (newSetting.name === "highlights" || newSetting.name === "highlightExceptions") {
|
2019-01-16 09:23:12 +00:00
|
|
|
client.compileCustomHighlights();
|
2019-03-11 17:25:02 +00:00
|
|
|
} else if (newSetting.name === "awayMessage") {
|
|
|
|
if (typeof newSetting.value !== "string") {
|
|
|
|
newSetting.value = "";
|
|
|
|
}
|
|
|
|
|
|
|
|
client.awayMessage = newSetting.value;
|
2019-01-16 09:23:12 +00:00
|
|
|
}
|
2017-12-11 19:01:15 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
socket.on("setting:get", () => {
|
2019-06-25 08:51:47 +00:00
|
|
|
if (!Object.prototype.hasOwnProperty.call(client.config, "clientSettings")) {
|
2017-12-11 19:01:15 +00:00
|
|
|
socket.emit("setting:all", {});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const clientSettings = client.config.clientSettings;
|
|
|
|
socket.emit("setting:all", clientSettings);
|
|
|
|
});
|
2020-03-07 12:56:50 +00:00
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
socket.on("search", async (query) => {
|
2022-11-26 16:14:09 +00:00
|
|
|
const results = await client.search(query);
|
|
|
|
socket.emit("search:results", results);
|
2020-03-07 12:56:50 +00:00
|
|
|
});
|
2022-02-11 01:56:17 +00:00
|
|
|
|
|
|
|
socket.on("mute:change", ({target, setMutedTo}) => {
|
2022-06-19 00:25:21 +00:00
|
|
|
const networkAndChan = client.find(target);
|
|
|
|
|
|
|
|
if (!networkAndChan) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const {chan, network} = networkAndChan;
|
2022-02-11 01:56:17 +00:00
|
|
|
|
|
|
|
// If the user mutes the lobby, we mute the entire network.
|
2022-06-19 00:25:21 +00:00
|
|
|
if (chan.type === ChanType.LOBBY) {
|
2022-02-11 01:56:17 +00:00
|
|
|
for (const channel of network.channels) {
|
2022-06-19 00:25:21 +00:00
|
|
|
if (channel.type !== ChanType.SPECIAL) {
|
2022-02-11 01:56:17 +00:00
|
|
|
channel.setMuteStatus(setMutedTo);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2022-06-19 00:25:21 +00:00
|
|
|
if (chan.type !== ChanType.SPECIAL) {
|
2022-02-11 01:56:17 +00:00
|
|
|
chan.setMuteStatus(setMutedTo);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const attachedClient of Object.keys(client.attachedClients)) {
|
2022-06-19 00:25:21 +00:00
|
|
|
manager!.sockets.in(attachedClient).emit("mute:changed", {
|
2022-02-11 01:56:17 +00:00
|
|
|
target,
|
|
|
|
status: setMutedTo,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
client.save();
|
|
|
|
});
|
2017-12-11 19:01:15 +00:00
|
|
|
}
|
|
|
|
|
2017-08-15 09:44:29 +00:00
|
|
|
socket.on("sign-out", (tokenToSignOut) => {
|
|
|
|
// If no token provided, sign same client out
|
2020-08-07 16:52:50 +00:00
|
|
|
if (!tokenToSignOut || typeof tokenToSignOut !== "string") {
|
2017-08-15 09:44:29 +00:00
|
|
|
tokenToSignOut = token;
|
|
|
|
}
|
|
|
|
|
2019-06-25 08:51:47 +00:00
|
|
|
if (!Object.prototype.hasOwnProperty.call(client.config.sessions, tokenToSignOut)) {
|
2017-08-15 09:44:29 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
delete client.config.sessions[tokenToSignOut];
|
2017-07-24 06:01:25 +00:00
|
|
|
|
2019-12-15 15:26:18 +00:00
|
|
|
client.save();
|
2017-07-24 06:01:25 +00:00
|
|
|
|
2017-08-15 09:44:29 +00:00
|
|
|
_.map(client.attachedClients, (attachedClient, socketId) => {
|
|
|
|
if (attachedClient.token !== tokenToSignOut) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
const socketToRemove = manager!.sockets.of("/").sockets.get(socketId);
|
2017-08-15 09:44:29 +00:00
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
socketToRemove!.emit("sign-out");
|
|
|
|
socketToRemove!.disconnect();
|
2017-08-15 09:44:29 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// Do not send updated session list if user simply logs out
|
|
|
|
if (tokenToSignOut !== token) {
|
|
|
|
sendSessionList();
|
|
|
|
}
|
2017-08-13 18:37:12 +00:00
|
|
|
});
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
// socket.join is a promise depending on the adapter.
|
2024-03-03 17:56:13 +00:00
|
|
|
void socket.join(client.id);
|
2017-08-13 18:37:12 +00:00
|
|
|
|
2024-03-24 19:27:56 +00:00
|
|
|
const sendInitEvent = (tokenToSend?: string) => {
|
2017-08-13 18:37:12 +00:00
|
|
|
socket.emit("init", {
|
2019-10-17 10:53:29 +00:00
|
|
|
active: openChannel,
|
2019-07-17 09:33:59 +00:00
|
|
|
networks: client.networks.map((network) =>
|
2019-10-17 10:53:29 +00:00
|
|
|
network.getFilteredClone(openChannel, lastMessage)
|
2019-07-17 09:33:59 +00:00
|
|
|
),
|
2017-11-15 06:35:15 +00:00
|
|
|
token: tokenToSend,
|
2017-08-13 18:37:12 +00:00
|
|
|
});
|
2019-07-02 16:02:02 +00:00
|
|
|
socket.emit("commands", inputs.getCommands());
|
2017-08-13 18:37:12 +00:00
|
|
|
};
|
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (Config.values.public) {
|
2024-03-24 19:27:56 +00:00
|
|
|
sendInitEvent();
|
2022-06-19 00:25:21 +00:00
|
|
|
} else if (!token) {
|
2017-08-13 18:37:12 +00:00
|
|
|
client.generateToken((newToken) => {
|
2019-12-15 15:07:17 +00:00
|
|
|
token = client.calculateTokenHash(newToken);
|
|
|
|
client.attachedClients[socket.id].token = token;
|
2017-08-13 18:37:12 +00:00
|
|
|
|
2017-10-12 07:57:25 +00:00
|
|
|
client.updateSession(token, getClientIp(socket), socket.request);
|
2018-01-05 13:26:12 +00:00
|
|
|
sendInitEvent(newToken);
|
2014-08-14 16:35:37 +00:00
|
|
|
});
|
2017-08-13 18:37:12 +00:00
|
|
|
} else {
|
2019-12-15 15:07:17 +00:00
|
|
|
client.updateSession(token, getClientIp(socket), socket.request);
|
2024-03-24 19:27:56 +00:00
|
|
|
sendInitEvent();
|
2014-08-14 16:35:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-29 06:53:23 +00:00
|
|
|
function getClientConfiguration(): SharedConfiguration | LockedSharedConfiguration {
|
|
|
|
const common = {
|
|
|
|
fileUpload: Config.values.fileUpload.enable,
|
|
|
|
ldapEnabled: Config.values.ldap.enable,
|
|
|
|
isUpdateAvailable: changelog.isUpdateAvailable,
|
|
|
|
applicationServerKey: manager!.webPush.vapidKeys!.publicKey,
|
|
|
|
version: Helper.getVersionNumber(),
|
|
|
|
gitCommit: Helper.getGitCommit(),
|
|
|
|
themes: themes.getAll(),
|
|
|
|
defaultTheme: Config.values.theme,
|
|
|
|
public: Config.values.public,
|
|
|
|
useHexIp: Config.values.useHexIp,
|
|
|
|
prefetch: Config.values.prefetch,
|
|
|
|
fileUploadMaxFileSize: Uploader ? Uploader.getMaxFileSize() : undefined, // TODO can't be undefined?
|
|
|
|
};
|
2017-11-04 17:32:18 +00:00
|
|
|
|
2024-02-29 06:53:23 +00:00
|
|
|
const defaultsOverride = {
|
|
|
|
nick: Config.getDefaultNick(), // expand the number part
|
2017-11-04 17:32:18 +00:00
|
|
|
|
2024-02-29 06:53:23 +00:00
|
|
|
// TODO: this doesn't seem right, if the client needs this as a buffer
|
|
|
|
// the client ought to add it on its own
|
|
|
|
sasl: "",
|
|
|
|
saslAccount: "",
|
|
|
|
saslPassword: "",
|
|
|
|
};
|
2017-11-15 18:43:38 +00:00
|
|
|
|
2024-02-29 06:53:23 +00:00
|
|
|
if (!Config.values.lockNetwork) {
|
|
|
|
const defaults: ConfigNetDefaults = {
|
|
|
|
..._.clone(Config.values.defaults),
|
|
|
|
...defaultsOverride,
|
|
|
|
};
|
|
|
|
const result: SharedConfiguration = {
|
|
|
|
...common,
|
|
|
|
defaults: defaults,
|
|
|
|
lockNetwork: Config.values.lockNetwork,
|
|
|
|
};
|
|
|
|
return result;
|
2018-09-03 07:30:05 +00:00
|
|
|
}
|
|
|
|
|
2024-02-29 06:53:23 +00:00
|
|
|
// Only send defaults that are visible on the client
|
|
|
|
const defaults: LockedConfigNetDefaults = {
|
|
|
|
..._.pick(Config.values.defaults, ["name", "username", "password", "realname", "join"]),
|
|
|
|
...defaultsOverride,
|
|
|
|
};
|
|
|
|
|
|
|
|
const result: LockedSharedConfiguration = {
|
|
|
|
...common,
|
|
|
|
lockNetwork: Config.values.lockNetwork,
|
|
|
|
defaults: defaults,
|
|
|
|
};
|
|
|
|
|
|
|
|
return result;
|
2017-11-04 17:32:18 +00:00
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
function getServerConfiguration(): ServerConfiguration {
|
2022-11-22 02:07:29 +00:00
|
|
|
return {...Config.values, ...{stylesheets: packages.getStylesheets()}};
|
2018-01-05 17:40:34 +00:00
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
function performAuthentication(this: Socket, data) {
|
2020-08-07 16:52:50 +00:00
|
|
|
if (!_.isPlainObject(data)) {
|
2018-08-29 10:55:30 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-06-21 07:58:29 +00:00
|
|
|
const socket = this;
|
|
|
|
let client;
|
2022-06-19 00:25:21 +00:00
|
|
|
let token: string;
|
2017-06-21 07:58:29 +00:00
|
|
|
|
2019-12-15 15:07:17 +00:00
|
|
|
const finalInit = () =>
|
2019-10-17 10:53:29 +00:00
|
|
|
initializeClient(socket, client, token, data.lastMessage || -1, data.openChannel);
|
2019-07-16 09:51:22 +00:00
|
|
|
|
2017-06-21 07:58:29 +00:00
|
|
|
const initClient = () => {
|
2019-11-12 10:39:46 +00:00
|
|
|
// Configuration does not change during runtime of TL,
|
|
|
|
// and the client listens to this event only once
|
|
|
|
if (!data.hasConfig) {
|
|
|
|
socket.emit("configuration", getClientConfiguration());
|
2019-11-12 20:03:59 +00:00
|
|
|
|
|
|
|
socket.emit(
|
|
|
|
"push:issubscribed",
|
|
|
|
token && client.config.sessions[token].pushSubscription ? true : false
|
|
|
|
);
|
2019-11-12 10:39:46 +00:00
|
|
|
}
|
2017-11-04 17:32:18 +00:00
|
|
|
|
2019-07-16 09:51:22 +00:00
|
|
|
client.config.browser = {
|
|
|
|
ip: getClientIp(socket),
|
|
|
|
isSecure: getClientSecure(socket),
|
|
|
|
language: getClientLanguage(socket),
|
|
|
|
};
|
2017-08-13 18:37:12 +00:00
|
|
|
|
|
|
|
// If webirc is enabled perform reverse dns lookup
|
2022-05-01 19:12:39 +00:00
|
|
|
if (Config.values.webirc === null) {
|
2017-08-13 18:37:12 +00:00
|
|
|
return finalInit();
|
2017-06-21 07:58:29 +00:00
|
|
|
}
|
2017-08-13 18:37:12 +00:00
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
reverseDnsLookup(client.config.browser?.ip, (hostname) => {
|
|
|
|
client.config.browser!.hostname = hostname;
|
2017-08-13 18:37:12 +00:00
|
|
|
|
|
|
|
finalInit();
|
|
|
|
});
|
2017-06-21 07:58:29 +00:00
|
|
|
};
|
|
|
|
|
2022-05-01 19:12:39 +00:00
|
|
|
if (Config.values.public) {
|
2022-06-19 00:25:21 +00:00
|
|
|
client = new Client(manager!);
|
2023-03-14 20:24:06 +00:00
|
|
|
client.connect();
|
2022-06-19 00:25:21 +00:00
|
|
|
manager!.clients.push(client);
|
2017-06-21 07:58:29 +00:00
|
|
|
|
2020-03-21 20:55:36 +00:00
|
|
|
socket.on("disconnect", function () {
|
2022-06-19 00:25:21 +00:00
|
|
|
manager!.clients = _.without(manager!.clients, client);
|
2014-08-14 16:35:37 +00:00
|
|
|
client.quit();
|
|
|
|
});
|
2017-06-21 07:58:29 +00:00
|
|
|
|
|
|
|
initClient();
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-15 09:51:01 +00:00
|
|
|
if (typeof data.user !== "string") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-06-21 07:58:29 +00:00
|
|
|
const authCallback = (success) => {
|
|
|
|
// Authorization failed
|
|
|
|
if (!success) {
|
2018-03-17 08:09:59 +00:00
|
|
|
if (!client) {
|
2019-07-17 09:33:59 +00:00
|
|
|
log.warn(
|
|
|
|
`Authentication for non existing user attempted from ${colors.bold(
|
|
|
|
getClientIp(socket)
|
|
|
|
)}`
|
|
|
|
);
|
2018-03-17 08:09:59 +00:00
|
|
|
} else {
|
2019-07-17 09:33:59 +00:00
|
|
|
log.warn(
|
|
|
|
`Authentication failed for user ${colors.bold(data.user)} from ${colors.bold(
|
|
|
|
getClientIp(socket)
|
|
|
|
)}`
|
|
|
|
);
|
2018-03-17 08:09:59 +00:00
|
|
|
}
|
|
|
|
|
2019-11-05 19:29:51 +00:00
|
|
|
socket.emit("auth:failed");
|
2017-06-21 07:58:29 +00:00
|
|
|
return;
|
2016-04-03 05:12:49 +00:00
|
|
|
}
|
2016-07-30 01:20:38 +00:00
|
|
|
|
2017-06-21 07:58:29 +00:00
|
|
|
// If authorization succeeded but there is no loaded user,
|
|
|
|
// load it and find the user again (this happens with LDAP)
|
|
|
|
if (!client) {
|
2022-06-19 00:25:21 +00:00
|
|
|
client = manager!.loadUser(data.user);
|
2016-07-30 01:20:38 +00:00
|
|
|
}
|
|
|
|
|
2017-06-21 07:58:29 +00:00
|
|
|
initClient();
|
|
|
|
};
|
2016-07-30 01:20:38 +00:00
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
client = manager!.findClient(data.user);
|
2017-06-21 07:58:29 +00:00
|
|
|
|
|
|
|
// We have found an existing user and client has provided a token
|
2018-01-05 13:26:12 +00:00
|
|
|
if (client && data.token) {
|
|
|
|
const providedToken = client.calculateTokenHash(data.token);
|
2017-06-21 07:58:29 +00:00
|
|
|
|
2019-06-25 08:51:47 +00:00
|
|
|
if (Object.prototype.hasOwnProperty.call(client.config.sessions, providedToken)) {
|
2018-01-05 13:26:12 +00:00
|
|
|
token = providedToken;
|
|
|
|
|
|
|
|
return authCallback(true);
|
|
|
|
}
|
2017-06-21 07:58:29 +00:00
|
|
|
}
|
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
Auth.initialize().then(() => {
|
|
|
|
// Perform password checking
|
|
|
|
Auth.auth(manager, client, data.user, data.password, authCallback);
|
|
|
|
});
|
2014-08-14 16:35:37 +00:00
|
|
|
}
|
2017-08-13 18:37:12 +00:00
|
|
|
|
2022-06-19 00:25:21 +00:00
|
|
|
function reverseDnsLookup(ip: string, callback: (hostname: string) => void) {
|
2023-11-03 19:40:56 +00:00
|
|
|
// node can throw, even if we provide valid input based on the DNS server
|
|
|
|
// returning SERVFAIL it seems: https://github.com/thelounge/thelounge/issues/4768
|
|
|
|
// so we manually resolve with the ip as a fallback in case something fails
|
|
|
|
try {
|
|
|
|
dns.reverse(ip, (reverseErr, hostnames) => {
|
|
|
|
if (reverseErr || hostnames.length < 1) {
|
2022-06-19 00:25:21 +00:00
|
|
|
return callback(ip);
|
|
|
|
}
|
|
|
|
|
2023-11-03 19:40:56 +00:00
|
|
|
dns.resolve(
|
|
|
|
hostnames[0],
|
|
|
|
net.isIP(ip) === 6 ? "AAAA" : "A",
|
|
|
|
(resolveErr, resolvedIps) => {
|
|
|
|
// TODO: investigate SoaRecord class
|
|
|
|
if (!Array.isArray(resolvedIps)) {
|
|
|
|
return callback(ip);
|
|
|
|
}
|
2019-07-11 20:10:03 +00:00
|
|
|
|
2023-11-03 19:40:56 +00:00
|
|
|
if (resolveErr || resolvedIps.length < 1) {
|
|
|
|
return callback(ip);
|
|
|
|
}
|
2019-07-11 20:10:03 +00:00
|
|
|
|
2023-11-03 19:40:56 +00:00
|
|
|
for (const resolvedIp of resolvedIps) {
|
|
|
|
if (ip === resolvedIp) {
|
|
|
|
return callback(hostnames[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return callback(ip);
|
|
|
|
}
|
|
|
|
);
|
2019-07-11 20:10:03 +00:00
|
|
|
});
|
2023-11-03 19:40:56 +00:00
|
|
|
} catch (err) {
|
|
|
|
log.error(`failed to resolve rDNS for ${ip}, using ip instead`, (err as any).toString());
|
|
|
|
setImmediate(callback, ip); // makes sure we always behave asynchronously
|
|
|
|
}
|
2017-08-13 18:37:12 +00:00
|
|
|
}
|