socket-io.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. const socketIo = require('socket.io');
  2. const expressSession = require('express-session');
  3. const passport = require('passport');
  4. const socketioSession = require('@kobalab/socket.io-session');
  5. const logger = require('@alias/logger')('growi:service:socket-io');
  6. /**
  7. * Serve socket.io for server-to-client messaging
  8. */
  9. class SocketIoService {
  10. constructor(crowi) {
  11. this.crowi = crowi;
  12. this.configManager = crowi.configManager;
  13. this.guestClients = new Set();
  14. }
  15. get isInitialized() {
  16. return (this.io != null);
  17. }
  18. attachServer(server) {
  19. this.io = socketIo(server, {
  20. transports: ['websocket'],
  21. });
  22. // create namespace for admin
  23. this.adminNamespace = this.io.of('/admin');
  24. // setup middlewares
  25. // !!CAUTION!! -- ORDER IS IMPORTANT
  26. this.setupSessionMiddleware();
  27. this.setupLoginRequiredMiddleware();
  28. this.setupAdminRequiredMiddleware();
  29. this.setupCheckConnectionLimitsMiddleware();
  30. this.setupStoreGuestIdEventHandler();
  31. }
  32. getDefaultSocket() {
  33. if (this.io == null) {
  34. throw new Error('Http server has not attached yet.');
  35. }
  36. return this.io.sockets;
  37. }
  38. getAdminSocket() {
  39. if (this.io == null) {
  40. throw new Error('Http server has not attached yet.');
  41. }
  42. return this.adminNamespace;
  43. }
  44. /**
  45. * use passport session
  46. * @see https://qiita.com/kobalab/items/083e507fb01159fe9774
  47. */
  48. setupSessionMiddleware() {
  49. const sessionMiddleware = socketioSession(expressSession(this.crowi.sessionConfig), passport);
  50. this.io.use(sessionMiddleware.express_session);
  51. this.io.use(sessionMiddleware.passport_initialize);
  52. this.io.use(sessionMiddleware.passport_session);
  53. }
  54. /**
  55. * use loginRequired middleware
  56. */
  57. setupLoginRequiredMiddleware() {
  58. const loginRequired = require('../middlewares/login-required')(this.crowi, true, (req, res, next) => {
  59. next(new Error('Login is required to connect.'));
  60. });
  61. // convert Connect/Express middleware to Socket.io middleware
  62. this.io.use((socket, next) => {
  63. loginRequired(socket.request, {}, next);
  64. });
  65. }
  66. /**
  67. * use adminRequired middleware
  68. */
  69. setupAdminRequiredMiddleware() {
  70. const adminRequired = require('../middlewares/admin-required')(this.crowi, (req, res, next) => {
  71. next(new Error('Admin priviledge is required to connect.'));
  72. });
  73. // convert Connect/Express middleware to Socket.io middleware
  74. this.getAdminSocket().use((socket, next) => {
  75. adminRequired(socket.request, {}, next);
  76. });
  77. }
  78. /**
  79. * use checkConnectionLimits middleware
  80. */
  81. setupCheckConnectionLimitsMiddleware() {
  82. this.getAdminSocket().use(this.checkConnectionLimitsForAdmin.bind(this));
  83. this.getDefaultSocket().use(this.checkConnectionLimitsForGuest.bind(this));
  84. this.getDefaultSocket().use(this.checkConnectionLimits.bind(this));
  85. }
  86. setupStoreGuestIdEventHandler() {
  87. this.io.on('connection', (socket) => {
  88. if (socket.request.user == null) {
  89. this.guestClients.add(socket.id);
  90. socket.on('disconnect', () => {
  91. this.guestClients.delete(socket.id);
  92. });
  93. }
  94. });
  95. }
  96. async getClients(namespace) {
  97. return new Promise((resolve, reject) => {
  98. namespace.clients((error, clients) => {
  99. if (error) {
  100. reject(error);
  101. }
  102. resolve(clients);
  103. });
  104. });
  105. }
  106. async checkConnectionLimitsForAdmin(socket, next) {
  107. const namespaceName = socket.nsp.name;
  108. if (namespaceName === '/admin') {
  109. const clients = await this.getClients(this.getAdminSocket());
  110. const clientsCount = clients.length;
  111. logger.debug('Current count of clients for \'/admin\':', clientsCount);
  112. const limit = this.configManager.getConfig('crowi', 's2cMessagingPubsub:connectionsLimitForAdmin');
  113. if (limit <= clientsCount) {
  114. const msg = `The connection was refused because the current count of clients for '/admin' is ${clientsCount} and exceeds the limit`;
  115. logger.warn(msg);
  116. next(new Error(msg));
  117. return;
  118. }
  119. }
  120. next();
  121. }
  122. async checkConnectionLimitsForGuest(socket, next) {
  123. if (socket.request.user == null) {
  124. const clientsCount = this.guestClients.size;
  125. logger.debug('Current count of clients for guests:', clientsCount);
  126. const limit = this.configManager.getConfig('crowi', 's2cMessagingPubsub:connectionsLimitForGuest');
  127. if (limit <= clientsCount) {
  128. const msg = `The connection was refused because the current count of clients for guests is ${clientsCount} and exceeds the limit`;
  129. logger.warn(msg);
  130. next(new Error(msg));
  131. return;
  132. }
  133. }
  134. next();
  135. }
  136. /**
  137. * @see https://socket.io/docs/server-api/#socket-client
  138. */
  139. async checkConnectionLimits(socket, next) {
  140. const clients = await this.getClients(this.getDefaultSocket());
  141. const clientsCount = clients.length;
  142. logger.debug('Current count of clients for \'/\':', clientsCount);
  143. const limit = this.configManager.getConfig('crowi', 's2cMessagingPubsub:connectionsLimit');
  144. if (limit <= clientsCount) {
  145. const msg = `The connection was refused because the current count of clients for '/' is ${clientsCount} and exceeds the limit`;
  146. logger.warn(msg);
  147. next(new Error(msg));
  148. return;
  149. }
  150. next();
  151. }
  152. }
  153. module.exports = SocketIoService;