From 71489c1272fbafde46aadea1384130b0d0c87b22 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 20 Aug 2023 19:55:26 -0400 Subject: [PATCH 1/6] adb: fix log_ex --- core/adb_handlers/log.py | 43 +++++++++++++++++++++++++++++++++++----- core/aimedb.py | 6 +++++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/core/adb_handlers/log.py b/core/adb_handlers/log.py index 8be36f9..28fbdf3 100644 --- a/core/adb_handlers/log.py +++ b/core/adb_handlers/log.py @@ -1,7 +1,15 @@ -from construct import Struct, Int32sl, Padding, Int8sl -from typing import Union +from construct import Struct, Padding, Int8sl +from typing import Final, List from .base import * +NUM_LOGS: Final[int] = 20 +NUM_LEN_LOG_EX: Final[int] = 48 + +class AmLogEx: + def __init__(self, data: bytes) -> None: + self.aime_id, status, self.user_id, self.credit_ct, self.bet_ct, self.won_ct, self.local_time, \ + self.tseq, self.place_id = struct.unpack(" None: @@ -18,6 +26,31 @@ class ADBLogRequest(ADBBaseRequest): class ADBLogExRequest(ADBBaseRequest): def __init__(self, data: bytes) -> None: super().__init__(data) - self.aime_id, status, self.user_id, self.credit_ct, self.bet_ct, self.won_ct, self.local_time, \ - self.tseq, self.place_id, self.num_logs = struct.unpack_from(" None: + super().__init__(code, length, status, game_id, store_id, keychip_id, protocol_ver) + + @classmethod + def from_req(cls, req: ADBHeader) -> "ADBLogExResponse": + c = cls(req.game_id, req.store_id, req.keychip_id, req.protocol_ver) + return c + + def make(self) -> bytes: + resp_struct = Struct( + "log_result" / Int8sl[NUM_LOGS], + Padding(12) + ) + + body = resp_struct.build(dict( + log_result = [1] * NUM_LOGS + )) + + self.head.length = HEADER_SIZE + len(body) + return self.head.make() + body diff --git a/core/aimedb.py b/core/aimedb.py index 552205f..0a1b4f5 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -277,7 +277,11 @@ class AimedbProtocol(Protocol): def handle_log_ex(self, data: bytes, resp_code: int) -> bytes: req = ADBLogExRequest(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}") + strs = [] + self.logger.info(f"Recieved {req.num_logs} or {len(req.logs)} logs") + + for x in range(req.num_logs): + 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 ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver) def handle_goodbye(self, data: bytes, resp_code: int) -> None: From fd6cadf2da9ce5b92dd22e6fcd913fc627f02510 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 20 Aug 2023 19:56:16 -0400 Subject: [PATCH 2/6] adb: hotfix --- core/adb_handlers/__init__.py | 2 +- core/aimedb.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/adb_handlers/__init__.py b/core/adb_handlers/__init__.py index a5c401f..0c96baf 100644 --- a/core/adb_handlers/__init__.py +++ b/core/adb_handlers/__init__.py @@ -3,4 +3,4 @@ from .base import CompanyCodes, ReaderFwVer, CMD_CODE_GOODBYE, HEADER_SIZE from .lookup import ADBLookupRequest, ADBLookupResponse, ADBLookupExResponse from .campaign import ADBCampaignClearRequest, ADBCampaignClearResponse, ADBCampaignResponse, ADBOldCampaignRequest, ADBOldCampaignResponse from .felica import ADBFelicaLookupRequest, ADBFelicaLookupResponse, ADBFelicaLookup2Request, ADBFelicaLookup2Response -from .log import ADBLogExRequest, ADBLogRequest, ADBStatusLogRequest +from .log import ADBLogExRequest, ADBLogRequest, ADBStatusLogRequest, ADBLogExResponse diff --git a/core/aimedb.py b/core/aimedb.py index 0a1b4f5..f608231 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -282,7 +282,7 @@ class AimedbProtocol(Protocol): for x in range(req.num_logs): 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 ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver) + 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}") From 2e8d99e5faa959ccc7f01a32c9ce6bfba09c636c Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 20 Aug 2023 23:25:50 -0400 Subject: [PATCH 3/6] allnet: add ip check config option --- core/config.py | 6 ++++++ example_config/core.yaml | 1 + 2 files changed, 7 insertions(+) diff --git a/core/config.py b/core/config.py index 3376c3c..83a941a 100644 --- a/core/config.py +++ b/core/config.py @@ -200,6 +200,12 @@ class AllnetConfig: 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( diff --git a/example_config/core.yaml b/example_config/core.yaml index 8d78008..1ecb7ff 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -34,6 +34,7 @@ frontend: allnet: loglevel: "info" port: 80 + ip_check: False allow_online_updates: False update_cfg_folder: "" From 984949d9022abc1b5a21b6d4fe1a8ed112e94616 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Mon, 21 Aug 2023 00:10:25 -0400 Subject: [PATCH 4/6] allnet: partial DFI implementation --- core/allnet.py | 94 +++++++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index e935359..533ec93 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -83,8 +83,16 @@ class AllnetServlet: def handle_poweron(self, request: Request, _: Dict): request_ip = Utils.get_ip_addr(request) + pragma_header = request.getHeader('Pragma') + is_dfi = pragma_header is not None and pragma_header == "DFI" + try: - req_dict = self.allnet_req_to_dict(request.content.getvalue()) + if is_dfi: + req_urlencode = self.from_dfi(request.content.getvalue()) + else: + req_urlencode = request.content.getvalue().decode() + + req_dict = self.allnet_req_to_dict(req_urlencode) if req_dict is None: raise AllnetRequestException() @@ -219,12 +227,24 @@ class AllnetServlet: self.logger.debug(f"Allnet response: {resp_dict}") resp_str += "\n" + """if is_dfi: + request.responseHeaders.addRawHeader('Pragma', 'DFI') + return self.to_dfi(resp_str)""" + return resp_str.encode("utf-8") def handle_dlorder(self, request: Request, _: Dict): request_ip = Utils.get_ip_addr(request) + pragma_header = request.getHeader('Pragma') + is_dfi = pragma_header is not None and pragma_header == "DFI" + try: - req_dict = self.allnet_req_to_dict(request.content.getvalue()) + if is_dfi: + req_urlencode = self.from_dfi(request.content.getvalue()) + else: + req_urlencode = request.content.getvalue().decode() + + req_dict = self.allnet_req_to_dict(req_urlencode) if req_dict is None: raise AllnetRequestException() @@ -266,7 +286,13 @@ class AllnetServlet: 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}") - return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" + res_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" + """if is_dfi: + request.responseHeaders.addRawHeader('Pragma', 'DFI') + return self.to_dfi(res_str)""" + + return res_str + def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: if "file" not in match: @@ -334,8 +360,16 @@ class AllnetServlet: return "OK".encode() def handle_billing_request(self, request: Request, _: Dict): - req_dict = self.billing_req_to_dict(request.content.getvalue()) + req_raw = request.content.getvalue() + + if request.getHeader('Content-Type') == "application/octet-stream": + req_unzip = zlib.decompressobj(-zlib.MAX_WBITS).decompress(req_raw) + else: + req_unzip = req_raw + + req_dict = self.billing_req_to_dict(req_unzip) 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"" @@ -369,7 +403,7 @@ class AllnetServlet: resp = BillingResponse("", "", "", "") resp.result = "1" - return self.dict_to_http_form_string([vars(resp)]) + return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n" msg = ( f"Billing checkin from {request_ip}: game {kc_game} keychip {kc_serial} playcount " @@ -396,9 +430,7 @@ class AllnetServlet: resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig) - resp_str = self.dict_to_http_form_string([vars(resp)]) - if resp_str is None: - self.logger.error(f"Failed to parse response {vars(resp)}") + resp_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n" self.logger.debug(f"response {vars(resp)}") return resp_str.encode("utf-8") @@ -412,9 +444,7 @@ class AllnetServlet: Parses an billing request string into a python dictionary """ try: - decomp = zlib.decompressobj(-zlib.MAX_WBITS) - unzipped = decomp.decompress(data) - sections = unzipped.decode("ascii").split("\r\n") + sections = data.decode("ascii").split("\r\n") ret = [] for x in sections: @@ -430,9 +460,7 @@ class AllnetServlet: Parses an allnet request string into a python dictionary """ try: - zipped = base64.b64decode(data) - unzipped = zlib.decompress(zipped) - sections = unzipped.decode("utf-8").split("\r\n") + sections = data.split("\r\n") ret = [] for x in sections: @@ -443,35 +471,15 @@ class AllnetServlet: self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}") return None - def dict_to_http_form_string( - self, - data: List[Dict[str, Any]], - crlf: bool = True, - trailing_newline: bool = True, - ) -> Optional[str]: - """ - Takes a python dictionary and parses it into an allnet response string - """ - try: - urlencode = "" - for item in data: - for k, v in item.items(): - if k is None or v is None: - continue - urlencode += f"{k}={v}&" - if crlf: - urlencode = urlencode[:-1] + "\r\n" - else: - urlencode = urlencode[:-1] + "\n" - if not trailing_newline: - if crlf: - urlencode = urlencode[:-2] - else: - urlencode = urlencode[:-1] - return urlencode - except Exception as e: - self.logger.error(f"dict_to_http_form_string: {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 AllnetPowerOnRequest: From d8b0e2ea2a4271c2ba9ece565ecfdeff63c84f39 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Mon, 21 Aug 2023 01:50:59 -0400 Subject: [PATCH 5/6] billing: add classes and validation, fix response --- core/allnet.py | 189 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 161 insertions(+), 28 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 533ec93..b229cba 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Any, Optional, Tuple, Union +from typing import Dict, List, Any, Optional, Tuple, Union, Final import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from twisted.web.http import Request @@ -14,12 +14,15 @@ from Crypto.Signature import PKCS1_v1_5 from time import strptime from os import path import urllib.parse +import math from core.config import CoreConfig from core.utils import Utils from core.data import Data from core.const import * +BILLING_DT_FORMAT: Final[str] = "%Y%m%d%H%M%S" + class DLIMG_TYPE(Enum): app = 0 opt = 1 @@ -379,45 +382,60 @@ class AllnetServlet: rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read()) signer = PKCS1_v1_5.new(rsa) digest = SHA.new() + traces: List[TraceData] = [] try: - kc_playlimit = int(req_dict[0]["playlimit"]) - kc_nearfull = int(req_dict[0]["nearfull"]) - kc_billigtype = int(req_dict[0]["billingtype"]) - kc_playcount = int(req_dict[0]["playcnt"]) - kc_serial: str = req_dict[0]["keychipid"] - kc_game: str = req_dict[0]["gameid"] - kc_date = strptime(req_dict[0]["date"], "%Y%m%d%H%M%S") - kc_serial_bytes = kc_serial.encode() + for x in range(len(req_dict)): + if not req_dict[x]: + continue + + if x == 0: + req = BillingInfo(req_dict[x]) + continue + + tmp = TraceData(req_dict[x]) + if tmp.trace_type == TraceDataType.CHARGE: + tmp = TraceDataCharge(req_dict[x]) + elif tmp.trace_type == TraceDataType.EVENT: + tmp = TraceDataEvent(req_dict[x]) + elif tmp.trace_type == TraceDataType.CREDIT: + tmp = TraceDataCredit(req_dict[x]) + + traces.append(tmp) + + kc_serial_bytes = req.keychipid.encode() except KeyError as e: - return f"result=5&linelimit=&message={e} field is missing".encode() + 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() - machine = self.data.arcade.get_machine(kc_serial) + machine = self.data.arcade.get_machine(req.keychipid) if machine is None and not self.config.server.allow_unregistered_serials: - msg = f"Unrecognised serial {kc_serial} attempted billing checkin from {request_ip} for game {kc_game}." + msg = f"Unrecognised serial {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}." self.data.base.log_event( "allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg ) self.logger.warning(msg) - resp = BillingResponse("", "", "", "") - resp.result = "1" - return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n" + return f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n".encode() msg = ( - f"Billing checkin from {request_ip}: game {kc_game} keychip {kc_serial} playcount " - f"{kc_playcount} billing_type {kc_billigtype} nearfull {kc_nearfull} playlimit {kc_playlimit}" + f"Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount " + f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}" ) self.logger.info(msg) self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, msg) + if req.traceleft > 0: + self.logger.warn(f"{req.traceleft} unsent tracelogs") + kc_playlimit = req.playlimit + kc_nearfull = req.nearfull - while kc_playcount > kc_playlimit: + while req.playcnt > req.playlimit: kc_playlimit += 1024 kc_nearfull += 1024 playlimit = kc_playlimit - nearfull = kc_nearfull + (kc_billigtype * 0x00010000) + nearfull = kc_nearfull + (req.billingtype.value * 0x00010000) digest.update(playlimit.to_bytes(4, "little") + kc_serial_bytes) playlimit_sig = signer.sign(digest).hex() @@ -428,11 +446,16 @@ class AllnetServlet: # TODO: playhistory - resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig) + #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" 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: @@ -565,6 +588,114 @@ class AllnetDownloadOrderResponse: self.serial = serial self.uri = uri +class TraceDataType(Enum): + CHARGE = 0 + EVENT = 1 + CREDIT = 2 + +class BillingType(Enum): + A = 1 + B = 0 + +class float5: + def __init__(self, n: str = "0") -> None: + nf = float(n) + if nf > 999.9 or nf < 0: + raise ValueError('float5 must be between 0.000 and 999.9 inclusive') + + return nf + + @classmethod + def to_str(cls, f: float): + return f"%.{4 - int(math.log10(f))+1}f" % f + +class BillingInfo: + def __init__(self, data: Dict) -> None: + try: + self.keychipid = str(data.get("keychipid", None)) + self.functype = int(data.get("functype", None)) + self.gameid = str(data.get("gameid", None)) + self.gamever = float(data.get("gamever", None)) + self.boardid = str(data.get("boardid", None)) + self.tenpoip = str(data.get("tenpoip", None)) + self.libalibver = float(data.get("libalibver", None)) + self.datamax = int(data.get("datamax", None)) + self.billingtype = BillingType(int(data.get("billingtype", None))) + self.protocolver = float(data.get("protocolver", None)) + self.operatingfix = bool(data.get("operatingfix", None)) + self.traceleft = int(data.get("traceleft", None)) + self.requestno = int(data.get("requestno", None)) + self.datesync = bool(data.get("datesync", None)) + self.timezone = str(data.get("timezone", None)) + self.date = datetime.strptime(data.get("date", None), BILLING_DT_FORMAT) + self.crcerrcnt = int(data.get("crcerrcnt", None)) + self.memrepair = bool(data.get("memrepair", None)) + self.playcnt = int(data.get("playcnt", None)) + self.playlimit = int(data.get("playlimit", None)) + self.nearfull = int(data.get("nearfull", None)) + except Exception as e: + raise KeyError(e) + +class TraceData: + def __init__(self, data: Dict) -> None: + try: + self.crc_err_flg = bool(data.get("cs", None)) + self.record_number = int(data.get("rn", None)) + self.seq_number = int(data.get("sn", None)) + self.trace_type = TraceDataType(int(data.get("tt", None))) + self.date_sync_flg = bool(data.get("ds", None)) + self.date = datetime.strptime(data.get("dt", None), BILLING_DT_FORMAT) + self.keychip = str(data.get("kn", None)) + self.lib_ver = float(data.get("alib", None)) + except Exception as e: + raise KeyError(e) + +class TraceDataCharge(TraceData): + def __init__(self, data: Dict) -> None: + super().__init__(data) + try: + self.game_id = str(data.get("gi", None)) + self.game_version = float(data.get("gv", None)) + self.board_serial = str(data.get("bn", None)) + self.shop_ip = str(data.get("ti", None)) + self.play_count = int(data.get("pc", None)) + self.play_limit = int(data.get("pl", None)) + self.product_code = int(data.get("ic", None)) + self.product_count = int(data.get("in", None)) + self.func_type = int(data.get("kk", None)) + self.player_number = int(data.get("playerno", None)) + except Exception as e: + raise KeyError(e) + +class TraceDataEvent(TraceData): + def __init__(self, data: Dict) -> None: + super().__init__(data) + try: + self.message = str(data.get("me", None)) + except Exception as e: + raise KeyError(e) + +class TraceDataCredit(TraceData): + def __init__(self, data: Dict) -> None: + super().__init__(data) + try: + self.chute_type = int(data.get("cct", None)) + self.service_type = int(data.get("cst", None)) + self.operation_type = int(data.get("cop", None)) + self.coin_rate0 = int(data.get("cr0", None)) + self.coin_rate1 = int(data.get("cr1", None)) + self.bonus_addition = int(data.get("cba", None)) + self.credit_rate = int(data.get("ccr", None)) + self.credit0 = int(data.get("cc0", None)) + self.credit1 = int(data.get("cc1", None)) + self.credit2 = int(data.get("cc2", None)) + self.credit3 = int(data.get("cc3", None)) + self.credit4 = int(data.get("cc4", None)) + self.credit5 = int(data.get("cc5", None)) + self.credit6 = int(data.get("cc6", None)) + self.credit7 = int(data.get("cc7", None)) + except Exception as e: + raise KeyError(e) class BillingResponse: def __init__( @@ -573,20 +704,22 @@ class BillingResponse: playlimit_sig: str = "", nearfull: str = "", nearfull_sig: str = "", + request_num: int = 1, + protocol_ver: float = 1.000, playhistory: str = "000000/0:000000/0:000000/0", ) -> None: - self.result = "0" - self.waitime = "100" - self.linelimit = "1" - self.message = "" + self.result = 0 + self.requestno = request_num + self.traceerase = 1 + self.fixinterval = 120 + self.fixlogcnt = 100 self.playlimit = playlimit self.playlimitsig = playlimit_sig - self.protocolver = "1.000" + self.playhistory = playhistory self.nearfull = nearfull self.nearfullsig = nearfull_sig - self.fixlogincnt = "0" - self.fixinterval = "5" - self.playhistory = playhistory + self.linelimit = 100 + self.protocolver = float5.to_str(protocol_ver) # playhistory -> YYYYMM/C:... # YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period From d4ea3bc12ad27d69c987cb7aee5b3e1c6c119831 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Mon, 21 Aug 2023 01:53:27 -0400 Subject: [PATCH 6/6] billing: float5 hotfix --- core/allnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/allnet.py b/core/allnet.py index b229cba..2fd6db2 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -607,7 +607,7 @@ class float5: @classmethod def to_str(cls, f: float): - return f"%.{4 - int(math.log10(f))+1}f" % f + return f"%.{2 - int(math.log10(f))+1}f" % f class BillingInfo: def __init__(self, data: Dict) -> None: