2023-08-10 15:30:40 -04:00
import logging
import hashlib
2023-08-12 12:53:50 -04:00
import json
2023-08-14 10:32:38 -04:00
import asyncio
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-14 10:41:02 -04:00
from redlight_alert_bot import RedlightAlertBot
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-14 10:32:38 -04:00
# Your homeserver's URL
self . _homeserver_url = " https:// " + config . get ( " homeserver_url " , " 127.0.0.1:8008 " )
# The API token of your redlight bot user
2023-08-14 10:41:02 -04:00
self . _redlight_alert_bot_token = config . get ( " redlight_alert_bot_token " , " " )
2023-08-14 10:32:38 -04:00
# The alert room your redlight bot will post too
self . _redlight_alert_room = config . get ( " redlight_alert_room " , " " )
# Redlight server endpoint, where we'll check if the room/user combination is allowed.
self . _redlight_endpoint = " https:// " + config . get ( " redlight_server " , " 127.0.0.1:8008 " ) + " /_matrix/loj/v1/abuse_lookup "
2023-08-15 18:41:29 -04:00
# Redlight API token
self . _redlight_api_token = config . get ( " redlight_api_token " , " " )
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
2023-08-14 10:41:02 -04:00
# Create an instance of the RedlightAlertBot
self . bot = RedlightAlertBot ( self . _homeserver_url , self . _redlight_alert_bot_token ) # Adjust the homeserver and token as required
2023-08-14 10:32:38 -04:00
2023-08-10 15:30:40 -04:00
logger . info ( " RedLightClientModule initialized. " )
2023-08-14 10:41:02 -04:00
logger . info ( f " Redlight bot user token: { self . _redlight_alert_bot_token } " )
2023-08-14 10:32:38 -04:00
logger . info ( f " Redlight alert room: { self . _redlight_alert_room } " )
logger . info ( f " Redlight server endpoint set to: { self . _redlight_endpoint } " )
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 ,
2023-08-15 18:41:29 -04:00
" user_id_hash " : hashed_user_id ,
" api_token " : self . _redlight_api_token
2023-08-12 12:53:50 -04:00
} )
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 " ,
2023-08-14 10:32:38 -04:00
self . _redlight_endpoint . encode ( ) ,
2023-08-13 10:29:02 -04:00
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 } . " )
2023-08-14 10:32:38 -04:00
# Create the alert message
alert_message = f " WARNING: Incident detected! User { user } was attempting to access this restricted room: { room } "
# Start the synchronous send_alert_message method in a thread but don't await it
loop = asyncio . get_event_loop ( )
loop . run_in_executor ( None , self . bot . send_alert_message , self . _redlight_alert_room , alert_message )
# Throw a 403 error that the user will see
raise AuthError ( 403 , " PERMISSION DENIED - This room violates server policy. " )
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-14 10:46:05 -04:00
alert_message = f " Unexpected response code { response . code } with body { response_body } . Defaulting to allowing user { user } to join due to unexpected response code. "
# Handle unexpected responses by alerting and logging them, and allowing the user to join as a fallback.
logger . error ( alert_message )
loop = asyncio . get_event_loop ( )
loop . run_in_executor ( None , self . bot . send_alert_message , self . _redlight_alert_room , alert_message )
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 )