Source: components/room-link-validator.js

  1. /**
  2. * The room link validator is used to determine if a room can be bridged.
  3. */
  4. const ConfigValidator = require("./config-validator");
  5. const log = require("./logging").get("room-link-validator");
  6. const VALIDATION_CACHE_LIFETIME = 30 * 60 * 1000;
  7. const PASSED = "RLV_PASSED";
  8. const ERROR = "RVL_ERROR";
  9. const ERROR_USER_CONFLICT = "RVL_USER_CONFLICT";
  10. const ERROR_CACHED = "RVL_ERROR_CACHED";
  11. const RULE_SCHEMA = {
  12. "$schema": "http://json-schema.org/draft-04/schema#",
  13. type: "object",
  14. properties: {
  15. userIds: {
  16. type: "object",
  17. properties: {
  18. exempt: {
  19. type: "array",
  20. items: {
  21. type: "string"
  22. }
  23. },
  24. conflict: {
  25. type: "array",
  26. items: {
  27. type: "string"
  28. }
  29. }
  30. }
  31. }
  32. }
  33. };
  34. /**
  35. * The RoomLinkValidator checks if a room should be linked to a remote
  36. * channel, given a set of rules supplied in a config. The ruleset is maintained
  37. * in a seperate config from the bridge config. It can be reloaded by triggering
  38. * an endpoint specified in the {@link Bridge} class.
  39. */
  40. class RoomLinkValidator {
  41. /**
  42. * @param {string} config Config for the validator.
  43. * @param {string} config.ruleFile Filename for the rule file.
  44. * @param {string} config.rules Rules if not using a rule file, will be
  45. * overwritten if both is set.
  46. * @param {AppServiceBot} asBot The AS bot.
  47. */
  48. constructor (config, asBot) {
  49. this.conflictCache = new Map(); // roomId => number
  50. this.waitingRooms = new Map(); // roomId => Promise
  51. this.asBot = asBot;
  52. if (config.ruleFile) {
  53. this.config = new ConfigValidator(RULE_SCHEMA);
  54. this.ruleFile = config.ruleFile;
  55. this.readRuleFile();
  56. }
  57. else if (config.rules) {
  58. this.rules = this.evaulateRules(config.rules);
  59. }
  60. else {
  61. throw new Error("Either config.ruleFile or config.rules must be set");
  62. }
  63. }
  64. readRuleFile (filename) {
  65. filename = filename || this.ruleFile;
  66. if (!filename) {
  67. throw new Error("No filename given and config is not using a file");
  68. }
  69. log.info(`Detected rule config change...`);
  70. const rules = this.config.validate(filename);
  71. if (rules === undefined) {
  72. throw Error("Rule file contents was undefined");
  73. }
  74. log.info(`Rule file ok, checking rules...`);
  75. this.rules = this.evaulateRules(rules);
  76. log.info(`Applied new ruleset`);
  77. this.conflictCache.clear();
  78. }
  79. evaulateRules (rules) {
  80. let newRules = {userIds: {}};
  81. if (!rules || !rules.userIds) {
  82. rules = {userIds: {}};
  83. }
  84. if (!rules.userIds.conflict) {
  85. rules.userIds.conflict = [];
  86. }
  87. if (!rules.userIds.exempt) {
  88. rules.userIds.exempt = [];
  89. }
  90. newRules.userIds.conflict = rules.userIds.conflict.map(
  91. (regexStr) => new RegExp(regexStr)
  92. );
  93. newRules.userIds.exempt = rules.userIds.exempt.map(
  94. (regexStr) => new RegExp(regexStr)
  95. );
  96. return newRules;
  97. }
  98. validateRoom (roomId, cache=true) {
  99. const status = cache ? this._checkConflictCache(roomId) : undefined;
  100. if (status !== undefined) {
  101. return Promise.reject(status);
  102. }
  103. // Get all users in the room.
  104. return this.asBot.getJoinedMembers(roomId).then((joined) => {
  105. for (const userId of Object.keys(joined)) {
  106. let rule;
  107. let ignoreUser = false;
  108. for (rule of this.rules.userIds.exempt) {
  109. if (rule.exec(userId) !== null) {
  110. ignoreUser = true;
  111. break;
  112. }
  113. }
  114. if (ignoreUser) {
  115. break;
  116. }
  117. for (rule of this.rules.userIds.conflict) {
  118. if (rule.exec(userId) !== null) {
  119. return false;
  120. }
  121. }
  122. }
  123. return true;
  124. }).then((isValid) => {
  125. if (isValid) {
  126. return PASSED;
  127. }
  128. this.conflictCache.set(roomId, Date.now());
  129. throw ERROR_USER_CONFLICT;
  130. });
  131. }
  132. _checkConflictCache (roomId) {
  133. if (this.conflictCache.has(roomId)) {
  134. if (
  135. this.conflictCache.get(roomId) > (Date.now() - VALIDATION_CACHE_LIFETIME)
  136. ) {
  137. return ERROR_CACHED;
  138. }
  139. this.conflictCache.delete(roomId);
  140. }
  141. return undefined;
  142. }
  143. }
  144. module.exports = {
  145. validationStatuses: {
  146. PASSED,
  147. ERROR_USER_CONFLICT,
  148. ERROR_CACHED,
  149. ERROR,
  150. },
  151. RoomLinkValidator
  152. };