2017-09-19 07:20:11 -04:00
|
|
|
# -*- coding: utf-8 -*-
|
2019-04-11 12:08:13 -04:00
|
|
|
# Copyright 2017 New Vector Ltd
|
2019-10-31 11:16:14 -04:00
|
|
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
2017-09-19 07:20:11 -04:00
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
|
2019-10-31 11:16:14 -04:00
|
|
|
import inspect
|
2021-03-16 08:41:41 -04:00
|
|
|
import logging
|
2020-12-11 14:05:15 -05:00
|
|
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
2019-10-31 11:16:14 -04:00
|
|
|
|
2021-02-03 11:44:16 -05:00
|
|
|
from synapse.rest.media.v1._base import FileInfo
|
|
|
|
from synapse.rest.media.v1.media_storage import ReadableFileWrapper
|
2020-10-07 07:03:26 -04:00
|
|
|
from synapse.spam_checker_api import RegistrationBehaviour
|
2020-08-20 15:42:58 -04:00
|
|
|
from synapse.types import Collection
|
2020-12-11 14:05:15 -05:00
|
|
|
from synapse.util.async_helpers import maybe_awaitable
|
2019-10-31 11:16:14 -04:00
|
|
|
|
2020-11-17 09:09:40 -05:00
|
|
|
if TYPE_CHECKING:
|
2020-10-07 07:03:26 -04:00
|
|
|
import synapse.events
|
2020-02-14 12:49:40 -05:00
|
|
|
import synapse.server
|
|
|
|
|
2021-03-16 08:41:41 -04:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2017-09-27 05:26:13 -04:00
|
|
|
|
2020-09-04 06:54:56 -04:00
|
|
|
class SpamChecker:
|
2020-02-14 12:49:40 -05:00
|
|
|
def __init__(self, hs: "synapse.server.HomeServer"):
|
2020-05-08 14:25:48 -04:00
|
|
|
self.spam_checkers = [] # type: List[Any]
|
2020-10-07 07:03:26 -04:00
|
|
|
api = hs.get_module_api()
|
2017-09-19 07:20:11 -04:00
|
|
|
|
2020-05-08 14:25:48 -04:00
|
|
|
for module, config in hs.config.spam_checkers:
|
2019-10-31 11:16:14 -04:00
|
|
|
# Older spam checkers don't accept the `api` argument, so we
|
|
|
|
# try and detect support.
|
|
|
|
spam_args = inspect.getfullargspec(module)
|
|
|
|
if "api" in spam_args.args:
|
2020-05-08 14:25:48 -04:00
|
|
|
self.spam_checkers.append(module(config=config, api=api))
|
2019-10-31 11:16:14 -04:00
|
|
|
else:
|
2020-05-08 14:25:48 -04:00
|
|
|
self.spam_checkers.append(module(config=config))
|
2017-09-19 07:20:11 -04:00
|
|
|
|
2020-12-11 14:05:15 -05:00
|
|
|
async def check_event_for_spam(
|
|
|
|
self, event: "synapse.events.EventBase"
|
|
|
|
) -> Union[bool, str]:
|
2017-09-26 14:20:23 -04:00
|
|
|
"""Checks if a given event is considered "spammy" by this server.
|
2017-09-19 07:20:11 -04:00
|
|
|
|
2017-09-26 14:20:23 -04:00
|
|
|
If the server considers an event spammy, then it will be rejected if
|
|
|
|
sent by a local user. If it is sent by a user on another server, then
|
|
|
|
users receive a blank event.
|
2017-09-19 07:20:11 -04:00
|
|
|
|
2017-09-26 14:20:23 -04:00
|
|
|
Args:
|
2020-02-14 12:49:40 -05:00
|
|
|
event: the event to be checked
|
2017-09-19 07:20:11 -04:00
|
|
|
|
2017-09-26 14:20:23 -04:00
|
|
|
Returns:
|
2020-12-11 14:05:15 -05:00
|
|
|
True or a string if the event is spammy. If a string is returned it
|
|
|
|
will be used as the error message returned to the user.
|
2017-09-26 14:20:23 -04:00
|
|
|
"""
|
2020-05-08 14:25:48 -04:00
|
|
|
for spam_checker in self.spam_checkers:
|
2020-12-11 14:05:15 -05:00
|
|
|
if await maybe_awaitable(spam_checker.check_event_for_spam(event)):
|
2020-05-08 14:25:48 -04:00
|
|
|
return True
|
2017-09-19 07:20:11 -04:00
|
|
|
|
2020-05-08 14:25:48 -04:00
|
|
|
return False
|
2017-10-03 09:28:12 -04:00
|
|
|
|
2020-12-11 14:05:15 -05:00
|
|
|
async def user_may_invite(
|
2020-02-14 12:49:40 -05:00
|
|
|
self, inviter_userid: str, invitee_userid: str, room_id: str
|
|
|
|
) -> bool:
|
2017-10-03 09:28:12 -04:00
|
|
|
"""Checks if a given user may send an invite
|
|
|
|
|
|
|
|
If this method returns false, the invite will be rejected.
|
|
|
|
|
|
|
|
Args:
|
2020-02-14 12:49:40 -05:00
|
|
|
inviter_userid: The user ID of the sender of the invitation
|
|
|
|
invitee_userid: The user ID targeted in the invitation
|
|
|
|
room_id: The room ID
|
2017-10-03 09:28:12 -04:00
|
|
|
|
|
|
|
Returns:
|
2020-02-14 12:49:40 -05:00
|
|
|
True if the user may send an invite, otherwise False
|
2017-10-03 09:28:12 -04:00
|
|
|
"""
|
2020-05-08 14:25:48 -04:00
|
|
|
for spam_checker in self.spam_checkers:
|
|
|
|
if (
|
2020-12-11 14:05:15 -05:00
|
|
|
await maybe_awaitable(
|
|
|
|
spam_checker.user_may_invite(
|
|
|
|
inviter_userid, invitee_userid, room_id
|
|
|
|
)
|
|
|
|
)
|
2020-05-08 14:25:48 -04:00
|
|
|
is False
|
|
|
|
):
|
|
|
|
return False
|
2017-10-03 09:28:12 -04:00
|
|
|
|
2020-05-08 14:25:48 -04:00
|
|
|
return True
|
2017-10-04 05:47:54 -04:00
|
|
|
|
2020-12-11 14:05:15 -05:00
|
|
|
async def user_may_create_room(self, userid: str) -> bool:
|
2017-10-04 05:47:54 -04:00
|
|
|
"""Checks if a given user may create a room
|
|
|
|
|
|
|
|
If this method returns false, the creation request will be rejected.
|
|
|
|
|
|
|
|
Args:
|
2020-02-14 12:49:40 -05:00
|
|
|
userid: The ID of the user attempting to create a room
|
2017-10-04 05:47:54 -04:00
|
|
|
|
|
|
|
Returns:
|
2020-02-14 12:49:40 -05:00
|
|
|
True if the user may create a room, otherwise False
|
2017-10-04 05:47:54 -04:00
|
|
|
"""
|
2020-05-08 14:25:48 -04:00
|
|
|
for spam_checker in self.spam_checkers:
|
2020-12-11 14:05:15 -05:00
|
|
|
if (
|
|
|
|
await maybe_awaitable(spam_checker.user_may_create_room(userid))
|
|
|
|
is False
|
|
|
|
):
|
2020-05-08 14:25:48 -04:00
|
|
|
return False
|
2017-10-04 05:47:54 -04:00
|
|
|
|
2020-05-08 14:25:48 -04:00
|
|
|
return True
|
2017-10-04 05:47:54 -04:00
|
|
|
|
2020-12-11 14:05:15 -05:00
|
|
|
async def user_may_create_room_alias(self, userid: str, room_alias: str) -> bool:
|
2017-10-04 05:47:54 -04:00
|
|
|
"""Checks if a given user may create a room alias
|
|
|
|
|
|
|
|
If this method returns false, the association request will be rejected.
|
|
|
|
|
|
|
|
Args:
|
2020-02-14 12:49:40 -05:00
|
|
|
userid: The ID of the user attempting to create a room alias
|
|
|
|
room_alias: The alias to be created
|
2017-10-04 05:47:54 -04:00
|
|
|
|
|
|
|
Returns:
|
2020-02-14 12:49:40 -05:00
|
|
|
True if the user may create a room alias, otherwise False
|
2017-10-04 05:47:54 -04:00
|
|
|
"""
|
2020-05-08 14:25:48 -04:00
|
|
|
for spam_checker in self.spam_checkers:
|
2020-12-11 14:05:15 -05:00
|
|
|
if (
|
|
|
|
await maybe_awaitable(
|
|
|
|
spam_checker.user_may_create_room_alias(userid, room_alias)
|
|
|
|
)
|
|
|
|
is False
|
|
|
|
):
|
2020-05-08 14:25:48 -04:00
|
|
|
return False
|
2017-10-04 05:47:54 -04:00
|
|
|
|
2020-05-08 14:25:48 -04:00
|
|
|
return True
|
2017-10-04 09:29:33 -04:00
|
|
|
|
2020-12-11 14:05:15 -05:00
|
|
|
async def user_may_publish_room(self, userid: str, room_id: str) -> bool:
|
2017-10-04 09:29:33 -04:00
|
|
|
"""Checks if a given user may publish a room to the directory
|
|
|
|
|
|
|
|
If this method returns false, the publish request will be rejected.
|
|
|
|
|
|
|
|
Args:
|
2020-02-14 12:49:40 -05:00
|
|
|
userid: The user ID attempting to publish the room
|
|
|
|
room_id: The ID of the room that would be published
|
2017-10-04 09:29:33 -04:00
|
|
|
|
|
|
|
Returns:
|
2020-02-14 12:49:40 -05:00
|
|
|
True if the user may publish the room, otherwise False
|
2017-10-04 09:29:33 -04:00
|
|
|
"""
|
2020-05-08 14:25:48 -04:00
|
|
|
for spam_checker in self.spam_checkers:
|
2020-12-11 14:05:15 -05:00
|
|
|
if (
|
|
|
|
await maybe_awaitable(
|
|
|
|
spam_checker.user_may_publish_room(userid, room_id)
|
|
|
|
)
|
|
|
|
is False
|
|
|
|
):
|
2020-05-08 14:25:48 -04:00
|
|
|
return False
|
2017-10-04 09:29:33 -04:00
|
|
|
|
2020-05-08 14:25:48 -04:00
|
|
|
return True
|
2020-02-14 07:17:54 -05:00
|
|
|
|
2020-12-11 14:05:15 -05:00
|
|
|
async def check_username_for_spam(self, user_profile: Dict[str, str]) -> bool:
|
2020-02-14 07:17:54 -05:00
|
|
|
"""Checks if a user ID or display name are considered "spammy" by this server.
|
|
|
|
|
|
|
|
If the server considers a username spammy, then it will not be included in
|
|
|
|
user directory results.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
user_profile: The user information to check, it contains the keys:
|
|
|
|
* user_id
|
|
|
|
* display_name
|
|
|
|
* avatar_url
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
True if the user is spammy.
|
|
|
|
"""
|
2020-05-08 14:25:48 -04:00
|
|
|
for spam_checker in self.spam_checkers:
|
|
|
|
# For backwards compatibility, only run if the method exists on the
|
|
|
|
# spam checker
|
|
|
|
checker = getattr(spam_checker, "check_username_for_spam", None)
|
|
|
|
if checker:
|
|
|
|
# Make a copy of the user profile object to ensure the spam checker
|
|
|
|
# cannot modify it.
|
2020-12-11 14:05:15 -05:00
|
|
|
if await maybe_awaitable(checker(user_profile.copy())):
|
2020-05-08 14:25:48 -04:00
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
2020-08-20 15:42:58 -04:00
|
|
|
|
2020-12-11 14:05:15 -05:00
|
|
|
async def check_registration_for_spam(
|
2020-08-20 15:42:58 -04:00
|
|
|
self,
|
|
|
|
email_threepid: Optional[dict],
|
|
|
|
username: Optional[str],
|
|
|
|
request_info: Collection[Tuple[str, str]],
|
2021-03-16 08:41:41 -04:00
|
|
|
auth_provider_id: Optional[str] = None,
|
2020-08-20 15:42:58 -04:00
|
|
|
) -> RegistrationBehaviour:
|
|
|
|
"""Checks if we should allow the given registration request.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
email_threepid: The email threepid used for registering, if any
|
|
|
|
username: The request user name, if any
|
|
|
|
request_info: List of tuples of user agent and IP that
|
|
|
|
were used during the registration process.
|
2021-03-16 08:41:41 -04:00
|
|
|
auth_provider_id: The SSO IdP the user used, e.g "oidc", "saml",
|
|
|
|
"cas". If any. Note this does not include users registered
|
|
|
|
via a password provider.
|
2020-08-20 15:42:58 -04:00
|
|
|
|
|
|
|
Returns:
|
|
|
|
Enum for how the request should be handled
|
|
|
|
"""
|
|
|
|
|
|
|
|
for spam_checker in self.spam_checkers:
|
|
|
|
# For backwards compatibility, only run if the method exists on the
|
|
|
|
# spam checker
|
|
|
|
checker = getattr(spam_checker, "check_registration_for_spam", None)
|
|
|
|
if checker:
|
2021-03-16 08:41:41 -04:00
|
|
|
# Provide auth_provider_id if the function supports it
|
|
|
|
checker_args = inspect.signature(checker)
|
|
|
|
if len(checker_args.parameters) == 4:
|
|
|
|
d = checker(
|
|
|
|
email_threepid,
|
|
|
|
username,
|
|
|
|
request_info,
|
|
|
|
auth_provider_id,
|
|
|
|
)
|
|
|
|
elif len(checker_args.parameters) == 3:
|
|
|
|
d = checker(email_threepid, username, request_info)
|
|
|
|
else:
|
|
|
|
logger.error(
|
|
|
|
"Invalid signature for %s.check_registration_for_spam. Denying registration",
|
|
|
|
spam_checker.__module__,
|
|
|
|
)
|
|
|
|
return RegistrationBehaviour.DENY
|
|
|
|
|
|
|
|
behaviour = await maybe_awaitable(d)
|
2020-08-20 15:42:58 -04:00
|
|
|
assert isinstance(behaviour, RegistrationBehaviour)
|
|
|
|
if behaviour != RegistrationBehaviour.ALLOW:
|
|
|
|
return behaviour
|
|
|
|
|
|
|
|
return RegistrationBehaviour.ALLOW
|
2021-02-03 11:44:16 -05:00
|
|
|
|
|
|
|
async def check_media_file_for_spam(
|
|
|
|
self, file_wrapper: ReadableFileWrapper, file_info: FileInfo
|
|
|
|
) -> bool:
|
|
|
|
"""Checks if a piece of newly uploaded media should be blocked.
|
|
|
|
|
|
|
|
This will be called for local uploads, downloads of remote media, each
|
|
|
|
thumbnail generated for those, and web pages/images used for URL
|
|
|
|
previews.
|
|
|
|
|
|
|
|
Note that care should be taken to not do blocking IO operations in the
|
|
|
|
main thread. For example, to get the contents of a file a module
|
|
|
|
should do::
|
|
|
|
|
|
|
|
async def check_media_file_for_spam(
|
|
|
|
self, file: ReadableFileWrapper, file_info: FileInfo
|
|
|
|
) -> bool:
|
|
|
|
buffer = BytesIO()
|
|
|
|
await file.write_chunks_to(buffer.write)
|
|
|
|
|
|
|
|
if buffer.getvalue() == b"Hello World":
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
file: An object that allows reading the contents of the media.
|
|
|
|
file_info: Metadata about the file.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
True if the media should be blocked or False if it should be
|
|
|
|
allowed.
|
|
|
|
"""
|
|
|
|
|
|
|
|
for spam_checker in self.spam_checkers:
|
|
|
|
# For backwards compatibility, only run if the method exists on the
|
|
|
|
# spam checker
|
|
|
|
checker = getattr(spam_checker, "check_media_file_for_spam", None)
|
|
|
|
if checker:
|
|
|
|
spam = await maybe_awaitable(checker(file_wrapper, file_info))
|
|
|
|
if spam:
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|