2023-02-18 00:00:30 -05:00
from typing import Dict , List , Any , Optional , Tuple
2023-02-16 00:06:42 -05:00
import logging , coloredlogs
from logging.handlers import TimedRotatingFileHandler
2023-02-16 17:13:41 -05:00
from twisted.web.http import Request
from datetime import datetime
import pytz
import base64
import zlib
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
from time import strptime
2023-02-16 00:06:42 -05:00
from core.config import CoreConfig
2023-02-16 17:13:41 -05:00
from core.utils import Utils
2023-03-12 01:00:51 -05:00
from core.data import Data
2023-03-03 15:45:21 -05:00
from core.const import *
2023-02-16 00:06:42 -05:00
2023-03-09 11:38:58 -05:00
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
super () . __init__ ()
self . config = core_cfg
self . config_folder = cfg_folder
self . data = Data ( core_cfg )
2023-02-18 00:00:30 -05:00
self . uri_registry : Dict [ str , Tuple [ str , str ]] = {}
2023-02-16 00:06:42 -05:00
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
2023-02-18 00:00:30 -05:00
plugins = Utils . get_all_titles ()
2023-02-16 17:13:41 -05:00
2023-02-18 00:00:30 -05:00
if len ( plugins ) == 0 :
2023-02-16 17:13:41 -05:00
self . logger . error ( "No games detected!" )
2023-03-09 11:38:58 -05:00
2023-02-18 00:00:30 -05:00
for _ , mod in plugins . items ():
2023-04-12 02:34:29 -04:00
if hasattr ( mod , "index" ) and hasattr ( mod . index , "get_allnet_info" ):
2023-03-04 21:27:52 -05:00
for code in mod . game_codes :
2023-03-09 11:38:58 -05:00
enabled , uri , host = mod . index . get_allnet_info (
code , self . config , self . config_folder
)
2023-03-04 21:27:52 -05:00
if enabled :
self . uri_registry [ code ] = ( uri , host )
2023-03-09 11:38:58 -05:00
self . logger . info (
2023-03-12 01:59:12 -05:00
f "Serving { len ( self . uri_registry ) } game codes port { core_cfg . allnet . port } "
2023-03-09 11:38:58 -05:00
)
2023-02-16 17:13:41 -05:00
2023-02-18 23:12:40 -05:00
def handle_poweron ( self , request : Request , _ : Dict ):
2023-03-12 01:00:51 -05:00
request_ip = Utils . get_ip_addr ( request )
2023-02-16 17:13:41 -05:00
try :
2023-02-24 14:07:54 -05:00
req_dict = self . allnet_req_to_dict ( request . content . getvalue ())
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-03-10 20:31:29 -05:00
if not req . game_id or not req . ver or not req . serial or not req . ip :
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 )
2023-02-18 00:00:30 -05:00
return b ""
2023-03-09 11:38:58 -05:00
2023-03-10 20:31:29 -05:00
if req . format_ver == "3" :
2023-02-18 00:00:30 -05:00
resp = AllnetPowerOnResponse3 ( req . token )
else :
resp = AllnetPowerOnResponse2 ()
2023-03-03 15:39:14 -05:00
self . logger . debug ( f "Allnet request: { vars ( req ) } " )
2023-02-18 00:00:30 -05:00
if req . game_id not in self . uri_registry :
2023-03-19 23:52:33 -04:00
if not self . config . server . is_develop :
msg = f "Unrecognised game { req . game_id } attempted allnet auth from { request_ip } ."
self . data . base . log_event (
"allnet" , "ALLNET_AUTH_UNKNOWN_GAME" , logging . WARN , msg
)
self . logger . warn ( msg )
2023-02-18 00:00:30 -05:00
2023-03-19 23:52:33 -04:00
resp . stat = 0
return self . dict_to_http_form_string ([ vars ( resp )])
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 } "
return self . dict_to_http_form_string ([ vars ( resp )])
2023-03-09 11:38:58 -05:00
2023-02-18 00:00:30 -05:00
resp . uri , resp . host = self . uri_registry [ req . game_id ]
machine = self . data . arcade . get_machine ( req . serial )
if machine is None and not self . config . server . allow_unregistered_serials :
msg = f "Unrecognised serial { req . serial } attempted allnet auth from { request_ip } ."
2023-03-09 11:38:58 -05:00
self . data . base . log_event (
"allnet" , "ALLNET_AUTH_UNKNOWN_SERIAL" , logging . WARN , msg
)
2023-02-18 00:00:30 -05:00
self . logger . warn ( msg )
resp . stat = 0
return self . dict_to_http_form_string ([ vars ( resp )])
2023-03-09 11:38:58 -05:00
2023-02-18 00:00:30 -05:00
if machine is not None :
arcade = self . data . arcade . get_arcade ( machine [ "arcade" ])
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 [ "country" ]
if arcade [ "country" ] is not None
else AllnetCountryCode . JAPAN . value
)
resp . region_name1 = (
arcade [ "state" ]
if arcade [ "state" ] is not None
else AllnetJapanRegionId . AICHI . name
)
2023-03-03 15:45:21 -05:00
resp . region_name2 = arcade [ "city" ] if arcade [ "city" ] is not None else ""
2023-03-09 11:38:58 -05:00
resp . client_timezone = (
arcade [ "timezone" ] if arcade [ "timezone" ] is not None else "+0900"
)
2023-02-18 23:40:19 -05:00
int_ver = req . ver . replace ( "." , "" )
2023-02-18 23:42:27 -05:00
resp . uri = resp . uri . replace ( "$v" , int_ver )
resp . host = resp . host . replace ( "$v" , int_ver )
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 } "
self . data . base . log_event ( "allnet" , "ALLNET_AUTH_SUCCESS" , logging . INFO , msg )
self . logger . info ( msg )
2023-03-03 15:39:14 -05:00
self . logger . debug ( f "Allnet response: { vars ( resp ) } " )
2023-02-18 00:00:30 -05:00
2023-02-18 23:40:19 -05:00
return self . dict_to_http_form_string ([ vars ( resp )]) . encode ( "utf-8" )
2023-02-18 00:00:30 -05:00
2023-02-18 23:12:40 -05:00
def handle_dlorder ( self , request : Request , _ : Dict ):
2023-03-12 01:00:51 -05:00
request_ip = Utils . get_ip_addr ( request )
2023-02-18 00:00:30 -05:00
try :
2023-02-24 14:07:54 -05:00
req_dict = self . allnet_req_to_dict ( request . content . getvalue ())
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 )
2023-02-16 17:13:41 -05:00
return b ""
2023-03-17 02:06:15 -04:00
self . logger . info ( f "DownloadOrder from { request_ip } -> { req . game_id } v { req . ver } serial { req . serial } " )
2023-02-18 00:00:30 -05:00
resp = AllnetDownloadOrderResponse ()
2023-03-17 02:06:15 -04:00
2023-02-18 00:00:30 -05:00
if not self . config . allnet . allow_online_updates :
2023-02-19 00:06:21 -05:00
return self . dict_to_http_form_string ([ vars ( resp )])
2023-03-09 11:38:58 -05:00
else : # TODO: Actual dlorder response
2023-02-19 00:06:21 -05:00
return self . dict_to_http_form_string ([ vars ( resp )])
2023-02-16 17:13:41 -05:00
2023-02-18 23:12:40 -05:00
def handle_billing_request ( self , request : Request , _ : Dict ):
2023-02-18 00:00:30 -05:00
req_dict = self . billing_req_to_dict ( request . content . getvalue ())
2023-03-17 02:11:49 -04:00
request_ip = Utils . get_ip_addr ( request )
2023-02-18 00:00:30 -05:00
if req_dict is None :
self . logger . error ( f "Failed to parse request { request . content . getvalue () } " )
return b ""
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 ()
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 ()
machine = self . data . arcade . get_machine ( kc_serial )
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 } ."
2023-03-09 11:38:58 -05:00
self . data . base . log_event (
"allnet" , "BILLING_CHECKIN_NG_SERIAL" , logging . WARN , msg
)
2023-02-18 00:00:30 -05:00
self . logger . warn ( msg )
resp = BillingResponse ( "" , "" , "" , "" )
resp . result = "1"
return self . dict_to_http_form_string ([ vars ( resp )])
2023-03-09 11:38:58 -05:00
msg = (
2023-03-17 02:11:49 -04:00
f "Billing checkin from { request_ip } : game { kc_game } keychip { kc_serial } playcount "
2023-02-18 00:00:30 -05:00
f " { kc_playcount } billing_type { kc_billigtype } nearfull { kc_nearfull } playlimit { kc_playlimit } "
2023-03-09 11:38:58 -05:00
)
2023-02-18 00:00:30 -05:00
self . logger . info ( msg )
2023-03-09 11:38:58 -05:00
self . data . base . log_event ( "billing" , "BILLING_CHECKIN_OK" , logging . INFO , msg )
2023-02-18 00:00:30 -05:00
while kc_playcount > kc_playlimit :
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
nearfull = kc_nearfull + ( kc_billigtype * 0x00010000 )
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
resp = BillingResponse ( playlimit , playlimit_sig , nearfull , nearfull_sig )
2023-02-18 23:42:27 -05:00
resp_str = self . dict_to_http_form_string ([ vars ( resp )], True )
2023-02-18 00:00:30 -05:00
if resp_str is None :
self . logger . error ( f "Failed to parse response { vars ( resp ) } " )
self . logger . debug ( f "response { vars ( resp ) } " )
return resp_str . encode ( "utf-8" )
2023-02-16 17:13:41 -05:00
2023-02-23 23:11:43 -05:00
def handle_naomitest ( self , request : Request , _ : Dict ) -> bytes :
2023-03-12 01:00:51 -05:00
self . logger . info ( f "Ping from { Utils . get_ip_addr ( request ) } " )
2023-02-23 23:11:43 -05:00
return b "naomi ok"
2023-02-18 23:24:38 -05:00
def kvp_to_dict ( self , kvp : List [ str ]) -> List [ Dict [ str , Any ]]:
2023-02-16 17:13:41 -05:00
ret : List [ Dict [ str , Any ]] = []
for x in kvp :
2023-03-09 11:38:58 -05:00
items = x . split ( "&" )
2023-02-16 17:13:41 -05:00
tmp = {}
for item in items :
2023-03-09 11:38:58 -05:00
kvp = item . split ( "=" )
2023-02-16 17:13:41 -05:00
if len ( kvp ) == 2 :
tmp [ kvp [ 0 ]] = kvp [ 1 ]
ret . append ( tmp )
2023-03-09 11:38:58 -05:00
2023-02-18 23:27:25 -05:00
return ret
2023-02-16 17:13:41 -05:00
2023-02-18 22:58:40 -05:00
def billing_req_to_dict ( self , data : bytes ):
2023-02-16 17:13:41 -05:00
"""
Parses an billing request string into a python dictionary
"""
try :
decomp = zlib . decompressobj ( - zlib . MAX_WBITS )
unzipped = decomp . decompress ( data )
2023-03-09 11:38:58 -05:00
sections = unzipped . decode ( "ascii" ) . split ( " \r\n " )
2023-02-18 23:02:50 -05:00
return self . kvp_to_dict ( sections )
2023-02-16 17:13:41 -05:00
except Exception as e :
2023-02-24 14:07:54 -05:00
self . logger . error ( f "billing_req_to_dict: { e } while parsing { data } " )
2023-02-16 17:13:41 -05:00
return None
2023-02-18 22:58:40 -05:00
def allnet_req_to_dict ( self , data : str ) -> Optional [ List [ Dict [ str , Any ]]]:
2023-02-16 17:13:41 -05:00
"""
Parses an allnet request string into a python dictionary
2023-03-09 11:38:58 -05:00
"""
2023-02-16 17:13:41 -05:00
try :
zipped = base64 . b64decode ( data )
unzipped = zlib . decompress ( zipped )
2023-03-09 11:38:58 -05:00
sections = unzipped . decode ( "utf-8" ) . split ( " \r\n " )
2023-02-18 23:02:50 -05:00
return self . kvp_to_dict ( sections )
2023-02-16 17:13:41 -05:00
except Exception as e :
2023-02-24 14:07:54 -05:00
self . logger . error ( f "allnet_req_to_dict: { e } while parsing { data } " )
2023-02-16 17:13:41 -05:00
return None
2023-03-09 11:38:58 -05:00
def dict_to_http_form_string (
self ,
data : List [ Dict [ str , Any ]],
crlf : bool = False ,
trailing_newline : bool = True ,
) -> Optional [ str ]:
2023-02-16 17:13:41 -05:00
"""
Takes a python dictionary and parses it into an allnet response string
"""
try :
urlencode = ""
for item in data :
2023-03-09 11:38:58 -05:00
for k , v in item . items ():
2023-02-16 17:13:41 -05:00
urlencode += f " { k } = { v } &"
if crlf :
urlencode = urlencode [: - 1 ] + " \r\n "
else :
urlencode = urlencode [: - 1 ] + " \n "
2023-03-09 11:38:58 -05:00
2023-02-16 17:13:41 -05:00
if not trailing_newline :
if crlf :
urlencode = urlencode [: - 2 ]
else :
urlencode = urlencode [: - 1 ]
return urlencode
2023-03-09 11:38:58 -05:00
2023-02-16 17:13:41 -05:00
except Exception as e :
2023-02-24 14:07:54 -05:00
self . logger . error ( f "dict_to_http_form_string: { e } while parsing { data } " )
2023-02-16 17:13:41 -05:00
return None
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-03-09 12:17:10 -05:00
self . game_id : str = req . get ( "game_id" , "" )
self . ver : str = req . get ( "ver" , "" )
self . serial : str = req . get ( "serial" , "" )
self . ip : str = req . get ( "ip" , "" )
self . firm_ver : str = req . get ( "firm_ver" , "" )
self . boot_ver : str = req . get ( "boot_ver" , "" )
self . encode : str = req . get ( "encode" , "" )
self . hops = int ( req . get ( "hops" , "0" ))
2023-03-10 20:31:29 -05:00
self . format_ver = req . get ( "format_ver" , "2" )
2023-03-09 16:59:50 -05:00
self . token = int ( req . get ( "token" , "0" ))
2023-02-16 00:06:42 -05:00
2023-03-09 11:38:58 -05:00
class AllnetPowerOnResponse3 :
2023-02-16 17:13:41 -05:00
def __init__ ( self , token ) -> None :
self . stat = 1
self . uri = ""
self . host = ""
self . place_id = "123"
self . name = ""
self . nickname = ""
self . region0 = "1"
self . region_name0 = "W"
self . region_name1 = ""
self . region_name2 = ""
self . region_name3 = ""
self . country = "JPN"
self . allnet_id = "123"
self . client_timezone = "+0900"
2023-03-09 11:38:58 -05:00
self . utc_time = datetime . now ( tz = pytz . timezone ( "UTC" )) . strftime (
"%Y-%m- %d T%H:%M:%SZ"
)
2023-03-16 22:31:41 -04:00
self . setting = "1"
2023-02-16 17:13:41 -05:00
self . res_ver = "3"
self . token = str ( token )
2023-02-16 00:06:42 -05:00
2023-03-09 11:38:58 -05:00
class AllnetPowerOnResponse2 :
2023-02-16 17:13:41 -05:00
def __init__ ( self ) -> None :
self . stat = 1
self . uri = ""
self . host = ""
self . place_id = "123"
self . name = "Test"
self . nickname = "Test123"
self . region0 = "1"
self . region_name0 = "W"
self . region_name1 = "X"
self . region_name2 = "Y"
self . region_name3 = "Z"
self . country = "JPN"
self . year = datetime . now () . year
self . month = datetime . now () . month
self . day = datetime . now () . day
self . hour = datetime . now () . hour
self . minute = datetime . now () . minute
self . second = datetime . now () . second
self . setting = "1"
self . timezone = "+0900"
self . res_class = "PowerOnResponseV2"
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-02-16 17:13:41 -05:00
def __init__ ( self , stat : int = 1 , serial : str = "" , uri : str = "null" ) -> None :
self . stat = stat
self . serial = serial
self . uri = uri
2023-03-09 11:38:58 -05:00
class BillingResponse :
def __init__ (
self ,
playlimit : str = "" ,
playlimit_sig : str = "" ,
nearfull : str = "" ,
nearfull_sig : str = "" ,
playhistory : str = "000000/0:000000/0:000000/0" ,
) -> None :
2023-02-16 17:13:41 -05:00
self . result = "0"
self . waitime = "100"
self . linelimit = "1"
self . message = ""
self . playlimit = playlimit
self . playlimitsig = playlimit_sig
self . protocolver = "1.000"
self . nearfull = nearfull
self . nearfullsig = nearfull_sig
self . fixlogincnt = "0"
self . fixinterval = "5"
2023-03-09 11:38:58 -05:00
self . playhistory = playhistory
2023-02-16 17:13:41 -05:00
# playhistory -> YYYYMM/C:...
# YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period
2023-03-09 11:38:58 -05:00
2023-02-16 17:13:41 -05:00
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 )