io.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. 'use strict';
  2. // This file contains all the logic for Socket.IO
  3. const coreClass = require("../core");
  4. const socketio = require("socket.io");
  5. const async = require("async");
  6. const config = require("config");
  7. module.exports = class extends coreClass {
  8. constructor(name, moduleManager) {
  9. super(name, moduleManager);
  10. this.dependsOn = ["app", "db", "cache", "utils"];
  11. }
  12. initialize() {
  13. return new Promise(resolve => {
  14. this.setStage(1);
  15. const logger = this.logger,
  16. app = this.moduleManager.modules["app"],
  17. cache = this.moduleManager.modules["cache"],
  18. utils = this.moduleManager.modules["utils"],
  19. db = this.moduleManager.modules["db"],
  20. punishments = this.moduleManager.modules["punishments"];
  21. const actions = require('../logic/actions');
  22. const SIDname = config.get("cookie.SIDname");
  23. // TODO: Check every 30s/60s, for all sockets, if they are still allowed to be in the rooms they are in, and on socket at all (permission changing/banning)
  24. this._io = socketio(app.server);
  25. this._io.use(async (socket, next) => {
  26. try { await this._validateHook(); } catch { return; }
  27. let SID;
  28. socket.ip = socket.request.headers['x-forwarded-for'] || '0.0.0.0';
  29. async.waterfall([
  30. (next) => {
  31. utils.parseCookies(
  32. socket.request.headers.cookie
  33. ).then(res => {
  34. SID = res[SIDname];
  35. next(null);
  36. });
  37. },
  38. (next) => {
  39. if (!SID) return next('No SID.');
  40. next();
  41. },
  42. (next) => {
  43. cache.hget('sessions', SID, next);
  44. },
  45. (session, next) => {
  46. if (!session) return next('No session found.');
  47. session.refreshDate = Date.now();
  48. socket.session = session;
  49. cache.hset('sessions', SID, session, next);
  50. },
  51. (res, next) => {
  52. // check if a session's user / IP is banned
  53. punishments.getPunishments((err, punishments) => {
  54. const isLoggedIn = !!(socket.session && socket.session.refreshDate);
  55. const userId = (isLoggedIn) ? socket.session.userId : null;
  56. let banishment = { banned: false, ban: 0 };
  57. punishments.forEach(punishment => {
  58. if (punishment.expiresAt > banishment.ban) banishment.ban = punishment;
  59. if (punishment.type === 'banUserId' && isLoggedIn && punishment.value === userId) banishment.banned = true;
  60. if (punishment.type === 'banUserIp' && punishment.value === socket.ip) banishment.banned = true;
  61. });
  62. socket.banishment = banishment;
  63. next();
  64. });
  65. }
  66. ], () => {
  67. if (!socket.session) socket.session = { socketId: socket.id };
  68. else socket.session.socketId = socket.id;
  69. next();
  70. });
  71. });
  72. this._io.on('connection', async socket => {
  73. try { await this._validateHook(); } catch { return; }
  74. let sessionInfo = '';
  75. if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
  76. // if session is banned
  77. if (socket.banishment && socket.banishment.banned) {
  78. logger.info('IO_BANNED_CONNECTION', `A user tried to connect, but is currently banned. IP: ${socket.ip}.${sessionInfo}`);
  79. socket.emit('keep.event:banned', socket.banishment.ban);
  80. socket.disconnect(true);
  81. } else {
  82. logger.info('IO_CONNECTION', `User connected. IP: ${socket.ip}.${sessionInfo}`);
  83. // catch when the socket has been disconnected
  84. socket.on('disconnect', () => {
  85. if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
  86. logger.info('IO_DISCONNECTION', `User disconnected. IP: ${socket.ip}.${sessionInfo}`);
  87. });
  88. socket.use((data, next) => {
  89. if (data.length === 0) return next(new Error("Not enough arguments specified."));
  90. else if (typeof data[0] !== "string") return next(new Error("First argument must be a string."));
  91. else {
  92. const namespaceAction = data[0];
  93. if (!namespaceAction || namespaceAction.indexOf(".") === -1 || namespaceAction.indexOf(".") !== namespaceAction.lastIndexOf(".")) return next(new Error("Invalid first argument"));
  94. const namespace = data[0].split(".")[0];
  95. const action = data[0].split(".")[1];
  96. if (!namespace) return next(new Error("Invalid namespace."));
  97. else if (!action) return next(new Error("Invalid action."));
  98. else if (!actions[namespace]) return next(new Error("Namespace not found."));
  99. else if (!actions[namespace][action]) return next(new Error("Action not found."));
  100. else return next();
  101. }
  102. });
  103. // catch errors on the socket (internal to socket.io)
  104. socket.on('error', console.error);
  105. // have the socket listen for each action
  106. Object.keys(actions).forEach(namespace => {
  107. Object.keys(actions[namespace]).forEach(action => {
  108. // the full name of the action
  109. let name = `${namespace}.${action}`;
  110. // listen for this action to be called
  111. socket.on(name, async (...args) => {
  112. let cb = args[args.length - 1];
  113. if (typeof cb !== "function")
  114. cb = () => {
  115. this.logger.info("IO_MODULE", `There was no callback provided for ${name}.`);
  116. }
  117. else args.pop();
  118. try { await this._validateHook(); } catch { return cb({status: 'failure', message: 'Lockdown'}); }
  119. // load the session from the cache
  120. cache.hget('sessions', socket.session.sessionId, (err, session) => {
  121. if (err && err !== true) {
  122. if (typeof cb === 'function') return cb({
  123. status: 'error',
  124. message: 'An error occurred while obtaining your session'
  125. });
  126. }
  127. // make sure the sockets sessionId isn't set if there is no session
  128. if (socket.session.sessionId && session === null) delete socket.session.sessionId;
  129. try {
  130. // call the action, passing it the session, and the arguments socket.io passed us
  131. actions[namespace][action].apply(null, [socket.session].concat(args).concat([
  132. (result) => {
  133. // respond to the socket with our message
  134. if (typeof cb === 'function') return cb(result);
  135. }
  136. ]));
  137. } catch(err) {
  138. this.logger.error("IO_ACTION_ERROR", `Some type of exception occurred in the action ${namespace}.${action}. Error message: ${err.message}`);
  139. if (typeof cb === 'function') return cb({
  140. status: "error",
  141. message: "An error occurred while executing the specified action."
  142. });
  143. }
  144. });
  145. })
  146. })
  147. });
  148. if (socket.session.sessionId) {
  149. cache.hget('sessions', socket.session.sessionId, (err, session) => {
  150. if (err && err !== true) socket.emit('ready', false);
  151. else if (session && session.userId) {
  152. db.models.user.findOne({ _id: session.userId }, (err, user) => {
  153. if (err || !user) return socket.emit('ready', false);
  154. let role = '';
  155. let username = '';
  156. let userId = '';
  157. if (user) {
  158. role = user.role;
  159. username = user.username;
  160. userId = session.userId;
  161. }
  162. socket.emit('ready', true, role, username, userId);
  163. });
  164. } else socket.emit('ready', false);
  165. })
  166. } else socket.emit('ready', false);
  167. }
  168. });
  169. resolve();
  170. });
  171. }
  172. async io () {
  173. try { await this._validateHook(); } catch { return; }
  174. return this._io;
  175. }
  176. }