UNPKG

15.8 kBJavaScriptView Raw
1"use strict";
2
3var _ = require("lodash");
4var pkg = require("../package.json");
5var Client = require("./client");
6var ClientManager = require("./clientManager");
7var express = require("express");
8var fs = require("fs");
9var path = require("path");
10var io = require("socket.io");
11var dns = require("dns");
12var Helper = require("./helper");
13var colors = require("colors/safe");
14const net = require("net");
15const Identification = require("./identification");
16const changelog = require("./plugins/changelog");
17
18const themes = require("./plugins/packages/themes");
19themes.loadLocalThemes();
20
21const packages = require("./plugins/packages/index");
22packages.loadPackages();
23
24// The order defined the priority: the first available plugin is used
25// ALways keep local auth in the end, which should always be enabled.
26const authPlugins = [
27 require("./plugins/auth/ldap"),
28 require("./plugins/auth/local"),
29];
30
31// A random number that will force clients to reload the page if it differs
32const serverHash = Math.floor(Date.now() * Math.random());
33
34var manager = null;
35
36module.exports = function() {
37 log.info(`The Lounge ${colors.green(Helper.getVersion())} \
38(Node.js ${colors.green(process.versions.node)} on ${colors.green(process.platform)} ${process.arch})`);
39 log.info(`Configuration file: ${colors.green(Helper.getConfigPath())}`);
40
41 var app = express()
42 .disable("x-powered-by")
43 .use(allRequests)
44 .use(index)
45 .use(express.static("public"))
46 .use("/storage/", express.static(Helper.getStoragePath(), {
47 redirect: false,
48 maxAge: 86400 * 1000,
49 }));
50
51 // This route serves *installed themes only*. Local themes are served directly
52 // from the `public/themes/` folder as static assets, without entering this
53 // handler. Remember this is you make changes to this function, serving of
54 // local themes will not get those changes.
55 app.get("/themes/:theme.css", (req, res) => {
56 const themeName = req.params.theme;
57 const theme = themes.getFilename(themeName);
58 if (theme === undefined) {
59 return res.status(404).send("Not found");
60 }
61 return res.sendFile(theme);
62 });
63
64 app.get("/packages/:package/:filename", (req, res) => {
65 const packageName = req.params.package;
66 const fileName = req.params.filename;
67 const packageFile = packages.getPackage(packageName);
68 if (!packageFile || !packages.getStylesheets().includes(`${packageName}/${fileName}`)) {
69 return res.status(404).send("Not found");
70 }
71 const packagePath = Helper.getPackageModulePath(packageName);
72 return res.sendFile(path.join(packagePath, fileName));
73 });
74
75 var config = Helper.config;
76 var server = null;
77
78 if (config.public && (config.ldap || {}).enable) {
79 log.warn("Server is public and set to use LDAP. Set to private mode if trying to use LDAP authentication.");
80 }
81
82 if (!config.https.enable) {
83 server = require("http");
84 server = server.createServer(app);
85 } else {
86 const keyPath = Helper.expandHome(config.https.key);
87 const certPath = Helper.expandHome(config.https.certificate);
88 const caPath = Helper.expandHome(config.https.ca);
89
90 if (!keyPath.length || !fs.existsSync(keyPath)) {
91 log.error("Path to SSL key is invalid. Stopping server...");
92 process.exit();
93 }
94
95 if (!certPath.length || !fs.existsSync(certPath)) {
96 log.error("Path to SSL certificate is invalid. Stopping server...");
97 process.exit();
98 }
99
100 if (caPath.length && !fs.existsSync(caPath)) {
101 log.error("Path to SSL ca bundle is invalid. Stopping server...");
102 process.exit();
103 }
104
105 server = require("spdy");
106 server = server.createServer({
107 key: fs.readFileSync(keyPath),
108 cert: fs.readFileSync(certPath),
109 ca: caPath ? fs.readFileSync(caPath) : undefined,
110 }, app);
111 }
112
113 let listenParams;
114
115 if (typeof config.host === "string" && config.host.startsWith("unix:")) {
116 listenParams = config.host.replace(/^unix:/, "");
117 } else {
118 listenParams = {
119 port: config.port,
120 host: config.host,
121 };
122 }
123
124 server.on("error", (err) => log.error(`${err}`));
125
126 server.listen(listenParams, () => {
127 if (typeof listenParams === "string") {
128 log.info("Available on socket " + colors.green(listenParams));
129 } else {
130 const protocol = config.https.enable ? "https" : "http";
131 const address = server.address();
132
133 log.info(
134 "Available at " +
135 colors.green(`${protocol}://${address.address}:${address.port}/`) +
136 ` in ${colors.bold(config.public ? "public" : "private")} mode`
137 );
138 }
139
140 const sockets = io(server, {
141 serveClient: false,
142 transports: config.transports,
143 });
144
145 sockets.on("connect", (socket) => {
146 if (config.public) {
147 performAuthentication.call(socket, {});
148 } else {
149 socket.emit("auth", {
150 serverHash: serverHash,
151 success: true,
152 });
153 socket.on("auth", performAuthentication);
154 }
155 });
156
157 manager = new ClientManager();
158
159 new Identification((identHandler) => {
160 manager.init(identHandler, sockets);
161 });
162
163 // Handle ctrl+c and kill gracefully
164 let suicideTimeout = null;
165 const exitGracefully = function() {
166 if (suicideTimeout !== null) {
167 return;
168 }
169
170 if (Helper.config.prefetchStorage) {
171 log.info("Clearing prefetch storage folder, this might take a while...");
172
173 require("./plugins/storage").emptyDir();
174 }
175
176 // Forcefully exit after 3 seconds
177 suicideTimeout = setTimeout(() => process.exit(1), 3000);
178
179 log.info("Exiting...");
180
181 // Close all client and IRC connections
182 manager.clients.forEach((client) => client.quit());
183
184 // Close http server
185 server.close(() => {
186 clearTimeout(suicideTimeout);
187 process.exit(0);
188 });
189 };
190
191 process.on("SIGINT", exitGracefully);
192 process.on("SIGTERM", exitGracefully);
193
194 // Clear storage folder after server starts successfully
195 if (Helper.config.prefetchStorage) {
196 require("./plugins/storage").emptyDir();
197 }
198 });
199
200 return server;
201};
202
203function getClientIp(socket) {
204 let ip = socket.handshake.address;
205
206 if (Helper.config.reverseProxy) {
207 const forwarded = (socket.request.headers["x-forwarded-for"] || "").split(/\s*,\s*/).filter(Boolean);
208
209 if (forwarded.length && net.isIP(forwarded[0])) {
210 ip = forwarded[0];
211 }
212 }
213
214 return ip.replace(/^::ffff:/, "");
215}
216
217function allRequests(req, res, next) {
218 res.setHeader("X-Content-Type-Options", "nosniff");
219 return next();
220}
221
222function index(req, res, next) {
223 if (req.url.split("?")[0] !== "/") {
224 return next();
225 }
226
227 const policies = [
228 "default-src 'none'", // default to nothing
229 "form-action 'none'", // no default-src fallback
230 "connect-src 'self' ws: wss:", // allow self for polling; websockets
231 "style-src 'self' https: 'unsafe-inline'", // allow inline due to use in irc hex colors
232 "script-src 'self'", // javascript
233 "worker-src 'self'", // service worker
234 "child-src 'self'", // deprecated fall back for workers, Firefox <58, see #1902
235 "manifest-src 'self'", // manifest.json
236 "font-src 'self' https:", // allow loading fonts from secure sites (e.g. google fonts)
237 "media-src 'self' https:", // self for notification sound; allow https media (audio previews)
238 ];
239
240 // If prefetch is enabled, but storage is not, we have to allow mixed content
241 // - https://user-images.githubusercontent.com is where we currently push our changelog screenshots
242 // - data: is required for the HTML5 video player
243 if (Helper.config.prefetchStorage || !Helper.config.prefetch) {
244 policies.push("img-src 'self' data: https://user-images.githubusercontent.com");
245 policies.unshift("block-all-mixed-content");
246 } else {
247 policies.push("img-src http: https: data:");
248 }
249
250 res.setHeader("Content-Type", "text/html");
251 res.setHeader("Content-Security-Policy", policies.join("; "));
252 res.setHeader("Referrer-Policy", "no-referrer");
253
254 return fs.readFile(path.join(__dirname, "..", "client", "index.html.tpl"), "utf-8", (err, file) => {
255 if (err) {
256 throw err;
257 }
258
259 res.send(_.template(file)(getServerConfiguration()));
260 });
261}
262
263function initializeClient(socket, client, token, lastMessage) {
264 socket.emit("authorized");
265
266 client.clientAttach(socket.id, token);
267
268 socket.on("disconnect", function() {
269 client.clientDetach(socket.id);
270 });
271
272 socket.on(
273 "input",
274 function(data) {
275 client.input(data);
276 }
277 );
278
279 socket.on(
280 "more",
281 function(data) {
282 const history = client.more(data);
283
284 if (history !== null) {
285 socket.emit("more", history);
286 }
287 }
288 );
289
290 socket.on(
291 "conn",
292 function(data) {
293 // prevent people from overriding webirc settings
294 data.ip = null;
295 data.hostname = null;
296
297 client.connect(data);
298 }
299 );
300
301 if (!Helper.config.public && !Helper.config.ldap.enable) {
302 socket.on(
303 "change-password",
304 function(data) {
305 var old = data.old_password;
306 var p1 = data.new_password;
307 var p2 = data.verify_password;
308 if (typeof p1 === "undefined" || p1 === "") {
309 socket.emit("change-password", {
310 error: "Please enter a new password",
311 });
312 return;
313 }
314 if (p1 !== p2) {
315 socket.emit("change-password", {
316 error: "Both new password fields must match",
317 });
318 return;
319 }
320
321 Helper.password
322 .compare(old || "", client.config.password)
323 .then((matching) => {
324 if (!matching) {
325 socket.emit("change-password", {
326 error: "The current password field does not match your account password",
327 });
328 return;
329 }
330 const hash = Helper.password.hash(p1);
331
332 client.setPassword(hash, (success) => {
333 const obj = {};
334
335 if (success) {
336 obj.success = "Successfully updated your password";
337 } else {
338 obj.error = "Failed to update your password";
339 }
340
341 socket.emit("change-password", obj);
342 });
343 }).catch((error) => {
344 log.error(`Error while checking users password. Error: ${error}`);
345 });
346 }
347 );
348 }
349
350 socket.on(
351 "open",
352 function(data) {
353 client.open(socket.id, data);
354 }
355 );
356
357 socket.on(
358 "sort",
359 function(data) {
360 client.sort(data);
361 }
362 );
363
364 socket.on(
365 "names",
366 function(data) {
367 client.names(data);
368 }
369 );
370
371 socket.on("changelog", function() {
372 changelog.fetch((data) => {
373 socket.emit("changelog", data);
374 });
375 });
376
377 socket.on("msg:preview:toggle", function(data) {
378 const networkAndChan = client.find(data.target);
379 if (!networkAndChan) {
380 return;
381 }
382
383 const message = networkAndChan.chan.findMessage(data.msgId);
384
385 if (!message) {
386 return;
387 }
388
389 const preview = message.findPreview(data.link);
390
391 if (preview) {
392 preview.shown = data.shown;
393 }
394 });
395
396 socket.on("push:register", (subscription) => {
397 if (!client.isRegistered() || !client.config.sessions.hasOwnProperty(token)) {
398 return;
399 }
400
401 const registration = client.registerPushSubscription(client.config.sessions[token], subscription);
402
403 if (registration) {
404 client.manager.webPush.pushSingle(client, registration, {
405 type: "notification",
406 timestamp: Date.now(),
407 title: "The Lounge",
408 body: "🚀 Push notifications have been enabled",
409 });
410 }
411 });
412
413 socket.on("push:unregister", () => {
414 if (!client.isRegistered()) {
415 return;
416 }
417
418 client.unregisterPushSubscription(token);
419 });
420
421 const sendSessionList = () => {
422 const sessions = _.map(client.config.sessions, (session, sessionToken) => ({
423 current: sessionToken === token,
424 active: _.find(client.attachedClients, (u) => u.token === sessionToken) !== undefined,
425 lastUse: session.lastUse,
426 ip: session.ip,
427 agent: session.agent,
428 token: sessionToken, // TODO: Ideally don't expose actual tokens to the client
429 }));
430
431 socket.emit("sessions:list", sessions);
432 };
433
434 socket.on("sessions:get", sendSessionList);
435
436 socket.on("sign-out", (tokenToSignOut) => {
437 // If no token provided, sign same client out
438 if (!tokenToSignOut) {
439 tokenToSignOut = token;
440 }
441
442 if (!client.config.sessions.hasOwnProperty(tokenToSignOut)) {
443 return;
444 }
445
446 delete client.config.sessions[tokenToSignOut];
447
448 client.manager.updateUser(client.name, {
449 sessions: client.config.sessions,
450 });
451
452 _.map(client.attachedClients, (attachedClient, socketId) => {
453 if (attachedClient.token !== tokenToSignOut) {
454 return;
455 }
456
457 const socketToRemove = manager.sockets.of("/").connected[socketId];
458
459 socketToRemove.emit("sign-out");
460 socketToRemove.disconnect();
461 });
462
463 // Do not send updated session list if user simply logs out
464 if (tokenToSignOut !== token) {
465 sendSessionList();
466 }
467 });
468
469 socket.join(client.id);
470
471 const sendInitEvent = (tokenToSend) => {
472 socket.emit("init", {
473 applicationServerKey: manager.webPush.vapidKeys.publicKey,
474 pushSubscription: client.config.sessions[token],
475 active: client.lastActiveChannel,
476 networks: client.networks.map((network) => network.getFilteredClone(client.lastActiveChannel, lastMessage)),
477 token: tokenToSend,
478 });
479 };
480
481 if (!Helper.config.public && token === null) {
482 client.generateToken((newToken) => {
483 client.attachedClients[socket.id].token = token = client.calculateTokenHash(newToken);
484
485 client.updateSession(token, getClientIp(socket), socket.request);
486
487 sendInitEvent(newToken);
488 });
489 } else {
490 sendInitEvent(null);
491 }
492}
493
494function getClientConfiguration() {
495 const config = _.pick(Helper.config, [
496 "public",
497 "lockNetwork",
498 "displayNetwork",
499 "useHexIp",
500 "themes",
501 "prefetch",
502 ]);
503
504 config.ldapEnabled = Helper.config.ldap.enable;
505 config.version = pkg.version;
506 config.gitCommit = Helper.getGitCommit();
507 config.themes = themes.getAll();
508
509 if (config.displayNetwork) {
510 config.defaults = Helper.config.defaults;
511 } else {
512 // Only send defaults that are visible on the client
513 config.defaults = _.pick(Helper.config.defaults, [
514 "nick",
515 "username",
516 "password",
517 "realname",
518 "join",
519 ]);
520 }
521
522 return config;
523}
524
525function getServerConfiguration() {
526 const config = _.clone(Helper.config);
527
528 config.stylesheets = packages.getStylesheets();
529
530 return config;
531}
532
533function performAuthentication(data) {
534 const socket = this;
535 let client;
536 let token = null;
537
538 const finalInit = () => initializeClient(socket, client, token, data.lastMessage || -1);
539
540 const initClient = () => {
541 socket.emit("configuration", getClientConfiguration());
542
543 client.ip = getClientIp(socket);
544
545 // If webirc is enabled perform reverse dns lookup
546 if (Helper.config.webirc === null) {
547 return finalInit();
548 }
549
550 reverseDnsLookup(client.ip, (hostname) => {
551 client.hostname = hostname;
552
553 finalInit();
554 });
555 };
556
557 if (Helper.config.public) {
558 client = new Client(manager);
559 manager.clients.push(client);
560
561 socket.on("disconnect", function() {
562 manager.clients = _.without(manager.clients, client);
563 client.quit();
564 });
565
566 initClient();
567
568 return;
569 }
570
571 const authCallback = (success) => {
572 // Authorization failed
573 if (!success) {
574 socket.emit("auth", {success: false});
575 return;
576 }
577
578 // If authorization succeeded but there is no loaded user,
579 // load it and find the user again (this happens with LDAP)
580 if (!client) {
581 client = manager.loadUser(data.user);
582 }
583
584 initClient();
585 };
586
587 client = manager.findClient(data.user);
588
589 // We have found an existing user and client has provided a token
590 if (client && data.token) {
591 const providedToken = client.calculateTokenHash(data.token);
592
593 if (client.config.sessions.hasOwnProperty(providedToken)) {
594 token = providedToken;
595
596 client.updateSession(providedToken, getClientIp(socket), socket.request);
597
598 return authCallback(true);
599 }
600 }
601
602 // Perform password checking
603 let auth = () => {
604 log.error("None of the auth plugins is enabled");
605 };
606 for (let i = 0; i < authPlugins.length; ++i) {
607 if (authPlugins[i].isEnabled()) {
608 auth = authPlugins[i].auth;
609 break;
610 }
611 }
612 auth(manager, client, data.user, data.password, authCallback);
613}
614
615function reverseDnsLookup(ip, callback) {
616 dns.reverse(ip, (err, hostnames) => {
617 if (!err && hostnames.length) {
618 return callback(hostnames[0]);
619 }
620
621 callback(ip);
622 });
623}