2023-08-10 15:30:40 -04:00
|
|
|
import logging
|
|
|
|
import hashlib
|
2023-08-12 12:53:50 -04:00
|
|
|
import json
|
2023-08-10 15:30:40 -04:00
|
|
|
from typing import Union
|
2023-08-13 10:29:02 -04:00
|
|
|
from synapse.module_api import ModuleApi, NOT_SPAM
|
2023-08-10 15:30:40 -04:00
|
|
|
from synapse.api.errors import AuthError
|
2023-08-12 12:53:50 -04:00
|
|
|
from twisted.web.client import Agent, readBody
|
|
|
|
from twisted.web.http_headers import Headers
|
2023-08-13 10:29:02 -04:00
|
|
|
from twisted.web.iweb import IBodyProducer
|
|
|
|
from twisted.internet import reactor
|
|
|
|
from twisted.internet import defer
|
|
|
|
from twisted.web.iweb import IBodyProducer
|
|
|
|
from zope.interface import implementer
|
2023-08-10 15:30:40 -04:00
|
|
|
|
2023-08-13 11:32:06 -04:00
|
|
|
# Setting up logging:
|
2023-08-13 10:59:19 -04:00
|
|
|
file_handler = logging.FileHandler('/var/log/matrix-synapse/redlight.log')
|
|
|
|
file_handler.setLevel(logging.INFO)
|
|
|
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
|
|
file_handler.setFormatter(formatter)
|
|
|
|
|
2023-08-10 15:30:40 -04:00
|
|
|
logger = logging.getLogger(__name__)
|
2023-08-13 10:59:19 -04:00
|
|
|
logger.setLevel(logging.INFO)
|
|
|
|
logger.addHandler(file_handler)
|
|
|
|
|
2023-08-13 11:32:06 -04:00
|
|
|
# Prevent logger's messages from propagating to the root logger.
|
2023-08-13 10:59:19 -04:00
|
|
|
logger.propagate = False
|
2023-08-10 15:30:40 -04:00
|
|
|
|
2023-08-13 11:32:06 -04:00
|
|
|
# Define a custom producer to convert our JSON data for HTTP requests.
|
2023-08-13 10:29:02 -04:00
|
|
|
@implementer(IBodyProducer)
|
|
|
|
class _JsonProducer:
|
|
|
|
def __init__(self, data):
|
|
|
|
self._data = json.dumps(data).encode("utf-8")
|
|
|
|
self.length = len(self._data)
|
|
|
|
|
|
|
|
def startProducing(self, consumer):
|
|
|
|
consumer.write(self._data)
|
|
|
|
return defer.succeed(None)
|
|
|
|
|
|
|
|
def pauseProducing(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def stopProducing(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2023-08-10 15:30:40 -04:00
|
|
|
class RedlightClientModule:
|
|
|
|
def __init__(self, config: dict, api: ModuleApi):
|
|
|
|
self._api = api
|
2023-08-13 11:32:06 -04:00
|
|
|
# URL where we'll check if the room/user combination is allowed.
|
2023-08-13 13:05:16 -04:00
|
|
|
self._redlight_url = config.get("redlight_url", "http://127.0.0.1:8008/_matrix/loj/v1/abuse_lookup")
|
2023-08-13 11:32:06 -04:00
|
|
|
self._agent = Agent(reactor) # Twisted agent for making HTTP requests.
|
2023-08-10 15:30:40 -04:00
|
|
|
|
|
|
|
logger.info("RedLightClientModule initialized.")
|
2023-08-13 13:05:16 -04:00
|
|
|
logger.info(f"Redlight Server URL set to: {self._redlight_url}")
|
2023-08-10 15:30:40 -04:00
|
|
|
|
2023-08-13 11:32:06 -04:00
|
|
|
# Register the user_may_join_room function to be called by Synapse before a user joins a room.
|
2023-08-10 15:30:40 -04:00
|
|
|
api.register_spam_checker_callbacks(
|
|
|
|
user_may_join_room=self.user_may_join_room
|
|
|
|
)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def double_hash_sha256(data: str) -> str:
|
2023-08-13 11:32:06 -04:00
|
|
|
"""Double-hash the data with SHA256 for added security."""
|
2023-08-10 15:30:40 -04:00
|
|
|
first_hash = hashlib.sha256(data.encode()).digest()
|
|
|
|
double_hashed = hashlib.sha256(first_hash).hexdigest()
|
|
|
|
return double_hashed
|
|
|
|
|
|
|
|
async def user_may_join_room(
|
|
|
|
self, user: str, room: str, is_invited: bool
|
|
|
|
) -> Union["synapse.module_api.NOT_SPAM", "synapse.module_api.errors.Codes"]:
|
|
|
|
|
|
|
|
logger.info(f"User {user} is attempting to join room {room}. Invitation status: {is_invited}.")
|
|
|
|
|
2023-08-13 11:32:06 -04:00
|
|
|
# Double-hash the room and user IDs.
|
2023-08-10 15:30:40 -04:00
|
|
|
hashed_room_id = self.double_hash_sha256(room)
|
2023-08-12 06:09:42 -04:00
|
|
|
hashed_user_id = self.double_hash_sha256(user)
|
2023-08-10 15:30:40 -04:00
|
|
|
|
2023-08-13 11:32:06 -04:00
|
|
|
# Prepare the HTTP body.
|
2023-08-13 10:29:02 -04:00
|
|
|
body = _JsonProducer({
|
2023-08-12 12:53:50 -04:00
|
|
|
"room_id_hash": hashed_room_id,
|
|
|
|
"user_id_hash": hashed_user_id
|
|
|
|
})
|
2023-08-13 10:29:02 -04:00
|
|
|
|
2023-08-13 11:32:06 -04:00
|
|
|
# Make the HTTP request to our redlight server.
|
2023-08-13 10:29:02 -04:00
|
|
|
response = await self._agent.request(
|
|
|
|
b"PUT",
|
|
|
|
self._redlight_url.encode(),
|
|
|
|
Headers({'Content-Type': [b'application/json']}),
|
|
|
|
body
|
|
|
|
)
|
|
|
|
|
2023-08-13 11:32:06 -04:00
|
|
|
# Extract the response body.
|
2023-08-13 10:29:02 -04:00
|
|
|
response_body_bytes = await readBody(response)
|
|
|
|
response_body = response_body_bytes.decode("utf-8")
|
|
|
|
|
2023-08-13 13:05:16 -04:00
|
|
|
# Log the response content
|
|
|
|
logger.info(f"Received response with code {response.code}. Content: {response_body}")
|
|
|
|
|
2023-08-13 10:29:02 -04:00
|
|
|
try:
|
2023-08-13 11:32:06 -04:00
|
|
|
# Try to parse the response body as JSON.
|
2023-08-13 10:29:02 -04:00
|
|
|
response_json = json.loads(response_body)
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
logger.error(f"Failed to decode response body: {response_body}")
|
|
|
|
|
2023-08-13 11:32:06 -04:00
|
|
|
# Handle the response based on its HTTP status code.
|
2023-08-13 10:29:02 -04:00
|
|
|
if response.code == 200:
|
2023-08-13 13:05:16 -04:00
|
|
|
logger.warn(f"User {user} not allowed to join room {room}.")
|
|
|
|
raise AuthError(403, "User not allowed to join this room.")
|
2023-08-13 10:29:02 -04:00
|
|
|
elif response.code == 204:
|
2023-08-13 13:05:16 -04:00
|
|
|
logger.info(f"User {user} allowed to join room {room}.")
|
2023-08-13 11:32:06 -04:00
|
|
|
return NOT_SPAM # Allow the user to join.
|
2023-08-12 12:53:50 -04:00
|
|
|
else:
|
2023-08-13 11:32:06 -04:00
|
|
|
# Handle unexpected responses by logging them and allowing the user to join as a fallback.
|
2023-08-13 10:29:02 -04:00
|
|
|
logger.error(f"Unexpected response code {response.code} with body: {response_body}")
|
2023-08-13 13:05:16 -04:00
|
|
|
logger.warn(f"Defaulting to allowing user {user} to join due to unexpected response code.")
|
2023-08-13 11:32:06 -04:00
|
|
|
return NOT_SPAM
|
2023-08-10 15:30:40 -04:00
|
|
|
|
2023-08-13 11:32:06 -04:00
|
|
|
# Function to parse the module's configuration.
|
2023-08-10 15:30:40 -04:00
|
|
|
def parse_config(config: dict) -> dict:
|
|
|
|
return config
|
|
|
|
|
2023-08-13 11:32:06 -04:00
|
|
|
# Factory function to create an instance of the RedlightClientModule.
|
2023-08-10 15:30:40 -04:00
|
|
|
def create_module(api: ModuleApi, config: dict) -> RedlightClientModule:
|
2023-08-12 12:53:50 -04:00
|
|
|
return RedlightClientModule(config, api)
|