@lordbex/thelounge 4.4.3-blowfish

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.thelounge_home +1 -0
  2. package/LICENSE +22 -0
  3. package/README.md +95 -0
  4. package/client/index.html.tpl +69 -0
  5. package/dist/defaults/config.js +465 -0
  6. package/dist/package.json +174 -0
  7. package/dist/server/client.js +678 -0
  8. package/dist/server/clientManager.js +220 -0
  9. package/dist/server/command-line/index.js +85 -0
  10. package/dist/server/command-line/install.js +123 -0
  11. package/dist/server/command-line/outdated.js +30 -0
  12. package/dist/server/command-line/start.js +34 -0
  13. package/dist/server/command-line/storage.js +103 -0
  14. package/dist/server/command-line/uninstall.js +40 -0
  15. package/dist/server/command-line/upgrade.js +64 -0
  16. package/dist/server/command-line/users/add.js +67 -0
  17. package/dist/server/command-line/users/edit.js +39 -0
  18. package/dist/server/command-line/users/index.js +17 -0
  19. package/dist/server/command-line/users/list.js +53 -0
  20. package/dist/server/command-line/users/remove.js +37 -0
  21. package/dist/server/command-line/users/reset.js +64 -0
  22. package/dist/server/command-line/utils.js +177 -0
  23. package/dist/server/config.js +138 -0
  24. package/dist/server/helper.js +161 -0
  25. package/dist/server/identification.js +139 -0
  26. package/dist/server/index.js +3 -0
  27. package/dist/server/log.js +35 -0
  28. package/dist/server/models/chan.js +275 -0
  29. package/dist/server/models/msg.js +92 -0
  30. package/dist/server/models/network.js +546 -0
  31. package/dist/server/models/prefix.js +31 -0
  32. package/dist/server/models/user.js +42 -0
  33. package/dist/server/plugins/auth/ldap.js +188 -0
  34. package/dist/server/plugins/auth/local.js +41 -0
  35. package/dist/server/plugins/auth.js +70 -0
  36. package/dist/server/plugins/changelog.js +103 -0
  37. package/dist/server/plugins/clientCertificate.js +115 -0
  38. package/dist/server/plugins/dev-server.js +33 -0
  39. package/dist/server/plugins/inputs/action.js +54 -0
  40. package/dist/server/plugins/inputs/away.js +20 -0
  41. package/dist/server/plugins/inputs/ban.js +45 -0
  42. package/dist/server/plugins/inputs/blow.js +44 -0
  43. package/dist/server/plugins/inputs/connect.js +41 -0
  44. package/dist/server/plugins/inputs/ctcp.js +29 -0
  45. package/dist/server/plugins/inputs/disconnect.js +15 -0
  46. package/dist/server/plugins/inputs/ignore.js +74 -0
  47. package/dist/server/plugins/inputs/ignorelist.js +50 -0
  48. package/dist/server/plugins/inputs/index.js +105 -0
  49. package/dist/server/plugins/inputs/invite.js +31 -0
  50. package/dist/server/plugins/inputs/kick.js +26 -0
  51. package/dist/server/plugins/inputs/kill.js +13 -0
  52. package/dist/server/plugins/inputs/list.js +12 -0
  53. package/dist/server/plugins/inputs/mode.js +55 -0
  54. package/dist/server/plugins/inputs/msg.js +106 -0
  55. package/dist/server/plugins/inputs/mute.js +56 -0
  56. package/dist/server/plugins/inputs/nick.js +55 -0
  57. package/dist/server/plugins/inputs/notice.js +42 -0
  58. package/dist/server/plugins/inputs/part.js +46 -0
  59. package/dist/server/plugins/inputs/quit.js +27 -0
  60. package/dist/server/plugins/inputs/raw.js +13 -0
  61. package/dist/server/plugins/inputs/rejoin.js +25 -0
  62. package/dist/server/plugins/inputs/topic.js +24 -0
  63. package/dist/server/plugins/inputs/whois.js +19 -0
  64. package/dist/server/plugins/irc-events/away.js +59 -0
  65. package/dist/server/plugins/irc-events/cap.js +62 -0
  66. package/dist/server/plugins/irc-events/chghost.js +29 -0
  67. package/dist/server/plugins/irc-events/connection.js +152 -0
  68. package/dist/server/plugins/irc-events/ctcp.js +72 -0
  69. package/dist/server/plugins/irc-events/error.js +80 -0
  70. package/dist/server/plugins/irc-events/help.js +21 -0
  71. package/dist/server/plugins/irc-events/info.js +21 -0
  72. package/dist/server/plugins/irc-events/invite.js +27 -0
  73. package/dist/server/plugins/irc-events/join.js +53 -0
  74. package/dist/server/plugins/irc-events/kick.js +39 -0
  75. package/dist/server/plugins/irc-events/link.js +442 -0
  76. package/dist/server/plugins/irc-events/list.js +47 -0
  77. package/dist/server/plugins/irc-events/message.js +187 -0
  78. package/dist/server/plugins/irc-events/mode.js +124 -0
  79. package/dist/server/plugins/irc-events/modelist.js +67 -0
  80. package/dist/server/plugins/irc-events/motd.js +29 -0
  81. package/dist/server/plugins/irc-events/names.js +21 -0
  82. package/dist/server/plugins/irc-events/nick.js +45 -0
  83. package/dist/server/plugins/irc-events/part.js +35 -0
  84. package/dist/server/plugins/irc-events/quit.js +32 -0
  85. package/dist/server/plugins/irc-events/sasl.js +26 -0
  86. package/dist/server/plugins/irc-events/topic.js +42 -0
  87. package/dist/server/plugins/irc-events/unhandled.js +31 -0
  88. package/dist/server/plugins/irc-events/welcome.js +22 -0
  89. package/dist/server/plugins/irc-events/whois.js +57 -0
  90. package/dist/server/plugins/messageStorage/sqlite.js +454 -0
  91. package/dist/server/plugins/messageStorage/text.js +124 -0
  92. package/dist/server/plugins/packages/index.js +200 -0
  93. package/dist/server/plugins/packages/publicClient.js +66 -0
  94. package/dist/server/plugins/packages/themes.js +61 -0
  95. package/dist/server/plugins/storage.js +88 -0
  96. package/dist/server/plugins/sts.js +85 -0
  97. package/dist/server/plugins/uploader.js +267 -0
  98. package/dist/server/plugins/webpush.js +99 -0
  99. package/dist/server/server.js +857 -0
  100. package/dist/server/storageCleaner.js +131 -0
  101. package/dist/server/utils/fish.js +432 -0
  102. package/dist/shared/irc.js +19 -0
  103. package/dist/shared/linkify.js +81 -0
  104. package/dist/shared/types/chan.js +22 -0
  105. package/dist/shared/types/changelog.js +2 -0
  106. package/dist/shared/types/config.js +2 -0
  107. package/dist/shared/types/mention.js +2 -0
  108. package/dist/shared/types/msg.js +34 -0
  109. package/dist/shared/types/network.js +2 -0
  110. package/dist/shared/types/storage.js +2 -0
  111. package/dist/shared/types/user.js +2 -0
  112. package/dist/webpack.config.js +224 -0
  113. package/index.js +38 -0
  114. package/package.json +174 -0
  115. package/public/audio/pop.wav +0 -0
  116. package/public/css/style.css +12 -0
  117. package/public/css/style.css.map +1 -0
  118. package/public/favicon.ico +0 -0
  119. package/public/fonts/fa-solid-900.woff +0 -0
  120. package/public/fonts/fa-solid-900.woff2 +0 -0
  121. package/public/img/favicon-alerted.ico +0 -0
  122. package/public/img/icon-alerted-black-transparent-bg-72x72px.png +0 -0
  123. package/public/img/icon-alerted-grey-bg-192x192px.png +0 -0
  124. package/public/img/icon-black-transparent-bg.svg +1 -0
  125. package/public/img/logo-grey-bg-120x120px.png +0 -0
  126. package/public/img/logo-grey-bg-152x152px.png +0 -0
  127. package/public/img/logo-grey-bg-167x167px.png +0 -0
  128. package/public/img/logo-grey-bg-180x180px.png +0 -0
  129. package/public/img/logo-grey-bg-192x192px.png +0 -0
  130. package/public/img/logo-grey-bg-512x512px.png +0 -0
  131. package/public/img/logo-grey-bg.svg +1 -0
  132. package/public/img/logo-horizontal-transparent-bg-inverted.svg +1 -0
  133. package/public/img/logo-horizontal-transparent-bg.svg +1 -0
  134. package/public/img/logo-transparent-bg-inverted.svg +1 -0
  135. package/public/img/logo-transparent-bg.svg +1 -0
  136. package/public/img/logo-vertical-transparent-bg-inverted.svg +1 -0
  137. package/public/img/logo-vertical-transparent-bg.svg +1 -0
  138. package/public/js/bundle.js +2 -0
  139. package/public/js/bundle.js.map +1 -0
  140. package/public/js/bundle.vendor.js +3 -0
  141. package/public/js/bundle.vendor.js.LICENSE.txt +18 -0
  142. package/public/js/bundle.vendor.js.map +1 -0
  143. package/public/js/loading-error-handlers.js +1 -0
  144. package/public/robots.txt +2 -0
  145. package/public/service-worker.js +1 -0
  146. package/public/thelounge.webmanifest +53 -0
  147. package/public/themes/default.css +35 -0
  148. package/public/themes/morning.css +183 -0
