Merge remote-tracking branch 'origin/develop' into fork_develop

This commit is contained in:
Dniel97 2023-08-21 10:33:25 +02:00
commit 9d74d60c14
Signed by untrusted user: Dniel97
GPG Key ID: 6180B3C768FB2E08
6 changed files with 263 additions and 78 deletions

View File

@ -3,4 +3,4 @@ from .base import CompanyCodes, ReaderFwVer, CMD_CODE_GOODBYE, HEADER_SIZE
from .lookup import ADBLookupRequest, ADBLookupResponse, ADBLookupExResponse from .lookup import ADBLookupRequest, ADBLookupResponse, ADBLookupExResponse
from .campaign import ADBCampaignClearRequest, ADBCampaignClearResponse, ADBCampaignResponse, ADBOldCampaignRequest, ADBOldCampaignResponse from .campaign import ADBCampaignClearRequest, ADBCampaignClearResponse, ADBCampaignResponse, ADBOldCampaignRequest, ADBOldCampaignResponse
from .felica import ADBFelicaLookupRequest, ADBFelicaLookupResponse, ADBFelicaLookup2Request, ADBFelicaLookup2Response from .felica import ADBFelicaLookupRequest, ADBFelicaLookupResponse, ADBFelicaLookup2Request, ADBFelicaLookup2Response
from .log import ADBLogExRequest, ADBLogRequest, ADBStatusLogRequest from .log import ADBLogExRequest, ADBLogRequest, ADBStatusLogRequest, ADBLogExResponse

View File

@ -1,7 +1,15 @@
from construct import Struct, Int32sl, Padding, Int8sl from construct import Struct, Padding, Int8sl
from typing import Union from typing import Final, List
from .base import * 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("<IIQiii4xQiI", data)
self.status = LogStatus(status)
class ADBStatusLogRequest(ADBBaseRequest): class ADBStatusLogRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None: def __init__(self, data: bytes) -> None:
@ -18,6 +26,31 @@ class ADBLogRequest(ADBBaseRequest):
class ADBLogExRequest(ADBBaseRequest): class ADBLogExRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None: def __init__(self, data: bytes) -> None:
super().__init__(data) super().__init__(data)
self.aime_id, status, self.user_id, self.credit_ct, self.bet_ct, self.won_ct, self.local_time, \ self.logs: List[AmLogEx] = []
self.tseq, self.place_id, self.num_logs = struct.unpack_from("<IIQiii4xQiII", data, 0x20)
self.status = LogStatus(status) for x in range(NUM_LOGS):
self.logs.append(AmLogEx(data[0x20 + (NUM_LEN_LOG_EX * x): 0x50 + (NUM_LEN_LOG_EX * x)]))
self.num_logs = struct.unpack_from("<I", data, 0x03E0)[0]
class ADBLogExResponse(ADBBaseResponse):
def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", protocol_ver: int = 12423, code: int = 20, length: int = 64, status: int = 1) -> 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

View File

@ -277,8 +277,12 @@ class AimedbProtocol(Protocol):
def handle_log_ex(self, data: bytes, resp_code: int) -> bytes: def handle_log_ex(self, data: bytes, resp_code: int) -> bytes:
req = ADBLogExRequest(data) 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 = []
return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver) 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 ADBLogExResponse.from_req(req.head)
def handle_goodbye(self, data: bytes, resp_code: int) -> None: def handle_goodbye(self, data: bytes, resp_code: int) -> None:
self.logger.info(f"goodbye from {self.transport.getPeer().host}") self.logger.info(f"goodbye from {self.transport.getPeer().host}")

View File

