Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 
 
 

603 rindas
23 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 (
  18. TYPE_CHECKING,
  19. Any,
  20. Awaitable,
  21. Callable,
  22. Collection,
  23. List,
  24. Optional,
  25. Tuple,
  26. Union,
  27. )
  28. from synapse.api.errors import Codes
  29. from synapse.rest.media.v1._base import FileInfo
  30. from synapse.rest.media.v1.media_storage import ReadableFileWrapper
  31. from synapse.spam_checker_api import RegistrationBehaviour
  32. from synapse.types import RoomAlias, UserProfile
  33. from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
  34. from synapse.util.metrics import Measure
  35. if TYPE_CHECKING:
  36. import synapse.events
  37. import synapse.server
  38. logger = logging.getLogger(__name__)
  39. CHECK_EVENT_FOR_SPAM_CALLBACK = Callable[
  40. ["synapse.events.EventBase"],
  41. Awaitable[
  42. Union[
  43. str,
  44. # Deprecated
  45. bool,
  46. ]
  47. ],
  48. ]
  49. SHOULD_DROP_FEDERATED_EVENT_CALLBACK = Callable[
  50. ["synapse.events.EventBase"],
  51. Awaitable[Union[bool, str]],
  52. ]
  53. USER_MAY_JOIN_ROOM_CALLBACK = Callable[[str, str, bool], Awaitable[bool]]
  54. USER_MAY_INVITE_CALLBACK = Callable[[str, str, str], Awaitable[bool]]
  55. USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[[str, str, str, str], Awaitable[bool]]
  56. USER_MAY_CREATE_ROOM_CALLBACK = Callable[[str], Awaitable[bool]]
  57. USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[[str, RoomAlias], Awaitable[bool]]
  58. USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[[str, str], Awaitable[bool]]
  59. CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[[UserProfile], Awaitable[bool]]
  60. LEGACY_CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[
  61. [
  62. Optional[dict],
  63. Optional[str],
  64. Collection[Tuple[str, str]],
  65. ],
  66. Awaitable[RegistrationBehaviour],
  67. ]
  68. CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[
  69. [
  70. Optional[dict],
  71. Optional[str],
  72. Collection[Tuple[str, str]],
  73. Optional[str],
  74. ],
  75. Awaitable[RegistrationBehaviour],
  76. ]
  77. CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK = Callable[
  78. [ReadableFileWrapper, FileInfo],
  79. Awaitable[bool],
  80. ]
  81. def load_legacy_spam_checkers(hs: "synapse.server.HomeServer") -> None:
  82. """Wrapper that loads spam checkers configured using the old configuration, and
  83. registers the spam checker hooks they implement.
  84. """
  85. spam_checkers: List[Any] = []
  86. api = hs.get_module_api()
  87. for module, config in hs.config.spamchecker.spam_checkers:
  88. # Older spam checkers don't accept the `api` argument, so we
  89. # try and detect support.
  90. spam_args = inspect.getfullargspec(module)
  91. if "api" in spam_args.args:
  92. spam_checkers.append(module(config=config, api=api))
  93. else:
  94. spam_checkers.append(module(config=config))
  95. # The known spam checker hooks. If a spam checker module implements a method
  96. # which name appears in this set, we'll want to register it.
  97. spam_checker_methods = {
  98. "check_event_for_spam",
  99. "user_may_invite",
  100. "user_may_create_room",
  101. "user_may_create_room_alias",
  102. "user_may_publish_room",
  103. "check_username_for_spam",
  104. "check_registration_for_spam",
  105. "check_media_file_for_spam",
  106. }
  107. for spam_checker in spam_checkers:
  108. # Methods on legacy spam checkers might not be async, so we wrap them around a
  109. # wrapper that will call maybe_awaitable on the result.
  110. def async_wrapper(f: Optional[Callable]) -> Optional[Callable[..., Awaitable]]:
  111. # f might be None if the callback isn't implemented by the module. In this
  112. # case we don't want to register a callback at all so we return None.
  113. if f is None:
  114. return None
  115. wrapped_func = f
  116. if f.__name__ == "check_registration_for_spam":
  117. checker_args = inspect.signature(f)
  118. if len(checker_args.parameters) == 3:
  119. # Backwards compatibility; some modules might implement a hook that
  120. # doesn't expect a 4th argument. In this case, wrap it in a function
  121. # that gives it only 3 arguments and drops the auth_provider_id on
  122. # the floor.
  123. def wrapper(
  124. email_threepid: Optional[dict],
  125. username: Optional[str],
  126. request_info: Collection[Tuple[str, str]],
  127. auth_provider_id: Optional[str],
  128. ) -> Union[Awaitable[RegistrationBehaviour], RegistrationBehaviour]:
  129. # Assertion required because mypy can't prove we won't
  130. # change `f` back to `None`. See
  131. # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions
  132. assert f is not None
  133. return f(
  134. email_threepid,
  135. username,
  136. request_info,
  137. )
  138. wrapped_func = wrapper
  139. elif len(checker_args.parameters) != 4:
  140. raise RuntimeError(
  141. "Bad signature for callback check_registration_for_spam",
  142. )
  143. def run(*args: Any, **kwargs: Any) -> Awaitable:
  144. # Assertion required because mypy can't prove we won't change `f`
  145. # back to `None`. See
  146. # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions
  147. assert wrapped_func is not None
  148. return maybe_awaitable(wrapped_func(*args, **kwargs))
  149. return run
  150. # Register the hooks through the module API.
  151. hooks = {
  152. hook: async_wrapper(getattr(spam_checker, hook, None))
  153. for hook in spam_checker_methods
  154. }
  155. api.register_spam_checker_callbacks(**hooks)
  156. class SpamChecker:
  157. NOT_SPAM = "NOT_SPAM"
  158. def __init__(self, hs: "synapse.server.HomeServer") -> None:
  159. self.hs = hs
  160. self.clock = hs.get_clock()
  161. self._check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = []
  162. self._should_drop_federated_event_callbacks: List[
  163. SHOULD_DROP_FEDERATED_EVENT_CALLBACK
  164. ] = []
  165. self._user_may_join_room_callbacks: List[USER_MAY_JOIN_ROOM_CALLBACK] = []
  166. self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = []
  167. self._user_may_send_3pid_invite_callbacks: List[
  168. USER_MAY_SEND_3PID_INVITE_CALLBACK
  169. ] = []
  170. self._user_may_create_room_callbacks: List[USER_MAY_CREATE_ROOM_CALLBACK] = []
  171. self._user_may_create_room_alias_callbacks: List[
  172. USER_MAY_CREATE_ROOM_ALIAS_CALLBACK
  173. ] = []
  174. self._user_may_publish_room_callbacks: List[USER_MAY_PUBLISH_ROOM_CALLBACK] = []
  175. self._check_username_for_spam_callbacks: List[
  176. CHECK_USERNAME_FOR_SPAM_CALLBACK
  177. ] = []
  178. self._check_registration_for_spam_callbacks: List[
  179. CHECK_REGISTRATION_FOR_SPAM_CALLBACK
  180. ] = []
  181. self._check_media_file_for_spam_callbacks: List[
  182. CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK
  183. ] = []
  184. def register_callbacks(
  185. self,
  186. check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None,
  187. should_drop_federated_event: Optional[
  188. SHOULD_DROP_FEDERATED_EVENT_CALLBACK
  189. ] = None,
  190. user_may_join_room: Optional[USER_MAY_JOIN_ROOM_CALLBACK] = None,
  191. user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None,
  192. user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None,
  193. user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None,
  194. user_may_create_room_alias: Optional[
  195. USER_MAY_CREATE_ROOM_ALIAS_CALLBACK
  196. ] = None,
  197. user_may_publish_room: Optional[USER_MAY_PUBLISH_ROOM_CALLBACK] = None,
  198. check_username_for_spam: Optional[CHECK_USERNAME_FOR_SPAM_CALLBACK] = None,
  199. check_registration_for_spam: Optional[
  200. CHECK_REGISTRATION_FOR_SPAM_CALLBACK
  201. ] = None,
  202. check_media_file_for_spam: Optional[CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK] = None,
  203. ) -> None:
  204. """Register callbacks from module for each hook."""
  205. if check_event_for_spam is not None:
  206. self._check_event_for_spam_callbacks.append(check_event_for_spam)
  207. if should_drop_federated_event is not None:
  208. self._should_drop_federated_event_callbacks.append(
  209. should_drop_federated_event
  210. )
  211. if user_may_join_room is not None:
  212. self._user_may_join_room_callbacks.append(user_may_join_room)
  213. if user_may_invite is not None:
  214. self._user_may_invite_callbacks.append(user_may_invite)
  215. if user_may_send_3pid_invite is not None:
  216. self._user_may_send_3pid_invite_callbacks.append(
  217. user_may_send_3pid_invite,
  218. )
  219. if user_may_create_room is not None:
  220. self._user_may_create_room_callbacks.append(user_may_create_room)
  221. if user_may_create_room_alias is not None:
  222. self._user_may_create_room_alias_callbacks.append(
  223. user_may_create_room_alias,
  224. )
  225. if user_may_publish_room is not None:
  226. self._user_may_publish_room_callbacks.append(user_may_publish_room)
  227. if check_username_for_spam is not None:
  228. self._check_username_for_spam_callbacks.append(check_username_for_spam)
  229. if check_registration_for_spam is not None:
  230. self._check_registration_for_spam_callbacks.append(
  231. check_registration_for_spam,
  232. )
  233. if check_media_file_for_spam is not None:
  234. self._check_media_file_for_spam_callbacks.append(check_media_file_for_spam)
  235. async def check_event_for_spam(self, event: "synapse.events.EventBase") -> str:
  236. """Checks if a given event is considered "spammy" by this server.
  237. If the server considers an event spammy, then it will be rejected if
  238. sent by a local user. If it is sent by a user on another server, the
  239. event is soft-failed.
  240. Args:
  241. event: the event to be checked
  242. Returns:
  243. - `NOT_SPAM` if the event is considered good (non-spammy) and should be let
  244. through. Other spamcheck filters may still reject it.
  245. - A `Code` if the event is considered spammy and is rejected with a specific
  246. error message/code.
  247. - A string that isn't `NOT_SPAM` if the event is considered spammy and the
  248. string should be used as the client-facing error message. This usage is
  249. generally discouraged as it doesn't support internationalization.
  250. """
  251. for callback in self._check_event_for_spam_callbacks:
  252. with Measure(
  253. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  254. ):
  255. res = await delay_cancellation(callback(event))
  256. if res is False or res == self.NOT_SPAM:
  257. # This spam-checker accepts the event.
  258. # Other spam-checkers may reject it, though.
  259. continue
  260. elif res is True:
  261. # This spam-checker rejects the event with deprecated
  262. # return value `True`
  263. return Codes.FORBIDDEN
  264. elif not isinstance(res, str):
  265. # mypy complains that we can't reach this code because of the
  266. # return type in CHECK_EVENT_FOR_SPAM_CALLBACK, but we don't know
  267. # for sure that the module actually returns it.
  268. logger.warning( # type: ignore[unreachable]
  269. "Module returned invalid value, rejecting message as spam"
  270. )
  271. res = "This message has been rejected as probable spam"
  272. else:
  273. # The module rejected the event either with a `Codes`
  274. # or some other `str`. In either case, we stop here.
  275. pass
  276. return res
  277. # No spam-checker has rejected the event, let it pass.
  278. return self.NOT_SPAM
  279. async def should_drop_federated_event(
  280. self, event: "synapse.events.EventBase"
  281. ) -> Union[bool, str]:
  282. """Checks if a given federated event is considered "spammy" by this
  283. server.
  284. If the server considers an event spammy, it will be silently dropped,
  285. and in doing so will split-brain our view of the room's DAG.
  286. Args:
  287. event: the event to be checked
  288. Returns:
  289. True if the event should be silently dropped
  290. """
  291. for callback in self._should_drop_federated_event_callbacks:
  292. with Measure(
  293. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  294. ):
  295. res: Union[bool, str] = await delay_cancellation(callback(event))
  296. if res:
  297. return res
  298. return False
  299. async def user_may_join_room(
  300. self, user_id: str, room_id: str, is_invited: bool
  301. ) -> bool:
  302. """Checks if a given users is allowed to join a room.
  303. Not called when a user creates a room.
  304. Args:
  305. userid: The ID of the user wanting to join the room
  306. room_id: The ID of the room the user wants to join
  307. is_invited: Whether the user is invited into the room
  308. Returns:
  309. Whether the user may join the room
  310. """
  311. for callback in self._user_may_join_room_callbacks:
  312. with Measure(
  313. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  314. ):
  315. may_join_room = await delay_cancellation(
  316. callback(user_id, room_id, is_invited)
  317. )
  318. if may_join_room is False:
  319. return False
  320. return True
  321. async def user_may_invite(
  322. self, inviter_userid: str, invitee_userid: str, room_id: str
  323. ) -> bool:
  324. """Checks if a given user may send an invite
  325. If this method returns false, the invite will be rejected.
  326. Args:
  327. inviter_userid: The user ID of the sender of the invitation
  328. invitee_userid: The user ID targeted in the invitation
  329. room_id: The room ID
  330. Returns:
  331. True if the user may send an invite, otherwise False
  332. """
  333. for callback in self._user_may_invite_callbacks:
  334. with Measure(
  335. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  336. ):
  337. may_invite = await delay_cancellation(
  338. callback(inviter_userid, invitee_userid, room_id)
  339. )
  340. if may_invite is False:
  341. return False
  342. return True
  343. async def user_may_send_3pid_invite(
  344. self, inviter_userid: str, medium: str, address: str, room_id: str
  345. ) -> bool:
  346. """Checks if a given user may invite a given threepid into the room
  347. If this method returns false, the threepid invite will be rejected.
  348. Note that if the threepid is already associated with a Matrix user ID, Synapse
  349. will call user_may_invite with said user ID instead.
  350. Args:
  351. inviter_userid: The user ID of the sender of the invitation
  352. medium: The 3PID's medium (e.g. "email")
  353. address: The 3PID's address (e.g. "alice@example.com")
  354. room_id: The room ID
  355. Returns:
  356. True if the user may send the invite, otherwise False
  357. """
  358. for callback in self._user_may_send_3pid_invite_callbacks:
  359. with Measure(
  360. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  361. ):
  362. may_send_3pid_invite = await delay_cancellation(
  363. callback(inviter_userid, medium, address, room_id)
  364. )
  365. if may_send_3pid_invite is False:
  366. return False
  367. return True
  368. async def user_may_create_room(self, userid: str) -> bool:
  369. """Checks if a given user may create a room
  370. If this method returns false, the creation request will be rejected.
  371. Args:
  372. userid: The ID of the user attempting to create a room
  373. Returns:
  374. True if the user may create a room, otherwise False
  375. """
  376. for callback in self._user_may_create_room_callbacks:
  377. with Measure(
  378. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  379. ):
  380. may_create_room = await delay_cancellation(callback(userid))
  381. if may_create_room is False:
  382. return False
  383. return True
  384. async def user_may_create_room_alias(
  385. self, userid: str, room_alias: RoomAlias
  386. ) -> bool:
  387. """Checks if a given user may create a room alias
  388. If this method returns false, the association request will be rejected.
  389. Args:
  390. userid: The ID of the user attempting to create a room alias
  391. room_alias: The alias to be created
  392. Returns:
  393. True if the user may create a room alias, otherwise False
  394. """
  395. for callback in self._user_may_create_room_alias_callbacks:
  396. with Measure(
  397. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  398. ):
  399. may_create_room_alias = await delay_cancellation(
  400. callback(userid, room_alias)
  401. )
  402. if may_create_room_alias is False:
  403. return False
  404. return True
  405. async def user_may_publish_room(self, userid: str, room_id: str) -> bool:
  406. """Checks if a given user may publish a room to the directory
  407. If this method returns false, the publish request will be rejected.
  408. Args:
  409. userid: The user ID attempting to publish the room
  410. room_id: The ID of the room that would be published
  411. Returns:
  412. True if the user may publish the room, otherwise False
  413. """
  414. for callback in self._user_may_publish_room_callbacks:
  415. with Measure(
  416. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  417. ):
  418. may_publish_room = await delay_cancellation(callback(userid, room_id))
  419. if may_publish_room is False:
  420. return False
  421. return True
  422. async def check_username_for_spam(self, user_profile: UserProfile) -> bool:
  423. """Checks if a user ID or display name are considered "spammy" by this server.
  424. If the server considers a username spammy, then it will not be included in
  425. user directory results.
  426. Args:
  427. user_profile: The user information to check, it contains the keys:
  428. * user_id
  429. * display_name
  430. * avatar_url
  431. Returns:
  432. True if the user is spammy.
  433. """
  434. for callback in self._check_username_for_spam_callbacks:
  435. with Measure(
  436. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  437. ):
  438. # Make a copy of the user profile object to ensure the spam checker cannot
  439. # modify it.
  440. res = await delay_cancellation(callback(user_profile.copy()))
  441. if res:
  442. return True
  443. return False
  444. async def check_registration_for_spam(
  445. self,
  446. email_threepid: Optional[dict],
  447. username: Optional[str],
  448. request_info: Collection[Tuple[str, str]],
  449. auth_provider_id: Optional[str] = None,
  450. ) -> RegistrationBehaviour:
  451. """Checks if we should allow the given registration request.
  452. Args:
  453. email_threepid: The email threepid used for registering, if any
  454. username: The request user name, if any
  455. request_info: List of tuples of user agent and IP that
  456. were used during the registration process.
  457. auth_provider_id: The SSO IdP the user used, e.g "oidc", "saml",
  458. "cas". If any. Note this does not include users registered
  459. via a password provider.
  460. Returns:
  461. Enum for how the request should be handled
  462. """
  463. for callback in self._check_registration_for_spam_callbacks:
  464. with Measure(
  465. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  466. ):
  467. behaviour = await delay_cancellation(
  468. callback(email_threepid, username, request_info, auth_provider_id)
  469. )
  470. assert isinstance(behaviour, RegistrationBehaviour)
  471. if behaviour != RegistrationBehaviour.ALLOW:
  472. return behaviour
  473. return RegistrationBehaviour.ALLOW
  474. async def check_media_file_for_spam(
  475. self, file_wrapper: ReadableFileWrapper, file_info: FileInfo
  476. ) -> bool:
  477. """Checks if a piece of newly uploaded media should be blocked.
  478. This will be called for local uploads, downloads of remote media, each
  479. thumbnail generated for those, and web pages/images used for URL
  480. previews.
  481. Note that care should be taken to not do blocking IO operations in the
  482. main thread. For example, to get the contents of a file a module
  483. should do::
  484. async def check_media_file_for_spam(
  485. self, file: ReadableFileWrapper, file_info: FileInfo
  486. ) -> bool:
  487. buffer = BytesIO()
  488. await file.write_chunks_to(buffer.write)
  489. if buffer.getvalue() == b"Hello World":
  490. return True
  491. return False
  492. Args:
  493. file: An object that allows reading the contents of the media.
  494. file_info: Metadata about the file.
  495. Returns:
  496. True if the media should be blocked or False if it should be
  497. allowed.
  498. """
  499. for callback in self._check_media_file_for_spam_callbacks:
  500. with Measure(
  501. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  502. ):
  503. spam = await delay_cancellation(callback(file_wrapper, file_info))
  504. if spam:
  505. return True
  506. return False