2023-02-16 17:13:41 -05:00
import pytz
import base64
import zlib
2023-07-23 12:47:10 -04:00
import json
2024-01-09 03:07:04 -05:00
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
2023-07-23 12:47:10 -04:00
from enum import Enum
2023-02-18 00:00:30 -05:00
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA
from Crypto.Signature import PKCS1_v1_5
2024-01-09 03:07:04 -05:00
from os import path , environ , mkdir , access , W_OK
2023-02-16 00:06:42 -05:00
2023-11-09 02:17:48 +00:00
from .config import CoreConfig
from .utils import Utils
from .data import Data
from .const import *
from .title import TitleServlet
2023-02-16 00:06:42 -05:00
2023-08-21 01:50:59 -04:00
BILLING_DT_FORMAT : Final [ str ] = "%Y%m %d %H%M%S"
2023-07-23 12:47:10 -04:00
class DLIMG_TYPE ( Enum ):
app = 0
opt = 1
2023-03-09 11:38:58 -05:00
2023-08-08 12:35:38 -04:00
class ALLNET_STAT ( Enum ):
ok = 0
bad_game = - 1
bad_machine = - 2
bad_shop = - 3
2023-11-01 20:08:20 -04:00
class DLI_STATUS ( Enum ):
START = 0
GET_DOWNLOAD_CONFIGURATION = 1
WAIT_DOWNLOAD = 2
DOWNLOADING = 3
NOT_SPECIFY_DLI = 100
ONLY_POST_REPORT = 101
STOPPED_BY_APP_RELEASE = 102
STOPPED_BY_OPT_RELEASE = 103
DOWNLOAD_COMPLETE_RECENTLY = 110
DOWNLOAD_COMPLETE_WAIT_RELEASE_TIME = 120
DOWNLOAD_COMPLETE_BUT_NOT_SYNC_SERVER = 121
DOWNLOAD_COMPLETE_BUT_NOT_FIRST_RESUME = 122
DOWNLOAD_COMPLETE_BUT_NOT_FIRST_LAUNCH = 123
DOWNLOAD_COMPLETE_WAIT_UPDATE = 124
DOWNLOAD_COMPLETE_AND_ALREADY_UPDATE = 130
ERROR_AUTH_FAILURE = 200
ERROR_GET_DLI_HTTP = 300
ERROR_GET_DLI = 301
ERROR_PARSE_DLI = 302
ERROR_INVALID_GAME_ID = 303
ERROR_INVALID_IMAGE_LIST = 304
ERROR_GET_DLI_APP = 305
ERROR_GET_BOOT_ID = 400
ERROR_ACCESS_SERVER = 401
ERROR_NO_IMAGE = 402
ERROR_ACCESS_IMAGE = 403
ERROR_DOWNLOAD_APP = 500
ERROR_DOWNLOAD_OPT = 501
ERROR_DISK_FULL = 600
ERROR_UNINSTALL = 601
ERROR_INSTALL_APP = 602
ERROR_INSTALL_OPT = 603
ERROR_GET_DLI_INTERNAL = 900
ERROR_ICF = 901
ERROR_CHECK_RELEASE_INTERNAL = 902
UNKNOWN = 999 # Not the actual enum val but it needs to be here as a catch-all
@classmethod
def from_int ( cls , num : int ) -> "DLI_STATUS" :
try :
return cls ( num )
except ValueError :
return cls . UNKNOWN
2023-02-17 01:37:59 -05:00
class AllnetServlet :
2023-03-09 11:38:58 -05:00
def __init__ ( self , core_cfg : CoreConfig , cfg_folder : str ):
2023-02-16 00:06:42 -05:00
self . config = core_cfg
self . config_folder = cfg_folder
self . data = Data ( core_cfg )
self . logger = logging . getLogger ( "allnet" )
2023-02-16 17:13:41 -05:00
if not hasattr ( self . logger , "initialized" ):
log_fmt_str = "[ %(asctime)s ] Allnet | %(levelname)s | %(message)s "
2023-03-09 11:38:58 -05:00
log_fmt = logging . Formatter ( log_fmt_str )
2023-02-16 00:06:42 -05:00
2023-03-09 11:38:58 -05:00
fileHandler = TimedRotatingFileHandler (
" {0} / {1} .log" . format ( self . config . server . log_dir , "allnet" ),
when = "d" ,
backupCount = 10 ,
)
2023-02-16 17:13:41 -05:00
fileHandler . setFormatter ( log_fmt )
2023-03-09 11:38:58 -05:00
2023-02-16 17:13:41 -05:00
consoleHandler = logging . StreamHandler ()
consoleHandler . setFormatter ( log_fmt )
self . logger . addHandler ( fileHandler )
self . logger . addHandler ( consoleHandler )
2023-03-09 11:38:58 -05:00
2023-02-16 17:13:41 -05:00
self . logger . setLevel ( core_cfg . allnet . loglevel )
2023-03-09 11:38:58 -05:00
coloredlogs . install (
level = core_cfg . allnet . loglevel , logger = self . logger , fmt = log_fmt_str
)
2023-02-16 17:13:41 -05:00
self . logger . initialized = True
2024-01-11 21:01:51 -05:00
def startup ( self ) -> None :
self . logger . info ( f "Ready on port { self . config . allnet . port if self . config . allnet . standalone else self . config . server . port } " )
2023-02-16 17:13:41 -05:00
2024-01-09 03:07:04 -05:00
async def handle_poweron ( self , request : Request ):
2023-03-12 01:00:51 -05:00
request_ip = Utils . get_ip_addr ( request )
2024-01-09 03:07:04 -05:00
pragma_header = request . headers . get ( 'Pragma' , "" )
2023-08-21 00:10:25 -04:00
is_dfi = pragma_header is not None and pragma_header == "DFI"
2024-01-09 03:07:04 -05:00
data = await request . body ()
2023-08-21 00:10:25 -04:00
2023-02-16 17:13:41 -05:00
try :
2023-08-21 00:10:25 -04:00
if is_dfi :
2024-01-09 03:07:04 -05:00
req_urlencode = self . from_dfi ( data )
2023-08-21 00:10:25 -04:00
else :
2024-01-09 03:07:04 -05:00
req_urlencode = data
2023-08-21 00:10:25 -04:00
req_dict = self . allnet_req_to_dict ( req_urlencode )
2023-02-24 14:07:54 -05:00
if req_dict is None :
raise AllnetRequestException ()
req = AllnetPowerOnRequest ( req_dict [ 0 ])
2023-02-16 17:13:41 -05:00
# Validate the request. Currently we only validate the fields we plan on using
2023-06-30 01:19:17 -04:00
if not req . game_id or not req . ver or not req . serial or not req . ip or not req . firm_ver or not req . boot_ver :
2023-03-09 11:38:58 -05:00
raise AllnetRequestException (
f "Bad auth request params from { request_ip } - { vars ( req ) } "
)
2023-02-18 00:00:30 -05:00
except AllnetRequestException as e :
2023-02-24 14:07:54 -05:00
if e . message != "" :
self . logger . error ( e )
2024-01-09 03:07:04 -05:00
return PlainTextResponse ()
2023-03-09 11:38:58 -05:00
2023-06-30 01:19:17 -04:00
if req . format_ver == 3 :
2023-02-18 00:00:30 -05:00
resp = AllnetPowerOnResponse3 ( req . token )
2023-06-30 01:19:17 -04:00
elif req . format_ver == 2 :
2023-02-18 00:00:30 -05:00
resp = AllnetPowerOnResponse2 ()
2023-06-30 01:19:17 -04:00
else :
resp = AllnetPowerOnResponse ()
2023-02-18 00:00:30 -05:00
2023-08-08 12:35:38 -04:00
self . logger . debug ( f "Allnet request: { vars ( req ) } " )
2023-02-18 00:00:30 -05:00
2024-01-09 14:42:17 -05:00
machine = await self . data . arcade . get_machine ( req . serial )
2023-02-18 00:00:30 -05:00
if machine is None and not self . config . server . allow_unregistered_serials :
msg = f "Unrecognised serial { req . serial } attempted allnet auth from { request_ip } ."
2024-01-09 14:42:17 -05:00
await self . data . base . log_event (
2023-03-09 11:38:58 -05:00
"allnet" , "ALLNET_AUTH_UNKNOWN_SERIAL" , logging . WARN , msg
)
2023-08-08 10:17:56 -04:00
self . logger . warning ( msg )
2023-02-18 00:00:30 -05:00
2023-08-08 12:35:38 -04:00
resp . stat = ALLNET_STAT . bad_machine . value
2023-06-30 01:19:17 -04:00
resp_dict = { k : v for k , v in vars ( resp ) . items () if v is not None }
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( urllib . parse . unquote ( urllib . parse . urlencode ( resp_dict )) + " \n " )
2023-03-09 11:38:58 -05:00
2023-02-18 00:00:30 -05:00
if machine is not None :
2024-01-09 14:42:17 -05:00
arcade = await self . data . arcade . get_arcade ( machine [ "arcade" ])
2023-08-08 12:35:38 -04:00
if self . config . server . check_arcade_ip :
if arcade [ "ip" ] and arcade [ "ip" ] is not None and arcade [ "ip" ] != req . ip :
msg = f "Serial { req . serial } attempted allnet auth from bad IP { req . ip } (expected { arcade [ 'ip' ] } )."
2024-01-09 14:42:17 -05:00
await self . data . base . log_event (
2023-08-08 12:35:38 -04:00
"allnet" , "ALLNET_AUTH_BAD_IP" , logging . ERROR , msg
)
self . logger . warning ( msg )
resp . stat = ALLNET_STAT . bad_shop . value
resp_dict = { k : v for k , v in vars ( resp ) . items () if v is not None }
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( urllib . parse . unquote ( urllib . parse . urlencode ( resp_dict )) + " \n " )
2023-08-08 12:35:38 -04:00
2023-10-16 13:07:05 +00:00
elif ( not arcade [ "ip" ] or arcade [ "ip" ] is None ) and self . config . server . strict_ip_checking :
2023-08-08 12:35:38 -04:00
msg = f "Serial { req . serial } attempted allnet auth from bad IP { req . ip } , but arcade { arcade [ 'id' ] } has no IP set! (strict checking enabled)."
2024-01-09 14:42:17 -05:00
await self . data . base . log_event (
2023-08-08 12:35:38 -04:00
"allnet" , "ALLNET_AUTH_NO_SHOP_IP" , logging . ERROR , msg
)
self . logger . warning ( msg )
resp . stat = ALLNET_STAT . bad_shop . value
resp_dict = { k : v for k , v in vars ( resp ) . items () if v is not None }
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( urllib . parse . unquote ( urllib . parse . urlencode ( resp_dict )) + " \n " )
2023-08-08 12:35:38 -04:00
2023-03-09 11:38:58 -05:00
country = (
arcade [ "country" ] if machine [ "country" ] is None else machine [ "country" ]
)
2023-03-03 15:52:58 -05:00
if country is None :
country = AllnetCountryCode . JAPAN . value
resp . country = country
2023-02-18 23:42:27 -05:00
resp . place_id = arcade [ "id" ]
resp . allnet_id = machine [ "id" ]
2023-03-03 15:45:21 -05:00
resp . name = arcade [ "name" ] if arcade [ "name" ] is not None else ""
resp . nickname = arcade [ "nickname" ] if arcade [ "nickname" ] is not None else ""
2023-03-09 11:38:58 -05:00
resp . region0 = (
arcade [ "region_id" ]
if arcade [ "region_id" ] is not None
else AllnetJapanRegionId . AICHI . value
)
resp . region_name0 = (
arcade [ "state" ]
if arcade [ "state" ] is not None
else AllnetJapanRegionId . AICHI . name
)
2023-09-19 02:15:22 -04:00
resp . region_name1 = (
arcade [ "country" ]
if arcade [ "country" ] is not None
else AllnetCountryCode . JAPAN . value
)
2023-03-03 15:45:21 -05:00
resp . region_name2 = arcade [ "city" ] if arcade [ "city" ] is not None else ""
2023-09-19 02:15:22 -04:00
resp . client_timezone = ( # lmao
arcade [ "timezone" ] if arcade [ "timezone" ] is not None else "+0900" if req . format_ver == 3 else "+09:00"
2023-03-09 11:38:58 -05:00
)
2023-08-08 12:35:38 -04:00
2023-11-09 02:17:48 +00:00
if req . game_id not in TitleServlet . title_registry :
2023-08-08 12:35:38 -04:00
if not self . config . server . is_develop :
msg = f "Unrecognised game { req . game_id } attempted allnet auth from { request_ip } ."
2024-01-09 14:42:17 -05:00
await self . data . base . log_event (
2023-08-08 12:35:38 -04:00
"allnet" , "ALLNET_AUTH_UNKNOWN_GAME" , logging . WARN , msg
)
self . logger . warning ( msg )
resp . stat = ALLNET_STAT . bad_game . value
resp_dict = { k : v for k , v in vars ( resp ) . items () if v is not None }
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( urllib . parse . unquote ( urllib . parse . urlencode ( resp_dict )) + " \n " )
2023-08-08 12:35:38 -04:00
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 } "
)
2024-01-09 03:07:04 -05:00
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 } "
2023-08-08 12:35:38 -04:00
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 } " )
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( resp_str + " \n " )
2023-08-08 12:35:38 -04:00
2023-11-09 02:17:48 +00:00
2023-02-18 23:40:19 -05:00
int_ver = req . ver . replace ( "." , "" )
2023-11-09 02:17:48 +00:00
resp . uri , resp . host = TitleServlet . title_registry [ req . game_id ] . get_allnet_info ( req . game_id , int ( int_ver ), req . serial )
2023-03-09 11:38:58 -05:00
2023-02-18 00:00:30 -05:00
msg = f " { req . serial } authenticated from { request_ip } : { req . game_id } v { req . ver } "
2024-01-09 14:42:17 -05:00
await self . data . base . log_event ( "allnet" , "ALLNET_AUTH_SUCCESS" , logging . INFO , msg )
2023-02-18 00:00:30 -05:00
self . logger . info ( msg )
2023-06-30 01:19:17 -04:00
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_dict } " )
resp_str += " \n "
2023-08-21 00:10:25 -04:00
"""if is_dfi:
request.responseHeaders.addRawHeader('Pragma', 'DFI')
return self.to_dfi(resp_str)"""
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( resp_str )
2023-02-18 00:00:30 -05:00
2024-01-09 03:07:04 -05:00
async def handle_dlorder ( self , request : Request ):
2023-03-12 01:00:51 -05:00
request_ip = Utils . get_ip_addr ( request )
2024-01-09 03:07:04 -05:00
pragma_header = request . headers . get ( 'Pragma' , "" )
2023-08-21 00:10:25 -04:00
is_dfi = pragma_header is not None and pragma_header == "DFI"
2024-01-09 03:07:04 -05:00
data = await request . body ()
2023-08-21 00:10:25 -04:00
2023-02-18 00:00:30 -05:00
try :
2023-08-21 00:10:25 -04:00
if is_dfi :
2024-01-09 03:07:04 -05:00
req_urlencode = self . from_dfi ( data )
2023-08-21 00:10:25 -04:00
else :
2024-01-09 03:07:04 -05:00
req_urlencode = data . decode ()
2023-08-21 00:10:25 -04:00
req_dict = self . allnet_req_to_dict ( req_urlencode )
2023-02-24 14:07:54 -05:00
if req_dict is None :
raise AllnetRequestException ()
req = AllnetDownloadOrderRequest ( req_dict [ 0 ])
2023-02-18 00:00:30 -05:00
# Validate the request. Currently we only validate the fields we plan on using
2023-02-19 00:01:39 -05:00
if not req . game_id or not req . ver or not req . serial :
2023-03-09 11:38:58 -05:00
raise AllnetRequestException (
f "Bad download request params from { request_ip } - { vars ( req ) } "
)
2023-02-16 17:13:41 -05:00
except AllnetRequestException as e :
2023-02-24 14:07:54 -05:00
if e . message != "" :
self . logger . error ( e )
2024-01-09 03:07:04 -05:00
return PlainTextResponse ()
2023-02-16 17:13:41 -05:00
2023-04-23 21:04:52 -04:00
self . logger . info (
f "DownloadOrder from { request_ip } -> { req . game_id } v { req . ver } serial { req . serial } "
)
2023-05-09 03:53:31 -04:00
resp = AllnetDownloadOrderResponse ( serial = req . serial )
2023-04-23 21:04:52 -04:00
if (
not self . config . allnet . allow_online_updates
or not self . config . allnet . update_cfg_folder
):
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( urllib . parse . unquote ( urllib . parse . urlencode ( vars ( resp ))) + " \n " )
2023-03-09 11:38:58 -05:00
2023-04-23 19:00:30 -04:00
else : # TODO: Keychip check
2023-04-23 21:04:52 -04:00
if path . exists (
2023-05-24 01:08:53 -04:00
f " { self . config . allnet . update_cfg_folder } / { req . game_id } - { req . ver . replace ( '.' , '' ) } -app.ini"
2023-04-23 21:04:52 -04:00
):
2024-01-09 03:07:04 -05:00
resp . uri = f "http:// { self . config . server . hostname } : { self . config . server . port } /dl/ini/ { req . game_id } - { req . ver . replace ( '.' , '' ) } -app.ini"
2023-04-23 21:04:52 -04:00
if path . exists (
2023-05-24 01:08:53 -04:00
f " { self . config . allnet . update_cfg_folder } / { req . game_id } - { req . ver . replace ( '.' , '' ) } -opt.ini"
2023-04-23 21:04:52 -04:00
):
2024-01-09 03:07:04 -05:00
resp . uri += f "|http:// { self . config . server . hostname } : { self . config . server . port } /dl/ini/ { req . game_id } - { req . ver . replace ( '.' , '' ) } -opt.ini"
2023-04-23 21:04:52 -04:00
2023-04-23 19:00:30 -04:00
self . logger . debug ( f "Sending download uri { resp . uri } " )
2024-01-09 14:42:17 -05:00
await 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 } " )
2023-06-30 01:19:17 -04:00
2023-08-21 00:10:25 -04:00
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)"""
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( res_str )
2023-02-16 17:13:41 -05:00
2024-01-09 03:07:04 -05:00
async def handle_dlorder_ini ( self , request : Request ) -> bytes :
2024-01-09 19:10:54 -05:00
req_file = request . path_params . get ( "file" , "" ) . replace ( "%0A" , "" ) . replace ( " \n " , "" )
2023-04-23 21:04:52 -04:00
2024-01-09 03:07:04 -05:00
if not req_file :
return PlainTextResponse ( status_code = 404 )
2023-04-23 19:00:30 -04:00
if path . exists ( f " { self . config . allnet . update_cfg_folder } / { req_file } " ):
2023-05-30 21:46:26 -04:00
self . logger . info ( f "Request for DL INI file { req_file } from { Utils . get_ip_addr ( request ) } successful" )
2024-01-09 14:42:17 -05:00
await self . data . base . log_event ( "allnet" , "DLORDER_INI_SENT" , logging . INFO , f " { Utils . get_ip_addr ( request ) } successfully recieved { req_file } " )
2023-07-23 12:47:10 -04:00
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( open (
2024-01-09 19:10:54 -05:00
f " { self . config . allnet . update_cfg_folder } / { req_file } " , "r" , encoding = "utf-8"
2024-01-09 03:07:04 -05:00
) . read ())
2023-04-23 21:04:52 -04:00
2023-04-23 19:00:30 -04:00
self . logger . info ( f "DL INI File { req_file } not found" )
2024-01-09 03:07:04 -05:00
return PlainTextResponse ()
2023-04-23 19:00:30 -04:00
2024-01-09 03:07:04 -05:00
async def handle_dlorder_report ( self , request : Request ) -> bytes :
req_raw = await request . body ()
2023-11-01 00:53:04 -04:00
client_ip = Utils . get_ip_addr ( request )
2023-07-23 12:47:10 -04:00
try :
req_dict : Dict = json . loads ( req_raw )
except Exception as e :
2023-08-08 10:17:56 -04:00
self . logger . warning ( f "Failed to parse DL Report: { e } " )
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( "NG" )
2023-07-23 12:47:10 -04:00
dl_data_type = DLIMG_TYPE . app
dl_data = req_dict . get ( "appimage" , {})
if dl_data is None or not dl_data :
dl_data_type = DLIMG_TYPE . opt
dl_data = req_dict . get ( "optimage" , {})
if dl_data is None or not dl_data :
2023-08-08 10:17:56 -04:00
self . logger . warning ( f "Failed to parse DL Report: Invalid format - contains neither appimage nor optimage" )
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( "NG" )
2023-07-23 12:47:10 -04:00
2023-11-01 00:53:04 -04:00
rep = DLReport ( dl_data , dl_data_type )
2023-07-23 12:47:10 -04:00
2023-11-01 00:53:04 -04:00
if not rep . validate ():
self . logger . warning ( f "Failed to parse DL Report: Invalid format - { rep . err } " )
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( "NG" )
2023-11-01 00:53:04 -04:00
2023-11-04 12:41:47 -04:00
msg = f " { rep . serial } @ { client_ip } reported { rep . rep_type . name } download state { rep . rf_state . name } for { rep . gd } v { rep . dav } :" \
2023-11-01 01:02:09 -04:00
f " { rep . tdsc } / { rep . tsc } segments downloaded for working files { rep . wfl } with { rep . dfl if rep . dfl else 'none' } complete."
2023-11-01 00:53:04 -04:00
2024-01-09 14:42:17 -05:00
await self . data . base . log_event ( "allnet" , "DL_REPORT" , logging . INFO , msg , dl_data )
2023-11-01 00:53:04 -04:00
self . logger . info ( msg )
2023-07-23 12:47:10 -04:00
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( "OK" )
2023-04-23 19:00:30 -04:00
2024-01-09 03:07:04 -05:00
async def handle_loaderstaterecorder ( self , request : Request ) -> bytes :
req_data = await request . body ()
2023-06-30 01:34:46 -04:00
sections = req_data . decode ( "utf-8" ) . split ( " \r\n " )
req_dict = dict ( urllib . parse . parse_qsl ( sections [ 0 ]))
2023-06-30 00:26:07 -04:00
serial : Union [ str , None ] = req_dict . get ( "serial" , None )
num_files_to_dl : Union [ str , None ] = req_dict . get ( "nb_ftd" , None )
num_files_dld : Union [ str , None ] = req_dict . get ( "nb_dld" , None )
dl_state : Union [ str , None ] = req_dict . get ( "dld_st" , None )
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 :
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( "NG" )
2023-06-30 00:26:07 -04:00
self . logger . info ( f "LoaderStateRecorder Request from { ip } { serial } : { num_files_dld } / { num_files_to_dl } Files download (State: { dl_state } )" )
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( "OK" )
2023-06-30 00:32:52 -04:00
2024-01-09 03:07:04 -05:00
async def handle_alive ( self , request : Request ) -> bytes :
return PlainTextResponse ( "OK" )
2023-06-30 00:26:07 -04:00
2024-01-09 03:07:04 -05:00
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
2024-01-11 21:01:51 -05:00
def startup ( self ) -> None :
self . logger . info ( f "Ready on port { self . config . billing . port if self . config . billing . standalone else self . config . server . port } " )
2024-01-09 03:07:04 -05:00
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 ()
2023-08-21 00:10:25 -04:00
2024-01-09 03:07:04 -05:00
if request . headers . get ( 'Content-Type' , '' ) == "application/octet-stream" :
2023-08-21 00:10:25 -04:00
req_unzip = zlib . decompressobj ( - zlib . MAX_WBITS ) . decompress ( req_raw )
else :
req_unzip = req_raw
req_dict = self . billing_req_to_dict ( req_unzip )
2023-03-17 02:11:49 -04:00
request_ip = Utils . get_ip_addr ( request )
2023-08-21 00:10:25 -04:00
2023-02-18 00:00:30 -05:00
if req_dict is None :
2024-01-09 03:07:04 -05:00
self . logger . error ( f "Failed to parse request { req_raw } " )
return PlainTextResponse ()
2023-03-09 11:38:58 -05:00
2023-02-18 00:00:30 -05:00
self . logger . debug ( f "request { req_dict } " )
2023-03-09 11:38:58 -05:00
rsa = RSA . import_key ( open ( self . config . billing . signing_key , "rb" ) . read ())
2023-02-18 00:00:30 -05:00
signer = PKCS1_v1_5 . new ( rsa )
digest = SHA . new ()
2023-08-21 01:50:59 -04:00
traces : List [ TraceData ] = []
2023-05-03 03:25:55 -04:00
try :
2023-12-02 21:01:33 -05:00
req = BillingInfo ( req_dict [ 0 ])
except KeyError as e :
self . logger . error ( f "Billing request failed to parse: { e } " )
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( "result=5&linelimit=&message=field is missing or formatting is incorrect \r\n " )
2023-08-21 01:50:59 -04:00
2023-12-02 21:01:33 -05:00
for x in range ( 1 , len ( req_dict )):
if not req_dict [ x ]:
continue
try :
2023-08-21 01:50:59 -04:00
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 )
2023-12-02 21:01:33 -05:00
except KeyError as e :
self . logger . warn ( f "Tracelog failed to parse: { e } " )
2023-08-21 01:50:59 -04:00
2023-12-02 21:01:33 -05:00
kc_serial_bytes = req . keychipid . encode ()
2023-05-03 03:25:55 -04:00
2023-02-18 00:00:30 -05:00
2024-01-09 14:42:17 -05:00
machine = await self . data . arcade . get_machine ( req . keychipid )
2023-02-18 00:00:30 -05:00
if machine is None and not self . config . server . allow_unregistered_serials :
2023-08-21 01:50:59 -04:00
msg = f "Unrecognised serial { req . keychipid } attempted billing checkin from { request_ip } for { req . gameid } v { req . gamever } ."
2024-01-09 14:42:17 -05:00
await self . data . base . log_event (
2023-03-09 11:38:58 -05:00
"allnet" , "BILLING_CHECKIN_NG_SERIAL" , logging . WARN , msg
)
2023-08-08 10:17:56 -04:00
self . logger . warning ( msg )
2023-02-18 00:00:30 -05:00
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( f "result=1&requestno= { req . requestno } &message=Keychip Serial bad \r\n " )
2023-02-18 00:00:30 -05:00
2023-03-09 11:38:58 -05:00
msg = (
2023-08-21 01:50:59 -04:00
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 } "
2023-03-09 11:38:58 -05:00
)
2023-02-18 00:00:30 -05:00
self . logger . info ( msg )
2024-01-09 14:42:17 -05:00
await self . data . base . log_event ( "billing" , "BILLING_CHECKIN_OK" , logging . INFO , msg )
2023-08-21 01:50:59 -04:00
if req . traceleft > 0 :
self . logger . warn ( f " { req . traceleft } unsent tracelogs" )
kc_playlimit = req . playlimit
kc_nearfull = req . nearfull
2023-02-18 00:00:30 -05:00
2023-08-21 01:50:59 -04:00
while req . playcnt > req . playlimit :
2023-02-18 00:00:30 -05:00
kc_playlimit += 1024
kc_nearfull += 1024
2023-03-09 11:38:58 -05:00
2023-02-18 00:00:30 -05:00
playlimit = kc_playlimit
2023-08-21 01:50:59 -04:00
nearfull = kc_nearfull + ( req . billingtype . value * 0x00010000 )
2023-02-18 00:00:30 -05:00
2023-03-09 11:38:58 -05:00
digest . update ( playlimit . to_bytes ( 4 , "little" ) + kc_serial_bytes )
2023-02-18 00:00:30 -05:00
playlimit_sig = signer . sign ( digest ) . hex ()
digest = SHA . new ()
2023-03-09 11:38:58 -05:00
digest . update ( nearfull . to_bytes ( 4 , "little" ) + kc_serial_bytes )
2023-02-18 00:00:30 -05:00
nearfull_sig = signer . sign ( digest ) . hex ()
# TODO: playhistory
2023-08-21 01:50:59 -04:00
resp = BillingResponse ( playlimit , playlimit_sig , nearfull , nearfull_sig , req . requestno , req . protocolver )
2023-02-18 00:00:30 -05:00
2023-08-21 00:10:25 -04:00
resp_str = urllib . parse . unquote ( urllib . parse . urlencode ( vars ( resp ))) + " \r\n "
2023-02-18 00:00:30 -05:00
self . logger . debug ( f "response { vars ( resp ) } " )
2023-08-21 01:50:59 -04:00
if req . traceleft > 0 :
self . logger . info ( f "Requesting 20 more of { req . traceleft } unsent tracelogs" )
2024-01-09 03:07:04 -05:00
return PlainTextResponse ( "result=6&waittime=0&linelimit=20 \r\n " )
return PlainTextResponse ( resp_str )
2023-03-09 11:38:58 -05:00
class AllnetPowerOnRequest :
2023-02-16 17:13:41 -05:00
def __init__ ( self , req : Dict ) -> None :
if req is None :
raise AllnetRequestException ( "Request processing failed" )
2023-06-30 01:19:17 -04:00
self . game_id : str = req . get ( "game_id" , None )
self . ver : str = req . get ( "ver" , None )
self . serial : str = req . get ( "serial" , None )
self . ip : str = req . get ( "ip" , None )
self . firm_ver : str = req . get ( "firm_ver" , None )
self . boot_ver : str = req . get ( "boot_ver" , None )
self . encode : str = req . get ( "encode" , "EUC-JP" )
self . hops = int ( req . get ( "hops" , "-1" ))
self . format_ver = float ( req . get ( "format_ver" , "1.00" ))
self . token : str = req . get ( "token" , "0" )
2023-02-16 00:06:42 -05:00
2023-06-30 01:19:17 -04:00
class AllnetPowerOnResponse :
def __init__ ( self ) -> None :
2023-02-16 17:13:41 -05:00
self . stat = 1
self . uri = ""
self . host = ""
2023-11-09 02:17:48 +00:00
self . place_id = "0123"
2023-05-26 21:41:16 -04:00
self . name = "ARTEMiS"
self . nickname = "ARTEMiS"
2023-02-16 17:13:41 -05:00
self . region0 = "1"
self . region_name0 = "W"
self . region_name1 = ""
self . region_name2 = ""
2023-06-30 01:19:17 -04:00
self . region_name3 = ""
2023-03-16 22:31:41 -04:00
self . setting = "1"
2023-02-16 17:13:41 -05:00
self . year = datetime . now () . year
self . month = datetime . now () . month
self . day = datetime . now () . day
self . hour = datetime . now () . hour
self . minute = datetime . now () . minute
self . second = datetime . now () . second
2023-06-30 01:19:17 -04:00
class AllnetPowerOnResponse3 ( AllnetPowerOnResponse ):
def __init__ ( self , token ) -> None :
super () . __init__ ()
# Added in v3
self . country = "JPN"
self . allnet_id = "123"
self . client_timezone = "+0900"
self . utc_time = datetime . now ( tz = pytz . timezone ( "UTC" )) . strftime (
"%Y-%m- %d T%H:%M:%SZ"
)
self . res_ver = "3"
self . token = token
# Removed in v3
self . year = None
self . month = None
self . day = None
self . hour = None
self . minute = None
self . second = None
class AllnetPowerOnResponse2 ( AllnetPowerOnResponse ):
def __init__ ( self ) -> None :
super () . __init__ ()
# Added in v2
self . country = "JPN"
self . timezone = "+09:00"
2023-02-16 17:13:41 -05:00
self . res_class = "PowerOnResponseV2"
2023-03-09 11:38:58 -05:00
class AllnetDownloadOrderRequest :
2023-02-16 17:13:41 -05:00
def __init__ ( self , req : Dict ) -> None :
2023-03-09 12:17:10 -05:00
self . game_id = req . get ( "game_id" , "" )
self . ver = req . get ( "ver" , "" )
self . serial = req . get ( "serial" , "" )
self . encode = req . get ( "encode" , "" )
2023-02-16 17:13:41 -05:00
2023-03-09 11:38:58 -05:00
class AllnetDownloadOrderResponse :
2023-04-23 19:00:30 -04:00
def __init__ ( self , stat : int = 1 , serial : str = "" , uri : str = "" ) -> None :
2023-02-16 17:13:41 -05:00
self . stat = stat
self . serial = serial
self . uri = uri
2023-08-21 01:50:59 -04:00
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 ):
2023-08-21 01:53:27 -04:00
return f "%. { 2 - int ( math . log10 ( f )) + 1 } f" % f
2023-08-21 01:50:59 -04:00
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 ))
2024-01-14 16:43:25 -05:00
self . datamax = int ( data . get ( "datamax" , None ))
2023-08-21 01:50:59 -04:00
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 ))
2023-12-02 21:01:33 -05:00
dt = data . get ( "dt" , None )
if dt is None :
raise KeyError ( "dt not present" )
if dt == "20000000000000" : # Not sure what causes it to send like this...
self . date = datetime ( 2000 , 1 , 1 , 0 , 0 , 0 , 0 )
else :
self . date = datetime . strptime ( data . get ( "dt" , None ), BILLING_DT_FORMAT )
2023-08-21 01:50:59 -04:00
self . keychip = str ( data . get ( "kn" , None ))
2023-12-02 21:01:33 -05:00
self . lib_ver = float ( data . get ( "alib" , 0 ))
2023-08-21 01:50:59 -04:00
except Exception as e :
raise KeyError ( e )
class TraceDataCharge ( TraceData ):
def __init__ ( self , data : Dict ) -> None :
super () . __init__ ( data )
try :
2023-12-02 21:01:33 -05:00
self . game_id = str ( data . get ( "gi" , None )) # these seem optional...?
self . game_version = float ( data . get ( "gv" , 0 ))
2023-08-21 01:50:59 -04:00
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 )
2023-02-16 17:13:41 -05:00
2023-03-09 11:38:58 -05:00
class BillingResponse :
def __init__ (
self ,
playlimit : str = "" ,
playlimit_sig : str = "" ,
nearfull : str = "" ,
nearfull_sig : str = "" ,
2023-08-21 01:50:59 -04:00
request_num : int = 1 ,
protocol_ver : float = 1.000 ,
2023-03-09 11:38:58 -05:00
playhistory : str = "000000/0:000000/0:000000/0" ,
) -> None :
2023-08-21 01:50:59 -04:00
self . result = 0
self . requestno = request_num
self . traceerase = 1
self . fixinterval = 120
self . fixlogcnt = 100
2023-02-16 17:13:41 -05:00
self . playlimit = playlimit
self . playlimitsig = playlimit_sig
2023-08-21 01:50:59 -04:00
self . playhistory = playhistory
2023-02-16 17:13:41 -05:00
self . nearfull = nearfull
self . nearfullsig = nearfull_sig
2023-08-21 01:50:59 -04:00
self . linelimit = 100
self . protocolver = float5 . to_str ( protocol_ver )
2023-02-16 17:13:41 -05:00
# playhistory -> YYYYMM/C:...
# YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period
class AllnetRequestException ( Exception ):
2023-02-24 14:07:54 -05:00
def __init__ ( self , message = "" ) -> None :
2023-02-18 00:00:30 -05:00
self . message = message
super () . __init__ ( self . message )
2023-07-23 12:47:10 -04:00
class DLReport :
def __init__ ( self , data : Dict , report_type : DLIMG_TYPE ) -> None :
self . serial = data . get ( "serial" )
self . dfl = data . get ( "dfl" )
self . wfl = data . get ( "wfl" )
self . tsc = data . get ( "tsc" )
self . tdsc = data . get ( "tdsc" )
self . at = data . get ( "at" )
self . ot = data . get ( "ot" )
self . rt = data . get ( "rt" )
self . as_ = data . get ( "as" )
2023-11-01 20:08:20 -04:00
self . rf_state = DLI_STATUS . from_int ( data . get ( "rf_state" ))
2023-07-23 12:47:10 -04:00
self . gd = data . get ( "gd" )
self . dav = data . get ( "dav" )
self . wdav = data . get ( "wdav" ) # app only
self . dov = data . get ( "dov" )
self . wdov = data . get ( "wdov" ) # app only
2023-11-04 12:41:47 -04:00
self . rep_type = report_type
2023-07-23 12:47:10 -04:00
self . err = ""
def validate ( self ) -> bool :
if self . serial is None :
self . err = "serial not provided"
return False
if self . tsc is None :
self . err = "tsc not provided"
return False
if self . tdsc is None :
self . err = "tdsc not provided"
return False
if self . as_ is None :
self . err = "as not provided"
return False
if self . rf_state is None :
self . err = "rf_state not provided"
return False
if self . gd is None :
self . err = "gd not provided"
return False
if self . dav is None :
self . err = "dav not provided"
return False
if self . dov is None :
self . err = "dov not provided"
return False
2023-11-04 12:41:47 -04:00
if ( self . wdav is None or self . wdov is None ) and self . rep_type == DLIMG_TYPE . app :
2023-07-23 12:47:10 -04:00
self . err = "wdav or wdov not provided in app image"
return False
2023-11-04 12:41:47 -04:00
if ( self . wdav is not None or self . wdov is not None ) and self . rep_type == DLIMG_TYPE . opt :
2023-07-23 12:47:10 -04:00
self . err = "wdav or wdov provided in opt image"
return False
return True
2024-01-09 03:07:04 -05:00
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 )
2024-01-11 12:08:22 -05:00
app_billing = Starlette (
2024-01-09 03:07:04 -05:00
cfg . server . is_develop ,
[
Route ( "/request" , billing . handle_billing_request , methods = [ "POST" ]),
Route ( "/request/" , billing . handle_billing_request , methods = [ "POST" ]),
2024-01-11 21:01:51 -05:00
],
on_startup = [ billing . startup ]
2024-01-09 03:07:04 -05:00
)
2024-01-11 12:08:22 -05:00
allnet = AllnetServlet ( cfg , cfg_dir )
route_lst = [
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 ( "/naomitest.html" , allnet . handle_naomitest ),
]
if cfg . allnet . allow_online_updates :
route_lst += [
Route ( "/report-api/Report" , allnet . handle_dlorder_report , methods = [ "POST" ]),
Route ( "/dl/ini/{file:str}" , allnet . handle_dlorder_ini ),
]
app_allnet = Starlette (
cfg . server . is_develop ,
2024-01-11 21:01:51 -05:00
route_lst ,
on_startup = [ allnet . startup ]
2024-01-11 12:08:22 -05:00
)