You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

286 lines
10 KiB

  1. # Copyright 2017 New Vector Ltd
  2. # Copyright 2019 The Matrix.org Foundation C.I.C.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import inspect
  16. import logging
  17. from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
  18. from synapse.rest.media.v1._base import FileInfo
  19. from synapse.rest.media.v1.media_storage import ReadableFileWrapper
  20. from synapse.spam_checker_api import RegistrationBehaviour
  21. from synapse.types import Collection
  22. from synapse.util.async_helpers import maybe_awaitable
  23. if TYPE_CHECKING:
  24. import synapse.events
  25. import synapse.server
  26. logger = logging.getLogger(__name__)
  27. class SpamChecker:
  28. def __init__(self, hs: "synapse.server.HomeServer"):
  29. self.spam_checkers = [] # type: List[Any]
  30. api = hs.get_module_api()
  31. for module, config in hs.config.spam_checkers:
  32. # Older spam checkers don't accept the `api` argument, so we
  33. # try and detect support.
  34. spam_args = inspect.getfullargspec(module)
  35. if "api" in spam_args.args:
  36. self.spam_checkers.append(module(config=config, api=api))
  37. else:
  38. self.spam_checkers.append(module(config=config))
  39. async def check_event_for_spam(
  40. self, event: "synapse.events.EventBase"
  41. ) -> Union[bool, str]:
  42. """Checks if a given event is considered "spammy" by this server.
  43. If the server considers an event spammy, then it will be rejected if
  44. sent by a local user. If it is sent by a user on another server, then
  45. users receive a blank event.
  46. Args:
  47. event: the event to be checked
  48. Returns:
  49. True or a string if the event is spammy. If a string is returned it
  50. will be used as the error message returned to the user.
  51. """
  52. for spam_checker in self.spam_checkers:
  53. if await maybe_awaitable(spam_checker.check_event_for_spam(event)):
  54. return True
  55. return False
  56. async def user_may_invite(
  57. self, inviter_userid: str, invitee_userid: str, room_id: str
  58. ) -> bool:
  59. """Checks if a given user may send an invite
  60. If this method returns false, the invite will be rejected.
  61. Args:
  62. inviter_userid: The user ID of the sender of the invitation
  63. invitee_userid: The user ID targeted in the invitation
  64. room_id: The room ID
  65. Returns:
  66. True if the user may send an invite, otherwise False
  67. """
  68. for spam_checker in self.spam_checkers:
  69. if (
  70. await maybe_awaitable(
  71. spam_checker.user_may_invite(
  72. inviter_userid, invitee_userid, room_id
  73. )
  74. )
  75. is False
  76. ):
  77. return False
  78. return True
  79. async def user_may_create_room(self, userid: str) -> bool:
  80. """Checks if a given user may create a room
  81. If this method returns false, the creation request will be rejected.
  82. Args:
  83. userid: The ID of the user attempting to create a room
  84. Returns:
  85. True if the user may create a room, otherwise False
  86. """
  87. for spam_checker in self.spam_checkers:
  88. if (
  89. await maybe_awaitable(spam_checker.user_may_create_room(userid))
  90. is False
  91. ):
  92. return False
  93. return True
  94. async def user_may_create_room_alias(self, userid: str, room_alias: str) -> bool:
  95. """Checks if a given user may create a room alias
  96. If this method returns false, the association request will be rejected.
  97. Args:
  98. userid: The ID of the user attempting to create a room alias
  99. room_alias: The alias to be created
  100. Returns:
  101. True if the user may create a room alias, otherwise False
  102. """
  103. for spam_checker in self.spam_checkers:
  104. if (
  105. await maybe_awaitable(
  106. spam_checker.user_may_create_room_alias(userid, room_alias)
  107. )
  108. is False
  109. ):
  110. return False
  111. return True
  112. async def user_may_publish_room(self, userid: str, room_id: str) -> bool:
  113. """Checks if a given user may publish a room to the directory
  114. If this method returns false, the publish request will be rejected.
  115. Args:
  116. userid: The user ID attempting to publish the room
  117. room_id: The ID of the room that would be published
  118. Returns:
  119. True if the user may publish the room, otherwise False
  120. """
  121. for spam_checker in self.spam_checkers:
  122. if (
  123. await maybe_awaitable(
  124. spam_checker.user_may_publish_room(userid, room_id)
  125. )
  126. is False
  127. ):
  128. return False
  129. return True
  130. async def check_username_for_spam(self, user_profile: Dict[str, str]) -> bool:
  131. """Checks if a user ID or display name are considered "spammy" by this server.
  132. If the server considers a username spammy, then it will not be included in
  133. user directory results.
  134. Args:
  135. user_profile: The user information to check, it contains the keys:
  136. * user_id
  137. * display_name
  138. * avatar_url
  139. Returns:
  140. True if the user is spammy.
  141. """
  142. for spam_checker in self.spam_checkers:
  143. # For backwards compatibility, only run if the method exists on the
  144. # spam checker
  145. checker = getattr(spam_checker, "check_username_for_spam", None)
  146. if checker:
  147. # Make a copy of the user profile object to ensure the spam checker
  148. # cannot modify it.
  149. if await maybe_awaitable(checker(user_profile.copy())):
  150. return True
  151. return False
  152. async def check_registration_for_spam(
  153. self,
  154. email_threepid: Optional[dict],
  155. username: Optional[str],
  156. request_info: Collection[Tuple[str, str]],
  157. auth_provider_id: Optional[str] = None,
  158. ) -> RegistrationBehaviour:
  159. """Checks if we should allow the given registration request.
  160. Args:
  161. email_threepid: The email threepid used for registering, if any
  162. username: The request user name, if any
  163. request_info: List of tuples of user agent and IP that
  164. were used during the registration process.
  165. auth_provider_id: The SSO IdP the user used, e.g "oidc", "saml",
  166. "cas". If any. Note this does not include users registered
  167. via a password provider.
  168. Returns:
  169. Enum for how the request should be handled
  170. """
  171. for spam_checker in self.spam_checkers:
  172. # For backwards compatibility, only run if the method exists on the
  173. # spam checker
  174. checker = getattr(spam_checker, "check_registration_for_spam", None)
  175. if checker:
  176. # Provide auth_provider_id if the function supports it
  177. checker_args = inspect.signature(checker)
  178. if len(checker_args.parameters) == 4:
  179. d = checker(
  180. email_threepid,
  181. username,
  182. request_info,
  183. auth_provider_id,
  184. )
  185. elif len(checker_args.parameters) == 3:
  186. d = checker(email_threepid, username, request_info)
  187. else:
  188. logger.error(
  189. "Invalid signature for %s.check_registration_for_spam. Denying registration",
  190. spam_checker.__module__,
  191. )
  192. return RegistrationBehaviour.DENY
  193. behaviour = await maybe_awaitable(d)
  194. assert isinstance(behaviour, RegistrationBehaviour)
  195. if behaviour != RegistrationBehaviour.ALLOW:
  196. return behaviour
  197. return RegistrationBehaviour.ALLOW
  198. async def check_media_file_for_spam(
  199. self, file_wrapper: ReadableFileWrapper, file_info: FileInfo
  200. ) -> bool:
  201. """Checks if a piece of newly uploaded media should be blocked.
  202. This will be called for local uploads, downloads of remote media, each
  203. thumbnail generated for those, and web pages/images used for URL
  204. previews.
  205. Note that care should be taken to not do blocking IO operations in the
  206. main thread. For example, to get the contents of a file a module
  207. should do::
  208. async def check_media_file_for_spam(
  209. self, file: ReadableFileWrapper, file_info: FileInfo
  210. ) -> bool:
  211. buffer = BytesIO()
  212. await file.write_chunks_to(buffer.write)
  213. if buffer.getvalue() == b"Hello World":
  214. return True
  215. return False
  216. Args:
  217. file: An object that allows reading the contents of the media.
  218. file_info: Metadata about the file.
  219. Returns:
  220. True if the media should be blocked or False if it should be
  221. allowed.
  222. """
  223. for spam_checker in self.spam_checkers:
  224. # For backwards compatibility, only run if the method exists on the
  225. # spam checker
  226. checker = getattr(spam_checker, "check_media_file_for_spam", None)
  227. if checker:
  228. spam = await maybe_awaitable(checker(file_wrapper, file_info))
  229. if spam:
  230. return True
  231. return False