@@ -0,0 +1,857 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ const lodash_1 = __importDefault(require("lodash"));
30
+ const ws_1 = require("ws");
31
+ const express_1 = __importDefault(require("express"));
32
+ const fs_1 = __importDefault(require("fs"));
33
+ const path_1 = __importDefault(require("path"));
34
+ const socket_io_1 = require("socket.io");
35
+ const dns_1 = __importDefault(require("dns"));
36
+ const chalk_1 = __importDefault(require("chalk"));
37
+ const net_1 = __importDefault(require("net"));
38
+ const log_1 = __importDefault(require("./log"));
39
+ const client_1 = __importDefault(require("./client"));
40
+ const clientManager_1 = __importDefault(require("./clientManager"));
41
+ const uploader_1 = __importDefault(require("./plugins/uploader"));
42
+ const helper_1 = __importDefault(require("./helper"));
43
+ const config_1 = __importDefault(require("./config"));
44
+ const identification_1 = __importDefault(require("./identification"));
45
+ const changelog_1 = __importDefault(require("./plugins/changelog"));
46
+ const inputs_1 = __importDefault(require("./plugins/inputs"));
47
+ const auth_1 = __importDefault(require("./plugins/auth"));
48
+ const themes_1 = __importDefault(require("./plugins/packages/themes"));
49
+ themes_1.default.loadLocalThemes();
50
+ const index_1 = __importDefault(require("./plugins/packages/index"));
51
+ const utils_1 = __importDefault(require("./command-line/utils"));
52
+ const chan_1 = require("../shared/types/chan");
53
+ // A random number that will force clients to reload the page if it differs
54
+ const serverHash = Math.floor(Date.now() * Math.random());
55
+ let manager = null;
56
+ async function default_1(options = {
57
+ dev: false,
58
+ }) {
59
+ log_1.default.info(`The Lounge ${chalk_1.default.green(helper_1.default.getVersion())} \
60
+ (Node.js ${chalk_1.default.green(process.versions.node)} on ${chalk_1.default.green(process.platform)} ${process.arch})`);
61
+ log_1.default.info(`Configuration file: ${chalk_1.default.green(config_1.default.getConfigPath())}`);
62
+ const staticOptions = {
63
+ redirect: false,
64
+ maxAge: 86400 * 1000,
65
+ };
66
+ const app = (0, express_1.default)();
67
+ if (options.dev) {
68
+ (await Promise.resolve().then(() => __importStar(require("./plugins/dev-server")))).default(app);
69
+ }
70
+ app.set("env", "production")
71
+ .disable("x-powered-by")
72
+ .use(allRequests)
73
+ .use(addSecurityHeaders)
74
+ .get("/", indexRequest)
75
+ .get("/service-worker.js", forceNoCacheRequest)
76
+ .get("/js/bundle.js.map", forceNoCacheRequest)
77
+ .get("/css/style.css.map", forceNoCacheRequest)
78
+ .use(express_1.default.static(utils_1.default.getFileFromRelativeToRoot("public"), staticOptions))
79
+ .use("/storage/", express_1.default.static(config_1.default.getStoragePath(), staticOptions));
80
+ if (config_1.default.values.fileUpload.enable) {
81
+ uploader_1.default.router(app);
82
+ }
83
+ // This route serves *installed themes only*. Local themes are served directly
84
+ // from the `public/themes/` folder as static assets, without entering this
85
+ // handler. Remember this if you make changes to this function, serving of
86
+ // local themes will not get those changes.
87
+ app.get("/themes/:theme.css", (req, res) => {
88
+ const themeName = encodeURIComponent(req.params.theme);
89
+ const theme = themes_1.default.getByName(themeName);
90
+ if (theme === undefined || theme.filename === undefined) {
91
+ return res.status(404).send("Not found");
92
+ }
93
+ return res.sendFile(theme.filename);
94
+ });
95
+ app.get("/packages/:package/:filename", (req, res) => {
96
+ const packageName = req.params.package;
97
+ const fileName = req.params.filename;
98
+ const packageFile = index_1.default.getPackage(packageName);
99
+ if (!packageFile || !index_1.default.getFiles().includes(`${packageName}/${fileName}`)) {
100
+ return res.status(404).send("Not found");
101
+ }
102
+ const packagePath = config_1.default.getPackageModulePath(packageName);
103
+ return res.sendFile(path_1.default.join(packagePath, fileName));
104
+ });
105
+ if (config_1.default.values.public && (config_1.default.values.ldap || {}).enable) {
106
+ log_1.default.warn("Server is public and set to use LDAP. Set to private mode if trying to use LDAP authentication.");
107
+ }
108
+ let server;
109
+ if (!config_1.default.values.https.enable) {
110
+ const createServer = (await Promise.resolve().then(() => __importStar(require("http")))).createServer;
111
+ server = createServer(app);
112
+ }
113
+ else {
114
+ const keyPath = helper_1.default.expandHome(config_1.default.values.https.key);
115
+ const certPath = helper_1.default.expandHome(config_1.default.values.https.certificate);
116
+ const caPath = helper_1.default.expandHome(config_1.default.values.https.ca);
117
+ if (!keyPath.length || !fs_1.default.existsSync(keyPath)) {
118
+ log_1.default.error("Path to SSL key is invalid. Stopping server...");
119
+ process.exit(1);
120
+ }
121
+ if (!certPath.length || !fs_1.default.existsSync(certPath)) {
122
+ log_1.default.error("Path to SSL certificate is invalid. Stopping server...");
123
+ process.exit(1);
124
+ }
125
+ if (caPath.length && !fs_1.default.existsSync(caPath)) {
126
+ log_1.default.error("Path to SSL ca bundle is invalid. Stopping server...");
127
+ process.exit(1);
128
+ }
129
+ const createServer = (await Promise.resolve().then(() => __importStar(require("https")))).createServer;
130
+ server = createServer({
131
+ key: fs_1.default.readFileSync(keyPath),
132
+ cert: fs_1.default.readFileSync(certPath),
133
+ ca: caPath ? fs_1.default.readFileSync(caPath) : undefined,
134
+ }, app);
135
+ }
136
+ let listenParams;
137
+ if (typeof config_1.default.values.host === "string" && config_1.default.values.host.startsWith("unix:")) {
138
+ listenParams = config_1.default.values.host.replace(/^unix:/, "");
139
+ }
140
+ else {
141
+ listenParams = {
142
+ port: config_1.default.values.port,
143
+ host: config_1.default.values.host,
144
+ };
145
+ }
146
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
147
+ server.on("error", (err) => log_1.default.error(`${err}`));
148
+ server.listen(listenParams, () => {
149
+ if (typeof listenParams === "string") {
150
+ log_1.default.info("Available on socket " + chalk_1.default.green(listenParams));
151
+ }
152
+ else {
153
+ const protocol = config_1.default.values.https.enable ? "https" : "http";
154
+ const address = server?.address();
155
+ if (address && typeof address !== "string") {
156
+ // TODO: Node may revert the Node 18 family string --> number change
157
+ // @ts-expect-error This condition will always return 'false' since the types 'string' and 'number' have no overlap.
158
+ if (address.family === "IPv6" || address.family === 6) {
159
+ address.address = "[" + address.address + "]";
160
+ }
161
+ log_1.default.info("Available at " +
162
+ chalk_1.default.green(`${protocol}://${address.address}:${address.port}/`) +
163
+ ` in ${chalk_1.default.bold(config_1.default.values.public ? "public" : "private")} mode`);
164
+ }
165
+ }
166
+ // This should never happen
167
+ if (!server) {
168
+ return;
169
+ }
170
+ const sockets = new socket_io_1.Server(server, {
171
+ wsEngine: ws_1.Server,
172
+ cookie: false,
173
+ serveClient: false,
174
+ // TODO: type as Server.Transport[]
175
+ transports: config_1.default.values.transports,
176
+ pingTimeout: 60000,
177
+ });
178
+ sockets.on("connect", (socket) => {
179
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
180
+ socket.on("error", (err) => log_1.default.error(`io socket error: ${err}`));
181
+ if (config_1.default.values.public) {
182
+ performAuthentication.call(socket, {});
183
+ }
184
+ else {
185
+ socket.on("auth:perform", performAuthentication);
186
+ socket.emit("auth:start", serverHash);
187
+ }
188
+ });
189
+ manager = new clientManager_1.default();
190
+ index_1.default.loadPackages();
191
+ const defaultTheme = themes_1.default.getByName(config_1.default.values.theme);
192
+ if (defaultTheme === undefined) {
193
+ log_1.default.warn(`The specified default theme "${chalk_1.default.red(config_1.default.values.theme)}" does not exist, verify your config.`);
194
+ config_1.default.values.theme = "default";
195
+ }
196
+ else if (defaultTheme.themeColor) {
197
+ config_1.default.values.themeColor = defaultTheme.themeColor;
198
+ }
199
+ new identification_1.default((identHandler, err) => {
200
+ if (err) {
201
+ log_1.default.error(`Could not start identd server, ${err.message}`);
202
+ process.exit(1);
203
+ }
204
+ else if (!manager) {
205
+ log_1.default.error("Could not start identd server, ClientManager is undefined");
206
+ process.exit(1);
207
+ }
208
+ manager.init(identHandler, sockets);
209
+ });
210
+ // Handle ctrl+c and kill gracefully
211
+ let suicideTimeout = null;
212
+ const exitGracefully = async function () {
213
+ if (suicideTimeout !== null) {
214
+ return;
215
+ }
216
+ log_1.default.info("Exiting...");
217
+ // Close all client and IRC connections
218
+ if (manager) {
219
+ manager.clients.forEach((client) => client.quit());
220
+ }
221
+ if (config_1.default.values.prefetchStorage) {
222
+ log_1.default.info("Clearing prefetch storage folder, this might take a while...");
223
+ (await Promise.resolve().then(() => __importStar(require("./plugins/storage")))).default.emptyDir();
224
+ }
225
+ // Forcefully exit after 3 seconds
226
+ suicideTimeout = setTimeout(() => process.exit(1), 3000);
227
+ // Close http server
228
+ server?.close(() => {
229
+ if (suicideTimeout !== null) {
230
+ clearTimeout(suicideTimeout);
231
+ }
232
+ process.exit(0);
233
+ });
234
+ };
235
+ /* eslint-disable @typescript-eslint/no-misused-promises */
236
+ process.on("SIGINT", exitGracefully);
237
+ process.on("SIGTERM", exitGracefully);
238
+ /* eslint-enable @typescript-eslint/no-misused-promises */
239
+ // Clear storage folder after server starts successfully
240
+ if (config_1.default.values.prefetchStorage) {
241
+ Promise.resolve().then(() => __importStar(require("./plugins/storage"))).then(({ default: storage }) => {
242
+ storage.emptyDir();
243
+ })
244
+ .catch((err) => {
245
+ log_1.default.error(`Could not clear storage folder, ${err.message}`);
246
+ });
247
+ }
248
+ changelog_1.default.checkForUpdates(manager);
249
+ });
250
+ return server;
251
+ }
252
+ exports.default = default_1;
253
+ function getClientLanguage(socket) {
254
+ const acceptLanguage = socket.handshake.headers["accept-language"];
255
+ if (typeof acceptLanguage === "string" && /^[\x00-\x7F]{1,50}$/.test(acceptLanguage)) {
256
+ // only allow ASCII strings between 1-50 characters in length
257
+ return acceptLanguage;
258
+ }
259
+ return undefined;
260
+ }
261
+ function getClientIp(socket) {
262
+ let ip = socket.handshake.address || "127.0.0.1";
263
+ if (config_1.default.values.reverseProxy) {
264
+ const forwarded = String(socket.handshake.headers["x-forwarded-for"])
265
+ .split(/\s*,\s*/)
266
+ .filter(Boolean);
267
+ if (forwarded.length && net_1.default.isIP(forwarded[0])) {
268
+ ip = forwarded[0];
269
+ }
270
+ }
271
+ return ip.replace(/^::ffff:/, "");
272
+ }
273
+ function getClientSecure(socket) {
274
+ let secure = socket.handshake.secure;
275
+ if (config_1.default.values.reverseProxy && socket.handshake.headers["x-forwarded-proto"] === "https") {
276
+ secure = true;
277
+ }
278
+ return secure;
279
+ }
280
+ function allRequests(_req, res, next) {
281
+ res.setHeader("X-Content-Type-Options", "nosniff");
282
+ return next();
283
+ }
284
+ function addSecurityHeaders(_req, res, next) {
285
+ const policies = [
286
+ "default-src 'none'", // default to nothing
287
+ "base-uri 'none'", // disallow <base>, has no fallback to default-src
288
+ "form-action 'self'", // 'self' to fix saving passwords in Firefox, even though login is handled in javascript
289
+ "connect-src 'self' ws: wss:", // allow self for polling; websockets
290
+ "style-src 'self' https: 'unsafe-inline'", // allow inline due to use in irc hex colors
291
+ "script-src 'self'", // javascript
292
+ "worker-src 'self'", // service worker
293
+ "manifest-src 'self'", // manifest.json
294
+ "font-src 'self' https:", // allow loading fonts from secure sites (e.g. google fonts)
295
+ "media-src 'self' https:", // self for notification sound; allow https media (audio previews)
296
+ ];
297
+ // If prefetch is enabled, but storage is not, we have to allow mixed content
298
+ // - https://user-images.githubusercontent.com is where we currently push our changelog screenshots
299
+ // - data: is required for the HTML5 video player
300
+ if (config_1.default.values.prefetchStorage || !config_1.default.values.prefetch) {
301
+ policies.push("img-src 'self' data: https://user-images.githubusercontent.com");
302
+ policies.unshift("block-all-mixed-content");
303
+ }
304
+ else {
305
+ policies.push("img-src http: https: data:");
306
+ }
307
+ res.setHeader("Content-Security-Policy", policies.join("; "));
308
+ res.setHeader("Referrer-Policy", "no-referrer");
309
+ return next();
310
+ }
311
+ function forceNoCacheRequest(_req, res, next) {
312
+ // Intermittent proxies must not cache the following requests,
313
+ // browsers must fetch the latest version of these files (service worker, source maps)
314
+ res.setHeader("Cache-Control", "no-cache, no-transform");
315
+ return next();
316
+ }
317
+ function indexRequest(_req, res) {
318
+ res.setHeader("Content-Type", "text/html");
319
+ fs_1.default.readFile(utils_1.default.getFileFromRelativeToRoot("client/index.html.tpl"), "utf-8", (err, file) => {
320
+ if (err) {
321
+ log_1.default.error(`failed to server index request: ${err.name}, ${err.message}`);
322
+ res.sendStatus(500);
323
+ return;
324
+ }
325
+ const config = {
326
+ ...getServerConfiguration(),
327
+ ...{ cacheBust: helper_1.default.getVersionCacheBust() },
328
+ };
329
+ res.send(lodash_1.default.template(file)(config));
330
+ });
331
+ }
332
+ function initializeClient(socket, client, token, lastMessage, openChannel) {
333
+ socket.off("auth:perform", performAuthentication);
334
+ socket.emit("auth:success");
335
+ client.clientAttach(socket.id, token);
336
+ // Client sends currently active channel on reconnect,
337
+ // pass it into `open` directly so it is verified and updated if necessary
338
+ if (openChannel) {
339
+ client.open(socket.id, openChannel);
340
+ // If client provided channel passes checks, use it. if client has invalid
341
+ // channel open (or windows like settings) then use last known server active channel
342
+ openChannel = client.attachedClients[socket.id].openChannel || client.lastActiveChannel;
343
+ }
344
+ else {
345
+ openChannel = client.lastActiveChannel;
346
+ }
347
+ if (config_1.default.values.fileUpload.enable) {
348
+ new uploader_1.default(socket);
349
+ }
350
+ socket.on("disconnect", function () {
351
+ process.nextTick(() => client.clientDetach(socket.id));
352
+ });
353
+ socket.on("input", (data) => {
354
+ if (lodash_1.default.isPlainObject(data)) {
355
+ client.input(data);
356
+ }
357
+ });
358
+ socket.on("more", (data) => {
359
+ if (lodash_1.default.isPlainObject(data)) {
360
+ const history = client.more(data);
361
+ if (history !== null) {
362
+ socket.emit("more", history);
363
+ }
364
+ }
365
+ });
366
+ socket.on("network:new", (data) => {
367
+ if (lodash_1.default.isPlainObject(data)) {
368
+ // prevent people from overriding webirc settings
369
+ data.uuid = null;
370
+ data.commands = null;
371
+ data.ignoreList = null;
372
+ client.connectToNetwork(data);
373
+ }
374
+ });
375
+ socket.on("network:get", (data) => {
376
+ if (typeof data !== "string") {
377
+ return;
378
+ }
379
+ const network = lodash_1.default.find(client.networks, { uuid: data });
380
+ if (!network) {
381
+ return;
382
+ }
383
+ socket.emit("network:info", network.exportForEdit());
384
+ });
385
+ socket.on("network:edit", (data) => {
386
+ if (!lodash_1.default.isPlainObject(data)) {
387
+ return;
388
+ }
389
+ const network = lodash_1.default.find(client.networks, { uuid: data.uuid });
390
+ if (!network) {
391
+ return;
392
+ }
393
+ network.edit(client, data);
394
+ });
395
+ socket.on("history:clear", (data) => {
396
+ if (lodash_1.default.isPlainObject(data)) {
397
+ client.clearHistory(data);
398
+ }
399
+ });
400
+ if (!config_1.default.values.public && !config_1.default.values.ldap.enable) {
401
+ socket.on("change-password", (data) => {
402
+ if (lodash_1.default.isPlainObject(data)) {
403
+ const old = data.old_password;
404
+ const p1 = data.new_password;
405
+ const p2 = data.verify_password;
406
+ if (typeof p1 === "undefined" || p1 === "" || p1 !== p2) {
407
+ socket.emit("change-password", {
408
+ error: "",
409
+ success: false,
410
+ });
411
+ return;
412
+ }
413
+ helper_1.default.password
414
+ .compare(old || "", client.config.password)
415
+ .then((matching) => {
416
+ if (!matching) {
417
+ socket.emit("change-password", {
418
+ error: "password_incorrect",
419
+ success: false,
420
+ });
421
+ return;
422
+ }
423
+ const hash = helper_1.default.password.hash(p1);
424
+ client.setPassword(hash, (success) => {
425
+ socket.emit("change-password", {
426
+ success: success,
427
+ error: success ? undefined : "update_failed",
428
+ });
429
+ });
430
+ })
431
+ .catch((error) => {
432
+ log_1.default.error(`Error while checking users password. Error: ${error.message}`);
433
+ });
434
+ }
435
+ });
436
+ }
437
+ socket.on("open", (data) => {
438
+ client.open(socket.id, data);
439
+ });
440
+ socket.on("sort:networks", (data) => {
441
+ if (!lodash_1.default.isPlainObject(data)) {
442
+ return;
443
+ }
444
+ if (!Array.isArray(data.order)) {
445
+ return;
446
+ }
447
+ client.sortNetworks(data.order);
448
+ });
449
+ socket.on("sort:channels", (data) => {
450
+ if (!lodash_1.default.isPlainObject(data)) {
451
+ return;
452
+ }
453
+ if (!Array.isArray(data.order) || typeof data.network !== "string") {
454
+ return;
455
+ }
456
+ client.sortChannels(data.network, data.order);
457
+ });
458
+ socket.on("names", (data) => {
459
+ if (lodash_1.default.isPlainObject(data)) {
460
+ client.names(data);
461
+ }
462
+ });
463
+ socket.on("changelog", () => {
464
+ Promise.all([changelog_1.default.fetch(), index_1.default.outdated()])
465
+ .then(([changelogData, packageUpdate]) => {
466
+ changelogData.packages = packageUpdate;
467
+ socket.emit("changelog", changelogData);
468
+ })
469
+ .catch((error) => {
470
+ log_1.default.error(`Error while fetching changelog. Error: ${error.message}`);
471
+ });
472
+ });
473
+ // In public mode only one client can be connected,
474
+ // so there's no need to handle msg:preview:toggle
475
+ if (!config_1.default.values.public) {
476
+ socket.on("msg:preview:toggle", (data) => {
477
+ if (lodash_1.default.isPlainObject(data)) {
478
+ return;
479
+ }
480
+ const networkAndChan = client.find(data.target);
481
+ const newState = Boolean(data.shown);
482
+ if (!networkAndChan) {
483
+ return;
484
+ }
485
+ // Process multiple message at once for /collapse and /expand commands
486
+ if (Array.isArray(data.messageIds)) {
487
+ for (const msgId of data.messageIds) {
488
+ const message = networkAndChan.chan.findMessage(msgId);
489
+ if (message) {
490
+ for (const preview of message.previews) {
491
+ preview.shown = newState;
492
+ }
493
+ }
494
+ }
495
+ return;
496
+ }
497
+ const message = data.msgId ? networkAndChan.chan.findMessage(data.msgId) : null;
498
+ if (!message) {
499
+ return;
500
+ }
501
+ const preview = data.link ? message.findPreview(data.link) : null;
502
+ if (preview) {
503
+ preview.shown = newState;
504
+ }
505
+ });
506
+ }
507
+ socket.on("mentions:get", () => {
508
+ socket.emit("mentions:list", client.mentions);
509
+ });
510
+ socket.on("mentions:dismiss", (msgId) => {
511
+ if (typeof msgId !== "number") {
512
+ return;
513
+ }
514
+ client.mentions.splice(client.mentions.findIndex((m) => m.msgId === msgId), 1);
515
+ });
516
+ socket.on("mentions:dismiss_all", () => {
517
+ client.mentions = [];
518
+ });
519
+ if (!config_1.default.values.public) {
520
+ socket.on("push:register", (subscription) => {
521
+ if (!Object.prototype.hasOwnProperty.call(client.config.sessions, token)) {
522
+ return;
523
+ }
524
+ const registration = client.registerPushSubscription(client.config.sessions[token], subscription);
525
+ if (registration) {
526
+ client.manager.webPush.pushSingle(client, registration, {
527
+ type: "notification",
528
+ timestamp: Date.now(),
529
+ title: "The Lounge",
530
+ body: "🚀 Push notifications have been enabled",
531
+ });
532
+ }
533
+ });
534
+ socket.on("push:unregister", () => client.unregisterPushSubscription(token));
535
+ }
536
+ const sendSessionList = () => {
537
+ // TODO: this should use the ClientSession type currently in client
538
+ const sessions = lodash_1.default.map(client.config.sessions, (session, sessionToken) => {
539
+ return {
540
+ current: sessionToken === token,
541
+ active: lodash_1.default.reduce(client.attachedClients, (count, attachedClient) => count + (attachedClient.token === sessionToken ? 1 : 0), 0),
542
+ lastUse: session.lastUse,
543
+ ip: session.ip,
544
+ agent: session.agent,
545
+ token: sessionToken, // TODO: Ideally don't expose actual tokens to the client
546
+ };
547
+ });
548
+ socket.emit("sessions:list", sessions);
549
+ };
550
+ socket.on("sessions:get", sendSessionList);
551
+ if (!config_1.default.values.public) {
552
+ socket.on("setting:set", (newSetting) => {
553
+ if (!lodash_1.default.isPlainObject(newSetting)) {
554
+ return;
555
+ }
556
+ if (typeof newSetting.value === "object" ||
557
+ typeof newSetting.name !== "string" ||
558
+ newSetting.name[0] === "_") {
559
+ return;
560
+ }
561
+ // We do not need to do write operations and emit events if nothing changed.
562
+ if (client.config.clientSettings[newSetting.name] !== newSetting.value) {
563
+ client.config.clientSettings[newSetting.name] = newSetting.value;
564
+ // Pass the setting to all clients.
565
+ client.emit("setting:new", {
566
+ name: newSetting.name,
567
+ value: newSetting.value,
568
+ });
569
+ client.save();
570
+ if (newSetting.name === "highlights" || newSetting.name === "highlightExceptions") {
571
+ client.compileCustomHighlights();
572
+ }
573
+ else if (newSetting.name === "awayMessage") {
574
+ if (typeof newSetting.value !== "string") {
575
+ newSetting.value = "";
576
+ }
577
+ client.awayMessage = newSetting.value;
578
+ }
579
+ }
580
+ });
581
+ socket.on("setting:get", () => {
582
+ if (!Object.prototype.hasOwnProperty.call(client.config, "clientSettings")) {
583
+ socket.emit("setting:all", {});
584
+ return;
585
+ }
586
+ const clientSettings = client.config.clientSettings;
587
+ socket.emit("setting:all", clientSettings);
588
+ });
589
+ socket.on("search", async (query) => {
590
+ const results = await client.search(query);
591
+ socket.emit("search:results", results);
592
+ });
593
+ socket.on("mute:change", ({ target, setMutedTo }) => {
594
+ const networkAndChan = client.find(target);
595
+ if (!networkAndChan) {
596
+ return;
597
+ }
598
+ const { chan, network } = networkAndChan;
599
+ // If the user mutes the lobby, we mute the entire network.
600
+ if (chan.type === chan_1.ChanType.LOBBY) {
601
+ for (const channel of network.channels) {
602
+ if (channel.type !== chan_1.ChanType.SPECIAL) {
603
+ channel.setMuteStatus(setMutedTo);
604
+ }
605
+ }
606
+ }
607
+ else {
608
+ if (chan.type !== chan_1.ChanType.SPECIAL) {
609
+ chan.setMuteStatus(setMutedTo);
610
+ }
611
+ }
612
+ for (const attachedClient of Object.keys(client.attachedClients)) {
613
+ manager.sockets.in(attachedClient).emit("mute:changed", {
614
+ target,
615
+ status: setMutedTo,
616
+ });
617
+ }
618
+ client.save();
619
+ });
620
+ }
621
+ socket.on("sign-out", (tokenToSignOut) => {
622
+ // If no token provided, sign same client out
623
+ if (!tokenToSignOut || typeof tokenToSignOut !== "string") {
624
+ tokenToSignOut = token;
625
+ }
626
+ if (!Object.prototype.hasOwnProperty.call(client.config.sessions, tokenToSignOut)) {
627
+ return;
628
+ }
629
+ delete client.config.sessions[tokenToSignOut];
630
+ client.save();
631
+ lodash_1.default.map(client.attachedClients, (attachedClient, socketId) => {
632
+ if (attachedClient.token !== tokenToSignOut) {
633
+ return;
634
+ }
635
+ const socketToRemove = manager.sockets.of("/").sockets.get(socketId);
636
+ socketToRemove.emit("sign-out");
637
+ socketToRemove.disconnect();
638
+ });
639
+ // Do not send updated session list if user simply logs out
640
+ if (tokenToSignOut !== token) {
641
+ sendSessionList();
642
+ }
643
+ });
644
+ // socket.join is a promise depending on the adapter.
645
+ void socket.join(client.id);
646
+ const sendInitEvent = (tokenToSend) => {
647
+ socket.emit("init", {
648
+ active: openChannel,
649
+ networks: client.networks.map((network) => network.getFilteredClone(openChannel, lastMessage)),
650
+ token: tokenToSend,
651
+ });
652
+ socket.emit("commands", inputs_1.default.getCommands());
653
+ };
654
+ if (config_1.default.values.public) {
655
+ sendInitEvent();
656
+ }
657
+ else if (!token) {
658
+ client.generateToken((newToken) => {
659
+ token = client.calculateTokenHash(newToken);
660
+ client.attachedClients[socket.id].token = token;
661
+ client.updateSession(token, getClientIp(socket), socket.request);
662
+ sendInitEvent(newToken);
663
+ });
664
+ }
665
+ else {
666
+ client.updateSession(token, getClientIp(socket), socket.request);
667
+ sendInitEvent();
668
+ }
669
+ }
670
+ function getClientConfiguration() {
671
+ const common = {
672
+ fileUpload: config_1.default.values.fileUpload.enable,
673
+ ldapEnabled: config_1.default.values.ldap.enable,
674
+ isUpdateAvailable: changelog_1.default.isUpdateAvailable,
675
+ applicationServerKey: manager.webPush.vapidKeys.publicKey,
676
+ version: helper_1.default.getVersionNumber(),
677
+ gitCommit: helper_1.default.getGitCommit(),
678
+ themes: themes_1.default.getAll(),
679
+ defaultTheme: config_1.default.values.theme,
680
+ public: config_1.default.values.public,
681
+ useHexIp: config_1.default.values.useHexIp,
682
+ prefetch: config_1.default.values.prefetch,
683
+ fileUploadMaxFileSize: uploader_1.default ? uploader_1.default.getMaxFileSize() : undefined, // TODO can't be undefined?
684
+ };
685
+ const defaultsOverride = {
686
+ nick: config_1.default.getDefaultNick(), // expand the number part
687
+ // TODO: this doesn't seem right, if the client needs this as a buffer
688
+ // the client ought to add it on its own
689
+ sasl: "",
690
+ saslAccount: "",
691
+ saslPassword: "",
692
+ };
693
+ if (!config_1.default.values.lockNetwork) {
694
+ const defaults = {
695
+ ...lodash_1.default.clone(config_1.default.values.defaults),
696
+ ...defaultsOverride,
697
+ };
698
+ const result = {
699
+ ...common,
700
+ defaults: defaults,
701
+ lockNetwork: config_1.default.values.lockNetwork,
702
+ };
703
+ return result;
704
+ }
705
+ // Only send defaults that are visible on the client
706
+ const defaults = {
707
+ ...lodash_1.default.pick(config_1.default.values.defaults, ["name", "username", "password", "realname", "join"]),
708
+ ...defaultsOverride,
709
+ };
710
+ const result = {
711
+ ...common,
712
+ lockNetwork: config_1.default.values.lockNetwork,
713
+ defaults: defaults,
714
+ };
715
+ return result;
716
+ }
717
+ function getServerConfiguration() {
718
+ return { ...config_1.default.values, ...{ stylesheets: index_1.default.getStylesheets() } };
719
+ }
720
+ function performAuthentication(data) {
721
+ if (!lodash_1.default.isPlainObject(data)) {
722
+ return;
723
+ }
724
+ const socket = this;
725
+ let client;
726
+ let token;
727
+ const finalInit = () => {
728
+ let lastMessage = -1;
729
+ if (data && "lastMessage" in data && data.lastMessage) {
730
+ lastMessage = data.lastMessage;
731
+ }
732
+ // TODO: bonkers, but for now good enough until we rewrite the logic properly
733
+ // initializeClient will check for if(openChannel) and as 0 is falsey it does the fallback...
734
+ let openChannel = 0;
735
+ if (data && "openChannel" in data && data.openChannel) {
736
+ openChannel = data.openChannel;
737
+ }
738
+ // TODO: remove this once the logic is cleaned up
739
+ if (!client) {
740
+ throw new Error("finalInit called with undefined client, this is a bug");
741
+ }
742
+ initializeClient(socket, client, token, lastMessage, openChannel);
743
+ };
744
+ const initClient = () => {
745
+ if (!client) {
746
+ throw new Error("initClient called with undefined client");
747
+ }
748
+ // Configuration does not change during runtime of TL,
749
+ // and the client listens to this event only once
750
+ if (data && (!("hasConfig" in data) || !data.hasConfig)) {
751
+ socket.emit("configuration", getClientConfiguration());
752
+ socket.emit("push:issubscribed", token && client.config.sessions[token].pushSubscription ? true : false);
753
+ }
754
+ const clientIP = getClientIp(socket);
755
+ client.config.browser = {
756
+ ip: clientIP,
757
+ isSecure: getClientSecure(socket),
758
+ language: getClientLanguage(socket),
759
+ };
760
+ // If webirc is enabled perform reverse dns lookup
761
+ if (config_1.default.values.webirc === null) {
762
+ return finalInit();
763
+ }
764
+ const cb_client = client; // ensure that TS figures out that client can't be nil
765
+ reverseDnsLookup(clientIP, (hostname) => {
766
+ cb_client.config.browser.hostname = hostname;
767
+ finalInit();
768
+ });
769
+ };
770
+ if (config_1.default.values.public) {
771
+ client = new client_1.default(manager);
772
+ client.connect();
773
+ manager.clients.push(client);
774
+ const cb_client = client; // ensure TS can see we never have a nil client
775
+ socket.on("disconnect", function () {
776
+ manager.clients = lodash_1.default.without(manager.clients, cb_client);
777
+ cb_client.quit();
778
+ });
779
+ initClient();
780
+ return;
781
+ }
782
+ if (typeof data.user !== "string") {
783
+ return;
784
+ }
785
+ const authCallback = (success) => {
786
+ // Authorization failed
787
+ if (!success) {
788
+ if (!client) {
789
+ log_1.default.warn(`Authentication for non existing user attempted from ${chalk_1.default.bold(getClientIp(socket))}`);
790
+ }
791
+ else {
792
+ log_1.default.warn(`Authentication failed for user ${chalk_1.default.bold(data.user)} from ${chalk_1.default.bold(getClientIp(socket))}`);
793
+ }
794
+ socket.emit("auth:failed");
795
+ return;
796
+ }
797
+ // If authorization succeeded but there is no loaded user,
798
+ // load it and find the user again (this happens with LDAP)
799
+ if (!client) {
800
+ client = manager.loadUser(data.user);
801
+ if (!client) {
802
+ throw new Error(`authCallback: ${data.user} not found after second lookup`);
803
+ }
804
+ }
805
+ initClient();
806
+ };
807
+ client = manager.findClient(data.user);
808
+ // We have found an existing user and client has provided a token
809
+ if (client && "token" in data && data.token) {
810
+ const providedToken = client.calculateTokenHash(data.token);
811
+ if (Object.prototype.hasOwnProperty.call(client.config.sessions, providedToken)) {
812
+ token = providedToken;
813
+ authCallback(true);
814
+ return;
815
+ }
816
+ }
817
+ if (!("user" in data && "password" in data)) {
818
+ log_1.default.warn("performAuthentication: callback data has no user or no password");
819
+ authCallback(false);
820
+ return;
821
+ }
822
+ auth_1.default.initialize().then(() => {
823
+ // Perform password checking
824
+ auth_1.default.auth(manager, client, data.user, data.password, authCallback);
825
+ });
826
+ }
827
+ function reverseDnsLookup(ip, callback) {
828
+ // node can throw, even if we provide valid input based on the DNS server
829
+ // returning SERVFAIL it seems: https://github.com/thelounge/thelounge/issues/4768
830
+ // so we manually resolve with the ip as a fallback in case something fails
831
+ try {
832
+ dns_1.default.reverse(ip, (reverseErr, hostnames) => {
833
+ if (reverseErr || hostnames.length < 1) {
834
+ return callback(ip);
835
+ }
836
+ dns_1.default.resolve(hostnames[0], net_1.default.isIP(ip) === 6 ? "AAAA" : "A", (resolveErr, resolvedIps) => {
837
+ // TODO: investigate SoaRecord class
838
+ if (!Array.isArray(resolvedIps)) {
839
+ return callback(ip);
840
+ }
841
+ if (resolveErr || resolvedIps.length < 1) {
842
+ return callback(ip);
843
+ }
844
+ for (const resolvedIp of resolvedIps) {
845
+ if (ip === resolvedIp) {
846
+ return callback(hostnames[0]);
847
+ }
848
+ }
849
+ return callback(ip);
850
+ });
851
+ });
852
+ }
853
+ catch (err) {
854
+ log_1.default.error(`failed to resolve rDNS for ${ip}, using ip instead`, err.toString());
855
+ setImmediate(callback, ip); // makes sure we always behave asynchronously
856
+ }
857
+ }