diff --git a/core/__init__.py b/core/__init__.py index 34517db..ddce50c 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,4 +1,5 @@ from core.config import CoreConfig -from core.allnet import AllnetServlet, BillingServlet +from core.allnet import AllnetServlet from core.aimedb import AimedbFactory -from core.title import TitleServlet \ No newline at end of file +from core.title import TitleServlet +from core.utils import Utils diff --git a/core/aimedb.py b/core/aimedb.py index 0523688..b8ce3b7 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -212,7 +212,7 @@ class AimedbFactory(Factory): log_fmt = logging.Formatter(log_fmt_str) self.logger = logging.getLogger("aimedb") - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.logs, "aimedb"), when="d", backupCount=10) + fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "aimedb"), when="d", backupCount=10) fileHandler.setFormatter(log_fmt) consoleHandler = logging.StreamHandler() diff --git a/core/allnet.py b/core/allnet.py index 640c3ce..b944ba4 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -1,12 +1,17 @@ -from twisted.web import resource +from typing import Dict, List, Any, Optional import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler +from twisted.web.http import Request +from datetime import datetime +import pytz +import base64 +import zlib from core.config import CoreConfig from core.data import Data +from core.utils import Utils -class AllnetServlet(resource.Resource): - isLeaf = True +class AllnetServlet(): def __init__(self, core_cfg: CoreConfig, cfg_folder: str): super().__init__() self.config = core_cfg @@ -14,26 +19,211 @@ class AllnetServlet(resource.Resource): self.data = Data(core_cfg) self.logger = logging.getLogger("allnet") - log_fmt_str = "[%(asctime)s] Allnet | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) + if not hasattr(self.logger, "initialized"): + log_fmt_str = "[%(asctime)s] Allnet | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "allnet"), when="d", backupCount=10) - fileHandler.setFormatter(log_fmt) + fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "allnet"), when="d", backupCount=10) + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(core_cfg.allnet.loglevel) + coloredlogs.install(level=core_cfg.allnet.loglevel, logger=self.logger, fmt=log_fmt_str) + self.logger.initialized = True + + if "game_registry" not in globals(): + globals()["game_registry"] = Utils.get_all_titles() + + if len(globals()["game_registry"]) == 0: + self.logger.error("No games detected!") + + def handle_poweron(self, request: Request): + try: + req = AllnetPowerOnRequest(self.allnet_req_to_dict(request.content.getvalue())) + # Validate the request. Currently we only validate the fields we plan on using + + if not req.game_id or not req.ver or not req.token or not req.serial or not req.ip: + raise AllnetRequestException(f"Bad request params {vars(req)}") + except AllnetRequestException as e: + self.logger.error(e) + return b"" + + def handle_dlorder(self, request: Request): + pass + + def handle_billing_request(self, request: Request): + pass + + def kvp_to_dict(self, *kvp: str) -> List[Dict[str, Any]]: + ret: List[Dict[str, Any]] = [] + for x in kvp: + items = x.split('&') + tmp = {} + + for item in items: + kvp = item.split('=') + if len(kvp) == 2: + tmp[kvp[0]] = kvp[1] + + ret.append(tmp) + + def allnet_req_to_dict(self, data: bytes): + """ + Parses an billing request string into a python dictionary + """ + try: + decomp = zlib.decompressobj(-zlib.MAX_WBITS) + unzipped = decomp.decompress(data) + sections = unzipped.decode('ascii').split('\r\n') + + return Utils.kvp_to_dict(sections) + + except Exception as e: + print(e) + return None + + def billing_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: + """ + Parses an allnet request string into a python dictionary + """ + try: + zipped = base64.b64decode(data) + unzipped = zlib.decompress(zipped) + sections = unzipped.decode('utf-8').split('\r\n') + + return Utils.kvp_to_dict(sections) + + except Exception as e: + print(e) + return None + + def dict_to_http_form_string(self, data:List[Dict[str, Any]], crlf: bool = False, trailing_newline: bool = True) -> Optional[str]: + """ + Takes a python dictionary and parses it into an allnet response string + """ + try: + urlencode = "" + for item in data: + for k,v in item.items(): + urlencode += f"{k}={v}&" + + if crlf: + urlencode = urlencode[:-1] + "\r\n" + else: + urlencode = urlencode[:-1] + "\n" + + if not trailing_newline: + if crlf: + urlencode = urlencode[:-2] + else: + urlencode = urlencode[:-1] + + return urlencode + + except Exception as e: + print(e) + return None + +class AllnetPowerOnRequest(): + def __init__(self, req: Dict) -> None: + if req is None: + raise AllnetRequestException("Request processing failed") + self.game_id: str = req["game_id"] if "game_id" in req else "" + self.ver: str = req["ver"] if "ver" in req else "" + self.serial: str = req["serial"] if "serial" in req else "" + self.ip: str = req["ip"] if "ip" in req else "" + self.firm_ver: str = req["firm_ver"] if "firm_ver" in req else "" + self.boot_ver: str = req["boot_ver"] if "boot_ver" in req else "" + self.encode: str = req["encode"] if "encode" in req else "" - consoleHandler = logging.StreamHandler() - consoleHandler.setFormatter(log_fmt) + try: + self.hops = int(req["hops"]) if "hops" in req else 0 + self.format_ver = int(req["format_ver"]) if "format_ver" in req else 2 + self.token = int(req["token"]) if "token" in req else 0 + except ValueError as e: + raise AllnetRequestException(f"Failed to parse int: {e}") - self.logger.addHandler(fileHandler) - self.logger.addHandler(consoleHandler) - - self.logger.setLevel(core_cfg.allnet.loglevel) - coloredlogs.install(level=core_cfg.allnet.loglevel, logger=self.logger, fmt=log_fmt_str) +class AllnetPowerOnResponse3(): + def __init__(self, token) -> None: + self.stat = 1 + self.uri = "" + self.host = "" + self.place_id = "123" + self.name = "" + self.nickname = "" + self.region0 = "1" + self.region_name0 = "W" + self.region_name1 = "" + self.region_name2 = "" + self.region_name3 = "" + self.country = "JPN" + self.allnet_id = "123" + self.client_timezone = "+0900" + self.utc_time = datetime.now(tz=pytz.timezone('UTC')).strftime("%Y-%m-%dT%H:%M:%SZ") + self.setting = "" + self.res_ver = "3" + self.token = str(token) -class BillingServlet(resource.Resource): - isLeaf = True - def __init__(self, core_cfg: CoreConfig, cfg_folder: str): - super().__init__() - self.config = core_cfg - self.config_folder = cfg_folder - self.data = Data(core_cfg) - self.logger = logging.getLogger('allnet') \ No newline at end of file +class AllnetPowerOnResponse2(): + def __init__(self) -> None: + self.stat = 1 + self.uri = "" + self.host = "" + self.place_id = "123" + self.name = "Test" + self.nickname = "Test123" + self.region0 = "1" + self.region_name0 = "W" + self.region_name1 = "X" + self.region_name2 = "Y" + self.region_name3 = "Z" + self.country = "JPN" + self.year = datetime.now().year + self.month = datetime.now().month + self.day = datetime.now().day + self.hour = datetime.now().hour + self.minute = datetime.now().minute + self.second = datetime.now().second + self.setting = "1" + self.timezone = "+0900" + self.res_class = "PowerOnResponseV2" + +class AllnetDownloadOrderRequest(): + def __init__(self, req: Dict) -> None: + self.game_id = req["game_id"] if "game_id" in req else "" + self.ver = req["ver"] if "ver" in req else "" + self.serial = req["serial"] if "serial" in req else "" + self.encode = req["encode"] if "encode" in req else "" + +class AllnetDownloadOrderResponse(): + def __init__(self, stat: int = 1, serial: str = "", uri: str = "null") -> None: + self.stat = stat + self.serial = serial + self.uri = uri + +class BillingResponse(): + def __init__(self, playlimit: str, playlimit_sig: str, nearfull: str, nearfull_sig: str, + playhistory: str = "000000/0:000000/0:000000/0") -> None: + + self.result = "0" + self.waitime = "100" + self.linelimit = "1" + self.message = "" + self.playlimit = playlimit + self.playlimitsig = playlimit_sig + self.protocolver = "1.000" + self.nearfull = nearfull + self.nearfullsig = nearfull_sig + self.fixlogincnt = "0" + self.fixinterval = "5" + self.playhistory = playhistory + # playhistory -> YYYYMM/C:... + # YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period + +class AllnetRequestException(Exception): + pass diff --git a/core/config.py b/core/config.py index da443f2..d11dcdc 100644 --- a/core/config.py +++ b/core/config.py @@ -7,27 +7,27 @@ class ServerConfig: @property def listen_address(self) -> str: - return CoreConfig.get_config_field(self.__config, '127.0.0.1', 'core', 'server', 'listen_address') + return CoreConfig.get_config_field(self.__config, 'core', 'server', 'listen_address', default='127.0.0.1') @property def allow_user_registration(self) -> bool: - return CoreConfig.get_config_field(self.__config, True, 'core', 'server', 'allow_user_registration') + return CoreConfig.get_config_field(self.__config, 'core', 'server', 'allow_user_registration', default=True) @property def allow_unregistered_games(self) -> bool: - return CoreConfig.get_config_field(self.__config, True, 'core', 'server', 'allow_unregistered_games') + return CoreConfig.get_config_field(self.__config, 'core', 'server', 'allow_unregistered_games', default=True) @property def name(self) -> str: - return CoreConfig.get_config_field(self.__config, "ARTEMiS", 'core', 'server', 'name') + return CoreConfig.get_config_field(self.__config, 'core', 'server', 'name', default="ARTEMiS") @property def is_develop(self) -> bool: - return CoreConfig.get_config_field(self.__config, True, 'core', 'server', 'is_develop') + return CoreConfig.get_config_field(self.__config, 'core', 'server', 'is_develop', default=True) @property def log_dir(self) -> str: - return CoreConfig.get_config_field(self.__config, 'logs', 'core', 'server', 'log_dir') + return CoreConfig.get_config_field(self.__config, 'core', 'server', 'log_dir', default='logs') class TitleConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -35,15 +35,15 @@ class TitleConfig: @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, "info", 'core', 'title', 'loglevel')) + return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'title', 'loglevel', default="info")) @property def hostname(self) -> str: - return CoreConfig.get_config_field(self.__config, "localhost", 'core', 'title', 'hostname') + return CoreConfig.get_config_field(self.__config, 'core', 'title', 'hostname', default="localhost") @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 8080, 'core', 'title', 'port') + return CoreConfig.get_config_field(self.__config, 'core', 'title', 'port', default=8080) class DatabaseConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -51,43 +51,43 @@ class DatabaseConfig: @property def host(self) -> str: - return CoreConfig.get_config_field(self.__config, "localhost", 'core', 'database', 'host') + return CoreConfig.get_config_field(self.__config, 'core', 'database', 'host', default="localhost") @property def username(self) -> str: - return CoreConfig.get_config_field(self.__config, 'aime', 'core', 'database', 'username') + return CoreConfig.get_config_field(self.__config, 'core', 'database', 'username', default='aime') @property def password(self) -> str: - return CoreConfig.get_config_field(self.__config, 'aime', 'core', 'database', 'password') + return CoreConfig.get_config_field(self.__config, 'core', 'database', 'password', default='aime') @property def name(self) -> str: - return CoreConfig.get_config_field(self.__config, 'aime', 'core', 'database', 'name') + return CoreConfig.get_config_field(self.__config, 'core', 'database', 'name', default='aime') @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 3306, 'core', 'database', 'port') + return CoreConfig.get_config_field(self.__config, 'core', 'database', 'port', default=3306) @property def protocol(self) -> str: - return CoreConfig.get_config_field(self.__config, "mysql", 'core', 'database', 'type') + return CoreConfig.get_config_field(self.__config, 'core', 'database', 'type', default="mysql") @property def sha2_password(self) -> bool: - return CoreConfig.get_config_field(self.__config, False, 'core', 'database', 'sha2_password') + return CoreConfig.get_config_field(self.__config, 'core', 'database', 'sha2_password', default=False) @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, "info", 'core', 'database', 'loglevel')) + return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'database', 'loglevel', default="info")) @property def user_table_autoincrement_start(self) -> int: - return CoreConfig.get_config_field(self.__config, 10000, 'core', 'database', 'user_table_autoincrement_start') + return CoreConfig.get_config_field(self.__config, 'core', 'database', 'user_table_autoincrement_start', default=10000) @property def memcached_host(self) -> str: - return CoreConfig.get_config_field(self.__config, "localhost", 'core', 'database', 'memcached_host') + return CoreConfig.get_config_field(self.__config, 'core', 'database', 'memcached_host', default="localhost") class FrontendConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -95,15 +95,15 @@ class FrontendConfig: @property def enable(self) -> int: - return CoreConfig.get_config_field(self.__config, False, 'core', 'frontend', 'enable') + return CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'enable', default=False) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 8090, 'core', 'frontend', 'port') + return CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'port', default=8090) @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'loglevel', "info")) + return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'loglevel', default="info")) class AllnetConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -111,15 +111,15 @@ class AllnetConfig: @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, "info", 'core', 'allnet', 'loglevel')) + return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'loglevel', default="info")) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 80, 'core', 'allnet', 'port') + return CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'port', default=80) @property def allow_online_updates(self) -> int: - return CoreConfig.get_config_field(self.__config, False, 'core', 'allnet', 'allow_online_updates') + return CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'allow_online_updates', default=False) class BillingConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -127,19 +127,19 @@ class BillingConfig: @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 8443, 'core', 'billing', 'port') + return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'port', default=8443) @property def ssl_key(self) -> str: - return CoreConfig.get_config_field(self.__config, "cert/server.key", 'core', 'billing', 'ssl_key') + return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'ssl_key', default="cert/server.key") @property def ssl_cert(self) -> str: - return CoreConfig.get_config_field(self.__config, "cert/server.pem", 'core', 'billing', 'ssl_cert') + return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'ssl_cert', default="cert/server.pem") @property def signing_key(self) -> str: - return CoreConfig.get_config_field(self.__config, "cert/billing.key", 'core', 'billing', 'signing_key') + return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'signing_key', default="cert/billing.key") class AimedbConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -147,15 +147,15 @@ class AimedbConfig: @property def loglevel(self) -> int: - return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, "info", 'core', 'aimedb', 'loglevel')) + return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'loglevel', default="info")) @property def port(self) -> int: - return CoreConfig.get_config_field(self.__config, 22345, 'core', 'aimedb', 'port') + return CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'port', default=22345) @property def key(self) -> str: - return CoreConfig.get_config_field(self.__config, "", 'core', 'aimedb', 'key') + return CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'key', default="") class CoreConfig(dict): def __init__(self) -> None: @@ -179,8 +179,8 @@ class CoreConfig(dict): return logging.INFO @classmethod - def get_config_field(cls, __config: dict, default: Any, *path: str) -> Any: - envKey = 'CFG_' + def get_config_field(cls, __config: dict, module, *path: str, default: Any = "") -> Any: + envKey = f'CFG_{module}_' for arg in path: envKey += arg + '_' diff --git a/core/data/database.py b/core/data/database.py index f22cc06..800b5d0 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -36,7 +36,7 @@ class Data: # Prevent the logger from adding handlers multiple times if not getattr(self.logger, 'handler_set', None): - fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.logs, "db"), encoding="utf-8", + fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "db"), encoding="utf-8", when="d", backupCount=10) fileHandler.setFormatter(log_fmt) diff --git a/core/title.py b/core/title.py index 34c1792..6d83f46 100644 --- a/core/title.py +++ b/core/title.py @@ -1,14 +1,42 @@ from twisted.web import resource import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler +from twisted.web.http import Request from core.config import CoreConfig from core.data import Data +from core.utils import Utils -class TitleServlet(resource.Resource): - isLeaf = True +class TitleServlet(): def __init__(self, core_cfg: CoreConfig, cfg_folder: str): super().__init__() self.config = core_cfg self.config_folder = cfg_folder - self.data = Data(core_cfg) \ No newline at end of file + self.data = Data(core_cfg) + + self.logger = logging.getLogger("title") + if not hasattr(self.logger, "initialized"): + log_fmt_str = "[%(asctime)s] Title | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + + fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "title"), when="d", backupCount=10) + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(core_cfg.title.loglevel) + coloredlogs.install(level=core_cfg.title.loglevel, logger=self.logger, fmt=log_fmt_str) + self.logger.initialized = True + + if "game_registry" not in globals(): + globals()["game_registry"] = Utils.get_all_titles() + + def handle_GET(self, request: Request): + pass + + def handle_POST(self, request: Request): + pass \ No newline at end of file diff --git a/core/utils.py b/core/utils.py new file mode 100644 index 0000000..bdb856d --- /dev/null +++ b/core/utils.py @@ -0,0 +1,21 @@ +from typing import Dict, List, Any, Optional +from types import ModuleType +import zlib, base64 +import importlib +from os import walk + +class Utils: + @classmethod + def get_all_titles(cls) -> Dict[str, ModuleType]: + ret: Dict[str, Any] = {} + + for root, dirs, files in walk("titles"): + for dir in dirs: + if not dir.startswith("__"): + try: + mod = importlib.import_module(f"titles.{dir}") + ret[dir] = mod + + except ImportError as e: + print(f"{dir} - {e}") + return ret diff --git a/example_config/core.yaml b/example_config/core.yaml index 0eb441e..cb08e69 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -4,7 +4,7 @@ server: allow_unregistered_games: True name: "ARTEMiS" is_develop: True - log_dir: False + log_dir: "logs" title: loglevel: "info" diff --git a/index.py b/index.py index bac4066..ad712b5 100644 --- a/index.py +++ b/index.py @@ -6,6 +6,7 @@ from core import * from twisted.web import server from twisted.internet import reactor, endpoints +from txroutes import Dispatcher if __name__ == "__main__": parser = argparse.ArgumentParser(description="ARTEMiS main entry point") @@ -26,14 +27,14 @@ if __name__ == "__main__": print(f"Log directory {cfg.server.log_dir} NOT writable, please check permissions") exit(1) - if cfg.aimedb.key == "": + if not cfg.aimedb.key: print("!!AIMEDB KEY BLANK, SET KEY IN CORE.YAML!!") exit(1) print(f"ARTEMiS starting in {'develop' if cfg.server.is_develop else 'production'} mode") allnet_server_str = f"tcp:{cfg.allnet.port}:interface={cfg.server.listen_address}" - title_server_str = f"tcp:{cfg.billing.port}:interface={cfg.server.listen_address}" + title_server_str = f"tcp:{cfg.title.port}:interface={cfg.server.listen_address}" adb_server_str = f"tcp:{cfg.aimedb.port}:interface={cfg.server.listen_address}" billing_server_str = f"tcp:{cfg.billing.port}:interface={cfg.server.listen_address}" @@ -41,13 +42,23 @@ if __name__ == "__main__": billing_server_str = f"ssl:{cfg.billing.port}:interface={cfg.server.listen_address}"\ f":privateKey={cfg.billing.ssl_key}:certKey={cfg.billing.ssl_cert}" - endpoints.serverFromString(reactor, allnet_server_str).listen(server.Site(AllnetServlet(cfg, args.config))) + allnet_cls = AllnetServlet(cfg, args.config) + title_cls = TitleServlet(cfg, args.config) + + dispatcher = Dispatcher() + dispatcher.connect('allnet_poweron', '/sys/servlet/PowerOn', allnet_cls, action='handle_poweron', conditions=dict(method=['POST'])) + dispatcher.connect('allnet_downloadorder', '/sys/servlet/DownloadOrder', allnet_cls, action='handle_dlorder', conditions=dict(method=['POST'])) + dispatcher.connect('allnet_billing', '/request', allnet_cls, action='handle_billing_request', conditions=dict(method=['POST'])) + dispatcher.connect("title_get", "/{game}/{version}/{endpoint}", title_cls, action="handle_GET", conditions=dict(method=['GET'])) + dispatcher.connect("title_post", "/{game}/{version}/{endpoint}", title_cls, action="handle_POST", conditions=dict(method=['POST'])) + + endpoints.serverFromString(reactor, allnet_server_str).listen(server.Site(dispatcher)) endpoints.serverFromString(reactor, adb_server_str).listen(AimedbFactory(cfg)) if cfg.billing.port > 0: - endpoints.serverFromString(reactor, billing_server_str).listen(server.Site(BillingServlet(cfg))) + endpoints.serverFromString(reactor, billing_server_str).listen(server.Site(dispatcher)) if cfg.title.port > 0: - endpoints.serverFromString(reactor, title_server_str).listen(server.Site(TitleServlet(cfg, args.config))) + endpoints.serverFromString(reactor, title_server_str).listen(server.Site(dispatcher)) reactor.run() # type: ignore \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8a7b53c..8dffde0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ inflection coloredlogs pylibmc wacky +txroutes diff --git a/requirements_win.txt b/requirements_win.txt index 1f2c607..412f961 100644 --- a/requirements_win.txt +++ b/requirements_win.txt @@ -11,3 +11,4 @@ PyCryptodome inflection coloredlogs wacky +txroutes