diff --git a/scratchattach/__init__.py b/scratchattach/__init__.py index a2329989..94805f83 100644 --- a/scratchattach/__init__.py +++ b/scratchattach/__init__.py @@ -1,7 +1,7 @@ from .cloud.cloud import CustomCloud, ScratchCloud, TwCloud, get_cloud, get_scratch_cloud, get_tw_cloud from .cloud._base import BaseCloud, AnyCloud -from .eventhandlers.cloud_server import init_cloud_server +from .eventhandlers.cloud_server import init_cloud_server, init_ssl_cloud_server from .eventhandlers._base import BaseEventHandler from .eventhandlers.filterbot import Filterbot, HardFilter, SoftFilter, SpamFilter from .eventhandlers.cloud_storage import Database diff --git a/scratchattach/eventhandlers/_base.py b/scratchattach/eventhandlers/_base.py index 4e4236ac..79a59edb 100644 --- a/scratchattach/eventhandlers/_base.py +++ b/scratchattach/eventhandlers/_base.py @@ -1,11 +1,17 @@ from __future__ import annotations +import json +import time +import ssl from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, Any from collections import defaultdict from threading import Thread, Event from collections.abc import Callable import traceback + +from SimpleWebSocketServer import WebSocket + from scratchattach.utils.requests import requests from scratchattach.utils import exceptions @@ -41,7 +47,7 @@ def start(self, *, thread=True, ignore_exceptions=True): else: self._thread = None self._updater() - + def call_event(self, event_name, args : list = []): try: # print(f"Calling for {event_name}...") @@ -69,7 +75,7 @@ def call_event(self, event_name, args : list = []): @abstractmethod def _updater(self): pass - + def __del__(self): self.stop() @@ -120,4 +126,169 @@ def inner(function): return inner else: # => the decorator doesn't provide arguments - inner(function) \ No newline at end of file + inner(function) + +class BaseCloudServer(BaseEventHandler): + def __init__(self, + hostname: str, + *, + certfile: str|None = None, + keyfile: str|None = None, + ssl_version: int = ssl.PROTOCOL_TLSv1_2, + ssl_context: ssl.SSLContext|None = None, + port: int, + websocketclass: WebSocket, + length_limit: int|None = None, + allow_non_numeric: bool = True, + whitelisted_projects: list[Any]|None = None, + allow_nonscratch_names: bool = True, + blocked_ips: list[str]|None = None, + sync_players: bool = True, + log_var_sets: bool = True): + + BaseEventHandler.__init__(self) + + self.running = False + self._events = {} # saves event functions called on cloud updates + + self.tw_clients = {} # saves connected clients + self.tw_variables = {} # holds cloud variable states + + self.hostname = hostname + self.port = port + + # server config + self.allow_non_numeric = allow_non_numeric + self.whitelisted_projects = whitelisted_projects + self.length_limit = length_limit + self.allow_nonscratch_names = allow_nonscratch_names + self.blocked_ips = blocked_ips + self.sync_players = sync_players + self.log_var_sets = log_var_sets + + def check_for_ip_ban(self, client): + if ( + client.address[0] in self.blocked_ips + or client.address[0] + ":" + str(client.address[1]) in self.blocked_ips + or client.address in self.blocked_ips + ): + client.sendMessage("You have been banned from this server") + client.close(4002) + print(client.address[0] + ":" + str(client.address[1]), "(IP-banned) was disconnected") + return True + return False + + def active_projects(self): + only_active = {} + for project_id in self.tw_variables: + if self.active_user_ips(project_id) != []: + only_active[project_id] = self.tw_variables[project_id] + return only_active + + def active_user_names(self, project_id): + return [self.tw_clients[user]["username"] for user in self.active_user_ips(project_id)] + + def active_user_ips(self, project_id): + return list(filter(lambda user: str(self.tw_clients[user]["project_id"]) == str(project_id), self.tw_clients)) + + def get_global_vars(self): + return self.tw_variables + + def get_project_vars(self, project_id): + project_id = str(project_id) + if project_id in self.tw_variables: + return self.tw_variables[project_id] + else: + return {} + + def get_var(self, project_id, var_name): + project_id = str(project_id) + var_name = var_name.replace("☁ ", "") + if project_id in self.tw_variables: + if var_name in self.tw_variables[project_id]: + return self.tw_variables[project_id][var_name] + else: + return None + else: + return None + + def set_global_vars(self, data): + for project_id in data: + self.set_project_vars(project_id, data[project_id]) + + def set_project_vars(self, project_id, data, *, user="@server"): + project_id = str(project_id) + self.tw_variables[project_id] = data + for client in [self.tw_clients[ip]["client"] for ip in self.active_user_ips(project_id)]: + client.sendMessage( + "\n".join( + [ + json.dumps( + { + "method": "set", + "project_id": project_id, + "name": "☁ " + varname, + "value": data[varname], + "server": "scratchattach/2.0.0", + "timestamp": time.time() * 1000, + "user": user, + } + ) + for varname in data + ] + ) + ) + + def set_var(self, project_id, var_name, value, *, user="@server", skip_forward=None): + var_name = var_name.replace("☁ ", "") + project_id = str(project_id) + if project_id not in self.tw_variables: + self.tw_variables[project_id] = {} + self.tw_variables[project_id][var_name] = value + + if self.sync_players is True: + for client in [self.tw_clients[ip]["client"] for ip in self.active_user_ips(project_id)]: + if client == skip_forward: + continue + client.sendMessage( + json.dumps( + { + "method": "set", + "project_id": project_id, + "name": "☁ " + var_name, + "value": value, + "timestamp": time.time() * 1000, + "user": user, + } + ) + ) + + def _check_value(self, value): + # Checks if a received cloud value satisfies the server's constraints + if self.length_limit is not None: + if len(str(value)) > self.length_limit: + return False + if self.allow_non_numeric is False: + x = value.replace(".", "") + x = x.replace("-", "") + if not (x.isnumeric() or x == ""): + return False + return True + + def _updater(self): + try: + # Function called when .start() is executed (.start is inherited from BaseEventHandler) + print(f"Serving websocket server: ws://{self.hostname}:{self.port}") + self.serveforever() + except Exception as e: + raise exceptions.WebsocketServerError(str(e)) + + def pause(self): + self.running = False + + def resume(self): + self.running = True + + def stop(self, wait_call_threads: bool = True): + BaseEventHandler.stop(self, wait_call_threads) + self.close() diff --git a/scratchattach/eventhandlers/cloud_server.py b/scratchattach/eventhandlers/cloud_server.py index 0bda1f84..7c4646b0 100644 --- a/scratchattach/eventhandlers/cloud_server.py +++ b/scratchattach/eventhandlers/cloud_server.py @@ -1,15 +1,16 @@ from __future__ import annotations -from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket -from threading import Thread -from scratchattach.utils import exceptions import json import time -from scratchattach.site import cloud_activity -from scratchattach.site.user import User -from ._base import BaseEventHandler +import ssl import traceback +from SimpleWebSocketServer import SimpleSSLWebSocketServer, SimpleWebSocketServer, WebSocket + +from scratchattach.utils import exceptions +from scratchattach.site import cloud_activity +from scratchattach.site.user import User +from ._base import BaseCloudServer class TwCloudSocket(WebSocket): server: TwCloudServer @@ -181,7 +182,7 @@ def handleClose(self): print("Internal error in handleClose:", e) -class TwCloudServer(SimpleWebSocketServer, BaseEventHandler): +class TwCloudServer(BaseCloudServer, SimpleWebSocketServer): def __init__( self, hostname, @@ -200,154 +201,69 @@ def __init__( blocked_ips = [] SimpleWebSocketServer.__init__(self, hostname, port=port, websocketclass=websocketclass) - BaseEventHandler.__init__(self) - - self.running = False - self._events = {} # saves event functions called on cloud updates - - self.tw_clients = {} # saves connected clients - self.tw_variables = {} # holds cloud variable states - - self.hostname = hostname - self.port = port - - # server config - self.allow_non_numeric = allow_non_numeric - self.whitelisted_projects = whitelisted_projects - self.length_limit = length_limit - self.allow_nonscratch_names = allow_nonscratch_names - self.blocked_ips = blocked_ips - self.sync_players = sync_players - self.log_var_sets = log_var_sets - - def check_for_ip_ban(self, client): - if ( - client.address[0] in self.blocked_ips - or client.address[0] + ":" + str(client.address[1]) in self.blocked_ips - or client.address in self.blocked_ips - ): - client.sendMessage("You have been banned from this server") - client.close(4002) - print(client.address[0] + ":" + str(client.address[1]), "(IP-banned) was disconnected") - return True - return False - - def active_projects(self): - only_active = {} - for project_id in self.tw_variables: - if self.active_user_ips(project_id) != []: - only_active[project_id] = self.tw_variables[project_id] - return only_active - - def active_user_names(self, project_id): - return [self.tw_clients[user]["username"] for user in self.active_user_ips(project_id)] - - def active_user_ips(self, project_id): - return list(filter(lambda user: str(self.tw_clients[user]["project_id"]) == str(project_id), self.tw_clients)) - - def get_global_vars(self): - return self.tw_variables - - def get_project_vars(self, project_id): - project_id = str(project_id) - if project_id in self.tw_variables: - return self.tw_variables[project_id] - else: - return {} - - def get_var(self, project_id, var_name): - project_id = str(project_id) - var_name = var_name.replace("☁ ", "") - if project_id in self.tw_variables: - if var_name in self.tw_variables[project_id]: - return self.tw_variables[project_id][var_name] - else: - return None - else: - return None - - def set_global_vars(self, data): - for project_id in data: - self.set_project_vars(project_id, data[project_id]) - def set_project_vars(self, project_id, data, *, user="@server"): - project_id = str(project_id) - self.tw_variables[project_id] = data - for client in [self.tw_clients[ip]["client"] for ip in self.active_user_ips(project_id)]: - client.sendMessage( - "\n".join( - [ - json.dumps( - { - "method": "set", - "project_id": project_id, - "name": "☁ " + varname, - "value": data[varname], - "server": "scratchattach/2.0.0", - "timestamp": time.time() * 1000, - "user": user, - } - ) - for varname in data - ] - ) - ) - - def set_var(self, project_id, var_name, value, *, user="@server", skip_forward=None): - var_name = var_name.replace("☁ ", "") - project_id = str(project_id) - if project_id not in self.tw_variables: - self.tw_variables[project_id] = {} - self.tw_variables[project_id][var_name] = value - - if self.sync_players is True: - for client in [self.tw_clients[ip]["client"] for ip in self.active_user_ips(project_id)]: - if client == skip_forward: - continue - client.sendMessage( - json.dumps( - { - "method": "set", - "project_id": project_id, - "name": "☁ " + var_name, - "value": value, - "timestamp": time.time() * 1000, - "user": user, - } - ) - ) + BaseCloudServer.__init__(self, + hostname=hostname, + port=port, + websocketclass=websocketclass, + length_limit=length_limit, + allow_non_numeric=allow_non_numeric, + whitelisted_projects=whitelisted_projects, + allow_nonscratch_names=allow_nonscratch_names, + blocked_ips=blocked_ips, + sync_players=sync_players, + log_var_sets=log_var_sets) + +class TwSSLCloudServer(BaseCloudServer, SimpleSSLWebSocketServer): + def __init__( + self, + hostname: str, + *, + certfile=None, + keyfile=None, + ssl_version=ssl.PROTOCOL_TLSv1_2, + ssl_context=None, + port, + websocketclass, + length_limit=None, + allow_non_numeric=True, + whitelisted_projects=None, + allow_nonscratch_names=True, + blocked_ips=None, + sync_players=True, + log_var_sets= True + ): + SimpleSSLWebSocketServer.__init__( + self, + hostname, + port=port, + websocketclass=websocketclass, + certfile=certfile, + keyfile=keyfile, + version=ssl_version, + ssl_context=ssl_context, + ) - def _check_value(self, value): - # Checks if a received cloud value satisfies the server's constraints - if self.length_limit is not None: - if len(str(value)) > self.length_limit: - return False - if self.allow_non_numeric is False: - x = value.replace(".", "") - x = x.replace("-", "") - if not (x.isnumeric() or x == ""): - return False - return True + BaseCloudServer.__init__(self, + hostname=hostname, + port=port, + websocketclass=websocketclass, + length_limit=length_limit, + allow_non_numeric=allow_non_numeric, + whitelisted_projects=whitelisted_projects, + allow_nonscratch_names=allow_nonscratch_names, + blocked_ips=blocked_ips, + sync_players=sync_players, + log_var_sets=log_var_sets) def _updater(self): try: # Function called when .start() is executed (.start is inherited from BaseEventHandler) - print(f"Serving websocket server: ws://{self.hostname}:{self.port}") + print(f"Serving websocket server: wss://{self.hostname}:{self.port}") self.serveforever() except Exception as e: raise exceptions.WebsocketServerError(str(e)) - def pause(self): - self.running = False - - def resume(self): - self.running = True - - def stop(self, wait_call_threads: bool = True): - BaseEventHandler.stop(self, wait_call_threads) - self.close() - - def init_cloud_server( hostname="127.0.0.1", port=8080, @@ -380,3 +296,45 @@ def init_cloud_server( sync_players=sync_players, log_var_sets=log_var_sets, ) + +def init_ssl_cloud_server( + hostname: str = "127.0.0.1", + port: int = 8080, + *, + certfile=None, + keyfile=None, + ssl_version=ssl.PROTOCOL_TLSv1_2, + ssl_context=None, + length_limit=None, + allow_non_numeric=True, + whitelisted_projects=None, + allow_nonscratch_names=True, + blocked_ips=None, + sync_players=True, + log_var_sets=True +) -> TwSSLCloudServer: + """ + Inits a websocket server which can be used with TurboWarp's ?cloud_host URL parameter. + + Prints out the websocket address in the console. + """ + if (certfile is None or keyfile is None) and ssl_context is None: + print("[yellow]WARNING: To init a ssl cloud server, you need provide `certfile` and "+ + "`keyfile` or `ssl_context`.[/]") + + return TwSSLCloudServer( + hostname, + port=port, + websocketclass=TwCloudSocket, + certfile=certfile, + keyfile=keyfile, + ssl_version=ssl_version, + ssl_context=ssl_context, + length_limit=length_limit, + allow_non_numeric=allow_non_numeric, + whitelisted_projects=whitelisted_projects, + allow_nonscratch_names=allow_nonscratch_names, + blocked_ips=blocked_ips, + sync_players=sync_players, + log_var_sets=log_var_sets + )