diff --git a/core/__init__.py b/core/__init__.py index 185d9bc..f5e306e 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,6 +1,6 @@ from core.config import CoreConfig -from core.allnet import AllnetServlet -from core.aimedb import AimedbFactory +from core.allnet import AllnetServlet, BillingServlet +from core.aimedb import AimedbServlette from core.title import TitleServlet from core.utils import Utils from core.mucha import MuchaServlet diff --git a/core/adb_handlers/base.py b/core/adb_handlers/base.py index 0f208dd..06b5267 100644 --- a/core/adb_handlers/base.py +++ b/core/adb_handlers/base.py @@ -102,7 +102,7 @@ class ADBHeader: magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id = struct.unpack_from("<5H6sI12s", data) head = cls(magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id) - if head.length != len(data): + if head.length > len(data): raise ADBHeaderException(f"Length is incorrect! Expect {head.length}, got {len(data)}") return head diff --git a/core/aimedb.py b/core/aimedb.py index e65c2c7..2fe1ef7 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -2,8 +2,8 @@ from twisted.internet.protocol import Factory, Protocol import logging, coloredlogs from Crypto.Cipher import AES import struct -from typing import Dict, Tuple, Callable, Union -from typing_extensions import Final +from typing import Dict, Tuple, Callable, Union, Optional +import asyncio from logging.handlers import TimedRotatingFileHandler from core.config import CoreConfig @@ -11,15 +11,37 @@ from core.utils import create_sega_auth_key from core.data import Data from .adb_handlers import * - -class AimedbProtocol(Protocol): +class AimedbServlette(): request_list: Dict[int, Tuple[Callable[[bytes, int], Union[ADBBaseResponse, bytes]], int, str]] = {} - - def __init__(self, core_cfg: CoreConfig) -> None: - self.logger = logging.getLogger("aimedb") - self.config = core_cfg + def __init__(self, core_cfg: CoreConfig) -> None: + self.config = core_cfg self.data = Data(core_cfg) - if core_cfg.aimedb.key == "": + + self.logger = logging.getLogger("aimedb") + if not hasattr(self.logger, "initted"): + log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "aimedb"), + 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(self.config.aimedb.loglevel) + coloredlogs.install( + level=core_cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.initted = True + + if not core_cfg.aimedb.key: self.logger.error("!!!KEY NOT SET!!!") exit(1) @@ -40,27 +62,30 @@ class AimedbProtocol(Protocol): self.register_handler(0x13, 0x14, self.handle_log_ex, 'aime_log_ex') self.register_handler(0x64, 0x65, self.handle_hello, 'hello') - self.register_handler(0x66, 0, self.handle_goodbye, 'goodbye') - + def register_handler(self, cmd: int, resp:int, handler: Callable[[bytes, int], Union[ADBBaseResponse, bytes]], name: str) -> None: self.request_list[cmd] = (handler, resp, name) + + def start(self) -> None: + self.logger.info(f"Start on port {self.config.aimedb.port}") + asyncio.create_task(asyncio.start_server(self.dataReceived, self.config.server.listen_address, self.config.aimedb.port)) + + async def dataReceived(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): + self.logger.debug(f"Connection made from {writer.get_extra_info('peername')[0]}") + while True: + try: + data: bytes = await reader.read() + if len(data) == 0: + self.logger.debug("Connection closed") + return + await self.process_data(data, reader, writer) + await writer.drain() + except ConnectionResetError as e: + self.logger.warn("Connection reset, disconnecting") + return - def append_padding(self, data: bytes): - """Appends 0s to the end of the data until it's at the correct size""" - length = struct.unpack_from(" None: - self.logger.debug(f"{self.transport.getPeer().host} Connected") - - def connectionLost(self, reason) -> None: - self.logger.debug( - f"{self.transport.getPeer().host} Disconnected - {reason.value}" - ) - - def dataReceived(self, data: bytes) -> None: + async def process_data(self, data: bytes, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> Optional[bytes]: + addr = writer.get_extra_info('peername')[0] cipher = AES.new(self.config.aimedb.key.encode(), AES.MODE_ECB) try: @@ -68,9 +93,9 @@ class AimedbProtocol(Protocol): except Exception as e: self.logger.error(f"Failed to decrypt {data.hex()} because {e}") - return None + return - self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}") + self.logger.debug(f"{addr} wrote {decrypted.hex()}") try: head = ADBHeader.from_data(decrypted) @@ -79,7 +104,9 @@ class AimedbProtocol(Protocol): self.logger.error(f"Error parsing ADB header: {e}") try: encrypted = cipher.encrypt(ADBBaseResponse().make()) - self.transport.write(encrypted) + writer.write(encrypted) + await writer.drain() + return except Exception as e: self.logger.error(f"Failed to encrypt default response because {e}") @@ -89,46 +116,51 @@ class AimedbProtocol(Protocol): if head.keychip_id == "ABCD1234567" or head.store_id == 0xfff0: self.logger.warning(f"Request from uninitialized AMLib: {vars(head)}") + if head.cmd == 0x66: + self.logger.info("Goodbye") + writer.close() + return + handler, resp_code, name = self.request_list.get(head.cmd, (self.handle_default, None, 'default')) if resp_code is None: self.logger.warning(f"No handler for cmd {hex(head.cmd)}") elif resp_code > 0: - self.logger.info(f"{name} from {head.keychip_id} ({head.game_id}) @ {self.transport.getPeer().host}") + self.logger.info(f"{name} from {head.keychip_id} ({head.game_id}) @ {addr}") - resp = handler(decrypted, resp_code) + resp = await handler(decrypted, resp_code) if type(resp) == ADBBaseResponse or issubclass(type(resp), ADBBaseResponse): resp_bytes = resp.make() - if len(resp_bytes) != resp.head.length: - resp_bytes = self.append_padding(resp_bytes) elif type(resp) == bytes: resp_bytes = resp elif resp is None: # Nothing to send, probably a goodbye + self.logger.warn(f"None return by handler for {name}") return else: + self.logger.error(f"Unsupported type returned by ADB handler for {name}: {type(resp)}") raise TypeError(f"Unsupported type returned by ADB handler for {name}: {type(resp)}") - try: + try: encrypted = cipher.encrypt(resp_bytes) self.logger.debug(f"Response {resp_bytes.hex()}") - self.transport.write(encrypted) + writer.write(encrypted) except Exception as e: self.logger.error(f"Failed to encrypt {resp_bytes.hex()} because {e}") - - def handle_default(self, data: bytes, resp_code: int, length: int = 0x20) -> ADBBaseResponse: + + async def handle_default(self, data: bytes, resp_code: int, length: int = 0x20) -> ADBBaseResponse: req = ADBHeader.from_data(data) return ADBBaseResponse(resp_code, length, 1, req.game_id, req.store_id, req.keychip_id, req.protocol_ver) - def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse: - return self.handle_default(data, resp_code) + async def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse: + return await self.handle_default(data, resp_code) - def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse: + async def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse: h = ADBHeader.from_data(data) if h.protocol_ver >= 0x3030: req = h @@ -143,26 +175,26 @@ class AimedbProtocol(Protocol): # We don't currently support campaigns return resp - def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse: + async def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse: req = ADBLookupRequest(data) user_id = self.data.card.get_user_id_from_card(req.access_code) is_banned = self.data.card.get_card_banned(req.access_code) is_locked = self.data.card.get_card_locked(req.access_code) - + + ret = ADBLookupResponse.from_req(req.head, user_id) if is_banned and is_locked: ret.head.status = ADBStatus.BAN_SYS_USER elif is_banned: ret.head.status = ADBStatus.BAN_SYS elif is_locked: ret.head.status = ADBStatus.LOCK_USER - ret = ADBLookupResponse.from_req(req.head, user_id) self.logger.info( f"access_code {req.access_code} -> user_id {ret.user_id}" ) return ret - def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse: + async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse: req = ADBLookupRequest(data) user_id = self.data.card.get_user_id_from_card(req.access_code) @@ -191,7 +223,7 @@ class AimedbProtocol(Protocol): return ret - def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes: + async def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes: """ On official, I think a card has to be registered for this to actually work, but I'm making the executive decision to not implement that and just kick back our @@ -207,7 +239,7 @@ class AimedbProtocol(Protocol): ) return ADBFelicaLookupResponse.from_req(req.head, ac) - def handle_felica_register(self, data: bytes, resp_code: int) -> bytes: + async def handle_felica_register(self, data: bytes, resp_code: int) -> bytes: """ I've never seen this used. """ @@ -239,7 +271,7 @@ class AimedbProtocol(Protocol): return ADBFelicaLookupResponse.from_req(req.head, ac) - def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes: + async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes: req = ADBFelicaLookup2Request(data) access_code = self.data.card.to_access_code(req.idm) user_id = self.data.card.get_user_id_from_card(access_code=access_code) @@ -263,7 +295,7 @@ class AimedbProtocol(Protocol): return resp - def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse: + async def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse: req = ADBCampaignClearRequest(data) resp = ADBCampaignClearResponse.from_req(req.head) @@ -271,7 +303,7 @@ class AimedbProtocol(Protocol): # We don't support campaign stuff return resp - def handle_register(self, data: bytes, resp_code: int) -> bytes: + async def handle_register(self, data: bytes, resp_code: int) -> bytes: req = ADBLookupRequest(data) user_id = -1 @@ -305,17 +337,17 @@ class AimedbProtocol(Protocol): return resp # TODO: Save these in some capacity, as deemed relevant - def handle_status_log(self, data: bytes, resp_code: int) -> bytes: + async def handle_status_log(self, data: bytes, resp_code: int) -> bytes: req = ADBStatusLogRequest(data) self.logger.info(f"User {req.aime_id} logged {req.status.name} event") return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver) - def handle_log(self, data: bytes, resp_code: int) -> bytes: + async def handle_log(self, data: bytes, resp_code: int) -> bytes: req = ADBLogRequest(data) self.logger.info(f"User {req.aime_id} logged {req.status.name} event, credit_ct: {req.credit_ct} bet_ct: {req.bet_ct} won_ct: {req.won_ct}") return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver) - def handle_log_ex(self, data: bytes, resp_code: int) -> bytes: + async def handle_log_ex(self, data: bytes, resp_code: int) -> bytes: req = ADBLogExRequest(data) strs = [] self.logger.info(f"Recieved {req.num_logs} or {len(req.logs)} logs") @@ -324,43 +356,3 @@ class AimedbProtocol(Protocol): self.logger.debug(f"User {req.logs[x].aime_id} logged {req.logs[x].status.name} event, credit_ct: {req.logs[x].credit_ct} bet_ct: {req.logs[x].bet_ct} won_ct: {req.logs[x].won_ct}") return ADBLogExResponse.from_req(req.head) - def handle_goodbye(self, data: bytes, resp_code: int) -> None: - self.logger.info(f"goodbye from {self.transport.getPeer().host}") - self.transport.loseConnection() - return - -class AimedbFactory(Factory): - protocol = AimedbProtocol - - def __init__(self, cfg: CoreConfig) -> None: - self.config = cfg - log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) - self.logger = logging.getLogger("aimedb") - - fileHandler = TimedRotatingFileHandler( - "{0}/{1}.log".format(self.config.server.log_dir, "aimedb"), - 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(self.config.aimedb.loglevel) - coloredlogs.install( - level=cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str - ) - - if self.config.aimedb.key == "": - self.logger.error("Please set 'key' field in your config file.") - exit(1) - - self.logger.info(f"Ready on port {self.config.aimedb.port}") - - def buildProtocol(self, addr): - return AimedbProtocol(self.config) diff --git a/core/allnet.py b/core/allnet.py index e83aae0..712b945 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -1,20 +1,24 @@ -from typing import Dict, List, Any, Optional, Tuple, Union, Final -import logging, coloredlogs -from logging.handlers import TimedRotatingFileHandler -from twisted.web.http import Request -from datetime import datetime import pytz import base64 import zlib import json +import yaml +import logging +import coloredlogs +import urllib.parse +import math +from typing import Dict, List, Any, Optional, Union, Final +from logging.handlers import TimedRotatingFileHandler +from starlette.requests import Request +from starlette.responses import PlainTextResponse +from starlette.applications import Starlette +from starlette.routing import Route +from datetime import datetime from enum import Enum from Crypto.PublicKey import RSA from Crypto.Hash import SHA from Crypto.Signature import PKCS1_v1_5 -from time import strptime -from os import path -import urllib.parse -import math +from os import path, environ, mkdir, access, W_OK from .config import CoreConfig from .utils import Utils @@ -91,7 +95,6 @@ class DLI_STATUS(Enum): class AllnetServlet: 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) @@ -126,19 +129,20 @@ class AllnetServlet: self.logger.error("No games detected!") self.logger.info( - f"Serving {len(TitleServlet.title_registry)} game codes port {core_cfg.allnet.port}" + f"Serving {len(TitleServlet.title_registry)} game codes" ) - def handle_poweron(self, request: Request, _: Dict): + async def handle_poweron(self, request: Request): request_ip = Utils.get_ip_addr(request) - pragma_header = request.getHeader('Pragma') + pragma_header = request.headers.get('Pragma', "") is_dfi = pragma_header is not None and pragma_header == "DFI" + data = await request.body() try: if is_dfi: - req_urlencode = self.from_dfi(request.content.getvalue()) + req_urlencode = self.from_dfi(data) else: - req_urlencode = request.content.getvalue().decode() + req_urlencode = data req_dict = self.allnet_req_to_dict(req_urlencode) if req_dict is None: @@ -155,7 +159,7 @@ class AllnetServlet: except AllnetRequestException as e: if e.message != "": self.logger.error(e) - return b"" + return PlainTextResponse() if req.format_ver == 3: resp = AllnetPowerOnResponse3(req.token) @@ -176,7 +180,7 @@ class AllnetServlet: resp.stat = ALLNET_STAT.bad_machine.value resp_dict = {k: v for k, v in vars(resp).items() if v is not None} - return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") if machine is not None: arcade = self.data.arcade.get_arcade(machine["arcade"]) @@ -190,7 +194,7 @@ class AllnetServlet: resp.stat = ALLNET_STAT.bad_shop.value resp_dict = {k: v for k, v in vars(resp).items() if v is not None} - return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") elif (not arcade["ip"] or arcade["ip"] is None) and self.config.server.strict_ip_checking: msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)." @@ -201,7 +205,7 @@ class AllnetServlet: resp.stat = ALLNET_STAT.bad_shop.value resp_dict = {k: v for k, v in vars(resp).items() if v is not None} - return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") country = ( @@ -245,20 +249,20 @@ class AllnetServlet: resp.stat = ALLNET_STAT.bad_game.value resp_dict = {k: v for k, v in vars(resp).items() if v is not None} - return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") else: self.logger.info( f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}" ) - resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" - resp.host = f"{self.config.title.hostname}:{self.config.title.port}" + resp.uri = f"http://{self.config.server.hostname}:{self.config.server.port}/{req.game_id}/{req.ver.replace('.', '')}/" + resp.host = f"{self.config.server.hostname}:{self.config.server.port}" resp_dict = {k: v for k, v in vars(resp).items() if v is not None} resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) self.logger.debug(f"Allnet response: {resp_str}") - return (resp_str + "\n").encode("utf-8") + return PlainTextResponse(resp_str + "\n") int_ver = req.ver.replace(".", "") @@ -277,18 +281,19 @@ class AllnetServlet: request.responseHeaders.addRawHeader('Pragma', 'DFI') return self.to_dfi(resp_str)""" - return resp_str.encode("utf-8") + return PlainTextResponse(resp_str) - def handle_dlorder(self, request: Request, _: Dict): + async def handle_dlorder(self, request: Request): request_ip = Utils.get_ip_addr(request) - pragma_header = request.getHeader('Pragma') + pragma_header = request.headers.get('Pragma', "") is_dfi = pragma_header is not None and pragma_header == "DFI" + data = await request.body() try: if is_dfi: - req_urlencode = self.from_dfi(request.content.getvalue()) + req_urlencode = self.from_dfi(data) else: - req_urlencode = request.content.getvalue().decode() + req_urlencode = data.decode() req_dict = self.allnet_req_to_dict(req_urlencode) if req_dict is None: @@ -305,7 +310,7 @@ class AllnetServlet: except AllnetRequestException as e: if e.message != "": self.logger.error(e) - return b"" + return PlainTextResponse() self.logger.info( f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}" @@ -316,18 +321,18 @@ class AllnetServlet: not self.config.allnet.allow_online_updates or not self.config.allnet.update_cfg_folder ): - return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n") else: # TODO: Keychip check if path.exists( f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini" ): - resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini" + resp.uri = f"http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini" if path.exists( f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" ): - resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" + resp.uri += f"|http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" self.logger.debug(f"Sending download uri {resp.uri}") self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}") @@ -337,33 +342,33 @@ class AllnetServlet: request.responseHeaders.addRawHeader('Pragma', 'DFI') return self.to_dfi(res_str)""" - return res_str + return PlainTextResponse(res_str) - def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: - if "file" not in match: - return b"" + async def handle_dlorder_ini(self, request: Request) -> bytes: + req_file = request.path_params.get("file", "").replace("%0A", "") - req_file = match["file"].replace("%0A", "") + if not req_file: + return PlainTextResponse(status_code=404) if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful") self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}") - return open( - f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb" - ).read() + return PlainTextResponse(open( + f"{self.config.allnet.update_cfg_folder}/{req_file}", "r" + ).read()) self.logger.info(f"DL INI File {req_file} not found") - return b"" + return PlainTextResponse() - def handle_dlorder_report(self, request: Request, match: Dict) -> bytes: - req_raw = request.content.getvalue() + async def handle_dlorder_report(self, request: Request) -> bytes: + req_raw = await request.body() client_ip = Utils.get_ip_addr(request) try: req_dict: Dict = json.loads(req_raw) except Exception as e: self.logger.warning(f"Failed to parse DL Report: {e}") - return "NG" + return PlainTextResponse("NG") dl_data_type = DLIMG_TYPE.app dl_data = req_dict.get("appimage", {}) @@ -374,13 +379,13 @@ class AllnetServlet: if dl_data is None or not dl_data: self.logger.warning(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage") - return "NG" + return PlainTextResponse("NG") rep = DLReport(dl_data, dl_data_type) if not rep.validate(): self.logger.warning(f"Failed to parse DL Report: Invalid format - {rep.err}") - return "NG" + return PlainTextResponse("NG") msg = f"{rep.serial} @ {client_ip} reported {rep.rep_type.name} download state {rep.rf_state.name} for {rep.gd} v{rep.dav}:"\ f" {rep.tdsc}/{rep.tsc} segments downloaded for working files {rep.wfl} with {rep.dfl if rep.dfl else 'none'} complete." @@ -388,10 +393,10 @@ class AllnetServlet: self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data) self.logger.info(msg) - return "OK" + return PlainTextResponse("OK") - def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes: - req_data = request.content.getvalue() + async def handle_loaderstaterecorder(self, request: Request) -> bytes: + req_data = await request.body() sections = req_data.decode("utf-8").split("\r\n") req_dict = dict(urllib.parse.parse_qsl(sections[0])) @@ -403,18 +408,94 @@ class AllnetServlet: ip = Utils.get_ip_addr(request) if serial is None or num_files_dld is None or num_files_to_dl is None or dl_state is None: - return "NG".encode() + return PlainTextResponse("NG") self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})") - return "OK".encode() + return PlainTextResponse("OK") - def handle_alive(self, request: Request, match: Dict) -> bytes: - return "OK".encode() + async def handle_alive(self, request: Request) -> bytes: + return PlainTextResponse("OK") - def handle_billing_request(self, request: Request, _: Dict): - req_raw = request.content.getvalue() + async def handle_naomitest(self, request: Request) -> bytes: + self.logger.info(f"Ping from {Utils.get_ip_addr(request)}") + return PlainTextResponse("naomi ok") + + def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: + """ + Parses an allnet request string into a python dictionary + """ + try: + sections = data.split("\r\n") + + ret = [] + for x in sections: + ret.append(dict(urllib.parse.parse_qsl(x))) + return ret + + except Exception as e: + self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}") + return None + + def from_dfi(self, data: bytes) -> str: + zipped = base64.b64decode(data) + unzipped = zlib.decompress(zipped) + return unzipped.decode("utf-8") + + def to_dfi(self, data: str) -> bytes: + unzipped = data.encode('utf-8') + zipped = zlib.compress(unzipped) + return base64.b64encode(zipped) + +class BillingServlet: + def __init__(self, core_cfg: CoreConfig, cfg_folder: str) -> None: + self.config = core_cfg + self.config_folder = cfg_folder + self.data = Data(core_cfg) + + self.logger = logging.getLogger("billing") + if not hasattr(self.logger, "initialized"): + log_fmt_str = "[%(asctime)s] Billing | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.config.server.log_dir, "billing"), + 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.billing.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.initialized = True + + def billing_req_to_dict(self, data: bytes): + """ + Parses an billing request string into a python dictionary + """ + try: + sections = data.decode("ascii").split("\r\n") + + ret = [] + for x in sections: + ret.append(dict(urllib.parse.parse_qsl(x))) + return ret + + except Exception as e: + self.logger.error(f"billing_req_to_dict: {e} while parsing {data}") + return None + + async def handle_billing_request(self, request: Request): + req_raw = await request.body() - if request.getHeader('Content-Type') == "application/octet-stream": + if request.headers.get('Content-Type', '') == "application/octet-stream": req_unzip = zlib.decompressobj(-zlib.MAX_WBITS).decompress(req_raw) else: req_unzip = req_raw @@ -423,8 +504,8 @@ class AllnetServlet: request_ip = Utils.get_ip_addr(request) if req_dict is None: - self.logger.error(f"Failed to parse request {request.content.getvalue()}") - return b"" + self.logger.error(f"Failed to parse request {req_raw}") + return PlainTextResponse() self.logger.debug(f"request {req_dict}") @@ -436,7 +517,7 @@ class AllnetServlet: req = BillingInfo(req_dict[0]) except KeyError as e: self.logger.error(f"Billing request failed to parse: {e}") - return f"result=5&linelimit=&message=field is missing or formatting is incorrect\r\n".encode() + return PlainTextResponse("result=5&linelimit=&message=field is missing or formatting is incorrect\r\n") for x in range(1, len(req_dict)): if not req_dict[x]: @@ -467,7 +548,7 @@ class AllnetServlet: ) self.logger.warning(msg) - return f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n".encode() + return PlainTextResponse(f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n") msg = ( f"Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount " @@ -496,7 +577,6 @@ class AllnetServlet: # TODO: playhistory - #resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig) resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig, req.requestno, req.protocolver) resp_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n" @@ -504,56 +584,9 @@ class AllnetServlet: self.logger.debug(f"response {vars(resp)}") if req.traceleft > 0: self.logger.info(f"Requesting 20 more of {req.traceleft} unsent tracelogs") - return f"result=6&waittime=0&linelimit=20\r\n".encode() - - return resp_str.encode("utf-8") - - def handle_naomitest(self, request: Request, _: Dict) -> bytes: - self.logger.info(f"Ping from {Utils.get_ip_addr(request)}") - return b"naomi ok" - - def billing_req_to_dict(self, data: bytes): - """ - Parses an billing request string into a python dictionary - """ - try: - sections = data.decode("ascii").split("\r\n") - - ret = [] - for x in sections: - ret.append(dict(urllib.parse.parse_qsl(x))) - return ret - - except Exception as e: - self.logger.error(f"billing_req_to_dict: {e} while parsing {data}") - return None - - def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: - """ - Parses an allnet request string into a python dictionary - """ - try: - sections = data.split("\r\n") - - ret = [] - for x in sections: - ret.append(dict(urllib.parse.parse_qsl(x))) - return ret - - except Exception as e: - self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}") - return None - - def from_dfi(self, data: bytes) -> str: - zipped = base64.b64decode(data) - unzipped = zlib.decompress(zipped) - return unzipped.decode("utf-8") - - def to_dfi(self, data: str) -> bytes: - unzipped = data.encode('utf-8') - zipped = zlib.compress(unzipped) - return base64.b64encode(zipped) - + return PlainTextResponse("result=6&waittime=0&linelimit=20\r\n") + + return PlainTextResponse(resp_str) class AllnetPowerOnRequest: def __init__(self, req: Dict) -> None: @@ -613,7 +646,6 @@ class AllnetPowerOnResponse3(AllnetPowerOnResponse): self.minute = None self.second = None - class AllnetPowerOnResponse2(AllnetPowerOnResponse): def __init__(self) -> None: super().__init__() @@ -623,7 +655,6 @@ class AllnetPowerOnResponse2(AllnetPowerOnResponse): self.timezone = "+09:00" self.res_class = "PowerOnResponseV2" - class AllnetDownloadOrderRequest: def __init__(self, req: Dict) -> None: self.game_id = req.get("game_id", "") @@ -631,7 +662,6 @@ class AllnetDownloadOrderRequest: self.serial = req.get("serial", "") self.encode = req.get("encode", "") - class AllnetDownloadOrderResponse: def __init__(self, stat: int = 1, serial: str = "", uri: str = "") -> None: self.stat = stat @@ -781,7 +811,6 @@ class BillingResponse: # playhistory -> YYYYMM/C:... # YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period - class AllnetRequestException(Exception): def __init__(self, message="") -> None: self.message = message @@ -849,3 +878,26 @@ class DLReport: return False return True + +cfg_dir = environ.get("DIANA_CFG_DIR", "config") +cfg: CoreConfig = CoreConfig() +if path.exists(f"{cfg_dir}/core.yaml"): + cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) + +if not path.exists(cfg.server.log_dir): + mkdir(cfg.server.log_dir) + +if not access(cfg.server.log_dir, W_OK): + print( + f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" + ) + exit(1) + +billing = BillingServlet(cfg, cfg_dir) +app = Starlette( + cfg.server.is_develop, + [ + Route("/request", billing.handle_billing_request, methods=["POST"]), + Route("/request/", billing.handle_billing_request, methods=["POST"]), + ] +) diff --git a/core/app.py b/core/app.py new file mode 100644 index 0000000..f20c041 --- /dev/null +++ b/core/app.py @@ -0,0 +1,89 @@ +import yaml +import logging +import coloredlogs +from logging.handlers import TimedRotatingFileHandler +from starlette.routing import Route +from starlette.requests import Request +from starlette.applications import Starlette +from starlette.responses import PlainTextResponse +from os import environ, path, mkdir, W_OK, access +from typing import List + +from core import CoreConfig, TitleServlet, MuchaServlet, AllnetServlet, BillingServlet, AimedbServlette +from core.frontend import FrontendServlet + +async def dummy_rt(request: Request): + return PlainTextResponse("Service OK") + +cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config") +cfg: CoreConfig = CoreConfig() +if path.exists(f"{cfg_dir}/core.yaml"): + cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) + +if not path.exists(cfg.server.log_dir): + mkdir(cfg.server.log_dir) + +if not access(cfg.server.log_dir, W_OK): + print( + f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" + ) + exit(1) + +logger = logging.getLogger("core") +log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s" +log_fmt = logging.Formatter(log_fmt_str) + +fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10 +) +fileHandler.setFormatter(log_fmt) + +consoleHandler = logging.StreamHandler() +consoleHandler.setFormatter(log_fmt) + +logger.addHandler(fileHandler) +logger.addHandler(consoleHandler) + +log_lv = logging.DEBUG if cfg.server.is_develop else logging.INFO +logger.setLevel(log_lv) +coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str) + +logger.info(f"Artemis starting in {'develop' if cfg.server.is_develop else 'production'} mode") + +title = TitleServlet(cfg, cfg_dir) # This has to be loaded first to load plugins +mucha = MuchaServlet(cfg, cfg_dir) +allnet = AllnetServlet(cfg, cfg_dir) + +route_lst: List[Route] = [ + # Allnet + Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]), + Route("/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"]), + Route("/sys/servlet/LoaderStateRecorder", allnet.handle_loaderstaterecorder, methods=["GET", "POST"]), + Route("/sys/servlet/Alive", allnet.handle_alive, methods=["GET", "POST"]), + Route("/report-api/Report", allnet.handle_dlorder_report, methods=["POST"]), + Route("/dl/ini/{file:str}", allnet.handle_dlorder_ini), + Route("/naomitest.html", allnet.handle_naomitest), + # Mucha + Route("/mucha_front/boardauth.do", mucha.handle_boardauth, methods=["POST"]), + Route("/mucha_front/updatacheck.do", mucha.handle_updatecheck, methods=["POST"]), + Route("/mucha_front/downloadstate.do", mucha.handle_dlstate, methods=["POST"]), +] + +if not cfg.billing.standalone: + billing = BillingServlet(cfg, cfg_dir) + route_lst += [ + Route("/request", billing.handle_billing_request, methods=["POST"]), + Route("/request/", billing.handle_billing_request, methods=["POST"]), + ] + +if not cfg.frontend.standalone: + frontend = FrontendServlet(cfg, cfg_dir) + route_lst += frontend.get_routes() +else: + route_lst.append(Route("/", dummy_rt)) + route_lst.append(Route("/robots.txt", FrontendServlet.robots)) + +for code, game in title.title_registry.items(): + route_lst += game.get_routes() + +app = Starlette(cfg.server.is_develop, route_lst) diff --git a/core/config.py b/core/config.py index 68db052..0a95742 100644 --- a/core/config.py +++ b/core/config.py @@ -1,16 +1,48 @@ import logging, os from typing import Any - class ServerConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @property def listen_address(self) -> str: + """ + Address Artemis will bind to and listen on + """ return CoreConfig.get_config_field( self.__config, "core", "server", "listen_address", default="127.0.0.1" ) + + @property + def hostname(self) -> str: + """ + Hostname sent to games + """ + return CoreConfig.get_config_field( + self.__config, "core", "server", "hostname", default="localhost" + ) + + @property + def port(self) -> int: + """ + Port the game will listen on + """ + return CoreConfig.get_config_field( + self.__config, "core", "server", "port", default=8080 + ) + + @property + def ssl_key(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "server", "ssl_key", default="cert/title.key" + ) + + @property + def ssl_cert(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "title", "ssl_cert", default="cert/title.pem" + ) @property def allow_user_registration(self) -> bool: @@ -43,9 +75,13 @@ class ServerConfig: ) @property - def threading(self) -> bool: + def proxy_port(self) -> int: + """ + What port the proxy is listening on. This will be sent instead of 'port' if + is_using_proxy is True and this value is non-zero + """ return CoreConfig.get_config_field( - self.__config, "core", "server", "threading", default=False + self.__config, "core", "title", "proxy_port", default=0 ) @property @@ -66,7 +102,6 @@ class ServerConfig: self.__config, "core", "server", "strict_ip_checking", default=False ) - class TitleConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @@ -79,49 +114,18 @@ class TitleConfig: ) ) - @property - def hostname(self) -> str: - return CoreConfig.get_config_field( - self.__config, "core", "title", "hostname", default="localhost" - ) - - @property - def port(self) -> int: - return CoreConfig.get_config_field( - self.__config, "core", "title", "port", default=8080 - ) - - @property - def port_ssl(self) -> int: - return CoreConfig.get_config_field( - self.__config, "core", "title", "port_ssl", default=0 - ) - - @property - def ssl_key(self) -> str: - return CoreConfig.get_config_field( - self.__config, "core", "title", "ssl_key", default="cert/title.key" - ) - - @property - def ssl_cert(self) -> str: - return CoreConfig.get_config_field( - self.__config, "core", "title", "ssl_cert", default="cert/title.pem" - ) - @property def reboot_start_time(self) -> str: return CoreConfig.get_config_field( - self.__config, "core", "title", "reboot_start_time", default="" + self.__config, "core", "title", "reboot_start_time", default="04:00" ) @property def reboot_end_time(self) -> str: return CoreConfig.get_config_field( - self.__config, "core", "title", "reboot_end_time", default="" + self.__config, "core", "title", "reboot_end_time", default="05:00" ) - class DatabaseConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @@ -176,16 +180,6 @@ class DatabaseConfig: ) ) - @property - def user_table_autoincrement_start(self) -> int: - return CoreConfig.get_config_field( - self.__config, - "core", - "database", - "user_table_autoincrement_start", - default=10000, - ) - @property def enable_memcached(self) -> bool: return CoreConfig.get_config_field( @@ -198,21 +192,14 @@ class DatabaseConfig: self.__config, "core", "database", "memcached_host", default="localhost" ) - class FrontendConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @property - def enable(self) -> int: + def standalone(self) -> int: return CoreConfig.get_config_field( - self.__config, "core", "frontend", "enable", default=False - ) - - @property - def port(self) -> int: - return CoreConfig.get_config_field( - self.__config, "core", "frontend", "port", default=8090 + self.__config, "core", "frontend", "standalone", default=True ) @property @@ -222,7 +209,12 @@ class FrontendConfig: self.__config, "core", "frontend", "loglevel", default="info" ) ) - + + @property + def secret(self) -> str: + CoreConfig.get_config_field( + self.__config, "core", "frontend", "secret", default="" + ) class AllnetConfig: def __init__(self, parent_config: "CoreConfig") -> None: @@ -236,18 +228,6 @@ class AllnetConfig: ) ) - @property - def port(self) -> int: - return CoreConfig.get_config_field( - self.__config, "core", "allnet", "port", default=80 - ) - - @property - def ip_check(self) -> bool: - return CoreConfig.get_config_field( - self.__config, "core", "allnet", "ip_check", default=False - ) - @property def allow_online_updates(self) -> int: return CoreConfig.get_config_field( @@ -260,10 +240,23 @@ class AllnetConfig: self.__config, "core", "allnet", "update_cfg_folder", default="" ) - class BillingConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config + + @property + def standalone(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "core", "billing", "standalone", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "core", "billing", "loglevel", default="info" + ) + ) @property def port(self) -> int: @@ -289,7 +282,6 @@ class BillingConfig: self.__config, "core", "billing", "signing_key", default="cert/billing.key" ) - class AimedbConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config @@ -326,17 +318,10 @@ class AimedbConfig: self.__config, "core", "aimedb", "id_lifetime_seconds", default=86400 ) - class MuchaConfig: def __init__(self, parent_config: "CoreConfig") -> None: self.__config = parent_config - @property - def enable(self) -> int: - return CoreConfig.get_config_field( - self.__config, "core", "mucha", "enable", default=False - ) - @property def loglevel(self) -> int: return CoreConfig.str_to_loglevel( @@ -345,13 +330,6 @@ class MuchaConfig: ) ) - @property - def hostname(self) -> str: - return CoreConfig.get_config_field( - self.__config, "core", "mucha", "hostname", default="localhost" - ) - - class CoreConfig(dict): def __init__(self) -> None: self.server = ServerConfig(self) diff --git a/core/data/alembic/README b/core/data/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/core/data/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/core/data/alembic/alembic.ini b/core/data/alembic/alembic.ini new file mode 100644 index 0000000..299f51f --- /dev/null +++ b/core/data/alembic/alembic.ini @@ -0,0 +1,89 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = . + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to ./versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat ./versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks=black +# black.type=console_scripts +# black.entrypoint=black +# black.options=-l 79 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/core/data/alembic/env.py b/core/data/alembic/env.py new file mode 100644 index 0000000..70518a2 --- /dev/null +++ b/core/data/alembic/env.py @@ -0,0 +1,77 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = None + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/core/data/alembic/script.py.mako b/core/data/alembic/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/core/data/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/core/frontend.py b/core/frontend.py index 0ee2211..054092c 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -2,7 +2,9 @@ import logging, coloredlogs from typing import Any, Dict, List from twisted.web import resource from twisted.web.util import redirectTo -from twisted.web.http import Request +from starlette.requests import Request +from starlette.routing import Route +from starlette.responses import Response, PlainTextResponse from logging.handlers import TimedRotatingFileHandler from twisted.web.server import Session from zope.interface import Interface, Attribute, implementer @@ -98,8 +100,15 @@ class FrontendServlet(resource.Resource): self.putChild(b"game", fe_game) self.logger.info( - f"Ready on port {self.config.frontend.port} serving {len(fe_game.children)} games" + f"Ready on port {self.config.server.port} serving {len(fe_game.children)} games" ) + + def get_routes(self) -> List[Route]: + return [] + + @classmethod + async def robots(cls, request: Request) -> PlainTextResponse: + return PlainTextResponse("User-agent: *\nDisallow: /\n\nUser-agent: AdsBot-Google\nDisallow: /") def render_GET(self, request): self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.uri.decode()}") diff --git a/core/mucha.py b/core/mucha.py index 7c6f0ab..64f20aa 100644 --- a/core/mucha.py +++ b/core/mucha.py @@ -2,7 +2,7 @@ from typing import Dict, Any, Optional, List import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from twisted.web import resource -from twisted.web.http import Request +from starlette.requests import Request from datetime import datetime from Crypto.Cipher import Blowfish import pytz @@ -12,7 +12,7 @@ from .utils import Utils from .title import TitleServlet class MuchaServlet: - mucha_registry: List[str] = [] + mucha_registry: Dict[str, str] = {} def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None: self.config = cfg self.config_dir = cfg_dir @@ -39,11 +39,12 @@ class MuchaServlet: for _, mod in TitleServlet.title_registry.items(): if hasattr(mod, "get_mucha_info"): - enabled, game_cd = mod.get_mucha_info( + enabled, game_cds, netid_prefixes = mod.get_mucha_info( self.config, self.config_dir ) if enabled: - self.mucha_registry.append(game_cd) + for x in range(len(game_cds)): + self.mucha_registry[game_cds[x]] = netid_prefixes[x] self.logger.info(f"Serving {len(self.mucha_registry)} games") @@ -75,7 +76,7 @@ class MuchaServlet: self.logger.debug(f"Decrypt SN to {sn_decrypt.hex()}") resp = MuchaAuthResponse( - f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}" + f"{self.config.server.hostname}{':' + str(self.config.server.port) if self.config.server.is_develop else ''}" ) self.logger.debug(f"Mucha response {vars(resp)}") @@ -100,7 +101,7 @@ class MuchaServlet: self.logger.warning(f"Unknown gameCd {req.gameCd}") return b"RESULTS=000" - resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}") + resp = MuchaUpdateResponse(req.gameVer, f"{self.config.server.hostname}{':' + str(self.config.server.port) if self.config.server.is_develop else ''}") self.logger.debug(f"Mucha response {vars(resp)}") diff --git a/core/title.py b/core/title.py index 3fdb30c..8bead43 100644 --- a/core/title.py +++ b/core/title.py @@ -1,7 +1,9 @@ from typing import Dict, List, Tuple import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler -from twisted.web.http import Request +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import Route from core.config import CoreConfig from core.data import Data @@ -28,18 +30,16 @@ class BaseServlet: """ return False - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: + def get_routes(self) -> List[Route]: """Called during boot to get all matcher endpoints this title servlet handles Returns: - Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: A 2-length tuple where offset 0 is GET and offset 1 is POST, - containing a list of 3-length tuples where offset 0 is the name of the function in the handler that should be called, offset 1 - is the matching string, and offset 2 is a dict containing rules for the matcher. + List[Route]: A list of Routes, WebSocketRoutes, or similar classes """ - return ( - [("render_GET", "/{game}/{version}/{endpoint}", {'game': R'S...'})], - [("render_POST", "/{game}/{version}/{endpoint}", {'game': R'S...'})] - ) + return [ + Route("/{game}/{version}/{endpoint}", self.render_POST, methods=["POST"]), + Route("/{game}/{version}/{endpoint}", self.render_GET, methods=["GET"]), + ] def setup(self) -> None: """Called once during boot, should contain any additional setup the handler must do, such as starting any sub-services @@ -58,11 +58,11 @@ class BaseServlet: Tuple[str, str]: A tuple where offset 0 is the allnet uri field, and offset 1 is the allnet host field """ if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: - return (f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", "") + return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", "") - return (f"http://{self.core_cfg.title.hostname}/{game_code}/{game_ver}/", "") + return (f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", "") - def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str]: + def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, List[str], List[str]]: """Called once during boot to check if this game is a mucha game Args: @@ -72,15 +72,15 @@ class BaseServlet: Returns: Tuple[bool, str]: Tuple where offset 0 is true if the game is enabled, false otherwise, and offset 1 is the game CD """ - return (False, "") + return (False, [], []) - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: - self.logger.warn(f"{game_code} Does not dispatch POST") - return None + async def render_POST(self, request: Request) -> bytes: + self.logger.warn(f"Game Does not dispatch POST") + return Response() - def render_GET(self, request: Request, game_code: str, matchers: Dict) -> bytes: - self.logger.warn(f"{game_code} Does not dispatch GET") - return None + async def render_GET(self, request: Request) -> bytes: + self.logger.warn(f"Game Does not dispatch GET") + return Response() class TitleServlet: title_registry: Dict[str, BaseServlet] = {} @@ -136,7 +136,7 @@ class TitleServlet: self.logger.error(f"{folder} missing game_code or index in __init__.py, or is_game_enabled in index") self.logger.info( - f"Serving {len(self.title_registry)} game codes {'on port ' + str(core_cfg.title.port) if core_cfg.title.port > 0 else ''}" + f"Serving {len(self.title_registry)} game codes {'on port ' + str(core_cfg.server.port) if core_cfg.server.port > 0 else ''}" ) def render_GET(self, request: Request, endpoints: dict) -> bytes: diff --git a/core/utils.py b/core/utils.py index 8264213..4dfb4dc 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,6 +1,6 @@ from typing import Dict, Any, Optional from types import ModuleType -from twisted.web.http import Request +from starlette.requests import Request import logging import importlib from os import walk @@ -34,36 +34,16 @@ class Utils: @classmethod def get_ip_addr(cls, req: Request) -> str: - return ( - req.getAllHeaders()[b"x-forwarded-for"].decode() - if b"x-forwarded-for" in req.getAllHeaders() - else req.getClientAddress().host - ) + return req.headers.get("x-forwarded-for", req.client.host) @classmethod def get_title_port(cls, cfg: CoreConfig): if cls.real_title_port is not None: return cls.real_title_port - if cfg.title.port == 0: - cls.real_title_port = cfg.allnet.port - - else: - cls.real_title_port = cfg.title.port + cls.real_title_port = cfg.server.proxy_port if cfg.server.is_using_proxy else cfg.server.port return cls.real_title_port - @classmethod - def get_title_port_ssl(cls, cfg: CoreConfig): - if cls.real_title_port_ssl is not None: return cls.real_title_port_ssl - - if cfg.title.port_ssl == 0: - cls.real_title_port_ssl = 443 - - else: - cls.real_title_port_ssl = cfg.title.port_ssl - - return cls.real_title_port_ssl - def create_sega_auth_key(aime_id: int, game: str, place_id: int, keychip_id: str, b64_secret: str, exp_seconds: int = 86400, err_logger: str = 'aimedb') -> Optional[str]: logger = logging.getLogger(err_logger) try: diff --git a/dbutils.py b/dbutils.py index 85b18a0..f5e57be 100644 --- a/dbutils.py +++ b/dbutils.py @@ -1,9 +1,11 @@ -import yaml +#!/usr/bin/env python3 import argparse import logging -from core.config import CoreConfig -from core.data import Data -from os import path, mkdir, access, W_OK +from os import mkdir, path, access, W_OK +import yaml +import asyncio + +from core import Data, CoreConfig if __name__ == "__main__": parser = argparse.ArgumentParser(description="Database utilities") @@ -16,19 +18,9 @@ if __name__ == "__main__": type=str, help="Version of the database to upgrade/rollback to", ) - parser.add_argument( - "--game", - "-g", - type=str, - help="Game code of the game who's schema will be updated/rolled back. Ex. SDFE", - ) parser.add_argument("--email", "-e", type=str, help="Email for the new user") - parser.add_argument("--old_ac", "-o", type=str, help="Access code to transfer from") - parser.add_argument("--new_ac", "-n", type=str, help="Access code to transfer to") - parser.add_argument("--force", "-f", type=bool, help="Force the action to happen") - parser.add_argument( - "action", type=str, help="DB Action, create, recreate, upgrade, or rollback" - ) + parser.add_argument("--access_code", "-a", type=str, help="Access code for new/transfer user", default="00000000000000000000") + parser.add_argument("action", type=str, help="create, upgrade, create-owner") args = parser.parse_args() cfg = CoreConfig() @@ -50,42 +42,13 @@ if __name__ == "__main__": if args.action == "create": data.create_database() - - elif args.action == "recreate": - data.recreate_database() - - elif args.action == "upgrade" or args.action == "rollback": - if args.version is None: - data.logger.warning("No version set, upgrading to latest") - - if args.game is None: - data.logger.warning("No game set, upgrading core schema") - data.migrate_database( - "CORE", - int(args.version) if args.version is not None else None, - args.action, - ) - - else: - data.migrate_database( - args.game, - int(args.version) if args.version is not None else None, - args.action, - ) - - elif args.action == "autoupgrade": - data.autoupgrade() + + elif args.action == "upgrade": + data.schema_upgrade(args.version) elif args.action == "create-owner": - data.create_owner(args.email) + loop = asyncio.get_event_loop() + loop.run_until_complete(data.create_owner(args.email, args.access_code)) - elif args.action == "migrate-card": - data.migrate_card(args.old_ac, args.new_ac, args.force) - - elif args.action == "cleanup": - data.delete_hanging_users() - - elif args.action == "version": - data.show_versions() - - data.logger.info("Done") + else: + logging.getLogger("database").info(f"Unknown action {args.action}") diff --git a/example_config/core.yaml b/example_config/core.yaml index 21b1a9d..f5b11f4 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -1,26 +1,24 @@ server: - listen_address: "127.0.0.1" + listen_address: "127.0.0.1" + hostname: "localhost" + port: 8080 + ssl_key: "cert/title.key" + ssl_cert: "cert/title.crt" allow_user_registration: True allow_unregistered_serials: True name: "ARTEMiS" is_develop: True is_using_proxy: False - threading: False + proxy_port: 0 log_dir: "logs" check_arcade_ip: False strict_ip_checking: False title: loglevel: "info" - hostname: "localhost" - port: 8080 - port_ssl: 0 - ssl_cert: "cert/title.crt" - ssl_key: "cert/title.key" reboot_start_time: "04:00" reboot_end_time: "05:00" - - + ssl_key: "cert/title.key" database: host: "localhost" username: "aime" @@ -30,23 +28,22 @@ database: protocol: "mysql" sha2_password: False loglevel: "warn" - user_table_autoincrement_start: 10000 enable_memcached: True memcached_host: "localhost" frontend: - enable: False - port: 8090 + standalone: True loglevel: "info" + secret: "" allnet: loglevel: "info" - port: 80 - ip_check: False allow_online_updates: False update_cfg_folder: "" billing: + standalone: True + loglevel: "info" port: 8443 ssl_key: "cert/server.key" ssl_cert: "cert/server.pem" @@ -60,6 +57,4 @@ aimedb: id_lifetime_seconds: 86400 mucha: - enable: False - hostname: "localhost" loglevel: "info" diff --git a/index.py b/index.py index 798519c..c820f22 100644 --- a/index.py +++ b/index.py @@ -1,335 +1,92 @@ #!/usr/bin/env python3 import argparse -import logging, coloredlogs -from logging.handlers import TimedRotatingFileHandler -from typing import Dict import yaml -from os import path, mkdir, access, W_OK -from core import * +from os import path, mkdir, access, W_OK, environ +import uvicorn +import logging +import asyncio -from twisted.web import server, resource -from twisted.internet import reactor, endpoints -from twisted.web.http import Request -from routes import Mapper -from threading import Thread +from core import CoreConfig, AimedbServlette -class HttpDispatcher(resource.Resource): - def __init__(self, cfg: CoreConfig, config_dir: str): - super().__init__() - self.config = cfg - self.isLeaf = True - self.map_get = Mapper() - self.map_post = Mapper() - self.logger = logging.getLogger("core") - - self.title = TitleServlet(cfg, config_dir) - self.allnet = AllnetServlet(cfg, config_dir) - self.mucha = MuchaServlet(cfg, config_dir) - - self.map_get.connect( - "allnet_downloadorder_ini", - "/dl/ini/{file}", - controller="allnet", - action="handle_dlorder_ini", - conditions=dict(method=["GET"]), +async def launch_main(cfg: CoreConfig, ssl: bool) -> None: + if ssl: + server_cfg = uvicorn.Config( + "core.app:app", + host=cfg.server.listen_address, + port=cfg.server.port if args.port == 0 else args.port, + reload=cfg.server.is_develop, + log_level="info" if cfg.server.is_develop else "critical", + ssl_version=3, + ssl_certfile=cfg.server.ssl_cert, + ssl_keyfile=cfg.server.ssl_key + ) + else: + server_cfg = uvicorn.Config( + "core.app:app", + host=cfg.server.listen_address, + port=cfg.server.port if args.port == 0 else args.port, + reload=cfg.server.is_develop, + log_level="info" if cfg.server.is_develop else "critical" ) + server = uvicorn.Server(server_cfg) + await server.serve() - self.map_post.connect( - "allnet_downloadorder_report", - "/report-api/Report", - controller="allnet", - action="handle_dlorder_report", - conditions=dict(method=["POST"]), +async def launch_billing(cfg: CoreConfig) -> None: + server_cfg = uvicorn.Config( + "core.allnet:app", + host=cfg.server.listen_address, + port=cfg.billing.port, + reload=cfg.server.is_develop, + log_level="info" if cfg.server.is_develop else "critical", + ssl_version=3, + ssl_certfile=cfg.billing.ssl_cert, + ssl_keyfile=cfg.billing.ssl_key ) + server = uvicorn.Server(server_cfg) + if cfg.billing.standalone: + await server.serve() + else: + while True: + pass - self.map_get.connect( - "allnet_ping", - "/naomitest.html", - controller="allnet", - action="handle_naomitest", - conditions=dict(method=["GET"]), - ) - self.map_post.connect( - "allnet_poweron", - "/sys/servlet/PowerOn", - controller="allnet", - action="handle_poweron", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "allnet_downloadorder", - "/sys/servlet/DownloadOrder", - controller="allnet", - action="handle_dlorder", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "allnet_loaderstaterecorder", - "/sys/servlet/LoaderStateRecorder", - controller="allnet", - action="handle_loaderstaterecorder", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "allnet_alive", - "/sys/servlet/Alive", - controller="allnet", - action="handle_alive", - conditions=dict(method=["POST"]), - ) - self.map_get.connect( - "allnet_alive", - "/sys/servlet/Alive", - controller="allnet", - action="handle_alive", - conditions=dict(method=["GET"]), - ) - self.map_post.connect( - "allnet_billing", - "/request", - controller="allnet", - action="handle_billing_request", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "allnet_billing", - "/request/", - controller="allnet", - action="handle_billing_request", - conditions=dict(method=["POST"]), - ) - # Maintain compatability - self.map_post.connect( - "mucha_boardauth", - "/mucha/boardauth.do", - controller="mucha", - action="handle_boardauth", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "mucha_updatacheck", - "/mucha/updatacheck.do", - controller="mucha", - action="handle_updatecheck", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "mucha_dlstate", - "/mucha/downloadstate.do", - controller="mucha", - action="handle_dlstate", - conditions=dict(method=["POST"]), - ) - - self.map_post.connect( - "mucha_boardauth", - "/mucha_front/boardauth.do", - controller="mucha", - action="handle_boardauth", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "mucha_updatacheck", - "/mucha_front/updatacheck.do", - controller="mucha", - action="handle_updatecheck", - conditions=dict(method=["POST"]), - ) - self.map_post.connect( - "mucha_dlstate", - "/mucha_front/downloadstate.do", - controller="mucha", - action="handle_dlstate", - conditions=dict(method=["POST"]), - ) - - for code, game in self.title.title_registry.items(): - get_matchers, post_matchers = game.get_endpoint_matchers() - - for m in get_matchers: - self.map_get.connect( - "title_get", - m[1], - controller="title", - action="render_GET", - title=code, - subaction=m[0], - conditions=dict(method=["GET"]), - requirements=m[2], - ) - - for m in post_matchers: - self.map_post.connect( - "title_post", - m[1], - controller="title", - action="render_POST", - title=code, - subaction=m[0], - conditions=dict(method=["POST"]), - requirements=m[2], - ) - - def render_GET(self, request: Request) -> bytes: - test = self.map_get.match(request.uri.decode()) - client_ip = Utils.get_ip_addr(request) - - if test is None: - self.logger.debug( - f"Unknown GET endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}" - ) - request.setResponseCode(404) - return b"Endpoint not found." - - return self.dispatch(test, request) - - def render_POST(self, request: Request) -> bytes: - test = self.map_post.match(request.uri.decode()) - client_ip = Utils.get_ip_addr(request) - - if test is None: - self.logger.debug( - f"Unknown POST endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}" - ) - request.setResponseCode(404) - return b"Endpoint not found." - - return self.dispatch(test, request) - - def dispatch(self, matcher: Dict, request: Request) -> bytes: - controller = getattr(self, matcher["controller"], None) - if controller is None: - self.logger.error( - f"Controller {matcher['controller']} not found via endpoint {request.uri.decode()}" - ) - request.setResponseCode(404) - return b"Endpoint not found." - - handler = getattr(controller, matcher["action"], None) - if handler is None: - self.logger.error( - f"Action {matcher['action']} not found in controller {matcher['controller']} via endpoint {request.uri.decode()}" - ) - request.setResponseCode(404) - return b"Endpoint not found." - - url_vars = matcher - url_vars.pop("controller") - url_vars.pop("action") - ret = handler(request, url_vars) - - if type(ret) == str: - return ret.encode() - - elif type(ret) == bytes or type(ret) == tuple: # allow for bytes or tuple (data, response code) responses - return ret - - elif ret is None: - self.logger.warning(f"None returned by controller for {request.uri.decode()} endpoint") - return b"" - - else: - self.logger.warning(f"Unknown data type returned by controller for {request.uri.decode()} endpoint") - return b"" +async def launcher(cfg: CoreConfig, ssl: bool) -> None: + AimedbServlette(cfg).start() + done, pending = await asyncio.wait( + [ + asyncio.create_task(launch_main(cfg, ssl)), + asyncio.create_task(launch_billing(cfg)), + ], + return_when=asyncio.FIRST_COMPLETED, + ) + + logging.getLogger("core").info("Shutdown") + for pending_task in pending: + pending_task.cancel("Another service died, server is shutting down") if __name__ == "__main__": - parser = argparse.ArgumentParser(description="ARTEMiS main entry point") + parser = argparse.ArgumentParser(description="Artemis main entry point") parser.add_argument( "--config", "-c", type=str, default="config", help="Configuration folder" ) + parser.add_argument( + "--port", "-p", type=int, default=0, help="Port override" + ) + parser.add_argument( + "--ssl", "-s", type=bool, help="Launch with SSL" + ) args = parser.parse_args() if not path.exists(f"{args.config}/core.yaml"): print( - f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml.\nDid you copy the example folder?" + f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml. Defaults will be used.\nDid you copy the example folder?" ) - exit(1) - + cfg: CoreConfig = CoreConfig() if path.exists(f"{args.config}/core.yaml"): cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml"))) - if not path.exists(cfg.server.log_dir): - mkdir(cfg.server.log_dir) + environ["ARTEMIS_CFG_DIR"] = args.config - if not access(cfg.server.log_dir, W_OK): - print( - f"Log directory {cfg.server.log_dir} NOT writable, please check permissions" - ) - exit(1) - - logger = logging.getLogger("core") - log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) - - fileHandler = TimedRotatingFileHandler( - "{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10 - ) - fileHandler.setFormatter(log_fmt) - - consoleHandler = logging.StreamHandler() - consoleHandler.setFormatter(log_fmt) - - logger.addHandler(fileHandler) - logger.addHandler(consoleHandler) - - log_lv = logging.DEBUG if cfg.server.is_develop else logging.INFO - logger.setLevel(log_lv) - coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str) - - if not cfg.aimedb.key: - logger.error("!!AIMEDB KEY BLANK, SET KEY IN CORE.YAML!!") - exit(1) - - logger.info( - 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.title.port}:interface={cfg.server.listen_address}" - title_https_server_str = f"ssl:{cfg.title.port_ssl}:interface={cfg.server.listen_address}:privateKey={cfg.title.ssl_key}:certKey={cfg.title.ssl_cert}" - adb_server_str = f"tcp:{cfg.aimedb.port}:interface={cfg.server.listen_address}" - frontend_server_str = ( - f"tcp:{cfg.frontend.port}:interface={cfg.server.listen_address}" - ) - - billing_server_str = f"tcp:{cfg.billing.port}:interface={cfg.server.listen_address}" - if cfg.server.is_develop: - billing_server_str = ( - f"ssl:{cfg.billing.port}:interface={cfg.server.listen_address}" - f":privateKey={cfg.billing.ssl_key}:certKey={cfg.billing.ssl_cert}" - ) - - dispatcher = HttpDispatcher(cfg, args.config) - - endpoints.serverFromString(reactor, allnet_server_str).listen( - server.Site(dispatcher) - ) - endpoints.serverFromString(reactor, adb_server_str).listen(AimedbFactory(cfg)) - - if cfg.frontend.enable: - endpoints.serverFromString(reactor, frontend_server_str).listen( - server.Site(FrontendServlet(cfg, args.config)) - ) - - if cfg.billing.port > 0: - endpoints.serverFromString(reactor, billing_server_str).listen( - server.Site(dispatcher) - ) - - if cfg.title.port > 0: - endpoints.serverFromString(reactor, title_server_str).listen( - server.Site(dispatcher) - ) - - if cfg.title.port_ssl > 0: - endpoints.serverFromString(reactor, title_https_server_str).listen( - server.Site(dispatcher) - ) - - if cfg.server.threading: - Thread(target=reactor.run, args=(False,)).start() - else: - reactor.run() + asyncio.run(launcher(cfg, args.ssl)) diff --git a/read.py b/read.py index fa34314..05bbd1d 100644 --- a/read.py +++ b/read.py @@ -1,4 +1,4 @@ -# vim: set fileencoding=utf-8 +#!/usr/bin/env python3 import argparse import re import os diff --git a/requirements.txt b/requirements.txt index c399e1f..fb27cb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ mypy wheel -twisted pytz pyyaml sqlalchemy==1.4.46 @@ -12,10 +11,11 @@ inflection coloredlogs pylibmc; platform_system != "Windows" wacky -Routes bcrypt jinja2 protobuf -autobahn pillow -pyjwt +pyjwt==2.8.0 +websockets +starlette +asyncio diff --git a/titles/chuni/air.py b/titles/chuni/air.py index b9bc1d3..094340c 100644 --- a/titles/chuni/air.py +++ b/titles/chuni/air.py @@ -11,7 +11,7 @@ class ChuniAir(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AIR - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.10.00" return ret diff --git a/titles/chuni/airplus.py b/titles/chuni/airplus.py index f0d8224..9b35c69 100644 --- a/titles/chuni/airplus.py +++ b/titles/chuni/airplus.py @@ -11,7 +11,7 @@ class ChuniAirPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AIR_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.15.00" return ret diff --git a/titles/chuni/amazon.py b/titles/chuni/amazon.py index b765c2f..84f5a12 100644 --- a/titles/chuni/amazon.py +++ b/titles/chuni/amazon.py @@ -13,7 +13,7 @@ class ChuniAmazon(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AMAZON - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.30.00" return ret diff --git a/titles/chuni/amazonplus.py b/titles/chuni/amazonplus.py index ea8d704..9ce13cf 100644 --- a/titles/chuni/amazonplus.py +++ b/titles/chuni/amazonplus.py @@ -13,7 +13,7 @@ class ChuniAmazonPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.35.00" return ret diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 8d22488..cffcb75 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -22,7 +22,7 @@ class ChuniBase: self.game = ChuniConstants.GAME_CODE self.version = ChuniConstants.VER_CHUNITHM - def handle_game_login_api_request(self, data: Dict) -> Dict: + async def handle_game_login_api_request(self, data: Dict) -> Dict: """ Handles the login bonus logic, required for the game because getUserLoginBonus gets called after getUserItem and therefore the @@ -119,11 +119,11 @@ class ChuniBase: return {"returnCode": 1} - def handle_game_logout_api_request(self, data: Dict) -> Dict: + async def handle_game_logout_api_request(self, data: Dict) -> Dict: # self.data.base.log_event("chuni", "logout", logging.INFO, {"version": self.version, "user": data["userId"]}) return {"returnCode": 1} - def handle_get_game_charge_api_request(self, data: Dict) -> Dict: + async def handle_get_game_charge_api_request(self, data: Dict) -> Dict: game_charge_list = self.data.static.get_enabled_charges(self.version) if game_charge_list is None or len(game_charge_list) == 0: @@ -145,7 +145,7 @@ class ChuniBase: ) return {"length": len(charges), "gameChargeList": charges} - def handle_get_game_event_api_request(self, data: Dict) -> Dict: + async def handle_get_game_event_api_request(self, data: Dict) -> Dict: game_events = self.data.static.get_enabled_events(self.version) if game_events is None or len(game_events) == 0: @@ -177,10 +177,10 @@ class ChuniBase: "gameEventList": event_list, } - def handle_get_game_idlist_api_request(self, data: Dict) -> Dict: + async def handle_get_game_idlist_api_request(self, data: Dict) -> Dict: return {"type": data["type"], "length": 0, "gameIdlistList": []} - def handle_get_game_message_api_request(self, data: Dict) -> Dict: + async def handle_get_game_message_api_request(self, data: Dict) -> Dict: return { "type": data["type"], "length": 1, @@ -193,14 +193,14 @@ class ChuniBase: }] } - def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: + async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: rankings = self.data.score.get_rankings(self.version) return {"type": data["type"], "gameRankingList": rankings} - def handle_get_game_sale_api_request(self, data: Dict) -> Dict: + async def handle_get_game_sale_api_request(self, data: Dict) -> Dict: return {"type": data["type"], "length": 0, "gameSaleList": []} - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # if reboot start/end time is not defined use the default behavior of being a few hours ago if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "": reboot_start = datetime.strftime( @@ -240,7 +240,7 @@ class ChuniBase: "isDumpUpload": "false", "isAou": "false", } - def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + async def handle_get_user_activity_api_request(self, data: Dict) -> Dict: user_activity_list = self.data.profile.get_profile_activity( data["userId"], data["kind"] ) @@ -261,7 +261,7 @@ class ChuniBase: "userActivityList": activity_list, } - def handle_get_user_character_api_request(self, data: Dict) -> Dict: + async def handle_get_user_character_api_request(self, data: Dict) -> Dict: characters = self.data.item.get_characters(data["userId"]) if characters is None: return { @@ -296,7 +296,7 @@ class ChuniBase: "userCharacterList": character_list, } - def handle_get_user_charge_api_request(self, data: Dict) -> Dict: + async def handle_get_user_charge_api_request(self, data: Dict) -> Dict: user_charge_list = self.data.profile.get_profile_charge(data["userId"]) charge_list = [] @@ -312,14 +312,14 @@ class ChuniBase: "userChargeList": charge_list, } - def handle_get_user_recent_player_api_request(self, data: Dict) -> Dict: + async def handle_get_user_recent_player_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 0, "userRecentPlayerList": [], # playUserId, playUserName, playDate, friendPoint } - def handle_get_user_course_api_request(self, data: Dict) -> Dict: + async def handle_get_user_course_api_request(self, data: Dict) -> Dict: user_course_list = self.data.score.get_courses(data["userId"]) if user_course_list is None: return { @@ -354,7 +354,7 @@ class ChuniBase: "userCourseList": course_list, } - def handle_get_user_data_api_request(self, data: Dict) -> Dict: + async def handle_get_user_data_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_data(data["userId"], self.version) if p is None: return {} @@ -366,7 +366,7 @@ class ChuniBase: return {"userId": data["userId"], "userData": profile} - def handle_get_user_data_ex_api_request(self, data: Dict) -> Dict: + async def handle_get_user_data_ex_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_data_ex(data["userId"], self.version) if p is None: return {} @@ -378,7 +378,7 @@ class ChuniBase: return {"userId": data["userId"], "userDataEx": profile} - def handle_get_user_duel_api_request(self, data: Dict) -> Dict: + async def handle_get_user_duel_api_request(self, data: Dict) -> Dict: user_duel_list = self.data.item.get_duels(data["userId"]) if user_duel_list is None: return {} @@ -396,7 +396,7 @@ class ChuniBase: "userDuelList": duel_list, } - def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_rival(data["rivalId"]) if p is None: return {} @@ -409,7 +409,7 @@ class ChuniBase: "userRivalData": userRivalData } - def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: rival_id = data["rivalId"] next_index = int(data["nextIndex"]) max_count = int(data["maxCount"]) @@ -462,7 +462,7 @@ class ChuniBase: return result - def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: + async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: user_fav_item_list = [] # still needs to be implemented on WebUI @@ -482,14 +482,14 @@ class ChuniBase: "userFavoriteItemList": user_fav_item_list, } - def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict: """ This is handled via the webui, which we don't have right now """ return {"userId": data["userId"], "length": 0, "userFavoriteMusicList": []} - def handle_get_user_item_api_request(self, data: Dict) -> Dict: + async def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(int(data["nextIndex"]) / 10000000000) next_idx = int(int(data["nextIndex"]) % 10000000000) user_item_list = self.data.item.get_items(data["userId"], kind) @@ -526,7 +526,7 @@ class ChuniBase: "userItemList": items, } - def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: + async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: user_id = data["userId"] user_login_bonus = self.data.item.get_all_login_bonus(user_id, self.version) # ignore the loginBonus request if its disabled in config @@ -552,7 +552,7 @@ class ChuniBase: "userLoginBonusList": user_login_list, } - def handle_get_user_map_api_request(self, data: Dict) -> Dict: + async def handle_get_user_map_api_request(self, data: Dict) -> Dict: user_map_list = self.data.item.get_maps(data["userId"]) if user_map_list is None: return {} @@ -570,7 +570,7 @@ class ChuniBase: "userMapList": map_list, } - def handle_get_user_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_music_api_request(self, data: Dict) -> Dict: music_detail = self.data.score.get_scores(data["userId"]) if music_detail is None: return { @@ -629,7 +629,7 @@ class ChuniBase: "userMusicList": song_list, # 240 } - def handle_get_user_option_api_request(self, data: Dict) -> Dict: + async def handle_get_user_option_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_option(data["userId"]) option = p._asdict() @@ -638,7 +638,7 @@ class ChuniBase: return {"userId": data["userId"], "userGameOption": option} - def handle_get_user_option_ex_api_request(self, data: Dict) -> Dict: + async def handle_get_user_option_ex_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_option_ex(data["userId"]) option = p._asdict() @@ -650,7 +650,7 @@ class ChuniBase: def read_wtf8(self, src): return bytes([ord(c) for c in src]).decode("utf-8") - def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_preview(data["userId"], self.version) if profile is None: return None @@ -692,7 +692,7 @@ class ChuniBase: "userNameEx": profile["userName"], } - def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: + async def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: recent_rating_list = self.data.profile.get_profile_recent_rating(data["userId"]) if recent_rating_list is None: return { @@ -707,7 +707,7 @@ class ChuniBase: "userRecentRatingList": recent_rating_list["recentRating"], } - def handle_get_user_region_api_request(self, data: Dict) -> Dict: + async def handle_get_user_region_api_request(self, data: Dict) -> Dict: # TODO: Region return { "userId": data["userId"], @@ -715,7 +715,7 @@ class ChuniBase: "userRegionList": [], } - def handle_get_user_team_api_request(self, data: Dict) -> Dict: + async def handle_get_user_team_api_request(self, data: Dict) -> Dict: # Default values team_id = 65535 team_name = self.game_cfg.team.team_name @@ -750,7 +750,7 @@ class ChuniBase: }, } - def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: + async def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 0, @@ -758,7 +758,7 @@ class ChuniBase: "teamCourseSettingList": [], } - def handle_get_team_course_setting_api_request_proto(self, data: Dict) -> Dict: + async def handle_get_team_course_setting_api_request_proto(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 1, @@ -782,7 +782,7 @@ class ChuniBase: ], } - def handle_get_team_course_rule_api_request(self, data: Dict) -> Dict: + async def handle_get_team_course_rule_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 0, @@ -790,7 +790,7 @@ class ChuniBase: "teamCourseRuleList": [] } - def handle_get_team_course_rule_api_request_proto(self, data: Dict) -> Dict: + async def handle_get_team_course_rule_api_request_proto(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 1, @@ -807,7 +807,7 @@ class ChuniBase: ], } - def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: upsert = data["upsertUserAll"] user_id = data["userId"] @@ -927,28 +927,28 @@ class ChuniBase: return {"returnCode": "1"} - def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: # add tickets after they got bought, this makes sure the tickets are # still valid after an unsuccessful logout self.data.profile.put_profile_charge(data["userId"], data["userCharge"]) return {"returnCode": "1"} - def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_upsert_client_error_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_error_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict: + async def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "userNetBattleData": {"recentNBSelectMusicList": []}, diff --git a/titles/chuni/crystal.py b/titles/chuni/crystal.py index a727ac3..9c08fd7 100644 --- a/titles/chuni/crystal.py +++ b/titles/chuni/crystal.py @@ -13,7 +13,7 @@ class ChuniCrystal(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.40.00" return ret diff --git a/titles/chuni/crystalplus.py b/titles/chuni/crystalplus.py index fbb3969..90a0479 100644 --- a/titles/chuni/crystalplus.py +++ b/titles/chuni/crystalplus.py @@ -13,7 +13,7 @@ class ChuniCrystalPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.45.00" return ret diff --git a/titles/chuni/index.py b/titles/chuni/index.py index fa8a394..b102d5e 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -1,4 +1,6 @@ -from twisted.web.http import Request +from starlette.requests import Request +from starlette.routing import Route +from starlette.responses import Response import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler import zlib @@ -33,7 +35,6 @@ from .newplus import ChuniNewPlus from .sun import ChuniSun from .sunplus import ChuniSunPlus - class ChuniServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: super().__init__(core_cfg, cfg_dir) @@ -124,15 +125,6 @@ class ChuniServlet(BaseServlet): f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}" ) - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [ - ("render_POST", "/{game}/{version}/ChuniServlet/{endpoint}", {}), - ("render_POST", "/{game}/{version}/ChuniServlet/MatchingServer/{endpoint}", {}) - ] - ) - @classmethod def is_game_enabled( cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str @@ -150,19 +142,25 @@ class ChuniServlet(BaseServlet): def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: - return (f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", self.core_cfg.title.hostname) + return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", self.core_cfg.server.hostname) - return (f"http://{self.core_cfg.title.hostname}/{game_code}/{game_ver}/", self.core_cfg.title.hostname) + return (f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", self.core_cfg.server.hostname) - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: - endpoint = matchers['endpoint'] - version = int(matchers['version']) - game_code = matchers['game'] + def get_routes(self) -> List[Route]: + return [ + Route("/{game:str}/{version:int}/ChuniServlet/{endpoint:str}", self.render_POST, methods=['POST']), + Route("/{game:str}/{version:int}/ChuniServlet/MatchingServer/{endpoint:str}", self.render_POST, methods=['POST']), + ] + + async def render_POST(self, request: Request) -> bytes: + endpoint: str = request.path_params.get('endpoint') + version: int = request.path_params.get('version') + game_code: str = request.path_params.get('game') if endpoint.lower() == "ping": - return zlib.compress(b'{"returnCode": "1"}') + return Response(zlib.compress(b'{"returnCode": "1"}')) - req_raw = request.content.getvalue() + req_raw = await request.body() encrtped = False internal_ver = 0 @@ -201,7 +199,7 @@ class ChuniServlet(BaseServlet): internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS elif game_code == "SDGS": # Int if version < 110: # SUPERSTAR - internal_ver = ChuniConstants.PARADISE + internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # FIXME: Not sure what was intended to go here? was just "PARADISE" elif version >= 110 and version < 115: # NEW internal_ver = ChuniConstants.VER_CHUNITHM_NEW elif version >= 115 and version < 120: # NEW PLUS!! @@ -216,20 +214,20 @@ class ChuniServlet(BaseServlet): # doing encrypted. The likelyhood of false positives is low but # technically not 0 if internal_ver < ChuniConstants.VER_CHUNITHM_NEW: - endpoint = request.getHeader("User-Agent").split("#")[0] + endpoint = request.headers.get("User-Agent").split("#")[0] else: if internal_ver not in self.hash_table: self.logger.error( f"v{version} does not support encryption or no keys entered" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) elif endpoint.lower() not in self.hash_table[internal_ver]: self.logger.error( f"No hash found for v{version} endpoint {endpoint}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) endpoint = self.hash_table[internal_ver][endpoint.lower()] @@ -246,7 +244,7 @@ class ChuniServlet(BaseServlet): self.logger.error( f"Failed to decrypt v{version} request to {endpoint} -> {e}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) encrtped = True @@ -258,7 +256,7 @@ class ChuniServlet(BaseServlet): self.logger.error( f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) try: unzip = zlib.decompress(req_raw) @@ -267,7 +265,7 @@ class ChuniServlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return b"" + return Response(zlib.compress(b'{"stat": "0"}')) req_data = json.loads(unzip) @@ -285,11 +283,11 @@ class ChuniServlet(BaseServlet): else: try: handler = getattr(handler_cls, func_to_find) - resp = handler(req_data) + resp = await handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) if resp == None: resp = {"returnCode": 1} @@ -299,7 +297,7 @@ class ChuniServlet(BaseServlet): zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) if not encrtped: - return zipped + return Response(zipped) padded = pad(zipped, 16) @@ -309,4 +307,4 @@ class ChuniServlet(BaseServlet): bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), ) - return crypt.encrypt(padded) \ No newline at end of file + return Response(crypt.encrypt(padded)) \ No newline at end of file diff --git a/titles/chuni/new.py b/titles/chuni/new.py index d8a71b1..9902906 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -33,7 +33,7 @@ class ChuniNew(ChuniBase): if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: return "215" - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # use UTC time and convert it to JST time by adding +9 # matching therefore starts one hour before and lasts for 8 hours match_start = datetime.strftime( @@ -82,26 +82,26 @@ class ChuniNew(ChuniBase): "matchErrorLimit": self.game_cfg.matching.match_error_limit, "romVersion": self.game_cfg.version.version(self.version)["rom"], "dataVersion": self.game_cfg.version.version(self.version)["data"], - "matchingUri": f"http://{self.core_cfg.title.hostname}:{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", - "matchingUriX": f"http://{self.core_cfg.title.hostname}:{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", + "matchingUri": f"http://{self.core_cfg.server.hostname}:{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", + "matchingUriX": f"http://{self.core_cfg.server.hostname}:{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", # might be really important for online battle to connect the cabs via UDP port 50201 - "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", - "reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", + "udpHolePunchUri": f"http://{self.core_cfg.server.hostname}:{self.core_cfg.server.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", + "reflectorUri": f"http://{self.core_cfg.server.hostname}:{self.core_cfg.server.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/", }, "isDumpUpload": False, "isAou": False, } - def handle_remove_token_api_request(self, data: Dict) -> Dict: + async def handle_remove_token_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_delete_token_api_request(self, data: Dict) -> Dict: + async def handle_delete_token_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_create_token_api_request(self, data: Dict) -> Dict: + async def handle_create_token_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} - def handle_get_user_map_area_api_request(self, data: Dict) -> Dict: + async def handle_get_user_map_area_api_request(self, data: Dict) -> Dict: user_map_areas = self.data.item.get_map_areas(data["userId"]) map_areas = [] @@ -113,10 +113,10 @@ class ChuniNew(ChuniBase): return {"userId": data["userId"], "userMapAreaList": map_areas} - def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict: + async def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "symbolCharInfoList": []} - def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_preview(data["userId"], self.version) if profile is None: return None @@ -164,7 +164,7 @@ class ChuniNew(ChuniBase): } return data1 - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_data(data["userId"], self.version) if p is None: return {} @@ -177,13 +177,13 @@ class ChuniNew(ChuniBase): "isLogin": False, } - def handle_printer_login_api_request(self, data: Dict) -> Dict: + async def handle_printer_login_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} - def handle_printer_logout_api_request(self, data: Dict) -> Dict: + async def handle_printer_logout_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} - def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: + async def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: """ returns all current active banners (gachas) """ @@ -213,7 +213,7 @@ class ChuniNew(ChuniBase): "registIdList": [], } - def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: + async def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: """ returns all valid cards for a given gachaId """ @@ -237,7 +237,7 @@ class ChuniNew(ChuniBase): "ssrBookCalcList": [], } - def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_data(data["userId"], self.version) if p is None: return {} @@ -262,7 +262,7 @@ class ChuniNew(ChuniBase): ], } - def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: + async def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: user_gachas = self.data.item.get_user_gachas(data["userId"]) if user_gachas is None: return {"userId": data["userId"], "length": 0, "userGachaList": []} @@ -281,7 +281,7 @@ class ChuniNew(ChuniBase): "userGachaList": user_gacha_list, } - def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict: + async def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict: user_print_list = self.data.item.get_user_print_states( data["userId"], has_completed=True ) @@ -316,7 +316,7 @@ class ChuniNew(ChuniBase): "userPrintedCardList": print_list, } - def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + async def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict: user_id = data["userId"] user_print_states = self.data.item.get_user_print_states( @@ -338,13 +338,13 @@ class ChuniNew(ChuniBase): "userCardPrintStateList": card_print_state_list, } - def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: return super().handle_get_user_character_api_request(data) - def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: return super().handle_get_user_item_api_request(data) - def handle_roll_gacha_api_request(self, data: Dict) -> Dict: + async def handle_roll_gacha_api_request(self, data: Dict) -> Dict: """ Handle a gacha roll API request, with: gachaId: the gachaId where the cards should be pulled from @@ -386,7 +386,7 @@ class ChuniNew(ChuniBase): return {"length": len(rolled_cards), "gameGachaCardList": rolled_cards} - def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict: upsert = data["cmUpsertUserGacha"] user_id = data["userId"] place_id = data["placeId"] @@ -441,7 +441,7 @@ class ChuniNew(ChuniBase): "userCardPrintStateList": card_print_state_list, } - def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: return { "returnCode": 1, "orderId": 0, @@ -449,7 +449,7 @@ class ChuniNew(ChuniBase): "apiName": "CMUpsertUserPrintlogApi", } - def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: user_print_detail = data["userPrintDetail"] user_id = data["userId"] @@ -474,7 +474,7 @@ class ChuniNew(ChuniBase): "apiName": "CMUpsertUserPrintApi", } - def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict: upsert = data["userCardPrintState"] user_id = data["userId"] place_id = data["placeId"] @@ -491,7 +491,7 @@ class ChuniNew(ChuniBase): return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"} - def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict: order_ids = data["orderIdList"] user_id = data["userId"] @@ -501,11 +501,11 @@ class ChuniNew(ChuniBase): return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"} - def handle_ping_request(self, data: Dict) -> Dict: + async def handle_ping_request(self, data: Dict) -> Dict: # matchmaking ping request return {"returnCode": "1"} - def handle_begin_matching_api_request(self, data: Dict) -> Dict: + async def handle_begin_matching_api_request(self, data: Dict) -> Dict: room_id = 1 # check if there is a free matching room matching_room = self.data.item.get_oldest_free_matching(self.version) @@ -554,7 +554,7 @@ class ChuniNew(ChuniBase): return {"roomId": 1, "matchingWaitState": matching_wait} - def handle_end_matching_api_request(self, data: Dict) -> Dict: + async def handle_end_matching_api_request(self, data: Dict) -> Dict: matching_room = self.data.item.get_matching(self.version, data["roomId"]) members = matching_room["matchingMemberInfoList"] @@ -579,10 +579,10 @@ class ChuniNew(ChuniBase): # no idea, maybe to differentiate between CPUs and real players? "matchingMemberRoleList": role_list, # TCP/UDP connection? - "reflectorUri": f"{self.core_cfg.title.hostname}", + "reflectorUri": f"{self.core_cfg.server.hostname}", } - def handle_remove_matching_member_api_request(self, data: Dict) -> Dict: + async def handle_remove_matching_member_api_request(self, data: Dict) -> Dict: # get all matching rooms, because Chuni only returns the userId # not the actual roomId matching_rooms = self.data.item.get_all_matchings(self.version) @@ -612,7 +612,7 @@ class ChuniNew(ChuniBase): return {"returnCode": "1"} - def handle_get_matching_state_api_request(self, data: Dict) -> Dict: + async def handle_get_matching_state_api_request(self, data: Dict) -> Dict: polling_interval = 1 # get the current active room matching_room = self.data.item.get_matching(self.version, data["roomId"]) diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index fa642d3..84467fb 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -11,8 +11,8 @@ class ChuniNewPlus(ChuniNew): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_NEW_PLUS - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = super().handle_cm_get_user_preview_api_request(data) + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) # hardcode lastDataVersion for CardMaker 1.35 A028 user_data["lastDataVersion"] = "2.05.00" diff --git a/titles/chuni/paradise.py b/titles/chuni/paradise.py index 19155d6..88ed91c 100644 --- a/titles/chuni/paradise.py +++ b/titles/chuni/paradise.py @@ -13,7 +13,7 @@ class ChuniParadise(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_PARADISE - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.50.00" return ret diff --git a/titles/chuni/plus.py b/titles/chuni/plus.py index 62d9e0d..094be06 100644 --- a/titles/chuni/plus.py +++ b/titles/chuni/plus.py @@ -11,7 +11,7 @@ class ChuniPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.05.00" return ret diff --git a/titles/chuni/star.py b/titles/chuni/star.py index 4c071e8..247934f 100644 --- a/titles/chuni/star.py +++ b/titles/chuni/star.py @@ -11,7 +11,7 @@ class ChuniStar(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_STAR - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.20.00" return ret diff --git a/titles/chuni/starplus.py b/titles/chuni/starplus.py index 8c24cc8..616c3c6 100644 --- a/titles/chuni/starplus.py +++ b/titles/chuni/starplus.py @@ -11,7 +11,7 @@ class ChuniStarPlus(ChuniBase): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_STAR_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.25.00" return ret diff --git a/titles/chuni/sun.py b/titles/chuni/sun.py index bfefd97..94dc1eb 100644 --- a/titles/chuni/sun.py +++ b/titles/chuni/sun.py @@ -11,8 +11,8 @@ class ChuniSun(ChuniNewPlus): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_SUN - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = super().handle_cm_get_user_preview_api_request(data) + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) # hardcode lastDataVersion for CardMaker 1.35 A032 user_data["lastDataVersion"] = "2.10.00" diff --git a/titles/chuni/sunplus.py b/titles/chuni/sunplus.py index fc86314..1f3f271 100644 --- a/titles/chuni/sunplus.py +++ b/titles/chuni/sunplus.py @@ -11,8 +11,8 @@ class ChuniSunPlus(ChuniSun): super().__init__(core_cfg, game_cfg) self.version = ChuniConstants.VER_CHUNITHM_SUN_PLUS - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = super().handle_cm_get_user_preview_api_request(data) + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) # I don't know if lastDataVersion is going to matter, I don't think CardMaker 1.35 works this far up user_data["lastDataVersion"] = "2.15.00" diff --git a/titles/cm/base.py b/titles/cm/base.py index b911983..e4fd1cb 100644 --- a/titles/cm/base.py +++ b/titles/cm/base.py @@ -29,11 +29,11 @@ class CardMakerBase: def _parse_int_ver(version: str) -> str: return version.replace(".", "")[:3] - def handle_get_game_connect_api_request(self, data: Dict) -> Dict: + async def handle_get_game_connect_api_request(self, data: Dict) -> Dict: if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: - uri = f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}" + uri = f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}" else: - uri = f"http://{self.core_cfg.title.hostname}" + uri = f"http://{self.core_cfg.server.hostname}" # grab the dict with all games version numbers from user config games_ver = self.game_cfg.version.version(self.version) @@ -62,7 +62,7 @@ class CardMakerBase: ], } - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # if reboot start/end time is not defined use the default behavior of being a few hours ago if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "": reboot_start = datetime.strftime( @@ -110,11 +110,11 @@ class CardMakerBase: "isAou": False, } - def handle_get_client_bookkeeping_api_request(self, data: Dict) -> Dict: + async def handle_get_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"placeId": data["placeId"], "length": 0, "clientBookkeepingList": []} - def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} - def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"} diff --git a/titles/cm/cm135.py b/titles/cm/cm135.py index e134974..5bc5460 100644 --- a/titles/cm/cm135.py +++ b/titles/cm/cm135.py @@ -12,7 +12,7 @@ class CardMaker135(CardMakerBase): super().__init__(core_cfg, game_cfg) self.version = CardMakerConstants.VER_CARD_MAKER_135 - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.35.00" return ret diff --git a/titles/cm/index.py b/titles/cm/index.py index 489b846..20d1e94 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -5,10 +5,11 @@ import string import logging import coloredlogs import zlib - +from starlette.routing import Route +from starlette.responses import Response +from starlette.requests import Request from os import path -from typing import Tuple, List, Dict -from twisted.web.http import Request +from typing import List from logging.handlers import TimedRotatingFileHandler from core.config import CoreConfig @@ -19,7 +20,6 @@ from .const import CardMakerConstants from .base import CardMakerBase from .cm135 import CardMaker135 - class CardMakerServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: super().__init__(core_cfg, cfg_dir) @@ -72,16 +72,15 @@ class CardMakerServlet(BaseServlet): return True - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [("render_POST", "/SDED/{version}/{endpoint}", {})] - ) - - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: - version = int(matchers['version']) - endpoint = matchers['endpoint'] - req_raw = request.content.getvalue() + def get_routes(self) -> List[Route]: + return [ + Route("/SDED/{version:int}/{endpoint:str}", self.render_POST) + ] + + async def render_POST(self, request: Request) -> bytes: + version: int = request.path_params.get('version') + endpoint: str = request.path_params.get('endpoint') + req_raw = await request.body() internal_ver = 0 client_ip = Utils.get_ip_addr(request) @@ -103,7 +102,7 @@ class CardMakerServlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) req_data = json.loads(unzip) @@ -114,7 +113,7 @@ class CardMakerServlet(BaseServlet): if not hasattr(self.versions[internal_ver], func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") - return zlib.compress(b'{"returnCode": 1}') + return Response(zlib.compress(b'{"returnCode": 1}')) try: handler = getattr(self.versions[internal_ver], func_to_find) @@ -123,11 +122,11 @@ class CardMakerServlet(BaseServlet): except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") raise - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) if resp is None: resp = {"returnCode": 1} self.logger.debug(f"Response {resp}") - return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + return Response(zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))) diff --git a/titles/cxb/base.py b/titles/cxb/base.py index fe583e6..691897d 100644 --- a/titles/cxb/base.py +++ b/titles/cxb/base.py @@ -28,13 +28,13 @@ class CxbBase: return [] - def handle_action_rpreq_request(self, data: Dict) -> Dict: + async def handle_action_rpreq_request(self, data: Dict) -> Dict: return {} - def handle_action_hitreq_request(self, data: Dict) -> Dict: + async def handle_action_hitreq_request(self, data: Dict) -> Dict: return {"data": []} - def handle_auth_usercheck_request(self, data: Dict) -> Dict: + async def handle_auth_usercheck_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_index( 0, data["usercheck"]["authid"], self.version ) @@ -45,11 +45,11 @@ class CxbBase: self.logger.info(f"No profile for aime id {data['usercheck']['authid']}") return {"exist": "false", "logout": "true"} - def handle_auth_entry_request(self, data: Dict) -> Dict: + async def handle_auth_entry_request(self, data: Dict) -> Dict: self.logger.info(f"New profile for {data['entry']['authid']}") return {"token": data["entry"]["authid"], "uid": data["entry"]["authid"]} - def handle_auth_login_request(self, data: Dict) -> Dict: + async def handle_auth_login_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_index( 0, data["login"]["authid"], self.version ) @@ -198,7 +198,7 @@ class CxbBase: ).decode("utf-8") ) - def handle_action_loadrange_request(self, data: Dict) -> Dict: + async def handle_action_loadrange_request(self, data: Dict) -> Dict: range_start = data["loadrange"]["range"][0] range_end = data["loadrange"]["range"][1] uid = data["loadrange"]["uid"] @@ -282,7 +282,7 @@ class CxbBase: return {"index": index, "data": data1, "version": versionindex} - def handle_action_saveindex_request(self, data: Dict) -> Dict: + async def handle_action_saveindex_request(self, data: Dict) -> Dict: save_data = data["saveindex"] try: @@ -443,7 +443,7 @@ class CxbBase: i += 1 return {} - def handle_action_sprankreq_request(self, data: Dict) -> Dict: + async def handle_action_sprankreq_request(self, data: Dict) -> Dict: uid = data["sprankreq"]["uid"] self.logger.info(f"Get best rankings for {uid}") p = self.data.score.get_best_rankings(uid) @@ -475,16 +475,16 @@ class CxbBase: "rankx": [1, 1, 1], } - def handle_action_getadv_request(self, data: Dict) -> Dict: + async def handle_action_getadv_request(self, data: Dict) -> Dict: return {"data": [{"r": "1", "i": "100300", "c": "20"}]} - def handle_action_getmsg_request(self, data: Dict) -> Dict: + async def handle_action_getmsg_request(self, data: Dict) -> Dict: return {"msgs": []} - def handle_auth_logout_request(self, data: Dict) -> Dict: + async def handle_auth_logout_request(self, data: Dict) -> Dict: return {"auth": True} - def handle_action_rankreg_request(self, data: Dict) -> Dict: + async def handle_action_rankreg_request(self, data: Dict) -> Dict: uid = data["rankreg"]["uid"] self.logger.info(f"Put {len(data['rankreg']['data'])} rankings for {uid}") @@ -527,7 +527,7 @@ class CxbBase: ) return {} - def handle_action_addenergy_request(self, data: Dict) -> Dict: + async def handle_action_addenergy_request(self, data: Dict) -> Dict: uid = data["addenergy"]["uid"] self.logger.info(f"Add energy to user {uid}") profile = self.data.profile.get_profile_index(0, uid, self.version) @@ -570,10 +570,10 @@ class CxbBase: ) return array[0] - def handle_action_eventreq_request(self, data: Dict) -> Dict: + async def handle_action_eventreq_request(self, data: Dict) -> Dict: self.logger.info(data) return {"eventreq": ""} - def handle_action_stampreq_request(self, data: Dict) -> Dict: + async def handle_action_stampreq_request(self, data: Dict) -> Dict: self.logger.info(data) return {"stampreq": ""} \ No newline at end of file diff --git a/titles/cxb/index.py b/titles/cxb/index.py index 04bbc92..3f719ff 100644 --- a/titles/cxb/index.py +++ b/titles/cxb/index.py @@ -1,4 +1,6 @@ -from twisted.web.http import Request +from starlette.requests import Request +from starlette.routing import Route +from starlette.responses import Response, PlainTextResponse, JSONResponse import traceback import sys import yaml @@ -62,6 +64,14 @@ class CxbServlet(BaseServlet): CxbRevSunriseS2(core_cfg, self.game_cfg), ] + def get_routes(self) -> List[Route]: + return [ + Route("/data", self.handle_data, methods=['POST']), + Route("/action", self.handle_action, methods=['POST']), + Route("/v2/action", self.handle_action, methods=['POST']), + Route("/auth", self.handle_auth, methods=['POST']), + ] + @classmethod def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: game_cfg = CxbConfig() @@ -88,26 +98,12 @@ class CxbServlet(BaseServlet): t_port = f":{title_port_int}" if title_port_int and not self.core_cfg.server.is_using_proxy else "" return ( - f"{proto}://{self.core_cfg.title.hostname}{t_port}", + f"{proto}://{self.core_cfg.server.hostname}{t_port}", "", ) - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [ - ("handle_data", "/data", {}), - ("handle_action", "/action", {}), - ("handle_action", "/v2/action", {}), - ("handle_auth", "/auth", {}), - ] - ) - - def preprocess(self, req: Request) -> Dict: - try: - req_bytes = req.content.getvalue() - except: - req_bytes = req.content.read() # Can we just use this one? + async def preprocess(self, req: Request) -> Dict: + req_bytes = await req.body() try: req_json: Dict = json.loads(req_bytes) @@ -126,8 +122,8 @@ class CxbServlet(BaseServlet): return req_json - def handle_data(self, request: Request, game_code: str, matchers: Dict) -> bytes: - req_json = self.preprocess(request) + async def handle_data(self, request: Request) -> bytes: + req_json = await self.preprocess(request) func_to_find = "handle_data_" version_string = "Base" internal_ver = 0 @@ -135,7 +131,7 @@ class CxbServlet(BaseServlet): if req_json == {}: self.logger.warning(f"Empty json request to /data") - return b"" + return Response() subcmd = list(req_json.keys())[0] if subcmd == "dldate": @@ -145,14 +141,14 @@ class CxbServlet(BaseServlet): or "filetype" not in req_json["dldate"] ): self.logger.warning(f"Malformed dldate request: {req_json}") - return b"" + return Response() filetype = req_json["dldate"]["filetype"] filetype_split = filetype.split("/") if len(filetype_split) < 2 or not filetype_split[0].isnumeric(): self.logger.warning(f"Malformed dldate request: {req_json}") - return b"" + return Response() version = int(filetype_split[0]) filename = filetype_split[len(filetype_split) - 1] @@ -184,7 +180,7 @@ class CxbServlet(BaseServlet): if not hasattr(self.versions[internal_ver], func_to_find): self.logger.warn(f"{version_string} has no handler for filetype {filetype} / {func_to_find}") - return({"data":""}) + return JSONResponse({"data":""}) self.logger.info(f"{version_string} request for filetype {filetype}") self.logger.debug(req_json) @@ -192,7 +188,7 @@ class CxbServlet(BaseServlet): handler = getattr(self.versions[internal_ver], func_to_find) try: - resp = handler(req_json) + resp = await handler(req_json) except Exception as e: self.logger.error(f"Error handling request for file {filetype} - {e}") @@ -201,19 +197,19 @@ class CxbServlet(BaseServlet): traceback.print_exception(tp, val, tb, limit=1) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: traceback.print_exception(tp, val, tb, limit=1, file=f) - return "" + return Response() self.logger.debug(f"{version_string} Response {resp}") - return json.dumps(resp, ensure_ascii=False).encode("utf-8") + return JSONResponse(resp, ensure_ascii=False) - def handle_action(self, request: Request, game_code: str, matchers: Dict) -> bytes: - req_json = self.preprocess(request) + async def handle_action(self, request: Request) -> bytes: + req_json = await self.preprocess(request) subcmd = list(req_json.keys())[0] func_to_find = f"handle_action_{subcmd}_request" if not hasattr(self.versions[0], func_to_find): self.logger.warn(f"No handler for action {subcmd} request") - return "" + return Response() self.logger.info(f"Action {subcmd} Request") self.logger.debug(req_json) @@ -221,7 +217,7 @@ class CxbServlet(BaseServlet): handler = getattr(self.versions[0], func_to_find) try: - resp = handler(req_json) + resp = await handler(req_json) except Exception as e: self.logger.error(f"Error handling action {subcmd} request - {e}") @@ -230,19 +226,19 @@ class CxbServlet(BaseServlet): traceback.print_exception(tp, val, tb, limit=1) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: traceback.print_exception(tp, val, tb, limit=1, file=f) - return "" + return Response() self.logger.debug(f"Response {resp}") - return json.dumps(resp, ensure_ascii=False).encode("utf-8") + return JSONResponse(resp) - def handle_auth(self, request: Request, game_code: str, matchers: Dict) -> bytes: - req_json = self.preprocess(request) + async def handle_auth(self, request: Request) -> bytes: + req_json = await self.preprocess(request) subcmd = list(req_json.keys())[0] func_to_find = f"handle_auth_{subcmd}_request" if not hasattr(self.versions[0], func_to_find): self.logger.warn(f"No handler for auth {subcmd} request") - return "" + return Response() self.logger.info(f"Action {subcmd} Request") self.logger.debug(req_json) @@ -250,7 +246,7 @@ class CxbServlet(BaseServlet): handler = getattr(self.versions[0], func_to_find) try: - resp = handler(req_json) + resp = await handler(req_json) except Exception as e: self.logger.error(f"Error handling auth {subcmd} request - {e}") @@ -259,7 +255,7 @@ class CxbServlet(BaseServlet): traceback.print_exception(tp, val, tb, limit=1) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: traceback.print_exception(tp, val, tb, limit=1, file=f) - return "" + return Response() self.logger.debug(f"Response {resp}") - return json.dumps(resp, ensure_ascii=False).encode("utf-8") + return JSONResponse(resp) diff --git a/titles/cxb/rev.py b/titles/cxb/rev.py index e311a1e..1a7c5ac 100644 --- a/titles/cxb/rev.py +++ b/titles/cxb/rev.py @@ -17,10 +17,10 @@ class CxbRev(CxbBase): super().__init__(cfg, game_cfg) self.version = CxbConstants.VER_CROSSBEATS_REV - def handle_data_path_list_request(self, data: Dict) -> Dict: + async def handle_data_path_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_putlog_request(self, data: Dict) -> Dict: + async def handle_data_putlog_request(self, data: Dict) -> Dict: if data["putlog"]["type"] == "ResultLog": score_data = json.loads(data["putlog"]["data"]) userid = score_data["usid"] @@ -45,7 +45,7 @@ class CxbRev(CxbBase): return {"data": True} @cached(lifetime=86400) - def handle_data_music_list_request(self, data: Dict) -> Dict: + async def handle_data_music_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss/MusicArchiveList.csv") as music: lines = music.readlines() @@ -56,7 +56,7 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_icon_request(self, data: Dict) -> Dict: + async def handle_data_item_list_icon_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListIcon\r\n" with open( r"titles/cxb/data/rss/Item/ItemArchiveList_Icon.csv", encoding="utf-8" @@ -67,7 +67,7 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_skin_notes_request(self, data: Dict) -> Dict: + async def handle_data_item_list_skin_notes_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListSkinNotes\r\n" with open( r"titles/cxb/data/rss/Item/ItemArchiveList_SkinNotes.csv", encoding="utf-8" @@ -78,7 +78,7 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_skin_effect_request(self, data: Dict) -> Dict: + async def handle_data_item_list_skin_effect_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListSkinEffect\r\n" with open( r"titles/cxb/data/rss/Item/ItemArchiveList_SkinEffect.csv", encoding="utf-8" @@ -89,7 +89,7 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_skin_bg_request(self, data: Dict) -> Dict: + async def handle_data_item_list_skin_bg_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListSkinBg\r\n" with open( r"titles/cxb/data/rss/Item/ItemArchiveList_SkinBg.csv", encoding="utf-8" @@ -100,7 +100,7 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_title_request(self, data: Dict) -> Dict: + async def handle_data_item_list_title_request(self, data: Dict) -> Dict: ret_str = "\r\n#ItemListTitle\r\n" with open( r"titles/cxb/data/rss/Item/ItemList_Title.csv", encoding="shift-jis" @@ -111,7 +111,7 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_shop_list_music_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_music_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListMusic\r\n" with open( r"titles/cxb/data/rss/Shop/ShopList_Music.csv", encoding="shift-jis" @@ -122,7 +122,7 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_shop_list_icon_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_icon_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListIcon\r\n" with open( r"titles/cxb/data/rss/Shop/ShopList_Icon.csv", encoding="shift-jis" @@ -133,7 +133,7 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_shop_list_title_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_title_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListTitle\r\n" with open( r"titles/cxb/data/rss/Shop/ShopList_Title.csv", encoding="shift-jis" @@ -143,17 +143,17 @@ class CxbRev(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_shop_list_skin_hud_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_skin_hud_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_shop_list_skin_arrow_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_skin_arrow_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_shop_list_skin_hit_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_skin_hit_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_shop_list_sale_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_sale_request(self, data: Dict) -> Dict: ret_str = "\r\n#ShopListSale\r\n" with open( r"titles/cxb/data/rss/Shop/ShopList_Sale.csv", encoding="shift-jis" @@ -163,11 +163,11 @@ class CxbRev(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: + async def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_exxxxx_request(self, data: Dict) -> Dict: + async def handle_data_exxxxx_request(self, data: Dict) -> Dict: extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" with open( @@ -178,14 +178,14 @@ class CxbRev(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: + async def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_free_coupon_request(self, data: Dict) -> Dict: + async def handle_data_free_coupon_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_news_list_request(self, data: Dict) -> Dict: + async def handle_data_news_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss/NewsList.csv", encoding="UTF-8") as news: lines = news.readlines() @@ -193,11 +193,11 @@ class CxbRev(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_tips_request(self, data: Dict) -> Dict: + async def handle_data_tips_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_license_request(self, data: Dict) -> Dict: + async def handle_data_license_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss/License_Offline.csv", encoding="UTF-8") as lic: lines = lic.readlines() @@ -206,7 +206,7 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_course_list_request(self, data: Dict) -> Dict: + async def handle_data_course_list_request(self, data: Dict) -> Dict: ret_str = "" with open( r"titles/cxb/data/rss/Course/CourseList.csv", encoding="UTF-8" @@ -217,7 +217,7 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_csxxxx_request(self, data: Dict) -> Dict: + async def handle_data_csxxxx_request(self, data: Dict) -> Dict: # Removed the CSVs since the format isnt quite right extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" @@ -230,7 +230,7 @@ class CxbRev(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_mission_list_request(self, data: Dict) -> Dict: + async def handle_data_mission_list_request(self, data: Dict) -> Dict: ret_str = "" with open( r"titles/cxb/data/rss/MissionList.csv", encoding="shift-jis" @@ -240,14 +240,14 @@ class CxbRev(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_mission_bonus_request(self, data: Dict) -> Dict: + async def handle_data_mission_bonus_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: + async def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_event_list_request(self, data: Dict) -> Dict: + async def handle_data_event_list_request(self, data: Dict) -> Dict: ret_str = "" with open( r"titles/cxb/data/rss/Event/EventArchiveList.csv", encoding="shift-jis" @@ -257,39 +257,39 @@ class CxbRev(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_event_music_list_request(self, data: Dict) -> Dict: + async def handle_data_event_music_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_mission_list_request(self, data: Dict) -> Dict: + async def handle_data_event_mission_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_achievement_single_high_score_list_request( + async def handle_data_event_achievement_single_high_score_list_request( self, data: Dict ) -> Dict: return {"data": ""} - def handle_data_event_achievement_single_accumulation_request( + async def handle_data_event_achievement_single_accumulation_request( self, data: Dict ) -> Dict: return {"data": ""} - def handle_data_event_ranking_high_score_list_request(self, data: Dict) -> Dict: + async def handle_data_event_ranking_high_score_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_ranking_accumulation_list_request(self, data: Dict) -> Dict: + async def handle_data_event_ranking_accumulation_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_ranking_stamp_list_request(self, data: Dict) -> Dict: + async def handle_data_event_ranking_stamp_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_ranking_store_list_request(self, data: Dict) -> Dict: + async def handle_data_event_ranking_store_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_ranking_area_list_request(self, data: Dict) -> Dict: + async def handle_data_event_ranking_area_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: + async def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: ret_str = "" with open( r"titles/cxb/data/rss/Event/EventStampList.csv", encoding="shift-jis" @@ -299,8 +299,8 @@ class CxbRev(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: + async def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} - def handle_data_server_state_request(self, data: Dict) -> Dict: + async def handle_data_server_state_request(self, data: Dict) -> Dict: return {"data": True} diff --git a/titles/cxb/rss1.py b/titles/cxb/rss1.py index fe43e42..cce0315 100644 --- a/titles/cxb/rss1.py +++ b/titles/cxb/rss1.py @@ -17,11 +17,11 @@ class CxbRevSunriseS1(CxbBase): super().__init__(cfg, game_cfg) self.version = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S1 - def handle_data_path_list_request(self, data: Dict) -> Dict: + async def handle_data_path_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_music_list_request(self, data: Dict) -> Dict: + async def handle_data_music_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss1/MusicArchiveList.csv") as music: lines = music.readlines() @@ -32,7 +32,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_detail_request(self, data: Dict) -> Dict: + async def handle_data_item_list_detail_request(self, data: Dict) -> Dict: # ItemListIcon load ret_str = "#ItemListIcon\r\n" with open( @@ -54,7 +54,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: # ShopListIcon load ret_str = "#ShopListIcon\r\n" with open( @@ -119,26 +119,26 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: + async def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_exxxxx_request(self, data: Dict) -> Dict: + async def handle_data_exxxxx_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: + async def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: + async def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_oexxxx_request(self, data: Dict) -> Dict: + async def handle_data_oexxxx_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_free_coupon_request(self, data: Dict) -> Dict: + async def handle_data_free_coupon_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_news_list_request(self, data: Dict) -> Dict: + async def handle_data_news_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss1/NewsList.csv", encoding="UTF-8") as news: lines = news.readlines() @@ -146,14 +146,14 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_tips_request(self, data: Dict) -> Dict: + async def handle_data_tips_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_release_info_list_request(self, data: Dict) -> Dict: + async def handle_data_release_info_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_random_music_list_request(self, data: Dict) -> Dict: + async def handle_data_random_music_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss1/MusicArchiveList.csv") as music: lines = music.readlines() @@ -167,7 +167,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_license_request(self, data: Dict) -> Dict: + async def handle_data_license_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss1/License.csv", encoding="UTF-8") as licenses: lines = licenses.readlines() @@ -176,7 +176,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_course_list_request(self, data: Dict) -> Dict: + async def handle_data_course_list_request(self, data: Dict) -> Dict: ret_str = "" with open( r"titles/cxb/data/rss1/Course/CourseList.csv", encoding="UTF-8" @@ -187,7 +187,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_csxxxx_request(self, data: Dict) -> Dict: + async def handle_data_csxxxx_request(self, data: Dict) -> Dict: extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" with open( @@ -198,16 +198,16 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_mission_list_request(self, data: Dict) -> Dict: + async def handle_data_mission_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_mission_bonus_request(self, data: Dict) -> Dict: + async def handle_data_mission_bonus_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: + async def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_partner_list_request(self, data: Dict) -> Dict: + async def handle_data_partner_list_request(self, data: Dict) -> Dict: ret_str = "" # Lord forgive me for the sins I am about to commit for i in range(0, 10): @@ -226,7 +226,7 @@ class CxbRevSunriseS1(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: + async def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: partner_num = int(data["dldate"]["filetype"][-4:]) ret_str = f"{partner_num},,{partner_num},1,10000,\r\n" with open(r"titles/cxb/data/rss1/Partner0000.csv") as partner: @@ -235,13 +235,13 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_server_state_request(self, data: Dict) -> Dict: + async def handle_data_server_state_request(self, data: Dict) -> Dict: return {"data": True} - def handle_data_settings_request(self, data: Dict) -> Dict: + async def handle_data_settings_request(self, data: Dict) -> Dict: return {"data": "2,\r\n"} - def handle_data_story_list_request(self, data: Dict) -> Dict: + async def handle_data_story_list_request(self, data: Dict) -> Dict: # story id, story name, game version, start time, end time, course arc, unlock flag, song mcode for menu ret_str = "\r\n" ret_str += ( @@ -253,23 +253,23 @@ class CxbRevSunriseS1(CxbBase): ret_str += f"st0002,REMNANT,10104,1502127790,4096483201,Cs1000,-1,overcl,\r\n" return {"data": ret_str} - def handle_data_stxxxx_request(self, data: Dict) -> Dict: + async def handle_data_stxxxx_request(self, data: Dict) -> Dict: story_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" for i in range(1, 11): ret_str += f"{i},st000{story_num}_{i-1},,,,,,,,,,,,,,,,1,,-1,1,\r\n" return {"data": ret_str} - def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: + async def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: return {"data": "Cs1032,1,1,1,1,1,1,1,1,1,1,\r\n"} - def handle_data_premium_list_request(self, data: Dict) -> Dict: + async def handle_data_premium_list_request(self, data: Dict) -> Dict: return {"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"} - def handle_data_event_list_request(self, data: Dict) -> Dict: + async def handle_data_event_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_event_detail_list_request(self, data: Dict) -> Dict: + async def handle_data_event_detail_list_request(self, data: Dict) -> Dict: event_id = data["dldate"]["filetype"].split("/")[2] if "EventStampMapListCs1002" in event_id: return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} @@ -278,7 +278,7 @@ class CxbRevSunriseS1(CxbBase): else: return {"data": ""} - def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: + async def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: event_id = data["dldate"]["filetype"].split("/")[2] if "EventStampMapListCs1002" in event_id: return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} diff --git a/titles/cxb/rss2.py b/titles/cxb/rss2.py index b15deda..070d88b 100644 --- a/titles/cxb/rss2.py +++ b/titles/cxb/rss2.py @@ -17,11 +17,11 @@ class CxbRevSunriseS2(CxbBase): super().__init__(cfg, game_cfg) self.version = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S2_OMNI - def handle_data_path_list_request(self, data: Dict) -> Dict: + async def handle_data_path_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_music_list_request(self, data: Dict) -> Dict: + async def handle_data_music_list_request(self, data: Dict) -> Dict: version = data["dldate"]["filetype"].split("/")[0] ret_str = "" @@ -41,7 +41,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_item_list_detail_request(self, data: Dict) -> Dict: + async def handle_data_item_list_detail_request(self, data: Dict) -> Dict: # ItemListIcon load ret_str = "#ItemListIcon\r\n" with open( @@ -63,7 +63,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: + async def handle_data_shop_list_detail_request(self, data: Dict) -> Dict: # ShopListIcon load ret_str = "#ShopListIcon\r\n" with open( @@ -128,7 +128,7 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: + async def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: ret_str="" with open(r"titles/cxb/data/rss2/ExtraStageList.csv") as extra: lines = extra.readlines() @@ -136,19 +136,19 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return({"data":ret_str}) - def handle_data_exxxxx_request(self, data: Dict) -> Dict: + async def handle_data_exxxxx_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: + async def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: + async def handle_data_bonus_list10100_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_oexxxx_request(self, data: Dict) -> Dict: + async def handle_data_oexxxx_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_free_coupon_request(self, data: Dict) -> Dict: + async def handle_data_free_coupon_request(self, data: Dict) -> Dict: ret_str="" with open(r"titles/cxb/data/rss2/FreeCoupon.csv") as coupon: lines = coupon.readlines() @@ -157,7 +157,7 @@ class CxbRevSunriseS2(CxbBase): return({"data":ret_str}) @cached(lifetime=86400) - def handle_data_news_list_request(self, data: Dict) -> Dict: + async def handle_data_news_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss2/NewsList.csv", encoding="UTF-8") as news: lines = news.readlines() @@ -165,14 +165,14 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_tips_request(self, data: Dict) -> Dict: + async def handle_data_tips_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_release_info_list_request(self, data: Dict) -> Dict: + async def handle_data_release_info_list_request(self, data: Dict) -> Dict: return {"data": ""} @cached(lifetime=86400) - def handle_data_random_music_list_request(self, data: Dict) -> Dict: + async def handle_data_random_music_list_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss2/MusicArchiveList.csv") as music: lines = music.readlines() @@ -186,7 +186,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_license_request(self, data: Dict) -> Dict: + async def handle_data_license_request(self, data: Dict) -> Dict: ret_str = "" with open(r"titles/cxb/data/rss2/License.csv", encoding="UTF-8") as licenses: lines = licenses.readlines() @@ -195,7 +195,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_course_list_request(self, data: Dict) -> Dict: + async def handle_data_course_list_request(self, data: Dict) -> Dict: ret_str = "" with open( r"titles/cxb/data/rss2/Course/CourseList.csv", encoding="UTF-8" @@ -206,7 +206,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_csxxxx_request(self, data: Dict) -> Dict: + async def handle_data_csxxxx_request(self, data: Dict) -> Dict: extra_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" with open( @@ -217,16 +217,16 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_mission_list_request(self, data: Dict) -> Dict: + async def handle_data_mission_list_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_mission_bonus_request(self, data: Dict) -> Dict: + async def handle_data_mission_bonus_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: + async def handle_data_unlimited_mission_request(self, data: Dict) -> Dict: return {"data": ""} - def handle_data_partner_list_request(self, data: Dict) -> Dict: + async def handle_data_partner_list_request(self, data: Dict) -> Dict: ret_str = "" # Lord forgive me for the sins I am about to commit for i in range(0, 10): @@ -245,7 +245,7 @@ class CxbRevSunriseS2(CxbBase): return {"data": ret_str} @cached(lifetime=86400) - def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: + async def handle_data_partnerxxxx_request(self, data: Dict) -> Dict: partner_num = int(data["dldate"]["filetype"][-4:]) ret_str = f"{partner_num},,{partner_num},1,10000,\r\n" with open(r"titles/cxb/data/rss2/Partner0000.csv") as partner: @@ -254,13 +254,13 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{line[:-1]}\r\n" return {"data": ret_str} - def handle_data_server_state_request(self, data: Dict) -> Dict: + async def handle_data_server_state_request(self, data: Dict) -> Dict: return {"data": True} - def handle_data_settings_request(self, data: Dict) -> Dict: + async def handle_data_settings_request(self, data: Dict) -> Dict: return {"data": "2,\r\n"} - def handle_data_story_list_request(self, data: Dict) -> Dict: + async def handle_data_story_list_request(self, data: Dict) -> Dict: # story id, story name, game version, start time, end time, course arc, unlock flag, song mcode for menu ret_str = "\r\n" ret_str += ( @@ -272,7 +272,7 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"st0002,REMNANT,10104,1502127790,4096483201,Cs1000,-1,overcl,\r\n" return {"data": ret_str} - def handle_data_stxxxx_request(self, data: Dict) -> Dict: + async def handle_data_stxxxx_request(self, data: Dict) -> Dict: story_num = int(data["dldate"]["filetype"][-4:]) ret_str = "" # Each stories appears to have 10 pieces based on the wiki but as on how they are set.... no clue @@ -280,18 +280,18 @@ class CxbRevSunriseS2(CxbBase): ret_str += f"{i},st000{story_num}_{i-1},,,,,,,,,,,,,,,,1,,-1,1,\r\n" return {"data": ret_str} - def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: + async def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: return {"data": "Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"} - def handle_data_premium_list_request(self, data: Dict) -> Dict: + async def handle_data_premium_list_request(self, data: Dict) -> Dict: return {"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"} - def handle_data_event_list_request(self, data: Dict) -> Dict: + async def handle_data_event_list_request(self, data: Dict) -> Dict: return { "data": "Cs4001,0,10000,1601510400,1604188799,1,nv2006,1,\r\nCs4005,0,10000,1609459200,1615766399,1,nv2006,1,\r\n" } - def handle_data_event_detail_list_request(self, data: Dict) -> Dict: + async def handle_data_event_detail_list_request(self, data: Dict) -> Dict: event_id = data["dldate"]["filetype"].split("/")[2] if "Cs4001" in event_id: return { @@ -308,7 +308,7 @@ class CxbRevSunriseS2(CxbBase): else: return {"data": ""} - def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: + async def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict: event_id = data["dldate"]["filetype"].split("/")[2] if "EventStampMapListCs1002" in event_id: return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"} diff --git a/titles/diva/base.py b/titles/diva/base.py index 6db0dbc..eb7d11e 100644 --- a/titles/diva/base.py +++ b/titles/diva/base.py @@ -1,8 +1,7 @@ import datetime -from typing import Any, List, Dict +from typing import Dict import logging -import json -import urllib +import urllib.parse from threading import Thread from core.config import CoreConfig @@ -24,13 +23,13 @@ class DivaBase: dt = datetime.datetime.now() self.time_lut = urllib.parse.quote(dt.strftime("%Y-%m-%d %H:%M:%S:16.0")) - def handle_test_request(self, data: Dict) -> Dict: + async def handle_test_request(self, data: Dict) -> Dict: return "" - def handle_game_init_request(self, data: Dict) -> Dict: + async def handle_game_init_request(self, data: Dict) -> Dict: return f"" - def handle_attend_request(self, data: Dict) -> Dict: + async def handle_attend_request(self, data: Dict) -> Dict: encoded = "&" params = { "atnd_prm1": "0,1,1,0,0,0,1,0,100,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1", @@ -44,7 +43,7 @@ class DivaBase: return encoded - def handle_ping_request(self, data: Dict) -> Dict: + async def handle_ping_request(self, data: Dict) -> Dict: encoded = "&" params = { "ping_b_msg": f"Welcome to {self.core_cfg.server.name} network!", @@ -89,7 +88,7 @@ class DivaBase: return encoded - def handle_pv_list_request(self, data: Dict) -> Dict: + async def handle_pv_list_request(self, data: Dict) -> Dict: pvlist = "" with open(r"titles/diva/data/PvList0.dat", encoding="utf-8") as shop: lines = shop.readlines() @@ -126,7 +125,7 @@ class DivaBase: return response - def handle_shop_catalog_request(self, data: Dict) -> Dict: + async def handle_shop_catalog_request(self, data: Dict) -> Dict: catalog = "" shopList = self.data.static.get_enabled_shops(self.version) @@ -164,7 +163,7 @@ class DivaBase: return response - def handle_buy_module_request(self, data: Dict) -> Dict: + async def handle_buy_module_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile(data["pd_id"], self.version) module = self.data.static.get_enabled_shop(self.version, int(data["mdl_id"])) @@ -191,7 +190,7 @@ class DivaBase: return response - def handle_cstmz_itm_ctlg_request(self, data: Dict) -> Dict: + async def handle_cstmz_itm_ctlg_request(self, data: Dict) -> Dict: catalog = "" itemList = self.data.static.get_enabled_items(self.version) @@ -229,7 +228,7 @@ class DivaBase: return response - def handle_buy_cstmz_itm_request(self, data: Dict) -> Dict: + async def handle_buy_cstmz_itm_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile(data["pd_id"], self.version) item = self.data.static.get_enabled_item( self.version, int(data["cstmz_itm_id"]) @@ -264,7 +263,7 @@ class DivaBase: return response - def handle_festa_info_request(self, data: Dict) -> Dict: + async def handle_festa_info_request(self, data: Dict) -> Dict: encoded = "&" params = { "fi_id": "1,2", @@ -287,7 +286,7 @@ class DivaBase: return encoded - def handle_contest_info_request(self, data: Dict) -> Dict: + async def handle_contest_info_request(self, data: Dict) -> Dict: response = "" response += f"&ci_lut={self.time_lut}" @@ -295,7 +294,7 @@ class DivaBase: return response - def handle_qst_inf_request(self, data: Dict) -> Dict: + async def handle_qst_inf_request(self, data: Dict) -> Dict: quest = "" questList = self.data.static.get_enabled_quests(self.version) @@ -345,43 +344,43 @@ class DivaBase: return response - def handle_nv_ranking_request(self, data: Dict) -> Dict: + async def handle_nv_ranking_request(self, data: Dict) -> Dict: return f"" - def handle_ps_ranking_request(self, data: Dict) -> Dict: + async def handle_ps_ranking_request(self, data: Dict) -> Dict: return f"" - def handle_ng_word_request(self, data: Dict) -> Dict: + async def handle_ng_word_request(self, data: Dict) -> Dict: return f"" - def handle_rmt_wp_list_request(self, data: Dict) -> Dict: + async def handle_rmt_wp_list_request(self, data: Dict) -> Dict: return f"" - def handle_pv_def_chr_list_request(self, data: Dict) -> Dict: + async def handle_pv_def_chr_list_request(self, data: Dict) -> Dict: return f"" - def handle_pv_ng_mdl_list_request(self, data: Dict) -> Dict: + async def handle_pv_ng_mdl_list_request(self, data: Dict) -> Dict: return f"" - def handle_cstmz_itm_ng_mdl_lst_request(self, data: Dict) -> Dict: + async def handle_cstmz_itm_ng_mdl_lst_request(self, data: Dict) -> Dict: return f"" - def handle_banner_info_request(self, data: Dict) -> Dict: + async def handle_banner_info_request(self, data: Dict) -> Dict: return f"" - def handle_banner_data_request(self, data: Dict) -> Dict: + async def handle_banner_data_request(self, data: Dict) -> Dict: return f"" - def handle_cm_ply_info_request(self, data: Dict) -> Dict: + async def handle_cm_ply_info_request(self, data: Dict) -> Dict: return f"" - def handle_pstd_h_ctrl_request(self, data: Dict) -> Dict: + async def handle_pstd_h_ctrl_request(self, data: Dict) -> Dict: return f"" - def handle_pstd_item_ng_lst_request(self, data: Dict) -> Dict: + async def handle_pstd_item_ng_lst_request(self, data: Dict) -> Dict: return f"" - def handle_pre_start_request(self, data: Dict) -> str: + async def handle_pre_start_request(self, data: Dict) -> str: profile = self.data.profile.get_profile(data["aime_id"], self.version) profile_shop = self.data.item.get_shop(data["aime_id"], self.version) @@ -422,13 +421,13 @@ class DivaBase: return response - def handle_registration_request(self, data: Dict) -> Dict: + async def handle_registration_request(self, data: Dict) -> Dict: self.data.profile.create_profile( self.version, data["aime_id"], data["player_name"] ) return f"&cd_adm_result=1&pd_id={data['aime_id']}" - def handle_start_request(self, data: Dict) -> Dict: + async def handle_start_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile(data["pd_id"], self.version) profile_shop = self.data.item.get_shop(data["pd_id"], self.version) if profile is None: @@ -583,10 +582,10 @@ class DivaBase: return response - def handle_pd_unlock_request(self, data: Dict) -> Dict: + async def handle_pd_unlock_request(self, data: Dict) -> Dict: return f"" - def handle_spend_credit_request(self, data: Dict) -> Dict: + async def handle_spend_credit_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile(data["pd_id"], self.version) if profile is None: return @@ -705,7 +704,7 @@ class DivaBase: pd_by_pv_id.append(urllib.parse.quote(f"{song}***")) pd_by_pv_id.append(",") - def handle_get_pv_pd_request(self, data: Dict) -> Dict: + async def handle_get_pv_pd_request(self, data: Dict) -> Dict: song_id = data["pd_pv_id_lst"].split(",") pv = "" @@ -732,10 +731,10 @@ class DivaBase: return response - def handle_stage_start_request(self, data: Dict) -> Dict: + async def handle_stage_start_request(self, data: Dict) -> Dict: return f"" - def handle_stage_result_request(self, data: Dict) -> Dict: + async def handle_stage_result_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile(data["pd_id"], self.version) pd_song_list = data["stg_ply_pv_id"].split(",") @@ -914,7 +913,7 @@ class DivaBase: return response - def handle_end_request(self, data: Dict) -> Dict: + async def handle_end_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile(data["pd_id"], self.version) self.data.profile.update_profile( @@ -922,7 +921,7 @@ class DivaBase: ) return f"" - def handle_shop_exit_request(self, data: Dict) -> Dict: + async def handle_shop_exit_request(self, data: Dict) -> Dict: self.data.item.put_shop( data["pd_id"], self.version, @@ -952,7 +951,7 @@ class DivaBase: response = "&shp_rslt=1" return response - def handle_card_procedure_request(self, data: Dict) -> str: + async def handle_card_procedure_request(self, data: Dict) -> str: profile = self.data.profile.get_profile(data["aime_id"], self.version) if profile is None: return "&cd_adm_result=0" @@ -972,7 +971,7 @@ class DivaBase: return response - def handle_change_name_request(self, data: Dict) -> str: + async def handle_change_name_request(self, data: Dict) -> str: profile = self.data.profile.get_profile(data["pd_id"], self.version) # make sure user has enough Vocaloid Points @@ -992,7 +991,7 @@ class DivaBase: return response - def handle_change_passwd_request(self, data: Dict) -> str: + async def handle_change_passwd_request(self, data: Dict) -> str: profile = self.data.profile.get_profile(data["pd_id"], self.version) # TODO: return correct error number instead of 0 diff --git a/titles/diva/index.py b/titles/diva/index.py index ac4114e..f414ab2 100644 --- a/titles/diva/index.py +++ b/titles/diva/index.py @@ -1,4 +1,6 @@ -from twisted.web.http import Request +from starlette.requests import Request +from starlette.responses import PlainTextResponse +from starlette.routing import Route import yaml import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler @@ -51,17 +53,16 @@ class DivaServlet(BaseServlet): level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str ) - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [("render_POST", "/DivaServlet/", {})] - ) + def get_routes(self) -> List[Route]: + return [ + Route("/DivaServlet/", self.render_POST, methods=['POST']) + ] def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: - return (f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/DivaServlet/", self.core_cfg.title.hostname) + return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/DivaServlet/", self.core_cfg.server.hostname) - return (f"http://{self.core_cfg.title.hostname}/DivaServlet/", self.core_cfg.title.hostname) + return (f"http://{self.core_cfg.server.hostname}/DivaServlet/", self.core_cfg.server.hostname) @classmethod def is_game_enabled( @@ -78,9 +79,9 @@ class DivaServlet(BaseServlet): return True - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: - req_raw = request.content.getvalue() - url_header = request.getAllHeaders() + async def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: + req_raw = await request.body() + url_header = request.headers # Ping Dispatch if "THIS_STRING_SEPARATES" in str(url_header): @@ -103,9 +104,7 @@ class DivaServlet(BaseServlet): self.logger.debug( f"Response cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}" ) - return f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}".encode( - "utf-8" - ) + return PlainTextResponse(f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}") # Main Dispatch json_string = json.dumps( @@ -122,7 +121,7 @@ class DivaServlet(BaseServlet): ) # Decompressing the gzip except zlib.error as e: self.logger.error(f"Failed to defalte! {e} -> {gz_string}") - return "stat=0" + return PlainTextResponse("stat=0") req_kvp = urllib.parse.unquote(url_data) req_data = {} @@ -141,27 +140,18 @@ class DivaServlet(BaseServlet): # Load the requests try: handler = getattr(self.base, func_to_find) - resp = handler(req_data) + resp = await handler(req_data) except AttributeError as e: self.logger.warning(f"Unhandled {req_data['cmd']} request {e}") - return f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok".encode( - "utf-8" - ) + return PlainTextResponse(f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok") except Exception as e: self.logger.error(f"Error handling method {func_to_find} {e}") - return f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok".encode( - "utf-8" - ) + return PlainTextResponse(f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok") - request.responseHeaders.addRawHeader(b"content-type", b"text/plain") self.logger.debug( f"Response cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}" ) - return ( - f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}".encode( - "utf-8" - ) - ) + return PlainTextResponse(f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}") diff --git a/titles/idac/frontend.py b/titles/idac/frontend.py index 78abae8..8845b35 100644 --- a/titles/idac/frontend.py +++ b/titles/idac/frontend.py @@ -3,7 +3,7 @@ import yaml import jinja2 from os import path from twisted.web.util import redirectTo -from twisted.web.http import Request +from starlette.requests import Request from twisted.web.server import Session from core.frontend import FE_Base, IUserSession diff --git a/titles/idac/index.py b/titles/idac/index.py index 7daedae..1aa8050 100644 --- a/titles/idac/index.py +++ b/titles/idac/index.py @@ -1,18 +1,18 @@ import json import traceback -import inflection +from starlette.routing import Route +from starlette.requests import Request +from starlette.responses import JSONResponse import yaml import logging import coloredlogs - from os import path from typing import Dict, List, Tuple from logging.handlers import TimedRotatingFileHandler -from twisted.web import server -from twisted.web.http import Request -from twisted.internet import reactor, endpoints + from core.config import CoreConfig +from core.title import BaseServlet from core.utils import Utils from titles.idac.base import IDACBase from titles.idac.season2 import IDACSeason2 @@ -22,7 +22,7 @@ from titles.idac.echo import IDACEchoUDP from titles.idac.matching import IDACMatching -class IDACServlet: +class IDACServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = IDACConfig() @@ -72,12 +72,11 @@ class IDACServlet: return False return True - - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [("render_POST", "/SDGT/{version}/initiald/{category}/{endpoint}", {})] - ) + + def get_routes(self) -> List[Route]: + return [ + Route("/{version:int}/initiald/{category:str}/{endpoint:str}", self.render_POST, methods=["POST"]) + ] def get_allnet_info( self, game_code: str, game_ver: int, keychip: str @@ -88,15 +87,15 @@ class IDACServlet: return ( f"", # requires http or else it defaults to https - f"http://{self.core_cfg.title.hostname}{t_port}/{game_code}/{game_ver}/", + f"http://{self.core_cfg.server.hostname}{t_port}/{game_code}/{game_ver}/", ) - def render_POST(self, request: Request, game_code: int, matchers: Dict) -> bytes: - req_raw = request.content.getvalue() + async def render_POST(self, request: Request) -> bytes: + req_raw = await request.body() internal_ver = 0 - version = int(matchers['version']) - category = matchers['category'] - endpoint = matchers['endpoint'] + version: int = request.path_params.get('version') + category: str = request.path_params.get('category') + endpoint: str = request.path_params.get('endpoint') client_ip = Utils.get_ip_addr(request) if version >= 100 and version < 140: # IDAC Season 1 @@ -104,7 +103,7 @@ class IDACServlet: elif version >= 140 and version < 171: # IDAC Season 2 internal_ver = IDACConstants.VER_IDAC_SEASON_2 - header_application = self.decode_header(request.getAllHeaders()) + header_application = self.decode_header(request.headers.get("application", "")) req_data = json.loads(req_raw) @@ -119,7 +118,7 @@ class IDACServlet: if not hasattr(self.versions[internal_ver], func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") - return '{"status_code": "0"}'.encode("utf-8") + return JSONResponse('{"status_code": "0"}') resp = None try: @@ -129,17 +128,16 @@ class IDACServlet: except Exception as e: traceback.print_exc() self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return '{"status_code": "0"}'.encode("utf-8") + return JSONResponse('{"status_code": "0"}') if resp is None: resp = {"status_code": "0"} self.logger.debug(f"Response {resp}") - return json.dumps(resp, ensure_ascii=False).encode("utf-8") + return JSONResponse(json.dumps(resp, ensure_ascii=False)) - def decode_header(self, data: Dict) -> Dict: - app: str = data[b"application"].decode() + def decode_header(self, app: str) -> Dict: ret = {} for x in app.split(", "): @@ -149,6 +147,8 @@ class IDACServlet: return ret def setup(self): + return + """ if self.game_cfg.server.enable: endpoints.serverFromString( reactor, @@ -165,3 +165,4 @@ class IDACServlet: ) self.logger.info(f"Matching listening on {self.game_cfg.server.matching} with echos on {self.game_cfg.server.echo1} and {self.game_cfg.server.echo2}") + """ diff --git a/titles/idac/season2.py b/titles/idac/season2.py index ca57392..fabcbcd 100644 --- a/titles/idac/season2.py +++ b/titles/idac/season2.py @@ -109,9 +109,9 @@ class IDACSeason2(IDACBase): ver_str = version.replace(".", "")[:3] if self.core_cfg.server.is_develop: - domain_api_game = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDGT/{ver_str}/" + domain_api_game = f"http://{self.core_cfg.server.hostname}:{self.core_cfg.server.port}/SDGT/{ver_str}/" else: - domain_api_game = f"http://{self.core_cfg.title.hostname}/SDGT/{ver_str}/" + domain_api_game = f"http://{self.core_cfg.server.hostname}/SDGT/{ver_str}/" return { "status_code": "0", @@ -136,10 +136,10 @@ class IDACSeason2(IDACBase): "server_maintenance_end_hour": 0, "server_maintenance_end_minutes": 0, "domain_api_game": domain_api_game, - "domain_matching": f"http://{self.core_cfg.title.hostname}:{self.game_config.server.matching}", - "domain_echo1": f"{self.core_cfg.title.hostname}:{self.game_config.server.echo1}", - "domain_echo2": f"{self.core_cfg.title.hostname}:{self.game_config.server.echo2}", - "domain_ping": f"{self.core_cfg.title.hostname}", + "domain_matching": f"http://{self.core_cfg.server.hostname}:{self.game_config.server.matching}", + "domain_echo1": f"{self.core_cfg.server.hostname}:{self.game_config.server.echo1}", + "domain_echo2": f"{self.core_cfg.server.hostname}:{self.game_config.server.echo2}", + "domain_ping": f"{self.core_cfg.server.hostname}", "battle_gift_event_master": [], "round_event": [ { diff --git a/titles/idz/handlers/load_server_info.py b/titles/idz/handlers/load_server_info.py index 4c60dd7..9eb63ab 100644 --- a/titles/idz/handlers/load_server_info.py +++ b/titles/idz/handlers/load_server_info.py @@ -24,10 +24,10 @@ class IDZHandlerLoadServerInfo(IDZHandlerBase): t_port = Utils.get_title_port(self.core_config) - news_str = f"http://{self.core_config.title.hostname}:{t_port}/idz/news/news80**.txt" - err_str = f"http://{self.core_config.title.hostname}:{t_port}/idz/error" + news_str = f"http://{self.core_config.server.hostname}:{t_port}/idz/news/news80**.txt" + err_str = f"http://{self.core_config.server.hostname}:{t_port}/idz/error" - len_hostname = len(self.core_config.title.hostname) + len_hostname = len(self.core_config.server.hostname) len_news = len(news_str) len_error = len(err_str) @@ -36,7 +36,7 @@ class IDZHandlerLoadServerInfo(IDZHandlerBase): f"{len_hostname}s", ret, 0x4 + offset, - self.core_config.title.hostname.encode(), + self.core_config.server.hostname.encode(), ) struct.pack_into(" Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return[ - [("render_GET", "/idz/news/{endpoint:.*?}", {}), - ("render_GET", "/idz/error", {})], - [] + def get_routes(self) -> List[Route]: + return [ + Route("/idz/news/{endpoint:str}", self.render_GET), + Route("/idz/error", self.render_GET) ] def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: hostname = ( - self.core_cfg.title.hostname + self.core_cfg.server.hostname if not self.game_cfg.server.hostname else self.game_cfg.server.hostname ) @@ -135,7 +134,8 @@ class IDZServlet(BaseServlet): except AttributeError as e: continue - + + """ endpoints.serverFromString( reactor, f"tcp:{self.game_cfg.ports.userdb}:interface={self.core_cfg.server.listen_address}", @@ -155,18 +155,15 @@ class IDZServlet(BaseServlet): reactor.listenUDP( self.game_cfg.ports.userdb + 1, IDZEcho(self.core_cfg, self.game_cfg) ) - + """ self.logger.info(f"UserDB Listening on port {self.game_cfg.ports.userdb}") - def render_GET(self, request: Request, game_code: str, matchers: Dict) -> bytes: - url_path = matchers['endpoint'] + async def render_GET(self, request: Request) -> bytes: + url_path = request.path_params.get('endpoint', '') + if not url_path: + return Response() + self.logger.info(f"IDZ GET request: {url_path}") - request.responseHeaders.setRawHeaders( - "Content-Type", [b"text/plain; charset=utf-8"] - ) - request.responseHeaders.setRawHeaders( - "Last-Modified", [b"Sun, 23 Apr 2023 05:33:20 GMT"] - ) news = ( self.game_cfg.server.news @@ -176,4 +173,4 @@ class IDZServlet(BaseServlet): news += "\r\n" news = "1979/01/01 00:00:00 2099/12/31 23:59:59 " + news - return news.encode() + return PlainTextResponse(news, media_type="text/plain; charset=utf-8", headers={"Last-Modified": "Sun, 23 Apr 2023 05:33:20 GMT"}) diff --git a/titles/idz/userdb.py b/titles/idz/userdb.py index fd555a1..b585bd2 100644 --- a/titles/idz/userdb.py +++ b/titles/idz/userdb.py @@ -5,7 +5,7 @@ import struct from typing import Dict, Optional, List, Type from twisted.web import server, resource from twisted.internet import reactor, endpoints -from twisted.web.http import Request +from starlette.requests import Request from routes import Mapper import random from os import walk diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 2f40176..a9ddcab 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -26,12 +26,12 @@ class Mai2Base: self.date_time_format = "%Y-%m-%d %H:%M:%S" if not self.core_config.server.is_using_proxy and Utils.get_title_port(self.core_config) != 80: - self.old_server = f"http://{self.core_config.title.hostname}:{Utils.get_title_port(cfg)}/197/MaimaiServlet/" + self.old_server = f"http://{self.core_config.server.hostname}:{Utils.get_title_port(cfg)}/197/MaimaiServlet/" else: - self.old_server = f"http://{self.core_config.title.hostname}/197/MaimaiServlet/" + self.old_server = f"http://{self.core_config.server.hostname}/197/MaimaiServlet/" - def handle_get_game_setting_api_request(self, data: Dict): + async def handle_get_game_setting_api_request(self, data: Dict): # if reboot start/end time is not defined use the default behavior of being a few hours ago if self.core_config.title.reboot_start_time == "" or self.core_config.title.reboot_end_time == "": reboot_start = datetime.strftime( @@ -74,14 +74,14 @@ class Mai2Base: }, } - def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: + async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: return {"length": 0, "gameRankingList": []} - def handle_get_game_tournament_info_api_request(self, data: Dict) -> Dict: + async def handle_get_game_tournament_info_api_request(self, data: Dict) -> Dict: # TODO: Tournament support return {"length": 0, "gameTournamentInfoList": []} - def handle_get_game_event_api_request(self, data: Dict) -> Dict: + async def handle_get_game_event_api_request(self, data: Dict) -> Dict: events = self.data.static.get_enabled_events(self.version) events_lst = [] if events is None or not events: @@ -108,10 +108,10 @@ class Mai2Base: "gameEventList": events_lst, } - def handle_get_game_ng_music_id_api_request(self, data: Dict) -> Dict: + async def handle_get_game_ng_music_id_api_request(self, data: Dict) -> Dict: return {"length": 0, "musicIdList": []} - def handle_get_game_charge_api_request(self, data: Dict) -> Dict: + async def handle_get_game_charge_api_request(self, data: Dict) -> Dict: game_charge_list = self.data.static.get_enabled_tickets(self.version, 1) if game_charge_list is None: return {"length": 0, "gameChargeList": []} @@ -130,19 +130,19 @@ class Mai2Base: return {"length": len(charge_list), "gameChargeList": charge_list} - def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} - def handle_upsert_client_upload_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_upload_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientUploadApi"} - def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"} - def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} - def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version, False) w = self.data.profile.get_web_option(data["userId"], self.version) if p is None or w is None: @@ -169,7 +169,7 @@ class Mai2Base: "totalLv": profile["totalLv"], } - def handle_user_login_api_request(self, data: Dict) -> Dict: + async def handle_user_login_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_detail(data["userId"], self.version) consec = self.data.profile.get_consec_login(data["userId"], self.version) @@ -210,7 +210,7 @@ class Mai2Base: "consecutiveLoginCount": consec_ct, # Number of consecutive days we've logged in. } - def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: + async def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] playlog = data["userPlaylog"] @@ -218,7 +218,7 @@ class Mai2Base: return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} - def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] charge = data["userCharge"] @@ -234,7 +234,7 @@ class Mai2Base: return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"} - def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: user_id = data["userId"] upsert = data["upsertUserAll"] @@ -375,10 +375,10 @@ class Mai2Base: return {"returnCode": 1, "apiName": "UpsertUserAllApi"} - def handle_user_logout_api_request(self, data: Dict) -> Dict: + async def handle_user_logout_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} - def handle_get_user_data_api_request(self, data: Dict) -> Dict: + async def handle_get_user_data_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_detail(data["userId"], self.version, False) if profile is None: return @@ -390,7 +390,7 @@ class Mai2Base: return {"userId": data["userId"], "userData": profile_dict} - def handle_get_user_extend_api_request(self, data: Dict) -> Dict: + async def handle_get_user_extend_api_request(self, data: Dict) -> Dict: extend = self.data.profile.get_profile_extend(data["userId"], self.version) if extend is None: return @@ -402,7 +402,7 @@ class Mai2Base: return {"userId": data["userId"], "userExtend": extend_dict} - def handle_get_user_option_api_request(self, data: Dict) -> Dict: + async def handle_get_user_option_api_request(self, data: Dict) -> Dict: options = self.data.profile.get_profile_option(data["userId"], self.version, False) if options is None: return @@ -414,7 +414,7 @@ class Mai2Base: return {"userId": data["userId"], "userOption": options_dict} - def handle_get_user_card_api_request(self, data: Dict) -> Dict: + async def handle_get_user_card_api_request(self, data: Dict) -> Dict: user_cards = self.data.item.get_cards(data["userId"]) if user_cards is None: return {"userId": data["userId"], "nextIndex": 0, "userCardList": []} @@ -448,7 +448,7 @@ class Mai2Base: "userCardList": card_list[start_idx:end_idx], } - def handle_get_user_charge_api_request(self, data: Dict) -> Dict: + async def handle_get_user_charge_api_request(self, data: Dict) -> Dict: user_charges = self.data.item.get_charges(data["userId"]) if user_charges is None: return {"userId": data["userId"], "length": 0, "userChargeList": []} @@ -467,16 +467,16 @@ class Mai2Base: "userChargeList": user_charge_list, } - def handle_get_user_present_api_request(self, data: Dict) -> Dict: + async def handle_get_user_present_api_request(self, data: Dict) -> Dict: return { "userId": data.get("userId", 0), "length": 0, "userPresentList": []} - def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict: + async def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict: return {} - def handle_get_user_present_event_api_request(self, data: Dict) -> Dict: + async def handle_get_user_present_event_api_request(self, data: Dict) -> Dict: return { "userId": data.get("userId", 0), "length": 0, "userPresentEventList": []} - def handle_get_user_boss_api_request(self, data: Dict) -> Dict: + async def handle_get_user_boss_api_request(self, data: Dict) -> Dict: b = self.data.profile.get_boss_list(data["userId"]) if b is None: return { "userId": data.get("userId", 0), "userBossData": {}} @@ -486,7 +486,7 @@ class Mai2Base: return { "userId": data.get("userId", 0), "userBossData": boss_lst} - def handle_get_user_item_api_request(self, data: Dict) -> Dict: + async def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(data["nextIndex"] / 10000000000) next_idx = int(data["nextIndex"] % 10000000000) user_item_list = self.data.item.get_items(data["userId"], kind) @@ -514,7 +514,7 @@ class Mai2Base: "userItemList": items, } - def handle_get_user_character_api_request(self, data: Dict) -> Dict: + async def handle_get_user_character_api_request(self, data: Dict) -> Dict: characters = self.data.item.get_characters(data["userId"]) chara_list = [] @@ -528,7 +528,7 @@ class Mai2Base: return {"userId": data["userId"], "userCharacterList": chara_list} - def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: + async def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: favorites = self.data.item.get_favorites(data["userId"], data["itemKind"]) if favorites is None: return @@ -545,7 +545,7 @@ class Mai2Base: return {"userId": data["userId"], "userFavoriteData": userFavs} - def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: + async def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: ghost = self.data.profile.get_profile_ghost(data["userId"], self.version) if ghost is None: return @@ -557,7 +557,7 @@ class Mai2Base: return {"userId": data["userId"], "userGhost": ghost_dict} - def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: + async def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: rating = self.data.profile.get_recent_rating(data["userId"]) if rating is None: return @@ -567,7 +567,7 @@ class Mai2Base: return {"userId": data["userId"], "length": len(lst), "userRecentRatingList": lst} - def handle_get_user_rating_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rating_api_request(self, data: Dict) -> Dict: rating = self.data.profile.get_profile_rating(data["userId"], self.version) if rating is None: return @@ -579,7 +579,7 @@ class Mai2Base: return {"userId": data["userId"], "userRating": rating_dict} - def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + async def handle_get_user_activity_api_request(self, data: Dict) -> Dict: """ kind 1 is playlist, kind 2 is music list """ @@ -607,7 +607,7 @@ class Mai2Base: return {"userActivity": {"playList": plst, "musicList": mlst}} - def handle_get_user_course_api_request(self, data: Dict) -> Dict: + async def handle_get_user_course_api_request(self, data: Dict) -> Dict: user_courses = self.data.score.get_courses(data["userId"]) if user_courses is None: return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []} @@ -621,11 +621,11 @@ class Mai2Base: return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list} - def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: + async def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: # No support for custom pfps return {"length": 0, "userPortraitList": []} - def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: + async def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"]) if friend_season_ranking is None: return { @@ -661,7 +661,7 @@ class Mai2Base: "userFriendSeasonRankingList": friend_season_ranking_list, } - def handle_get_user_map_api_request(self, data: Dict) -> Dict: + async def handle_get_user_map_api_request(self, data: Dict) -> Dict: maps = self.data.item.get_maps(data["userId"]) if maps is None: return { @@ -694,7 +694,7 @@ class Mai2Base: "userMapList": map_list, } - def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: + async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: login_bonuses = self.data.item.get_login_bonuses(data["userId"]) if login_bonuses is None: return { @@ -727,10 +727,10 @@ class Mai2Base: "userLoginBonusList": login_bonus_list, } - def handle_get_user_region_api_request(self, data: Dict) -> Dict: + async def handle_get_user_region_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userRegionList": []} - def handle_get_user_web_option_api_request(self, data: Dict) -> Dict: + async def handle_get_user_web_option_api_request(self, data: Dict) -> Dict: w = self.data.profile.get_web_option(data["userId"], self.version) if w is None: return {"userId": data["userId"], "userWebOption": {}} @@ -742,10 +742,10 @@ class Mai2Base: return {"userId": data["userId"], "userWebOption": web_opt} - def handle_get_user_survival_api_request(self, data: Dict) -> Dict: + async def handle_get_user_survival_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userSurvivalList": []} - def handle_get_user_grade_api_request(self, data: Dict) -> Dict: + async def handle_get_user_grade_api_request(self, data: Dict) -> Dict: g = self.data.profile.get_grade_status(data["userId"]) if g is None: return {"userId": data["userId"], "userGradeStatus": {}, "length": 0, "userGradeList": []} @@ -755,7 +755,7 @@ class Mai2Base: return {"userId": data["userId"], "userGradeStatus": grade_stat, "length": 0, "userGradeList": []} - def handle_get_user_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_music_api_request(self, data: Dict) -> Dict: user_id = data.get("userId", 0) next_index = data.get("nextIndex", 0) max_ct = data.get("maxCount", 50) @@ -794,10 +794,10 @@ class Mai2Base: "userMusicList": [{"userMusicDetailList": music_detail_list}], } - def handle_upload_user_portrait_api_request(self, data: Dict) -> Dict: + async def handle_upload_user_portrait_api_request(self, data: Dict) -> Dict: self.logger.debug(data) - def handle_upload_user_photo_api_request(self, data: Dict) -> Dict: + async def handle_upload_user_photo_api_request(self, data: Dict) -> Dict: if not self.game_config.uploads.photos or not self.game_config.uploads.photos_dir: return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'} diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 508cebb..80a3bc2 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -15,7 +15,7 @@ class Mai2DX(Mai2Base): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX - def handle_get_game_setting_api_request(self, data: Dict): + async def handle_get_game_setting_api_request(self, data: Dict): return { "gameSetting": { "isMaintenance": False, @@ -33,7 +33,7 @@ class Mai2DX(Mai2Base): "isAouAccession": False, } - def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version) o = self.data.profile.get_profile_option(data["userId"], self.version) if p is None or o is None: @@ -69,7 +69,7 @@ class Mai2DX(Mai2Base): else 0, # New with uni+ } - def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: + async def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] playlog = data["userPlaylog"] @@ -77,7 +77,7 @@ class Mai2DX(Mai2Base): return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} - def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] charge = data["userCharge"] @@ -93,7 +93,7 @@ class Mai2DX(Mai2Base): return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"} - def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: user_id = data["userId"] upsert = data["upsertUserAll"] @@ -215,7 +215,7 @@ class Mai2DX(Mai2Base): return {"returnCode": 1, "apiName": "UpsertUserAllApi"} - def handle_get_user_data_api_request(self, data: Dict) -> Dict: + async def handle_get_user_data_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_detail(data["userId"], self.version) if profile is None: return @@ -227,7 +227,7 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userData": profile_dict} - def handle_get_user_extend_api_request(self, data: Dict) -> Dict: + async def handle_get_user_extend_api_request(self, data: Dict) -> Dict: extend = self.data.profile.get_profile_extend(data["userId"], self.version) if extend is None: return @@ -239,7 +239,7 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userExtend": extend_dict} - def handle_get_user_option_api_request(self, data: Dict) -> Dict: + async def handle_get_user_option_api_request(self, data: Dict) -> Dict: options = self.data.profile.get_profile_option(data["userId"], self.version) if options is None: return @@ -251,7 +251,7 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userOption": options_dict} - def handle_get_user_card_api_request(self, data: Dict) -> Dict: + async def handle_get_user_card_api_request(self, data: Dict) -> Dict: user_cards = self.data.item.get_cards(data["userId"]) if user_cards is None: return {"userId": data["userId"], "nextIndex": 0, "userCardList": []} @@ -285,7 +285,7 @@ class Mai2DX(Mai2Base): "userCardList": card_list[start_idx:end_idx], } - def handle_get_user_charge_api_request(self, data: Dict) -> Dict: + async def handle_get_user_charge_api_request(self, data: Dict) -> Dict: user_charges = self.data.item.get_charges(data["userId"]) if user_charges is None: return {"userId": data["userId"], "length": 0, "userChargeList": []} @@ -310,7 +310,7 @@ class Mai2DX(Mai2Base): "userChargeList": user_charge_list, } - def handle_get_user_item_api_request(self, data: Dict) -> Dict: + async def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(data["nextIndex"] / 10000000000) next_idx = int(data["nextIndex"] % 10000000000) user_item_list = self.data.item.get_items(data["userId"], kind) @@ -338,7 +338,7 @@ class Mai2DX(Mai2Base): "userItemList": items, } - def handle_get_user_character_api_request(self, data: Dict) -> Dict: + async def handle_get_user_character_api_request(self, data: Dict) -> Dict: characters = self.data.item.get_characters(data["userId"]) chara_list = [] @@ -350,7 +350,7 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userCharacterList": chara_list} - def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: + async def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: favorites = self.data.item.get_favorites(data["userId"], data["itemKind"]) if favorites is None: return @@ -367,7 +367,7 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userFavoriteData": userFavs} - def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: + async def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: ghost = self.data.profile.get_profile_ghost(data["userId"], self.version) if ghost is None: return @@ -379,7 +379,7 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userGhost": ghost_dict} - def handle_get_user_rating_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rating_api_request(self, data: Dict) -> Dict: rating = self.data.profile.get_profile_rating(data["userId"], self.version) if rating is None: return @@ -391,7 +391,7 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "userRating": rating_dict} - def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + async def handle_get_user_activity_api_request(self, data: Dict) -> Dict: """ kind 1 is playlist, kind 2 is music list """ @@ -419,7 +419,7 @@ class Mai2DX(Mai2Base): return {"userActivity": {"playList": plst, "musicList": mlst}} - def handle_get_user_course_api_request(self, data: Dict) -> Dict: + async def handle_get_user_course_api_request(self, data: Dict) -> Dict: user_courses = self.data.score.get_courses(data["userId"]) if user_courses is None: return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []} @@ -433,11 +433,11 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list} - def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: + async def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: # No support for custom pfps return {"length": 0, "userPortraitList": []} - def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: + async def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"]) if friend_season_ranking is None: return { @@ -473,7 +473,7 @@ class Mai2DX(Mai2Base): "userFriendSeasonRankingList": friend_season_ranking_list, } - def handle_get_user_map_api_request(self, data: Dict) -> Dict: + async def handle_get_user_map_api_request(self, data: Dict) -> Dict: maps = self.data.item.get_maps(data["userId"]) if maps is None: return { @@ -506,7 +506,7 @@ class Mai2DX(Mai2Base): "userMapList": map_list, } - def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: + async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: login_bonuses = self.data.item.get_login_bonuses(data["userId"]) if login_bonuses is None: return { @@ -539,7 +539,7 @@ class Mai2DX(Mai2Base): "userLoginBonusList": login_bonus_list, } - def handle_get_user_region_api_request(self, data: Dict) -> Dict: + async def handle_get_user_region_api_request(self, data: Dict) -> Dict: """ class UserRegionList: regionId: int @@ -548,7 +548,7 @@ class Mai2DX(Mai2Base): """ return {"userId": data["userId"], "length": 0, "userRegionList": []} - def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: user_id = data["userId"] rival_id = data["rivalId"] @@ -559,7 +559,7 @@ class Mai2DX(Mai2Base): """ return {"userId": user_id, "userRivalData": {}} - def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: user_id = data["userId"] rival_id = data["rivalId"] next_idx = data["nextIndex"] @@ -577,7 +577,7 @@ class Mai2DX(Mai2Base): """ return {"userId": user_id, "nextIndex": 0, "userRivalMusicList": []} - def handle_get_user_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_music_api_request(self, data: Dict) -> Dict: user_id = data.get("userId", 0) next_index = data.get("nextIndex", 0) max_ct = data.get("maxCount", 50) @@ -616,8 +616,8 @@ class Mai2DX(Mai2Base): "userMusicList": [{"userMusicDetailList": music_detail_list}], } - def handle_user_login_api_request(self, data: Dict) -> Dict: - ret = super().handle_user_login_api_request(data) + async def handle_user_login_api_request(self, data: Dict) -> Dict: + ret = await super().handle_user_login_api_request(data) if ret is None or not ret: return ret ret['loginId'] = ret.get('loginCount', 0) diff --git a/titles/mai2/festival.py b/titles/mai2/festival.py index 145fa71..94ce3ec 100644 --- a/titles/mai2/festival.py +++ b/titles/mai2/festival.py @@ -11,26 +11,26 @@ class Mai2Festival(Mai2UniversePlus): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = super().handle_cm_get_user_preview_api_request(data) + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) # hardcode lastDataVersion for CardMaker user_data["lastDataVersion"] = "1.30.00" return user_data - def handle_user_login_api_request(self, data: Dict) -> Dict: - user_login = super().handle_user_login_api_request(data) + async def handle_user_login_api_request(self, data: Dict) -> Dict: + user_login = await super().handle_user_login_api_request(data) # useless? user_login["Bearer"] = "ARTEMiSTOKEN" return user_login - def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: """ userRecommendRateMusicIdList: list[int] """ return {"userId": data["userId"], "userRecommendRateMusicIdList": []} - def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: """ userRecommendSelectionMusicIdList: list[int] """ diff --git a/titles/mai2/festivalplus.py b/titles/mai2/festivalplus.py index 7deeb98..375d546 100644 --- a/titles/mai2/festivalplus.py +++ b/titles/mai2/festivalplus.py @@ -11,14 +11,14 @@ class Mai2FestivalPlus(Mai2Festival): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = super().handle_cm_get_user_preview_api_request(data) + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) # hardcode lastDataVersion for CardMaker user_data["lastDataVersion"] = "1.35.00" return user_data - def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: + async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: user_id = data.get("userId", 0) kind = data.get("kind", 2) next_index = data.get("nextIndex", 0) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 793aaef..2ee4cae 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -1,9 +1,9 @@ -from twisted.web.http import Request -from twisted.web.server import NOT_DONE_YET +from starlette.requests import Request +from starlette.responses import Response, JSONResponse +from starlette.routing import Route import json import inflection import yaml -import string import logging, coloredlogs import zlib from logging.handlers import TimedRotatingFileHandler @@ -101,33 +101,29 @@ class Mai2Servlet(BaseServlet): return True - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [ - ("handle_movie", "/{version}/MaimaiServlet/api/movie/{endpoint}", {}), - ("handle_old_srv", "/{version}/MaimaiServlet/old/{endpoint}", {}), - ("handle_old_srv_userdata", "/{version}/MaimaiServlet/old/{endpoint}/{placeid}/{keychip}/{userid}", {}), - ("handle_old_srv_userdata", "/{version}/MaimaiServlet/old/{endpoint}/{userid}", {}), - ("handle_usbdl", "/{version}/MaimaiServlet/usbdl/{endpoint}", {}), - ("handle_deliver", "/{version}/MaimaiServlet/deliver/{endpoint}", {}), - ], - [ - ("handle_movie", "/{version}/MaimaiServlet/api/movie/{endpoint}", {}), - ("handle_mai", "/{version}/MaimaiServlet/{endpoint}", {}), - ("handle_mai2", "/{version}/Maimai2Servlet/{endpoint}", {}), - ] - ) - + def get_routes(self) -> List[Route]: + return [ + Route("/{version:int}/MaimaiServlet/api/movie/{endpoint:str}", self.handle_movie, methods=['GET', 'POST']), + Route("/{version:int}/MaimaiServlet/old/{endpoint:str}", self.handle_old_srv), + Route("/{version:int}/MaimaiServlet/old/{endpoint:str}/{placeid:str}/{keychip:str}/{userid:int}", self.handle_old_srv_userdata), + Route("/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", self.handle_old_srv_userdata), + Route("/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", self.handle_old_srv_userdata), + Route("/{version:int}/MaimaiServlet/usbdl/{endpoint:str}", self.handle_usbdl), + Route("/{version:int}/MaimaiServlet/deliver/{endpoint:str}", self.handle_deliver), + Route("/{version:int}/MaimaiServlet/{endpoint:str}", self.handle_mai, methods=['POST']), + Route("/{version:int}/Maimai2Servlet/{endpoint:str}", self.handle_mai2, methods=['POST']), + ] + def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: return ( - f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_ver}/", - f"{self.core_cfg.title.hostname}", + f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_ver}/", + f"{self.core_cfg.server.hostname}", ) return ( - f"http://{self.core_cfg.title.hostname}/{game_ver}/", - f"{self.core_cfg.title.hostname}", + f"http://{self.core_cfg.server.hostname}/{game_ver}/", + f"{self.core_cfg.server.hostname}", ) def setup(self): @@ -155,13 +151,22 @@ class Mai2Servlet(BaseServlet): f"Failed to make movie upload directory at {self.game_cfg.uploads.movies_dir}" ) - def handle_mai(self, request: Request, game_code: str, matchers: Dict) -> bytes: - endpoint = matchers['endpoint'] - version = int(matchers['version']) + async def handle_movie(self, request: Request): + return JSONResponse() + + async def handle_usbdl(self, request: Request): + return Response("OK") + + async def handle_deliver(self, request: Request): + return Response(status_code=404) + + async def handle_mai(self, request: Request) -> bytes: + endpoint: str = request.path_params.get('endpoint') + version: int = request.path_params.get('version') if endpoint.lower() == "ping": - return zlib.compress(b'{"returnCode": "1"}') + return Response(zlib.compress(b'{"returnCode": "1"}')) - req_raw = request.content.getvalue() + req_raw = await request.body() internal_ver = 0 client_ip = Utils.get_ip_addr(request) @@ -199,7 +204,7 @@ class Mai2Servlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) req_data = json.loads(unzip) @@ -216,26 +221,26 @@ class Mai2Servlet(BaseServlet): else: try: handler = getattr(handler_cls, func_to_find) - resp = handler(req_data) + resp = await handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress(b'{"returnCode": "0"}') + return Response(zlib.compress(b'{"returnCode": "0"}')) if resp == None: resp = {"returnCode": 1} self.logger.debug(f"Response {resp}") - return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + return Response(zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))) - def handle_mai2(self, request: Request, game_code: str, matchers: Dict) -> bytes: - endpoint = matchers['endpoint'] - version = int(matchers['version']) + async def handle_mai2(self, request: Request) -> bytes: + endpoint: str = request.path_params.get('endpoint') + version: int = request.path_params.get('version') if endpoint.lower() == "ping": - return zlib.compress(b'{"returnCode": "1"}') + return Response(zlib.compress(b'{"returnCode": "1"}')) - req_raw = request.content.getvalue() + req_raw = await request.body() internal_ver = 0 client_ip = Utils.get_ip_addr(request) if version < 105: # 1.0 @@ -256,17 +261,17 @@ class Mai2Servlet(BaseServlet): internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS if ( - request.getHeader("Mai-Encoding") is not None - or request.getHeader("X-Mai-Encoding") is not None + request.headers.get("Mai-Encoding") is not None + or request.headers.get("X-Mai-Encoding") is not None ): # The has is some flavor of MD5 of the endpoint with a constant bolted onto the end of it. # See cake.dll's Obfuscator function for details. Hopefully most DLL edits will remove # these two(?) headers to not cause issues, but given the general quality of SEGA data... - enc_ver = request.getHeader("Mai-Encoding") + enc_ver = request.headers.get("Mai-Encoding") if enc_ver is None: - enc_ver = request.getHeader("X-Mai-Encoding") + enc_ver = request.headers.get("X-Mai-Encoding") self.logger.debug( - f"Encryption v{enc_ver} - User-Agent: {request.getHeader('User-Agent')}" + f"Encryption v{enc_ver} - User-Agent: {request.headers.get('User-Agent')}" ) try: @@ -276,7 +281,7 @@ class Mai2Servlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) req_data = json.loads(unzip) @@ -293,80 +298,27 @@ class Mai2Servlet(BaseServlet): else: try: handler = getattr(handler_cls, func_to_find) - resp = handler(req_data) + resp = await handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) if resp == None: resp = {"returnCode": 1} self.logger.debug(f"Response {resp}") - return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + return Response(zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))) - def handle_old_srv(self, request: Request, game_code: str, matchers: Dict) -> bytes: - endpoint = matchers['endpoint'] - version = matchers['version'] + async def handle_old_srv(self, request: Request) -> bytes: + endpoint = request.path_params.get('endpoint') + version = request.path_params.get('version') self.logger.info(f"v{version} old server {endpoint}") - return zlib.compress(b"ok") + return Response(zlib.compress(b"ok")) - def handle_old_srv_userdata(self, request: Request, game_code: str, matchers: Dict) -> bytes: - endpoint = matchers['endpoint'] - version = matchers['version'] + async def handle_old_srv_userdata(self, request: Request) -> bytes: + endpoint = request.path_params.get('endpoint') + version = request.path_params.get('version') self.logger.info(f"v{version} old server {endpoint}") - return zlib.compress(b"{}") - - def render_GET(self, request: Request, version: int, url_path: str) -> bytes: - self.logger.debug(f"v{version} GET {url_path}") - url_split = url_path.split("/") - - if (url_split[0] == "api" and url_split[1] == "movie") or url_split[ - 0 - ] == "movie": - if url_split[2] == "moviestart": - return json.dumps({"moviestart": {"status": "OK"}}).encode() - - else: - request.setResponseCode(404) - return b"" - - elif url_split[0] == "usbdl": - if url_split[1] == "CONNECTIONTEST": - self.logger.info(f"v{version} usbdl server test") - return b"" - - elif self.game_cfg.deliver.udbdl_enable and path.exists( - f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}" - ): - with open( - f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}", "rb" - ) as f: - return f.read() - - else: - request.setResponseCode(404) - return b"" - - elif url_split[0] == "deliver": - file = url_split[len(url_split) - 1] - self.logger.info(f"v{version} {file} deliver inquire") - self.logger.debug( - f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}" - ) - - if self.game_cfg.deliver.enable and path.exists( - f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}" - ): - with open( - f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}", "rb" - ) as f: - return f.read() - - else: - request.setResponseCode(404) - return b"" - - else: - return zlib.compress(b"{}") + return Response(zlib.compress(b"{}")) diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index d25a295..a8d5b46 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -15,7 +15,7 @@ class Mai2Universe(Mai2SplashPlus): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version) if p is None: return {} @@ -30,7 +30,7 @@ class Mai2Universe(Mai2SplashPlus): "isExistSellingCard": True, } - def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: # user already exists, because the preview checks that already p = self.data.profile.get_profile_detail(data["userId"], self.version) @@ -52,13 +52,13 @@ class Mai2Universe(Mai2SplashPlus): return {"userId": data["userId"], "userData": user_data} - def handle_cm_login_api_request(self, data: Dict) -> Dict: + async def handle_cm_login_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} - def handle_cm_logout_api_request(self, data: Dict) -> Dict: + async def handle_cm_logout_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} - def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: selling_cards = self.data.static.get_enabled_cards(self.version) if selling_cards is None: return {"length": 0, "sellingCardList": []} @@ -88,7 +88,7 @@ class Mai2Universe(Mai2SplashPlus): return {"length": len(selling_card_list), "sellingCardList": selling_card_list} - def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: user_cards = self.data.item.get_cards(data["userId"]) if user_cards is None: return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []} @@ -124,10 +124,10 @@ class Mai2Universe(Mai2SplashPlus): "userCardList": card_list[start_idx:end_idx], } - def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: super().handle_get_user_item_api_request(data) - def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: characters = self.data.item.get_characters(data["userId"]) chara_list = [] @@ -153,10 +153,10 @@ class Mai2Universe(Mai2SplashPlus): "userCharacterList": chara_list, } - def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: return {"length": 0, "userPrintDetailList": []} - def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: user_id = data["userId"] upsert = data["userPrintDetail"] @@ -209,12 +209,12 @@ class Mai2Universe(Mai2SplashPlus): "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT), } - def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: return { "returnCode": 1, "orderId": 0, "serialId": data["userPrintlog"]["serialId"], } - def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: return {"returnCode": 1} diff --git a/titles/mai2/universeplus.py b/titles/mai2/universeplus.py index e45c719..909300e 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -11,8 +11,8 @@ class Mai2UniversePlus(Mai2Universe): super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - user_data = super().handle_cm_get_user_preview_api_request(data) + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) # hardcode lastDataVersion for CardMaker 1.35 user_data["lastDataVersion"] = "1.25.00" diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index 596fb22..2d50184 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -103,7 +103,7 @@ class OngekiBase: self.game = OngekiConstants.GAME_CODE self.version = OngekiConstants.VER_ONGEKI - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # if reboot start/end time is not defined use the default behavior of being a few hours ago if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "": reboot_start = datetime.strftime( @@ -148,7 +148,7 @@ class OngekiBase: "isAou": "true", } - def handle_get_game_idlist_api_request(self, data: Dict) -> Dict: + async def handle_get_game_idlist_api_request(self, data: Dict) -> Dict: """ Gets lists of song IDs, either disabled songs or recomended songs depending on type? """ @@ -156,7 +156,7 @@ class OngekiBase: # id - int return {"type": data["type"], "length": 0, "gameIdlistList": []} - def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: + async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: game_ranking_list = self.data.static.get_ranking_list(self.version) ranking_list = [] @@ -171,7 +171,7 @@ class OngekiBase: "gameRankingList": ranking_list, } - def handle_get_game_point_api_request(self, data: Dict) -> Dict: + async def handle_get_game_point_api_request(self, data: Dict) -> Dict: get_game_point = self.data.static.get_static_game_point() game_point = [] @@ -194,16 +194,16 @@ class OngekiBase: "gamePointList": game_point, } - def handle_game_login_api_request(self, data: Dict) -> Dict: + async def handle_game_login_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "gameLogin"} - def handle_game_logout_api_request(self, data: Dict) -> Dict: + async def handle_game_logout_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "gameLogout"} - def handle_extend_lock_time_api_request(self, data: Dict) -> Dict: + async def handle_extend_lock_time_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "ExtendLockTimeApi"} - def handle_get_game_reward_api_request(self, data: Dict) -> Dict: + async def handle_get_game_reward_api_request(self, data: Dict) -> Dict: get_game_rewards = self.data.static.get_reward_list(self.version) reward_list = [] @@ -221,7 +221,7 @@ class OngekiBase: "gameRewardList": reward_list, } - def handle_get_game_present_api_request(self, data: Dict) -> Dict: + async def handle_get_game_present_api_request(self, data: Dict) -> Dict: get_present = self.data.static.get_present_list(self.version) present_list = [] @@ -238,13 +238,13 @@ class OngekiBase: "gamePresentList": present_list, } - def handle_get_game_message_api_request(self, data: Dict) -> Dict: + async def handle_get_game_message_api_request(self, data: Dict) -> Dict: return {"length": 0, "gameMessageList": []} - def handle_get_game_sale_api_request(self, data: Dict) -> Dict: + async def handle_get_game_sale_api_request(self, data: Dict) -> Dict: return {"length": 0, "gameSaleList": []} - def handle_get_game_tech_music_api_request(self, data: Dict) -> Dict: + async def handle_get_game_tech_music_api_request(self, data: Dict) -> Dict: music_list = self.data.static.get_tech_music(self.version) prep_music_list = [] @@ -262,7 +262,7 @@ class OngekiBase: "gameTechMusicList": prep_music_list, } - def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: if self.core_cfg.server.is_develop: return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} @@ -273,7 +273,7 @@ class OngekiBase: self.data.static.put_client_setting_data(cab['id'], client_setting_data) return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} - def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: if self.core_cfg.server.is_develop: return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} @@ -282,16 +282,16 @@ class OngekiBase: self.data.static.put_client_testmode_data(region_id, client_testmode_data) return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} - def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "upsertClientBookkeeping"} - def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "upsertClientDevelop"} - def handle_upsert_client_error_api_request(self, data: Dict) -> Dict: + async def handle_upsert_client_error_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "upsertClientError"} - def handle_upsert_user_gplog_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_gplog_api_request(self, data: Dict) -> Dict: user = data["userId"] if user >= 200000000000000: # Account for guest play user = None @@ -309,10 +309,10 @@ class OngekiBase: return {"returnCode": 1, "apiName": "UpsertUserGplogApi"} - def handle_extend_lock_time_api_request(self, data: Dict) -> Dict: + async def handle_extend_lock_time_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "ExtendLockTimeApi"} - def handle_get_game_event_api_request(self, data: Dict) -> Dict: + async def handle_get_game_event_api_request(self, data: Dict) -> Dict: evts = self.data.static.get_enabled_events(self.version) if evts is None: @@ -342,7 +342,7 @@ class OngekiBase: "gameEventList": evt_list, } - def handle_get_game_id_list_api_request(self, data: Dict) -> Dict: + async def handle_get_game_id_list_api_request(self, data: Dict) -> Dict: game_idlist: List[str, Any] = [] # 1 to 230 & 8000 to 8050 if data["type"] == 1: @@ -362,10 +362,10 @@ class OngekiBase: "gameIdlistList": game_idlist, } - def handle_get_user_region_api_request(self, data: Dict) -> Dict: + async def handle_get_user_region_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userRegionList": []} - def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + async def handle_get_user_preview_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_preview(data["userId"], self.version) if profile is None: @@ -417,7 +417,7 @@ class OngekiBase: "isWarningConfirmed": True, } - def handle_get_user_tech_count_api_request(self, data: Dict) -> Dict: + async def handle_get_user_tech_count_api_request(self, data: Dict) -> Dict: """ Gets the number of AB and ABPs a player has per-difficulty (7, 7+, 8, etc) The game sends this in upsert so we don't have to calculate it all out thankfully @@ -436,7 +436,7 @@ class OngekiBase: "userTechCountList": userTechCountList, } - def handle_get_user_tech_event_api_request(self, data: Dict) -> Dict: + async def handle_get_user_tech_event_api_request(self, data: Dict) -> Dict: user_tech_event_list = self.data.item.get_tech_event(self.version, data["userId"]) if user_tech_event_list is None: return {} @@ -455,7 +455,7 @@ class OngekiBase: "userTechEventList": tech_evt, } - def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict: + async def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict: user_tech_event_ranks = self.data.item.get_tech_event_ranking(self.version, data["userId"]) if user_tech_event_ranks is None: return { @@ -481,7 +481,7 @@ class OngekiBase: "userTechEventRankingList": evt_ranking, } - def handle_get_user_kop_api_request(self, data: Dict) -> Dict: + async def handle_get_user_kop_api_request(self, data: Dict) -> Dict: kop_list = self.data.profile.get_kop(data["userId"]) if kop_list is None: return {} @@ -496,7 +496,7 @@ class OngekiBase: "userKopList": kop_list, } - def handle_get_user_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_music_api_request(self, data: Dict) -> Dict: song_list = self.util_generate_music_list(data["userId"]) max_ct = data["maxCount"] next_idx = data["nextIndex"] @@ -516,7 +516,7 @@ class OngekiBase: "userMusicList": song_list[start_idx:end_idx], } - def handle_get_user_item_api_request(self, data: Dict) -> Dict: + async def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = data["nextIndex"] / 10000000000 p = self.data.item.get_items(data["userId"], kind) @@ -552,7 +552,7 @@ class OngekiBase: "userItemList": items, } - def handle_get_user_option_api_request(self, data: Dict) -> Dict: + async def handle_get_user_option_api_request(self, data: Dict) -> Dict: o = self.data.profile.get_profile_options(data["userId"]) if o is None: return {} @@ -566,7 +566,7 @@ class OngekiBase: return {"userId": data["userId"], "userOption": user_opts} - def handle_get_user_data_api_request(self, data: Dict) -> Dict: + async def handle_get_user_data_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_data(data["userId"], self.version) if p is None: return {} @@ -594,7 +594,7 @@ class OngekiBase: return {"userId": data["userId"], "userData": user_data} - def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict: + async def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict: user_event_ranking_list = self.data.item.get_ranking_event_ranks(self.version, data["userId"]) if user_event_ranking_list is None: return {} @@ -617,7 +617,7 @@ class OngekiBase: "userEventRankingList": prep_event_ranking, } - def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: + async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: user_login_bonus_list = self.data.item.get_login_bonuses(data["userId"]) if user_login_bonus_list is None: return {} @@ -635,7 +635,7 @@ class OngekiBase: "userLoginBonusList": login_bonuses, } - def handle_get_user_bp_base_request(self, data: Dict) -> Dict: + async def handle_get_user_bp_base_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile( self.game, self.version, user_id=data["userId"] ) @@ -648,7 +648,7 @@ class OngekiBase: "userBpBaseList": profile["userBpBaseList"], } - def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: + async def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: recent_rating = self.data.profile.get_profile_recent_rating(data["userId"]) if recent_rating is None: return { @@ -665,7 +665,7 @@ class OngekiBase: "userRecentRatingList": userRecentRatingList, } - def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + async def handle_get_user_activity_api_request(self, data: Dict) -> Dict: activity = self.data.profile.get_profile_activity(data["userId"], data["kind"]) if activity is None: return {} @@ -692,7 +692,7 @@ class OngekiBase: "userActivityList": user_activity, } - def handle_get_user_story_api_request(self, data: Dict) -> Dict: + async def handle_get_user_story_api_request(self, data: Dict) -> Dict: user_stories = self.data.item.get_stories(data["userId"]) if user_stories is None: return {} @@ -710,7 +710,7 @@ class OngekiBase: "userStoryList": story_list, } - def handle_get_user_chapter_api_request(self, data: Dict) -> Dict: + async def handle_get_user_chapter_api_request(self, data: Dict) -> Dict: user_chapters = self.data.item.get_chapters(data["userId"]) if user_chapters is None: return {} @@ -728,14 +728,14 @@ class OngekiBase: "userChapterList": chapter_list, } - def handle_get_user_training_room_by_key_api_request(self, data: Dict) -> Dict: + async def handle_get_user_training_room_by_key_api_request(self, data: Dict) -> Dict: return { "userId": data["userId"], "length": 0, "userTrainingRoomList": [], } - def handle_get_user_character_api_request(self, data: Dict) -> Dict: + async def handle_get_user_character_api_request(self, data: Dict) -> Dict: user_characters = self.data.item.get_characters(data["userId"]) if user_characters is None: return {} @@ -753,7 +753,7 @@ class OngekiBase: "userCharacterList": character_list, } - def handle_get_user_card_api_request(self, data: Dict) -> Dict: + async def handle_get_user_card_api_request(self, data: Dict) -> Dict: user_cards = self.data.item.get_cards(data["userId"]) if user_cards is None: return {} @@ -771,7 +771,7 @@ class OngekiBase: "userCardList": card_list, } - def handle_get_user_deck_by_key_api_request(self, data: Dict) -> Dict: + async def handle_get_user_deck_by_key_api_request(self, data: Dict) -> Dict: # Auth key doesn't matter, it just wants all the decks decks = self.data.item.get_decks(data["userId"]) if decks is None: @@ -790,7 +790,7 @@ class OngekiBase: "userDeckList": deck_list, } - def handle_get_user_trade_item_api_request(self, data: Dict) -> Dict: + async def handle_get_user_trade_item_api_request(self, data: Dict) -> Dict: user_trade_items = self.data.item.get_trade_items(data["userId"]) if user_trade_items is None: return {} @@ -808,7 +808,7 @@ class OngekiBase: "userTradeItemList": trade_item_list, } - def handle_get_user_scenario_api_request(self, data: Dict) -> Dict: + async def handle_get_user_scenario_api_request(self, data: Dict) -> Dict: user_scenerio = self.data.item.get_scenerios(data["userId"]) if user_scenerio is None: return {} @@ -826,7 +826,7 @@ class OngekiBase: "userScenarioList": scenerio_list, } - def handle_get_user_ratinglog_api_request(self, data: Dict) -> Dict: + async def handle_get_user_ratinglog_api_request(self, data: Dict) -> Dict: rating_log = self.data.profile.get_profile_rating_log(data["userId"]) if rating_log is None: return {} @@ -844,7 +844,7 @@ class OngekiBase: "userRatinglogList": userRatinglogList, } - def handle_get_user_mission_point_api_request(self, data: Dict) -> Dict: + async def handle_get_user_mission_point_api_request(self, data: Dict) -> Dict: user_mission_point_list = self.data.item.get_mission_points(self.version, data["userId"]) if user_mission_point_list is None: return {} @@ -864,7 +864,7 @@ class OngekiBase: "userMissionPointList": mission_point_list, } - def handle_get_user_event_point_api_request(self, data: Dict) -> Dict: + async def handle_get_user_event_point_api_request(self, data: Dict) -> Dict: user_event_point_list = self.data.item.get_event_points(data["userId"]) if user_event_point_list is None: return {} @@ -886,7 +886,7 @@ class OngekiBase: "userEventPointList": event_point_list, } - def handle_get_user_music_item_api_request(self, data: Dict) -> Dict: + async def handle_get_user_music_item_api_request(self, data: Dict) -> Dict: user_music_item_list = self.data.item.get_music_items(data["userId"]) if user_music_item_list is None: return {} @@ -904,7 +904,7 @@ class OngekiBase: "userMusicItemList": music_item_list, } - def handle_get_user_event_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_event_music_api_request(self, data: Dict) -> Dict: user_evt_music_list = self.data.item.get_event_music(data["userId"]) if user_evt_music_list is None: return {} @@ -922,7 +922,7 @@ class OngekiBase: "userEventMusicList": evt_music_list, } - def handle_get_user_boss_api_request(self, data: Dict) -> Dict: + async def handle_get_user_boss_api_request(self, data: Dict) -> Dict: p = self.data.item.get_bosses(data["userId"]) if p is None: return {} @@ -940,7 +940,7 @@ class OngekiBase: "userBossList": boss_list, } - def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: upsert = data["upsertUserAll"] user_id = data["userId"] @@ -1070,7 +1070,7 @@ class OngekiBase: return {"returnCode": 1, "apiName": "upsertUserAll"} - def handle_get_user_rival_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rival_api_request(self, data: Dict) -> Dict: """ Added in Bright """ @@ -1094,7 +1094,7 @@ class OngekiBase: "userRivalList": rival_list, } - def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: """ Added in Bright """ @@ -1112,7 +1112,7 @@ class OngekiBase: "userRivalDataList": rivals, } - def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: """ Added in Bright """ diff --git a/titles/ongeki/bright.py b/titles/ongeki/bright.py index 49d6216..0af5127 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -15,13 +15,13 @@ class OngekiBright(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_BRIGHT - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.30.00" ret["gameSetting"]["onlineDataVersion"] = "1.30.00" return ret - def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: # check for a bright profile p = self.data.profile.get_profile_data(data["userId"], self.version) if p is None: @@ -55,13 +55,13 @@ class OngekiBright(OngekiBase): return {"userId": data["userId"], "userData": user_data} - def handle_printer_login_api_request(self, data: Dict): + async def handle_printer_login_api_request(self, data: Dict): return {"returnCode": 1} - def handle_printer_logout_api_request(self, data: Dict): + async def handle_printer_logout_api_request(self, data: Dict): return {"returnCode": 1} - def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: user_cards = self.data.item.get_cards(data["userId"]) if user_cards is None: return {} @@ -90,7 +90,7 @@ class OngekiBright(OngekiBase): "userCardList": card_list[start_idx:end_idx], } - def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: user_characters = self.data.item.get_characters(data["userId"]) if user_characters is None: return { @@ -124,7 +124,7 @@ class OngekiBright(OngekiBase): "userCharacterList": character_list[start_idx:end_idx], } - def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: + async def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: user_gachas = self.data.item.get_user_gachas(data["userId"]) if user_gachas is None: return {"userId": data["userId"], "length": 0, "userGachaList": []} @@ -143,10 +143,10 @@ class OngekiBright(OngekiBase): "userGachaList": user_gacha_list, } - def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: return self.handle_get_user_item_api_request(data) - def handle_cm_get_user_gacha_supply_api_request(self, data: Dict) -> Dict: + async def handle_cm_get_user_gacha_supply_api_request(self, data: Dict) -> Dict: # not used for now? not sure what it even does user_gacha_supplies = self.data.item.get_user_gacha_supplies(data["userId"]) if user_gacha_supplies is None: @@ -160,7 +160,7 @@ class OngekiBright(OngekiBase): "supplyCardList": supply_list, } - def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: + async def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: """ returns all current active banners (gachas) "Select Gacha" requires maxSelectPoint set and isCeiling set to 1 @@ -207,7 +207,7 @@ class OngekiBright(OngekiBase): "registIdList": [], } - def handle_roll_gacha_api_request(self, data: Dict) -> Dict: + async def handle_roll_gacha_api_request(self, data: Dict) -> Dict: """ Handle a gacha roll API request """ @@ -323,7 +323,7 @@ class OngekiBright(OngekiBase): "gameGachaCardList": game_gacha_card_list, } - def handle_cm_upsert_user_gacha_api_request(self, data: Dict): + async def handle_cm_upsert_user_gacha_api_request(self, data: Dict): upsert = data["cmUpsertUserGacha"] user_id = data["userId"] @@ -405,7 +405,7 @@ class OngekiBright(OngekiBase): return {"returnCode": 1, "apiName": "CMUpsertUserGachaApi"} - def handle_cm_upsert_user_select_gacha_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_select_gacha_api_request(self, data: Dict) -> Dict: upsert = data["cmUpsertUserSelectGacha"] user_id = data["userId"] @@ -442,7 +442,7 @@ class OngekiBright(OngekiBase): return {"returnCode": 1, "apiName": "cmUpsertUserSelectGacha"} - def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: + async def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"]) if game_gacha_cards == []: # fallback to be at least able to select that gacha @@ -522,7 +522,7 @@ class OngekiBright(OngekiBase): "ssrBookCalcList": [], } - def handle_get_game_theater_api_request(self, data: Dict) -> Dict: + async def handle_get_game_theater_api_request(self, data: Dict) -> Dict: """ shows a banner after every print, not sure what its used for """ @@ -548,7 +548,7 @@ class OngekiBright(OngekiBase): return {"length": 0, "gameTheaterList": [], "registIdList": []} - def handle_cm_upsert_user_print_playlog_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_print_playlog_api_request(self, data: Dict) -> Dict: return { "returnCode": 1, "orderId": 0, @@ -556,7 +556,7 @@ class OngekiBright(OngekiBase): "apiName": "CMUpsertUserPrintPlaylogApi", } - def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: return { "returnCode": 1, "orderId": 0, @@ -564,7 +564,7 @@ class OngekiBright(OngekiBase): "apiName": "CMUpsertUserPrintlogApi", } - def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: user_print_detail = data["userPrintDetail"] # generate random serial id @@ -589,7 +589,7 @@ class OngekiBright(OngekiBase): "apiName": "CMUpsertUserPrintApi", } - def handle_cm_upsert_user_all_api_request(self, data: Dict) -> Dict: + async def handle_cm_upsert_user_all_api_request(self, data: Dict) -> Dict: upsert = data["cmUpsertUserAll"] user_id = data["userId"] diff --git a/titles/ongeki/brightmemory.py b/titles/ongeki/brightmemory.py index d7103a3..0bff264 100644 --- a/titles/ongeki/brightmemory.py +++ b/titles/ongeki/brightmemory.py @@ -15,8 +15,8 @@ class OngekiBrightMemory(OngekiBright): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.35.00" ret["gameSetting"]["onlineDataVersion"] = "1.35.00" ret["gameSetting"]["maxCountCharacter"] = 50 @@ -27,7 +27,7 @@ class OngekiBrightMemory(OngekiBright): ret["gameSetting"]["maxCountRivalMusic"] = 300 return ret - def handle_get_user_memory_chapter_api_request(self, data: Dict) -> Dict: + async def handle_get_user_memory_chapter_api_request(self, data: Dict) -> Dict: memories = self.data.item.get_memorychapters(data["userId"]) if not memories: return { @@ -134,5 +134,5 @@ class OngekiBrightMemory(OngekiBright): "userMemoryChapterList": memory_chp, } - def handle_get_game_music_release_state_api_request(self, data: Dict) -> Dict: + async def handle_get_game_music_release_state_api_request(self, data: Dict) -> Dict: return {"techScore": 0, "cardNum": 0} diff --git a/titles/ongeki/frontend.py b/titles/ongeki/frontend.py index 987776f..4c17165 100644 --- a/titles/ongeki/frontend.py +++ b/titles/ongeki/frontend.py @@ -1,6 +1,6 @@ import yaml import jinja2 -from twisted.web.http import Request +from starlette.requests import Request from os import path from twisted.web.util import redirectTo from twisted.web.server import Session diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index baf1ee3..f80214b 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -1,4 +1,6 @@ -from twisted.web.http import Request +from starlette.requests import Request +from starlette.routing import Route +from starlette.responses import Response import json import inflection import yaml @@ -120,35 +122,28 @@ class OngekiServlet(BaseServlet): return True - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [("render_POST", "/SDDT/{version}/{endpoint}", {})] - ) + def get_routes(self) -> List[Route]: + return [ + Route("/SDDT/{version:int}/{endpoint:str}", self.render_POST, methods=['POST']) + ] def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: title_port_int = Utils.get_title_port(self.core_cfg) - title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg) proto = "https" if self.game_cfg.server.use_https and game_ver >= 120 else "http" - - if proto == "https": - t_port = f":{title_port_ssl_int}" if title_port_ssl_int and not self.core_cfg.server.is_using_proxy else "" - - else: - t_port = f":{title_port_int}" if title_port_int and not self.core_cfg.server.is_using_proxy else "" + t_port = f":{title_port_int}" if title_port_int and not self.core_cfg.server.is_using_proxy else "" return ( - f"{proto}://{self.core_cfg.title.hostname}{t_port}/{game_code}/{game_ver}/", - f"{self.core_cfg.title.hostname}{t_port}/", + f"{proto}://{self.core_cfg.server.hostname}{t_port}/{game_code}/{game_ver}/", + f"{self.core_cfg.server.hostname}{t_port}/", ) - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: - endpoint = matchers['endpoint'] - version = int(matchers['version']) + async def render_POST(self, request: Request) -> bytes: + endpoint: str = request.path_params.get('endpoint', '') + version: int = request.path_params.get('version', 0) if endpoint.lower() == "ping": - return zlib.compress(b'{"returnCode": 1}') + return Response(zlib.compress(b'{"returnCode": 1}')) - req_raw = request.content.getvalue() + req_raw = await request.body() encrtped = False internal_ver = 0 client_ip = Utils.get_ip_addr(request) @@ -178,13 +173,13 @@ class OngekiServlet(BaseServlet): self.logger.error( f"v{version} does not support encryption or no keys entered" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) elif endpoint.lower() not in self.hash_table[internal_ver]: self.logger.error( f"No hash found for v{version} endpoint {endpoint}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) endpoint = self.hash_table[internal_ver][endpoint.lower()] @@ -201,7 +196,7 @@ class OngekiServlet(BaseServlet): self.logger.error( f"Failed to decrypt v{version} request to {endpoint} -> {e}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) encrtped = True @@ -213,7 +208,7 @@ class OngekiServlet(BaseServlet): self.logger.error( f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) try: unzip = zlib.decompress(req_raw) @@ -222,7 +217,7 @@ class OngekiServlet(BaseServlet): self.logger.error( f"Failed to decompress v{version} {endpoint} request -> {e}" ) - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) req_data = json.loads(unzip) @@ -235,15 +230,15 @@ class OngekiServlet(BaseServlet): if not hasattr(self.versions[internal_ver], func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") - return zlib.compress(b'{"returnCode": 1}') + return Response(zlib.compress(b'{"returnCode": 1}')) try: handler = getattr(self.versions[internal_ver], func_to_find) - resp = handler(req_data) + resp = await handler(req_data) except Exception as e: self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress(b'{"stat": "0"}') + return Response(zlib.compress(b'{"stat": "0"}')) if resp == None: resp = {"returnCode": 1} @@ -253,7 +248,7 @@ class OngekiServlet(BaseServlet): zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) if not encrtped or version < 120: - return zipped + return Response(zipped) padded = pad(zipped, 16) @@ -263,4 +258,4 @@ class OngekiServlet(BaseServlet): bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), ) - return crypt.encrypt(padded) \ No newline at end of file + return Response(crypt.encrypt(padded)) \ No newline at end of file diff --git a/titles/ongeki/plus.py b/titles/ongeki/plus.py index 9168576..12c46b2 100644 --- a/titles/ongeki/plus.py +++ b/titles/ongeki/plus.py @@ -11,8 +11,8 @@ class OngekiPlus(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.05.00" ret["gameSetting"]["onlineDataVersion"] = "1.05.00" return ret diff --git a/titles/ongeki/red.py b/titles/ongeki/red.py index 52b9d59..eac20ae 100644 --- a/titles/ongeki/red.py +++ b/titles/ongeki/red.py @@ -11,8 +11,8 @@ class OngekiRed(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_RED - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.20.00" ret["gameSetting"]["onlineDataVersion"] = "1.20.00" return ret diff --git a/titles/ongeki/redplus.py b/titles/ongeki/redplus.py index 1f69690..99e63db 100644 --- a/titles/ongeki/redplus.py +++ b/titles/ongeki/redplus.py @@ -11,8 +11,8 @@ class OngekiRedPlus(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_RED_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.25.00" ret["gameSetting"]["onlineDataVersion"] = "1.25.00" ret["gameSetting"]["maxCountCharacter"] = 50 diff --git a/titles/ongeki/summer.py b/titles/ongeki/summer.py index adc8c0f..c3f2ce0 100644 --- a/titles/ongeki/summer.py +++ b/titles/ongeki/summer.py @@ -11,8 +11,8 @@ class OngekiSummer(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_SUMMER - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.10.00" ret["gameSetting"]["onlineDataVersion"] = "1.10.00" return ret diff --git a/titles/ongeki/summerplus.py b/titles/ongeki/summerplus.py index 8b2cd03..576d615 100644 --- a/titles/ongeki/summerplus.py +++ b/titles/ongeki/summerplus.py @@ -11,8 +11,8 @@ class OngekiSummerPlus(OngekiBase): super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_SUMMER_PLUS - def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - ret = super().handle_get_game_setting_api_request(data) + async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = await super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.15.00" ret["gameSetting"]["onlineDataVersion"] = "1.15.00" return ret diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 9663e81..9eab298 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -20,21 +20,21 @@ class PokkenBase: self.data = PokkenData(core_cfg) self.SUPPORT_SET_NONE = 4294967295 - def handle_noop(self, request: Any) -> bytes: + async def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = request.type return res.SerializeToString() - def handle_ping(self, request: jackal_pb2.Request) -> bytes: + async def handle_ping(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.PING return res.SerializeToString() - def handle_register_pcb(self, request: jackal_pb2.Request) -> bytes: + async def handle_register_pcb(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.REGISTER_PCB @@ -61,35 +61,35 @@ class PokkenBase: "logfilename": "JackalMatchingLibrary.log", "biwalogfilename": "./biwa.log", } - regist_pcb.bnp_baseuri = f"{self.core_cfg.title.hostname}/bna" + regist_pcb.bnp_baseuri = f"{self.core_cfg.server.hostname}/bna" regist_pcb.biwa_setting = json.dumps(biwa_setting) res.register_pcb.CopyFrom(regist_pcb) return res.SerializeToString() - def handle_save_ads(self, request: jackal_pb2.Request) -> bytes: + async def handle_save_ads(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.SAVE_ADS return res.SerializeToString() - def handle_save_client_log(self, request: jackal_pb2.Request) -> bytes: + async def handle_save_client_log(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.SAVE_CLIENT_LOG return res.SerializeToString() - def handle_check_diagnosis(self, request: jackal_pb2.Request) -> bytes: + async def handle_check_diagnosis(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.CHECK_DIAGNOSIS return res.SerializeToString() - def handle_load_client_settings(self, request: jackal_pb2.Request) -> bytes: + async def handle_load_client_settings(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS @@ -112,7 +112,7 @@ class PokkenBase: return res.SerializeToString() - def handle_load_ranking(self, request: jackal_pb2.Request) -> bytes: + async def handle_load_ranking(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.LOAD_RANKING @@ -126,7 +126,7 @@ class PokkenBase: res.load_ranking.CopyFrom(ranking) return res.SerializeToString() - def handle_load_user(self, request: jackal_pb2.Request) -> bytes: + async def handle_load_user(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.LOAD_USER @@ -287,13 +287,13 @@ class PokkenBase: res.load_user.CopyFrom(load_usr) return res.SerializeToString() - def handle_set_bnpassid_lock(self, data: jackal_pb2.Request) -> bytes: + async def handle_set_bnpassid_lock(self, data: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK return res.SerializeToString() - def handle_save_user(self, request: jackal_pb2.Request) -> bytes: + async def handle_save_user(self, request: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.SAVE_USER @@ -394,38 +394,31 @@ class PokkenBase: return res.SerializeToString() - def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: + async def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.SAVE_INGAME_LOG return res.SerializeToString() - def handle_save_charge(self, data: jackal_pb2.Request) -> bytes: + async def handle_save_charge(self, data: jackal_pb2.Request) -> bytes: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.SAVE_CHARGE return res.SerializeToString() - def handle_matching_noop( + async def handle_matching_noop( self, data: Dict = {}, client_ip: str = "127.0.0.1" ) -> Dict: return {} - def handle_matching_start_matching( + async def handle_matching_start_matching( self, data: Dict = {}, client_ip: str = "127.0.0.1" ) -> Dict: return {} - def handle_matching_is_matching( + async def handle_matching_is_matching( self, data: Dict = {}, client_ip: str = "127.0.0.1" ) -> Dict: - """ - "sessionId":"12345678", - "A":{ - "pcb_id": data["data"]["must"]["pcb_id"], - "gip": client_ip - }, - """ return { "data": { "sessionId":"12345678", @@ -437,15 +430,15 @@ class PokkenBase: } } - def handle_matching_stop_matching( + async def handle_matching_stop_matching( self, data: Dict = {}, client_ip: str = "127.0.0.1" ) -> Dict: return {} - def handle_admission_noop(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict: + async def handle_admission_noop(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict: return {} - def handle_admission_joinsession(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict: + async def handle_admission_joinsession(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict: self.logger.info(f"Admission: JoinSession from {req_ip}") return { 'data': { diff --git a/titles/pokken/const.py b/titles/pokken/const.py index 9fa3b06..4983d50 100644 --- a/titles/pokken/const.py +++ b/titles/pokken/const.py @@ -3,6 +3,7 @@ from enum import Enum class PokkenConstants: GAME_CODE = "SDAK" + GAME_CDS = ["PKF1"] CONFIG_NAME = "pokken.yaml" @@ -10,6 +11,12 @@ class PokkenConstants: VERSION_NAMES = "Pokken Tournament" + SERIAL_IDENT = [2747] + NETID_PREFIX = ["ABGN"] + SERIAL_REGIONS = [1] + SERIAL_ROLES = [3] + SERIAL_CAB_IDENTS = [19] + class BATTLE_TYPE(Enum): TUTORIAL = 1 AI = 2 diff --git a/titles/pokken/frontend.py b/titles/pokken/frontend.py index af344dc..be22c50 100644 --- a/titles/pokken/frontend.py +++ b/titles/pokken/frontend.py @@ -1,6 +1,6 @@ import yaml import jinja2 -from twisted.web.http import Request +from starlette.requests import Request from os import path from twisted.web.server import Session diff --git a/titles/pokken/index.py b/titles/pokken/index.py index 30b6617..ffd916c 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -1,8 +1,10 @@ from typing import Tuple, List, Dict -from twisted.web.http import Request -from twisted.web import resource -from twisted.internet import reactor -import json, ast +from starlette.requests import Request +from starlette.requests import Request +from starlette.responses import Response, JSONResponse +from starlette.routing import Route, WebSocketRoute +from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect +import ast from datetime import datetime import yaml import logging, coloredlogs @@ -17,8 +19,6 @@ from .config import PokkenConfig from .base import PokkenBase from .const import PokkenConstants from .proto import jackal_pb2 -from .services import PokkenAdmissionFactory - class PokkenServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: @@ -69,47 +69,73 @@ class PokkenServlet(BaseServlet): return True - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [ - ("render_POST", "/pokken/", {}), - ("handle_matching", "/pokken/matching", {}), - ] - ) + def get_routes(self) -> List[Route]: + return [ + Route("/pokken/", self.render_POST, methods=['POST']), + Route("/pokken/matching", self.handle_matching, methods=['POST']), + WebSocketRoute("/pokken/admission", self.handle_admission) + ] def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: - if self.game_cfg.ports.game != 443: - return ( - f"https://{self.game_cfg.server.hostname}:{self.game_cfg.ports.game}/pokken/", - f"{self.game_cfg.server.hostname}/pokken/", - ) return ( - f"https://{self.game_cfg.server.hostname}/pokken/", - f"{self.game_cfg.server.hostname}/pokken/", + f"https://{self.game_cfg.server.hostname}:{self.game_cfg.ports.game}/pokken/", + f"{self.game_cfg.server.hostname}:{self.game_cfg.ports.game}/pokken/", ) def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str]: - game_cfg = PokkenConfig() + if not self.game_cfg.server.enable: + return (False, [], []) - if path.exists(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"): - game_cfg.update( - yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}")) - ) + return (True, PokkenConstants.GAME_CDS, PokkenConstants.NETID_PREFIX) + + async def handle_admission(self, ws: WebSocket) -> None: + client_ip = Utils.get_ip_addr(ws) + await ws.accept() + while True: + try: + msg: Dict = await ws.receive_json() + except WebSocketDisconnect as e: + self.logger.debug(f"Client {client_ip} disconnected - {e}") + break + except Exception as e: + self.logger.error(f"Could not load JSON from message from {client_ip} - {e}") + if ws.client_state != WebSocketState.DISCONNECTED: + await ws.close() + break + + self.logger.debug(f"Admission: Message from {client_ip}:{ws.client.port} - {msg}") + + api = msg.get("api", "noop") + handler = getattr(self.base, f"handle_admission_{api.lower()}") + resp = await handler(msg, client_ip) + + if resp is None: + resp = {} - if not game_cfg.server.enable: - return (False, "") + if "type" not in resp: + resp['type'] = "res" + if "data" not in resp: + resp['data'] = {} + if "api" not in resp: + resp['api'] = api + if "result" not in resp: + resp['result'] = 'true' + + self.logger.debug(f"Websocket response: {resp}") + try: + await ws.send_json(resp) + except WebSocketDisconnect as e: + self.logger.debug(f"Client {client_ip} disconnected - {e}") + break + except Exception as e: + self.logger.error(f"Could not send JSON message to {client_ip} - {e}") + break + + if ws.client_state != WebSocketState.DISCONNECTED: + await ws.close() - return (True, "PKF1") - - def setup(self) -> None: - if self.game_cfg.server.enable_matching: - reactor.listenTCP( - self.game_cfg.ports.admission, PokkenAdmissionFactory(self.core_cfg, self.game_cfg) - ) - - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: - content = request.content.getvalue() + async def render_POST(self, request: Request) -> bytes: + content = await request.body() if content == b"": self.logger.info("Empty request") return b"" @@ -134,19 +160,19 @@ class PokkenServlet(BaseServlet): self.logger.info(f"{endpoint} request from {Utils.get_ip_addr(request)}") - ret = handler(pokken_request) - return ret + ret = await handler(pokken_request) + return Response(ret) - def handle_matching(self, request: Request, game_code: str, matchers: Dict) -> bytes: + async def handle_matching(self, request: Request) -> bytes: if not self.game_cfg.server.enable_matching: - return b"" + return Response() - content = request.content.getvalue() + content = await request.body() client_ip = Utils.get_ip_addr(request) if content is None or content == b"": self.logger.info("Empty matching request") - return json.dumps(self.base.handle_matching_noop()).encode() + return JSONResponse(self.base.handle_matching_noop()) json_content = ast.literal_eval( content.decode() @@ -166,7 +192,7 @@ class PokkenServlet(BaseServlet): self.logger.warning( f"No handler found for message type {json_content['call']}" ) - return json.dumps(self.base.handle_matching_noop()).encode() + return JSONResponse(self.base.handle_matching_noop()) ret = handler(json_content, client_ip) @@ -181,4 +207,4 @@ class PokkenServlet(BaseServlet): self.logger.debug(f"Response {ret}") - return json.dumps(ret).encode() + return JSONResponse(ret) diff --git a/titles/sao/base.py b/titles/sao/base.py index 09a9f88..9d933ed 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -32,42 +32,42 @@ class SaoBase: self.logger.warning(f"Failed to find csv file {file}.csv") return ret - def handle_noop(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_noop(self, header: SaoRequestHeader, request: bytes) -> bytes: self.logger.info(f"Using Generic handler") resp_thing = SaoNoopResponse(header.cmd + 1) return resp_thing.make() - def handle_c122(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c122(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/get_maintenance_info resp = SaoGetMaintResponse(header.cmd +1) return resp.make() - def handle_c12e(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c12e(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/ac_cabinet_boot_notification resp = SaoCommonAcCabinetBootNotificationResponse(header.cmd +1) return resp.make() - def handle_c100(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c100(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/get_app_versions resp = SaoCommonGetAppVersionsRequest(header.cmd +1) return resp.make() - def handle_c102(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c102(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/master_data_version_check resp = SaoMasterDataVersionCheckResponse(header.cmd +1) return resp.make() - def handle_c10a(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c10a(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/paying_play_start resp = SaoCommonPayingPlayStartRequest(header.cmd +1) return resp.make() - def handle_ca02(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_ca02(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest_multi_play_room/get_quest_scene_multi_play_photon_server resp = SaoGetQuestSceneMultiPlayPhotonServerResponse(header.cmd +1) return resp.make() - def handle_c11e(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c11e(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/get_auth_card_data req = SaoGetAuthCardDataRequest(header, request) @@ -120,12 +120,12 @@ class SaoBase: resp = SaoGetAuthCardDataResponse(header.cmd +1, profile_data) return resp.make() - def handle_c40c(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c40c(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/check_ac_login_bonus resp = SaoHomeCheckAcLoginBonusResponse(header.cmd +1) return resp.make() - def handle_c104(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c104(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/login req = SaoCommonLoginRequest(header, request) @@ -135,17 +135,17 @@ class SaoBase: resp = SaoCommonLoginResponse(header.cmd +1, profile_data) return resp.make() - def handle_c404(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c404(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/check_comeback_event resp = SaoCheckComebackEventRequest(header.cmd +1) return resp.make() - def handle_c000(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c000(self, header: SaoRequestHeader, request: bytes) -> bytes: #ticket/ticket resp = SaoTicketResponse(header.cmd +1) return resp.make() - def handle_c500(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c500(self, header: SaoRequestHeader, request: bytes) -> bytes: #user_info/get_user_basic_data req = SaoGetUserBasicDataRequest(header, request) @@ -154,7 +154,7 @@ class SaoBase: resp = SaoGetUserBasicDataResponse(header.cmd +1, profile_data) return resp.make() - def handle_c600(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c600(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_hero_log_user_data_list req = SaoGetHeroLogUserDataListRequest(header, request) @@ -163,7 +163,7 @@ class SaoBase: resp = SaoGetHeroLogUserDataListResponse(header.cmd +1, hero_data) return resp.make() - def handle_c602(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c602(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_equipment_user_data_list req = SaoGetEquipmentUserDataListRequest(header, request) @@ -172,7 +172,7 @@ class SaoBase: resp = SaoGetEquipmentUserDataListResponse(header.cmd +1, equipment_data) return resp.make() - def handle_c604(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c604(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_item_user_data_list req = SaoGetItemUserDataListRequest(header, request) @@ -181,21 +181,21 @@ class SaoBase: resp = SaoGetItemUserDataListResponse(header.cmd +1, item_data) return resp.make() - def handle_c606(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c606(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_support_log_user_data_list supportIdsData = self.game_data.static.get_support_log_ids(0, True) resp = SaoGetSupportLogUserDataListResponse(header.cmd +1, supportIdsData) return resp.make() - def handle_c800(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c800(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/get_title_user_data_list titleIdsData = self.game_data.static.get_title_ids(0, True) resp = SaoGetTitleUserDataListResponse(header.cmd +1, titleIdsData) return resp.make() - def handle_c608(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c608(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_episode_append_data_list req = SaoGetEpisodeAppendDataListRequest(header, request) @@ -204,7 +204,7 @@ class SaoBase: resp = SaoGetEpisodeAppendDataListResponse(header.cmd +1, profile_data) return resp.make() - def handle_c804(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c804(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/get_party_data_list req = SaoGetPartyDataListRequest(header, request) @@ -216,17 +216,17 @@ class SaoBase: resp = SaoGetPartyDataListResponse(header.cmd +1, hero1_data, hero2_data, hero3_data) return resp.make() - def handle_c902(self, header: SaoRequestHeader, request: bytes) -> bytes: # for whatever reason, having all entries empty or filled changes nothing + async def handle_c902(self, header: SaoRequestHeader, request: bytes) -> bytes: # for whatever reason, having all entries empty or filled changes nothing #quest/get_quest_scene_prev_scan_profile_card resp = SaoGetQuestScenePrevScanProfileCardResponse(header.cmd +1) return resp.make() - def handle_c124(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c124(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/get_resource_path_info resp = SaoGetResourcePathInfoResponse(header.cmd +1) return resp.make() - def handle_c900(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c900(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/get_quest_scene_user_data_list // QuestScene.csv req = SaoGetQuestSceneUserDataListRequest(header, request) @@ -235,22 +235,22 @@ class SaoBase: resp = SaoGetQuestSceneUserDataListResponse(header.cmd +1, quest_data) return resp.make() - def handle_c400(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c400(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/check_yui_medal_get_condition resp = SaoCheckYuiMedalGetConditionResponse(header.cmd +1) return resp.make() - def handle_c402(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c402(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/get_yui_medal_bonus_user_data resp = SaoGetYuiMedalBonusUserDataResponse(header.cmd +1) return resp.make() - def handle_c40a(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c40a(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/check_profile_card_used_reward resp = SaoCheckProfileCardUsedRewardResponse(header.cmd +1) return resp.make() - def handle_c814(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c814(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/synthesize_enhancement_hero_log req = SaoSynthesizeEnhancementHeroLogRequest(header, request) @@ -324,7 +324,7 @@ class SaoBase: resp = SaoSynthesizeEnhancementHeroLogResponse(header.cmd +1, synthesize_hero_log_data) return resp.make() - def handle_c816(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c816(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/synthesize_enhancement_equipment req_data = SaoSynthesizeEnhancementEquipmentRequest(header, request) synthesize_equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id) @@ -386,7 +386,7 @@ class SaoBase: resp = SaoSynthesizeEnhancementEquipmentResponse(header.cmd +1, synthesize_equipment_data) return resp.make() - def handle_c806(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c806(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/change_party req_data = SaoChangePartyRequest(header, request) party_hero_list = [] @@ -421,7 +421,7 @@ class SaoBase: resp = SaoNoopResponse(header.cmd +1) return resp.make() - def handle_c904(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c904(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/episode_play_start req_data = SaoEpisodePlayStartRequest(header, request) @@ -439,7 +439,7 @@ class SaoBase: resp = SaoEpisodePlayStartResponse(header.cmd +1, profile_data) return resp.make() - def handle_c908(self, header: SaoRequestHeader, request: bytes) -> bytes: # Level calculation missing for the profile and heroes + async def handle_c908(self, header: SaoRequestHeader, request: bytes) -> bytes: # Level calculation missing for the profile and heroes #quest/episode_play_end req_data = SaoEpisodePlayEndRequest(header, request) @@ -599,7 +599,7 @@ class SaoBase: resp = SaoEpisodePlayEndResponse(header.cmd +1) return resp.make() - def handle_c914(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c914(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/trial_tower_play_start req_data = SaoTrialTowerPlayStartRequest(header, request) @@ -618,7 +618,7 @@ class SaoBase: resp = SaoEpisodePlayStartResponse(header.cmd +1, profile_data) return resp.make() - def handle_c918(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c918(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/trial_tower_play_end req_data = SaoTrialTowerPlayEndRequest(header, request) @@ -799,7 +799,7 @@ class SaoBase: resp = SaoTrialTowerPlayEndResponse(header.cmd +1) return resp.make() - def handle_c90a(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c90a(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/episode_play_end_unanalyzed_log_fixed req = SaoEpisodePlayEndUnanalyzedLogFixedRequest(header, request) @@ -809,7 +809,7 @@ class SaoBase: resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) return resp.make() - def handle_c91a(self, header: SaoRequestHeader, request: bytes) -> bytes: # handler is identical to the episode + async def handle_c91a(self, header: SaoRequestHeader, request: bytes) -> bytes: # handler is identical to the episode #quest/trial_tower_play_end_unanalyzed_log_fixed req = TrialTowerPlayEndUnanalyzedLogFixed(header, request) @@ -818,58 +818,58 @@ class SaoBase: resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) return resp.make() - def handle_cd00(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_cd00(self, header: SaoRequestHeader, request: bytes) -> bytes: #defrag_match/get_defrag_match_basic_data resp = SaoGetDefragMatchBasicDataResponse(header.cmd +1) return resp.make() - def handle_cd02(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_cd02(self, header: SaoRequestHeader, request: bytes) -> bytes: #defrag_match/get_defrag_match_ranking_user_data resp = SaoGetDefragMatchRankingUserDataResponse(header.cmd +1) return resp.make() - def handle_cd04(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_cd04(self, header: SaoRequestHeader, request: bytes) -> bytes: #defrag_match/get_defrag_match_league_point_ranking_list resp = SaoGetDefragMatchLeaguePointRankingListResponse(header.cmd +1) return resp.make() - def handle_cd06(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_cd06(self, header: SaoRequestHeader, request: bytes) -> bytes: #defrag_match/get_defrag_match_league_score_ranking_list resp = SaoGetDefragMatchLeagueScoreRankingListResponse(header.cmd +1) return resp.make() - def handle_d404(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_d404(self, header: SaoRequestHeader, request: bytes) -> bytes: #other/bnid_serial_code_check resp = SaoBnidSerialCodeCheckResponse(header.cmd +1) return resp.make() - def handle_c306(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c306(self, header: SaoRequestHeader, request: bytes) -> bytes: #card/scan_qr_quest_profile_card resp = SaoScanQrQuestProfileCardResponse(header.cmd +1) return resp.make() - def handle_c700(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c700(self, header: SaoRequestHeader, request: bytes) -> bytes: # shop/get_shop_resource_sales_data_list # TODO: Get user shop data req = GetShopResourceSalesDataListRequest(header, request) resp = GetShopResourceSalesDataListResponse(header.cmd + 1) return resp.make() - def handle_d100(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_d100(self, header: SaoRequestHeader, request: bytes) -> bytes: # shop/get_yui_medal_shop_user_data_list # TODO: Get user shop data req = GetYuiMedalShopUserDataListRequest(header, request) resp = GetYuiMedalShopUserDataListResponse(header.cmd + 1) return resp.make() - def handle_cf0e(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_cf0e(self, header: SaoRequestHeader, request: bytes) -> bytes: # gasha/get_gasha_medal_shop_user_data_list # TODO: Get user shop data req = GetGashaMedalShopUserDataListRequest(header, request) resp = GetGashaMedalShopUserDataListResponse(header.cmd + 1) return resp.make() - def handle_d5da(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_d5da(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data/get_m_yui_medal_shops req = GetMYuiMedalShopDataRequest(header, request) resp = GetMYuiMedalShopDataResponse(header.cmd + 1) @@ -897,7 +897,7 @@ class SaoBase: self.logger.debug(f"Load {len(resp.data_list)} Yui Medal Shops") return resp.make() - def handle_d5dc(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_d5dc(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data/get_m_yui_medal_shop_items req = GetMYuiMedalShopItemsRequest(header, request) resp = GetMYuiMedalShopItemsResponse(header.cmd + 1) @@ -927,7 +927,7 @@ class SaoBase: self.logger.debug(f"Load {len(resp.data_list)} Yui Medal Shop Items") return resp.make() - def handle_d5fc(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_d5fc(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data/get_m_gasha_medal_shops req = GetMGashaMedalShopsRequest(header, request) resp = GetMGashaMedalShopsResponse(header.cmd + 1) @@ -942,11 +942,11 @@ class SaoBase: self.logger.debug(f"Load {len(resp.data_list)} Gasha Medal Shops") return resp.make() - def handle_d5fe(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_d5fe(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data/get_m_gasha_medal_shop_items return SaoNoopResponse(header.cmd + 1).make() - def handle_d604(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_d604(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data_2/get_m_res_earn_campaign_shops req = GetMResEarnCampaignShopsRequest(header, request) resp = GetMResEarnCampaignShopsResponse(header.cmd + 1) @@ -968,6 +968,6 @@ class SaoBase: #self.logger.debug(f"Load {len(resp.data_list)} Res Earn Campaign Shops") return SaoNoopResponse(header.cmd + 1).make() - def handle_d606(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_d606(self, header: SaoRequestHeader, request: bytes) -> bytes: # master_data_2/get_m_res_earn_campaign_shop_items return SaoNoopResponse(header.cmd + 1).make() \ No newline at end of file diff --git a/titles/sao/const.py b/titles/sao/const.py index 8bdea0f..d181556 100644 --- a/titles/sao/const.py +++ b/titles/sao/const.py @@ -3,12 +3,22 @@ from enum import Enum class SaoConstants: GAME_CODE = "SDEW" + GAME_CDS = ["SAO1"] CONFIG_NAME = "sao.yaml" VER_SAO = 0 VERSION_NAMES = ("Sword Art Online Arcade") + + SERIAL_IDENT_SATALITE = 4 + SERIAL_IDENT_TERMINAL = 5 + + SERIAL_IDENT = [2825] + NETID_PREFIX = ["ABLN"] + SERIAL_REGIONS = [1] + SERIAL_ROLES = [3] + SERIAL_CAB_IDENTS = [SERIAL_IDENT_SATALITE, SERIAL_IDENT_TERMINAL] @classmethod def game_ver_to_string(cls, ver: int): diff --git a/titles/sao/index.py b/titles/sao/index.py index 78d641e..4cabe30 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -1,5 +1,7 @@ from typing import Tuple, Dict, List -from twisted.web.http import Request +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import Route import yaml import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler @@ -55,11 +57,10 @@ class SaoServlet(BaseServlet): if self.game_cfg.hash.verify_hash: self.static_hash = md5(self.game_cfg.hash.hash_base.encode()).digest() # Greate hashing guys, really validates the data - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [("render_POST", "/{datecode}/proto/if/{category}/{endpoint}", {})] - ) + def get_routes(self) -> List[Route]: + return [ + Route("/{datecode:int}/proto/if/{category:str}/{endpoint:str}", self.render_POST, methods=['POST']) + ] @classmethod def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: @@ -86,30 +87,29 @@ class SaoServlet(BaseServlet): proto = "https" port = f":{port_ssl}" if not self.core_cfg.server.is_using_proxy and port_ssl != 443 else "" - return (f"{proto}://{self.core_cfg.title.hostname}{port}/", "") + return (f"{proto}://{self.core_cfg.server.hostname}{port}/", "") def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str]: if not self.game_cfg.server.enable: - return (False, "") + return (False, [], []) - return (True, "SAO1") + return (True, SaoConstants.GAME_CDS, SaoConstants.NETID_PREFIX) - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: - endpoint = matchers.get('endpoint', '') - request.responseHeaders.addRawHeader(b"content-type", b"text/html; charset=utf-8") + async def render_POST(self, request: Request) -> bytes: + endpoint = request.path_params.get('endpoint', '') iv = b"" - req_raw = request.content.read() + req_raw = await request.body() if len(req_raw) < 40: self.logger.warn(f"Malformed request to {endpoint} - {req_raw.hex()}") - return b"" + return Response() req_header = SaoRequestHeader(req_raw) cmd_str = f"{req_header.cmd:04x}" if self.game_cfg.hash.verify_hash and self.static_hash != req_header.hash: self.logger.error(f"Hash mismatch! Expecting {self.static_hash} but recieved {req_header.hash}") - return b"" + return Response() if self.game_cfg.crypt.enable: iv = req_raw[40:48] @@ -124,7 +124,7 @@ class SaoServlet(BaseServlet): handler = getattr(self.base, f"handle_{cmd_str}", self.base.handle_noop) self.logger.info(f"{endpoint} - {cmd_str} request") self.logger.debug(f"Request: {req_raw.hex()}") - resp = handler(req_header, req_data) + resp = await handler(req_header, req_data) if resp is None: resp = SaoNoopResponse(req_header.cmd + 1).make() @@ -137,7 +137,7 @@ class SaoServlet(BaseServlet): else: self.logger.error(f"Unknown response type {type(resp)}") - return b"" + return Response() self.logger.debug(f"Response: {resp.hex()}") @@ -153,5 +153,6 @@ class SaoServlet(BaseServlet): tmp = struct.pack("!I", crypt_data_len) # does it want the length of the encrypted response?? resp = resp[:20] + tmp + iv + data_crypt self.logger.debug(f"Encrypted Response: {resp.hex()}") - - return resp \ No newline at end of file + + + return Response(resp, media_type="text/html; charset=utf-8") \ No newline at end of file diff --git a/titles/wacca/base.py b/titles/wacca/base.py index dd3fe24..c0be449 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -83,18 +83,18 @@ class WaccaBase: else: self.region_id = WaccaConstants.Region[prefecture_name] - def handle_housing_get_request(self, data: Dict) -> Dict: + async def handle_housing_get_request(self, data: Dict) -> Dict: req = BaseRequest(data) housing_id = 1337 self.logger.info(f"{req.chipId} -> {housing_id}") resp = HousingGetResponse(housing_id) return resp.make() - def handle_advertise_GetRanking_request(self, data: Dict) -> Dict: + async def handle_advertise_GetRanking_request(self, data: Dict) -> Dict: req = AdvertiseGetRankingRequest(data) return AdvertiseGetRankingResponse().make() - def handle_housing_start_request(self, data: Dict) -> Dict: + async def handle_housing_start_request(self, data: Dict) -> Dict: req = HousingStartRequestV1(data) allnet_region_id = None @@ -126,16 +126,16 @@ class WaccaBase: resp = HousingStartResponseV1(region_id) return resp.make() - def handle_advertise_GetNews_request(self, data: Dict) -> Dict: + async def handle_advertise_GetNews_request(self, data: Dict) -> Dict: resp = GetNewsResponseV1() return resp.make() - def handle_user_status_logout_request(self, data: Dict) -> Dict: + async def handle_user_status_logout_request(self, data: Dict) -> Dict: req = UserStatusLogoutRequest(data) self.logger.info(f"Log out user {req.userId} from {req.chipId}") return BaseResponse().make() - def handle_user_status_get_request(self, data: Dict) -> Dict: + async def handle_user_status_get_request(self, data: Dict) -> Dict: req = UserStatusGetRequest(data) resp = UserStatusGetV1Response() @@ -181,7 +181,7 @@ class WaccaBase: return resp.make() - def handle_user_status_login_request(self, data: Dict) -> Dict: + async def handle_user_status_login_request(self, data: Dict) -> Dict: req = UserStatusLoginRequest(data) resp = UserStatusLoginResponseV1() is_consec_day = True @@ -227,7 +227,7 @@ class WaccaBase: return resp.make() - def handle_user_status_create_request(self, data: Dict) -> Dict: + async def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) profileId = self.data.profile.create_profile( @@ -268,7 +268,7 @@ class WaccaBase: return UserStatusCreateResponseV2(profileId, req.username).make() - def handle_user_status_getDetail_request(self, data: Dict) -> Dict: + async def handle_user_status_getDetail_request(self, data: Dict) -> Dict: req = UserStatusGetDetailRequest(data) resp = UserStatusGetDetailResponseV1() @@ -433,7 +433,7 @@ class WaccaBase: return resp.make() - def handle_user_trial_get_request(self, data: Dict) -> Dict: + async def handle_user_trial_get_request(self, data: Dict) -> Dict: req = UserTrialGetRequest(data) resp = UserTrialGetResponse() @@ -475,7 +475,7 @@ class WaccaBase: return resp.make() - def handle_user_trial_update_request(self, data: Dict) -> Dict: + async def handle_user_trial_update_request(self, data: Dict) -> Dict: req = UserTrialUpdateRequest(data) total_score = 0 @@ -571,7 +571,7 @@ class WaccaBase: ) return BaseResponse().make() - def handle_user_sugoroku_update_request(self, data: Dict) -> Dict: + async def handle_user_sugoroku_update_request(self, data: Dict) -> Dict: ver_split = data["appVersion"].split(".") resp = BaseResponse() @@ -603,10 +603,10 @@ class WaccaBase: ) return resp.make() - def handle_user_info_getMyroom_request(self, data: Dict) -> Dict: + async def handle_user_info_getMyroom_request(self, data: Dict) -> Dict: return UserInfogetMyroomResponseV1().make() - def handle_user_music_unlock_request(self, data: Dict) -> Dict: + async def handle_user_music_unlock_request(self, data: Dict) -> Dict: req = UserMusicUnlockRequest(data) profile = self.data.profile.get_profile(req.profileId) @@ -672,12 +672,12 @@ class WaccaBase: return UserMusicUnlockResponse(current_wp, new_tickets).make() - def handle_user_info_getRanking_request(self, data: Dict) -> Dict: + async def handle_user_info_getRanking_request(self, data: Dict) -> Dict: # total score, high score by song, cumulative socre, stage up score, other score, WP ranking # This likely requies calculating standings at regular intervals and caching the results return UserInfogetRankingResponse().make() - def handle_user_music_update_request(self, data: Dict) -> Dict: + async def handle_user_music_update_request(self, data: Dict) -> Dict: ver_split = data["appVersion"].split(".") if int(ver_split[0]) >= 3: resp = UserMusicUpdateResponseV3() @@ -831,18 +831,18 @@ class WaccaBase: return resp.make() # TODO: Coop and vs data - def handle_user_music_updateCoop_request(self, data: Dict) -> Dict: + async def handle_user_music_updateCoop_request(self, data: Dict) -> Dict: coop_info = data["params"][4] return self.handle_user_music_update_request(data) - def handle_user_music_updateVersus_request(self, data: Dict) -> Dict: + async def handle_user_music_updateVersus_request(self, data: Dict) -> Dict: vs_info = data["params"][4] return self.handle_user_music_update_request(data) - def handle_user_music_updateTrial_request(self, data: Dict) -> Dict: + async def handle_user_music_updateTrial_request(self, data: Dict) -> Dict: return self.handle_user_music_update_request(data) - def handle_user_mission_update_request(self, data: Dict) -> Dict: + async def handle_user_mission_update_request(self, data: Dict) -> Dict: req = UserMissionUpdateRequest(data) page_status = req.params[1][1] @@ -860,7 +860,7 @@ class WaccaBase: return BaseResponse().make() - def handle_user_goods_purchase_request(self, data: Dict) -> Dict: + async def handle_user_goods_purchase_request(self, data: Dict) -> Dict: req = UserGoodsPurchaseRequest(data) resp = UserGoodsPurchaseResponse() @@ -905,13 +905,13 @@ class WaccaBase: return resp.make() - def handle_competition_status_login_request(self, data: Dict) -> Dict: + async def handle_competition_status_login_request(self, data: Dict) -> Dict: return BaseResponse().make() - def handle_competition_status_update_request(self, data: Dict) -> Dict: + async def handle_competition_status_update_request(self, data: Dict) -> Dict: return BaseResponse().make() - def handle_user_rating_update_request(self, data: Dict) -> Dict: + async def handle_user_rating_update_request(self, data: Dict) -> Dict: req = UserRatingUpdateRequest(data) user_id = self.data.profile.profile_to_aime_user(req.profileId) @@ -931,7 +931,7 @@ class WaccaBase: return BaseResponse().make() - def handle_user_status_update_request(self, data: Dict) -> Dict: + async def handle_user_status_update_request(self, data: Dict) -> Dict: req = UserStatusUpdateRequestV1(data) user_id = self.data.profile.profile_to_aime_user(req.profileId) @@ -970,7 +970,7 @@ class WaccaBase: ) return BaseResponse().make() - def handle_user_info_update_request(self, data: Dict) -> Dict: + async def handle_user_info_update_request(self, data: Dict) -> Dict: req = UserInfoUpdateRequest(data) user_id = self.data.profile.profile_to_aime_user(req.profileId) @@ -989,7 +989,7 @@ class WaccaBase: return BaseResponse().make() - def handle_user_vip_get_request(self, data: Dict) -> Dict: + async def handle_user_vip_get_request(self, data: Dict) -> Dict: req = UserVipGetRequest(data) resp = UserVipGetResponse() @@ -1021,7 +1021,7 @@ class WaccaBase: return resp.make() - def handle_user_vip_start_request(self, data: Dict) -> Dict: + async def handle_user_vip_start_request(self, data: Dict) -> Dict: req = UserVipStartRequest(data) profile = self.data.profile.get_profile(req.profileId) diff --git a/titles/wacca/frontend.py b/titles/wacca/frontend.py index cc40644..fb55de1 100644 --- a/titles/wacca/frontend.py +++ b/titles/wacca/frontend.py @@ -1,6 +1,6 @@ import yaml import jinja2 -from twisted.web.http import Request +from starlette.requests import Request from os import path from twisted.web.server import Session diff --git a/titles/wacca/index.py b/titles/wacca/index.py index 90e4a0b..3f58a9d 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -1,16 +1,19 @@ +from starlette.routing import Route import yaml import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler import logging import json from hashlib import md5 -from twisted.web.http import Request +from starlette.requests import Request +from starlette.responses import JSONResponse, Response from typing import Dict, Tuple, List from os import path import traceback import sys from core import CoreConfig, Utils +from core.title import BaseServlet from .config import WaccaConfig from .config import WaccaConfig from .const import WaccaConstants @@ -23,7 +26,7 @@ from .handlers.base import BaseResponse, BaseRequest from .handlers.helpers import Version -class WaccaServlet: +class WaccaServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = WaccaConfig() @@ -62,15 +65,12 @@ class WaccaServlet: coloredlogs.install( level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str ) - - def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: - return ( - [], - [ - ("render_POST", "/WaccaServlet/api/{api}/{endpoint}", {}), - ("render_POST", "/WaccaServlet/api/{api}/{branch}/{endpoint}", {}) - ] - ) + + def get_routes(self) -> List[Route]: + return [ + Route("/WaccaServlet/api/{api:str}/{endpoint:str}", self.render_POST, methods=['POST']), + Route("/WaccaServlet/api/{api:str}/{branch:str}/{endpoint:str}", self.render_POST, methods=['POST']), + ] @classmethod def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: @@ -88,22 +88,22 @@ class WaccaServlet: def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: return ( - f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/WaccaServlet", - self.core_cfg.title.hostname, + f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/WaccaServlet", + self.core_cfg.server.hostname, ) - return (f"http://{self.core_cfg.title.hostname}/WaccaServlet", self.core_cfg.title.hostname) + return (f"http://{self.core_cfg.server.hostname}/WaccaServlet", self.core_cfg.server.hostname) - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: + async def render_POST(self, request: Request) -> bytes: def end(resp: Dict) -> bytes: hash = md5(json.dumps(resp, ensure_ascii=False).encode()).digest() - request.responseHeaders.addRawHeader(b"X-Wacca-Hash", hash.hex().encode()) - return json.dumps(resp).encode() + return JSONResponse(resp, headers=["X-Wacca-Hash", hash.hex()]) - api = matchers['api'] - branch = matchers.get('branch', '') - endpoint = matchers['endpoint'] + api = request.path_params.get('api', '') + branch = request.path_params.get('branch', '') + endpoint = request.path_params.get('endpoint', '') client_ip = Utils.get_ip_addr(request) + bod = await request.body() if branch: url_path = f"{api}/{branch}/{endpoint}" @@ -114,13 +114,13 @@ class WaccaServlet: func_to_find = f"handle_{api}_{endpoint}_request" try: - req_json = json.loads(request.content.getvalue()) + req_json = json.loads(bod) version_full = Version(req_json["appVersion"]) req = BaseRequest(req_json) except KeyError as e: self.logger.error( - f"Failed to parse request to {request.content.getvalue()} -> Missing required value {e}" + f"Failed to parse request to {bod} -> Missing required value {e}" ) resp = BaseResponse() resp.status = 1 @@ -129,7 +129,7 @@ class WaccaServlet: except Exception as e: self.logger.error( - f"Failed to parse request to {url_path} -> {request.content.getvalue()} -> {e}" + f"Failed to parse request to {url_path} -> {bod} -> {e}" ) resp = BaseResponse() resp.status = 1 @@ -176,7 +176,7 @@ class WaccaServlet: try: handler = getattr(self.versions[internal_ver], func_to_find) - resp = handler(req_json) + resp = await handler(req_json) self.logger.debug(f"{req.appVersion} response {resp}") return end(resp) diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 33928ec..d92e417 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -37,13 +37,13 @@ class WaccaLily(WaccaS): (210003, 0), ] - def handle_advertise_GetNews_request(self, data: Dict) -> Dict: + async def handle_advertise_GetNews_request(self, data: Dict) -> Dict: resp = GetNewsResponseV3() return resp.make() - def handle_user_status_create_request(self, data: Dict) -> Dict: + async def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) - ret = super().handle_user_status_create_request(data) + ret = await super().handle_user_status_create_request(data) new_user = self.data.profile.get_profile(aime_id=req.aimeId) @@ -64,7 +64,7 @@ class WaccaLily(WaccaS): return ret - def handle_user_status_get_request(self, data: Dict) -> Dict: + async def handle_user_status_get_request(self, data: Dict) -> Dict: req = UserStatusGetRequest(data) resp = UserStatusGetV2Response() @@ -145,7 +145,7 @@ class WaccaLily(WaccaS): return resp.make() - def handle_user_status_login_request(self, data: Dict) -> Dict: + async def handle_user_status_login_request(self, data: Dict) -> Dict: req = UserStatusLoginRequest(data) resp = UserStatusLoginResponseV2() is_consec_day = True @@ -189,7 +189,7 @@ class WaccaLily(WaccaS): return resp.make() - def handle_user_status_getDetail_request(self, data: Dict) -> Dict: + async def handle_user_status_getDetail_request(self, data: Dict) -> Dict: req = UserStatusGetDetailRequest(data) if req.appVersion.minor >= 53: resp = UserStatusGetDetailResponseV3() @@ -440,10 +440,10 @@ class WaccaLily(WaccaS): return resp.make() - def handle_user_info_getMyroom_request(self, data: Dict) -> Dict: + async def handle_user_info_getMyroom_request(self, data: Dict) -> Dict: return UserInfogetMyroomResponseV2().make() - def handle_user_status_update_request(self, data: Dict) -> Dict: + async def handle_user_status_update_request(self, data: Dict) -> Dict: super().handle_user_status_update_request(data) req = UserStatusUpdateRequestV2(data) self.data.profile.update_profile_lastplayed( diff --git a/titles/wacca/lilyr.py b/titles/wacca/lilyr.py index 7204761..83f9bf6 100644 --- a/titles/wacca/lilyr.py +++ b/titles/wacca/lilyr.py @@ -39,7 +39,7 @@ class WaccaLilyR(WaccaLily): (210003, 0), ] - def handle_housing_start_request(self, data: Dict) -> Dict: + async def handle_housing_start_request(self, data: Dict) -> Dict: req = HousingStartRequestV2(data) allnet_region_id = None @@ -71,9 +71,9 @@ class WaccaLilyR(WaccaLily): resp = HousingStartResponseV1(region_id) return resp.make() - def handle_user_status_create_request(self, data: Dict) -> Dict: + async def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) - resp = super().handle_user_status_create_request(data) + resp = await super().handle_user_status_create_request(data) self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210054 @@ -102,5 +102,5 @@ class WaccaLilyR(WaccaLily): return resp - def handle_user_status_logout_request(self, data: Dict) -> Dict: + async def handle_user_status_logout_request(self, data: Dict) -> Dict: return BaseResponse().make() diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index 6206940..51e8c63 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -47,12 +47,12 @@ class WaccaReverse(WaccaLilyR): (310006, 0), ] - def handle_user_status_login_request(self, data: Dict) -> Dict: - resp = super().handle_user_status_login_request(data) + async def handle_user_status_login_request(self, data: Dict) -> Dict: + resp = await super().handle_user_status_login_request(data) resp["params"].append([]) return resp - def handle_user_status_getDetail_request(self, data: Dict) -> Dict: + async def handle_user_status_getDetail_request(self, data: Dict) -> Dict: req = UserStatusGetDetailRequest(data) resp = UserStatusGetDetailResponseV4() @@ -305,9 +305,9 @@ class WaccaReverse(WaccaLilyR): return resp.make() - def handle_user_status_create_request(self, data: Dict) -> Dict: + async def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) - resp = super().handle_user_status_create_request(data) + resp = await super().handle_user_status_create_request(data) self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310001 diff --git a/titles/wacca/s.py b/titles/wacca/s.py index 4b1e997..aab2659 100644 --- a/titles/wacca/s.py +++ b/titles/wacca/s.py @@ -31,6 +31,6 @@ class WaccaS(WaccaBase): super().__init__(cfg, game_cfg) self.version = WaccaConstants.VER_WACCA_S - def handle_advertise_GetNews_request(self, data: Dict) -> Dict: + async def handle_advertise_GetNews_request(self, data: Dict) -> Dict: resp = GetNewsResponseV2() return resp.make()