@ -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 import logging, coloredlogs
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from twisted.web.http import Request from twisted.web.http import Request
@ -14,12 +14,15 @@ from Crypto.Signature import PKCS1_v1_5
from time import strptime from time import strptime
from os import path from os import path
import urllib.parse import urllib.parse
import math
from core.config import CoreConfig from core.config import CoreConfig
from core.utils import Utils from core.utils import Utils
from core.data import Data from core.data import Data
from core.const import * from core.const import *
BILLING_DT_FORMAT: Final[str] = "%Y%m%d%H%M%S"
class DLIMG_TYPE(Enum): class DLIMG_TYPE(Enum):
app = 0 app = 0
opt = 1 opt = 1
@ -83,8 +86,16 @@ class AllnetServlet:
def handle_poweron(self, request: Request, _: Dict): def handle_poweron(self, request: Request, _: Dict):
request_ip = Utils.get_ip_addr(request) request_ip = Utils.get_ip_addr(request)
pragma_header = request.getHeader('Pragma')
is_dfi = pragma_header is not None and pragma_header == "DFI"
try: 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: if req_dict is None:
raise AllnetRequestException() raise AllnetRequestException()
@ -219,12 +230,24 @@ class AllnetServlet:
self.logger.debug(f"Allnet response: {resp_dict}") self.logger.debug(f"Allnet response: {resp_dict}")
resp_str += "\n" resp_str += "\n"
"""if is_dfi:
request.responseHeaders.addRawHeader('Pragma', 'DFI')
return self.to_dfi(resp_str)"""
return resp_str.encode("utf-8") return resp_str.encode("utf-8")
def handle_dlorder(self, request: Request, _: Dict): def handle_dlorder(self, request: Request, _: Dict):
request_ip = Utils.get_ip_addr(request) request_ip = Utils.get_ip_addr(request)
pragma_header = request.getHeader('Pragma')
is_dfi = pragma_header is not None and pragma_header == "DFI"
try: 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: if req_dict is None:
raise AllnetRequestException() raise AllnetRequestException()
@ -266,7 +289,13 @@ class AllnetServlet:
self.logger.debug(f"Sending download uri {resp.uri}") 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}") 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: def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes:
if "file" not in match: if "file" not in match:
@ -334,8 +363,16 @@ class AllnetServlet:
return "OK".encode() return "OK".encode()
def handle_billing_request(self, request: Request, _: Dict): 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) request_ip = Utils.get_ip_addr(request)
if req_dict is None: if req_dict is None:
self.logger.error(f"Failed to parse request {request.content.getvalue()}") self.logger.error(f"Failed to parse request {request.content.getvalue()}")
return b"" return b""
@ -345,45 +382,60 @@ class AllnetServlet:
rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read()) rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read())
signer = PKCS1_v1_5.new(rsa) signer = PKCS1_v1_5.new(rsa)
digest = SHA.new() digest = SHA.new()
traces: List[TraceData] = []
try: try:
kc_playlimit = int(req_dict[0]["playlimit"]) for x in range(len(req_dict)):
kc_nearfull = int(req_dict[0]["nearfull"]) if not req_dict[x]:
kc_billigtype = int(req_dict[0]["billingtype"]) continue
kc_playcount = int(req_dict[0]["playcnt"])
kc_serial: str = req_dict[0]["keychipid"] if x == 0:
kc_game: str = req_dict[0]["gameid"] req = BillingInfo(req_dict[x])
kc_date = strptime(req_dict[0]["date"], "%Y%m%d%H%M%S") continue
kc_serial_bytes = kc_serial.encode()
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: 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: 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( self.data.base.log_event(
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg "allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg
) )
self.logger.warning(msg) self.logger.warning(msg)
resp = BillingResponse("", "", "", "") return f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n".encode()
resp.result = "1"
return self.dict_to_http_form_string([vars(resp)])
msg = ( msg = (
f"Billing checkin from {request_ip}: game {kc_game} keychip {kc_serial} playcount " f"Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount "
f"{kc_playcount} billing_type {kc_billigtype} nearfull {kc_nearfull} playlimit {kc_playlimit}" f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}"
) )
self.logger.info(msg) self.logger.info(msg)
self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.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_playlimit += 1024
kc_nearfull += 1024 kc_nearfull += 1024
playlimit = kc_playlimit 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) digest.update(playlimit.to_bytes(4, "little") + kc_serial_bytes)
playlimit_sig = signer.sign(digest).hex() playlimit_sig = signer.sign(digest).hex()
@ -394,13 +446,16 @@ class AllnetServlet:
# TODO: playhistory # 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 = self.dict_to_http_form_string([vars(resp)]) resp_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n"
if resp_str is None:
self.logger.error(f"Failed to parse response {vars(resp)}")
self.logger.debug(f"response {vars(resp)}") 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") return resp_str.encode("utf-8")
def handle_naomitest(self, request: Request, _: Dict) -> bytes: def handle_naomitest(self, request: Request, _: Dict) -> bytes:
@ -412,9 +467,7 @@ class AllnetServlet:
Parses an billing request string into a python dictionary Parses an billing request string into a python dictionary
""" """
try: try:
decomp = zlib.decompressobj(-zlib.MAX_WBITS) sections = data.decode("ascii").split("\r\n")
unzipped = decomp.decompress(data)
sections = unzipped.decode("ascii").split("\r\n")
ret = [] ret = []
for x in sections: for x in sections:
@ -430,9 +483,7 @@ class AllnetServlet:
Parses an allnet request string into a python dictionary Parses an allnet request string into a python dictionary
""" """
try: try:
zipped = base64.b64decode(data) sections = data.split("\r\n")
unzipped = zlib.decompress(zipped)
sections = unzipped.decode("utf-8").split("\r\n")
ret = [] ret = []
for x in sections: for x in sections:
@ -443,35 +494,15 @@ class AllnetServlet:
self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}") self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}")
return None return None
def dict_to_http_form_string( def from_dfi(self, data: bytes) -> str:
self, zipped = base64.b64decode(data)
data: List[Dict[str, Any]], unzipped = zlib.decompress(zipped)
crlf: bool = True, return unzipped.decode("utf-8")
trailing_newline: bool = True,
) -> Optional[str]: def to_dfi(self, data: str) -> bytes:
""" unzipped = data.encode('utf-8')
Takes a python dictionary and parses it into an allnet response string zipped = zlib.compress(unzipped)
""" return base64.b64encode(zipped)
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
class AllnetPowerOnRequest: class AllnetPowerOnRequest:
@ -557,6 +588,114 @@ class AllnetDownloadOrderResponse:
self.serial = serial self.serial = serial
self.uri = uri 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"%.{2 - 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: class BillingResponse:
def __init__( def __init__(
@ -565,20 +704,22 @@ class BillingResponse:
playlimit_sig: str = "", playlimit_sig: str = "",
nearfull: str = "", nearfull: str = "",
nearfull_sig: str = "", nearfull_sig: str = "",
request_num: int = 1,
protocol_ver: float = 1.000,
playhistory: str = "000000/0:000000/0:000000/0", playhistory: str = "000000/0:000000/0:000000/0",
) -> None: ) -> None:
self.result = "0" self.result = 0
self.waitime = "100" self.requestno = request_num
self.linelimit = "1" self.traceerase = 1
self.message = "" self.fixinterval = 120
self.fixlogcnt = 100
self.playlimit = playlimit self.playlimit = playlimit
self.playlimitsig = playlimit_sig self.playlimitsig = playlimit_sig
self.protocolver = "1.000" self.playhistory = playhistory
self.nearfull = nearfull self.nearfull = nearfull
self.nearfullsig = nearfull_sig self.nearfullsig = nearfull_sig
self.fixlogincnt = "0" self.linelimit = 100
self.fixinterval = "5" self.protocolver = float5.to_str(protocol_ver)
self.playhistory = playhistory
# playhistory -> YYYYMM/C:... # playhistory -> YYYYMM/C:...
# YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period # YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period

View File

@ -200,6 +200,12 @@ class AllnetConfig:
self.__config, "core", "allnet", "port", default=80 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 @property
def allow_online_updates(self) -> int: def allow_online_updates(self) -> int:
return CoreConfig.get_config_field( return CoreConfig.get_config_field(

View File

@ -34,6 +34,7 @@ frontend:
allnet: allnet:
loglevel: "info" loglevel: "info"
port: 80 port: 80
ip_check: False
allow_online_updates: False allow_online_updates: False
update_cfg_folder: "" update_cfg_folder: ""