forked from Hay1tsme/artemis
Sync Develop branch's update
This commit is contained in:
164
core/allnet.py
164
core/allnet.py
@ -7,6 +7,7 @@ import logging
|
||||
import coloredlogs
|
||||
import urllib.parse
|
||||
import math
|
||||
import random
|
||||
from typing import Dict, List, Any, Optional, Union, Final
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from starlette.requests import Request
|
||||
@ -17,7 +18,10 @@ from datetime import datetime
|
||||
from enum import Enum
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Hash import SHA
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
import os
|
||||
from os import path, environ, mkdir, access, W_OK
|
||||
|
||||
from .config import CoreConfig
|
||||
@ -132,12 +136,29 @@ class AllnetServlet:
|
||||
async def handle_poweron(self, request: Request):
|
||||
request_ip = Utils.get_ip_addr(request)
|
||||
pragma_header = request.headers.get('Pragma', "")
|
||||
useragent_header = request.headers.get('User-Agent', "")
|
||||
is_dfi = pragma_header == "DFI"
|
||||
is_lite = useragent_header[5:] == "Windows/Lite"
|
||||
lite_id = useragent_header[:4]
|
||||
data = await request.body()
|
||||
|
||||
if not self.config.allnet.allnet_lite_keys and is_lite:
|
||||
self.logger.error("!!!LITE KEYS NOT SET!!!")
|
||||
raise AllnetRequestException()
|
||||
elif is_lite:
|
||||
for gameids, key in self.config.allnet.allnet_lite_keys.items():
|
||||
if gameids == lite_id:
|
||||
litekey = key
|
||||
|
||||
if is_lite and "litekey" not in locals():
|
||||
self.logger.error("!!!UNIQUE LITE KEY NOT FOUND!!!")
|
||||
raise AllnetRequestException()
|
||||
|
||||
try:
|
||||
if is_dfi:
|
||||
req_urlencode = self.from_dfi(data)
|
||||
elif is_lite:
|
||||
req_urlencode = self.dec_lite(litekey, data[:16], data)
|
||||
else:
|
||||
req_urlencode = data
|
||||
|
||||
@ -145,20 +166,30 @@ class AllnetServlet:
|
||||
if req_dict is None:
|
||||
raise AllnetRequestException()
|
||||
|
||||
req = AllnetPowerOnRequest(req_dict[0])
|
||||
if is_lite:
|
||||
req = AllnetPowerOnRequestLite(req_dict[0])
|
||||
else:
|
||||
req = AllnetPowerOnRequest(req_dict[0])
|
||||
# Validate the request. Currently we only validate the fields we plan on using
|
||||
|
||||
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:
|
||||
if not req.game_id or not req.ver or not req.serial or not req.token and is_lite:
|
||||
raise AllnetRequestException(
|
||||
f"Bad auth request params from {request_ip} - {vars(req)}"
|
||||
)
|
||||
elif not is_lite:
|
||||
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:
|
||||
raise AllnetRequestException(
|
||||
f"Bad auth request params from {request_ip} - {vars(req)}"
|
||||
)
|
||||
|
||||
except AllnetRequestException as e:
|
||||
if e.message != "":
|
||||
self.logger.error(e)
|
||||
return PlainTextResponse()
|
||||
|
||||
if req.format_ver == 3:
|
||||
if is_lite:
|
||||
resp = AllnetPowerOnResponseLite(req.token)
|
||||
elif req.format_ver == 3:
|
||||
resp = AllnetPowerOnResponse3(req.token)
|
||||
elif req.format_ver == 2:
|
||||
resp = AllnetPowerOnResponse2()
|
||||
@ -175,11 +206,14 @@ class AllnetServlet:
|
||||
)
|
||||
self.logger.warning(msg)
|
||||
|
||||
resp.stat = ALLNET_STAT.bad_machine.value
|
||||
if is_lite:
|
||||
resp.result = ALLNET_STAT.bad_machine.value
|
||||
else:
|
||||
resp.stat = ALLNET_STAT.bad_machine.value
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
|
||||
|
||||
if machine is not None:
|
||||
if machine is not None and not is_lite:
|
||||
arcade = await self.data.arcade.get_arcade(machine["arcade"])
|
||||
if self.config.server.check_arcade_ip:
|
||||
if arcade["ip"] and arcade["ip"] is not None and arcade["ip"] != req.ip:
|
||||
@ -257,7 +291,10 @@ class AllnetServlet:
|
||||
)
|
||||
self.logger.warning(msg)
|
||||
|
||||
resp.stat = ALLNET_STAT.bad_game.value
|
||||
if is_lite:
|
||||
resp.result = ALLNET_STAT.bad_game.value
|
||||
else:
|
||||
resp.stat = ALLNET_STAT.bad_game.value
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
|
||||
|
||||
@ -265,8 +302,12 @@ class AllnetServlet:
|
||||
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.server.hostname}:{self.config.server.port}/{req.game_id}/{req.ver.replace('.', '')}/"
|
||||
resp.host = f"{self.config.server.hostname}:{self.config.server.port}"
|
||||
if is_lite:
|
||||
resp.uri1 = f"http://{self.config.server.hostname}:{self.config.server.port}/{req.game_id}/{req.ver.replace('.', '')}/"
|
||||
resp.uri2 = f"{self.config.server.hostname}:{self.config.server.port}"
|
||||
else:
|
||||
resp.uri = f"http://{self.config.server.hostname}:{self.config.server.port}/{req.game_id}/{req.ver.replace('.', '')}/"
|
||||
resp.host = f"{self.config.server.hostname}:{self.config.server.port}"
|
||||
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict))
|
||||
@ -277,10 +318,16 @@ class AllnetServlet:
|
||||
|
||||
int_ver = req.ver.replace(".", "")
|
||||
try:
|
||||
resp.uri, resp.host = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial)
|
||||
if is_lite:
|
||||
resp.uri1, resp.uri2 = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial)
|
||||
else:
|
||||
resp.uri, resp.host = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error running get_allnet_info for {req.game_id} - {e}")
|
||||
resp.stat = ALLNET_STAT.bad_game.value
|
||||
if is_lite:
|
||||
resp.result = ALLNET_STAT.bad_game.value
|
||||
else:
|
||||
resp.stat = ALLNET_STAT.bad_game.value
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
|
||||
|
||||
@ -308,18 +355,38 @@ class AllnetServlet:
|
||||
"Pragma": "DFI",
|
||||
},
|
||||
)
|
||||
elif is_lite:
|
||||
iv = bytes([random.randint(2, 255) for _ in range(16)])
|
||||
return PlainTextResponse(content=self.enc_lite(litekey, iv, resp_str))
|
||||
|
||||
return PlainTextResponse(resp_str)
|
||||
|
||||
async def handle_dlorder(self, request: Request):
|
||||
request_ip = Utils.get_ip_addr(request)
|
||||
pragma_header = request.headers.get('Pragma', "")
|
||||
useragent_header = request.headers.get('User-Agent', "")
|
||||
is_dfi = pragma_header == "DFI"
|
||||
is_lite = useragent_header[5:] == "Windows/Lite"
|
||||
lite_id = useragent_header[:4]
|
||||
data = await request.body()
|
||||
|
||||
if not self.config.allnet.allnet_lite_keys and is_lite:
|
||||
self.logger.error("!!!LITE KEYS NOT SET!!!")
|
||||
raise AllnetRequestException()
|
||||
elif is_lite:
|
||||
for gameids, key in self.config.allnet.allnet_lite_keys.items():
|
||||
if gameids == lite_id:
|
||||
litekey = key
|
||||
|
||||
if is_lite and "litekey" not in locals():
|
||||
self.logger.error("!!!UNIQUE LITE KEY NOT FOUND!!!")
|
||||
raise AllnetRequestException()
|
||||
|
||||
try:
|
||||
if is_dfi:
|
||||
req_urlencode = self.from_dfi(data)
|
||||
elif is_lite:
|
||||
req_urlencode = self.dec_lite(litekey, data[:16], data)
|
||||
else:
|
||||
req_urlencode = data.decode()
|
||||
|
||||
@ -327,7 +394,10 @@ class AllnetServlet:
|
||||
if req_dict is None:
|
||||
raise AllnetRequestException()
|
||||
|
||||
req = AllnetDownloadOrderRequest(req_dict[0])
|
||||
if is_lite:
|
||||
req = AllnetDownloadOrderRequestLite(req_dict[0])
|
||||
else:
|
||||
req = AllnetDownloadOrderRequest(req_dict[0])
|
||||
# Validate the request. Currently we only validate the fields we plan on using
|
||||
|
||||
if not req.game_id or not req.ver or not req.serial:
|
||||
@ -343,7 +413,11 @@ class AllnetServlet:
|
||||
self.logger.info(
|
||||
f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}"
|
||||
)
|
||||
resp = AllnetDownloadOrderResponse(serial=req.serial)
|
||||
|
||||
if is_lite:
|
||||
resp = AllnetDownloadOrderResponseLite()
|
||||
else:
|
||||
resp = AllnetDownloadOrderResponse(serial=req.serial)
|
||||
|
||||
if (
|
||||
not self.config.allnet.allow_online_updates
|
||||
@ -354,6 +428,9 @@ class AllnetServlet:
|
||||
return PlainTextResponse(
|
||||
self.to_dfi(resp) + b"\r\n", headers={ "Pragma": "DFI" }
|
||||
)
|
||||
elif is_lite:
|
||||
iv = bytes([random.randint(2, 255) for _ in range(16)])
|
||||
return PlainTextResponse(content=self.enc_lite(litekey, iv, resp))
|
||||
return PlainTextResponse(resp)
|
||||
|
||||
else:
|
||||
@ -364,6 +441,9 @@ class AllnetServlet:
|
||||
return PlainTextResponse(
|
||||
self.to_dfi(resp) + b"\r\n", headers={ "Pragma": "DFI" }
|
||||
)
|
||||
elif is_lite:
|
||||
iv = bytes([random.randint(2, 255) for _ in range(16)])
|
||||
return PlainTextResponse(content=self.enc_lite(litekey, iv, resp))
|
||||
return PlainTextResponse(resp)
|
||||
|
||||
if path.exists(
|
||||
@ -393,6 +473,9 @@ class AllnetServlet:
|
||||
"Pragma": "DFI",
|
||||
},
|
||||
)
|
||||
elif is_lite:
|
||||
iv = bytes([random.randint(2, 255) for _ in range(16)])
|
||||
return PlainTextResponse(content=self.enc_lite(litekey, iv, res_str))
|
||||
|
||||
return PlainTextResponse(res_str)
|
||||
|
||||
@ -517,6 +600,17 @@ class AllnetServlet:
|
||||
zipped = zlib.compress(unzipped)
|
||||
return base64.b64encode(zipped)
|
||||
|
||||
def dec_lite(self, key, iv, data):
|
||||
cipher = AES.new(bytes(key), AES.MODE_CBC, iv)
|
||||
decrypted = cipher.decrypt(data)
|
||||
return decrypted[16:].decode("utf-8")
|
||||
|
||||
def enc_lite(self, key, iv, data):
|
||||
unencrypted = pad(bytes([0] * 16) + data.encode('utf-8'), 16)
|
||||
cipher = AES.new(bytes(key), AES.MODE_CBC, iv)
|
||||
encrypted = cipher.encrypt(unencrypted)
|
||||
return encrypted
|
||||
|
||||
class BillingServlet:
|
||||
def __init__(self, core_cfg: CoreConfig, cfg_folder: str) -> None:
|
||||
self.config = core_cfg
|
||||
@ -773,6 +867,15 @@ class AllnetPowerOnResponse:
|
||||
self.minute = datetime.now().minute
|
||||
self.second = datetime.now().second
|
||||
|
||||
class AllnetPowerOnRequestLite:
|
||||
def __init__(self, req: Dict) -> None:
|
||||
if req is None:
|
||||
raise AllnetRequestException("Request processing failed")
|
||||
self.game_id: str = req.get("title_id", None)
|
||||
self.ver: str = req.get("title_ver", None)
|
||||
self.serial: str = req.get("client_id", None)
|
||||
self.token: str = req.get("token", None)
|
||||
|
||||
class AllnetPowerOnResponse3(AllnetPowerOnResponse):
|
||||
def __init__(self, token) -> None:
|
||||
super().__init__()
|
||||
@ -804,6 +907,30 @@ class AllnetPowerOnResponse2(AllnetPowerOnResponse):
|
||||
self.timezone = "+09:00"
|
||||
self.res_class = "PowerOnResponseV2"
|
||||
|
||||
class AllnetPowerOnResponseLite:
|
||||
def __init__(self, token) -> None:
|
||||
# Custom Allnet Lite response
|
||||
self.result = 1
|
||||
self.place_id = "0123"
|
||||
self.uri1 = ""
|
||||
self.uri2 = ""
|
||||
self.name = "ARTEMiS"
|
||||
self.nickname = "ARTEMiS"
|
||||
self.setting = "1"
|
||||
self.region0 = "1"
|
||||
self.region_name0 = "W"
|
||||
self.region_name1 = ""
|
||||
self.region_name2 = ""
|
||||
self.region_name3 = ""
|
||||
self.country = "CHN"
|
||||
self.location_type = "1"
|
||||
self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ"
|
||||
)
|
||||
self.client_timezone = "+0800"
|
||||
self.res_ver = "3"
|
||||
self.token = token
|
||||
|
||||
class AllnetDownloadOrderRequest:
|
||||
def __init__(self, req: Dict) -> None:
|
||||
self.game_id = req.get("game_id", "")
|
||||
@ -811,12 +938,23 @@ class AllnetDownloadOrderRequest:
|
||||
self.serial = req.get("serial", "")
|
||||
self.encode = req.get("encode", "")
|
||||
|
||||
class AllnetDownloadOrderRequestLite:
|
||||
def __init__(self, req: Dict) -> None:
|
||||
self.game_id = req.get("title_id", "")
|
||||
self.ver = req.get("title_ver", "")
|
||||
self.serial = req.get("client_id", "")
|
||||
|
||||
class AllnetDownloadOrderResponse:
|
||||
def __init__(self, stat: int = 1, serial: str = "", uri: str = "null") -> None:
|
||||
self.stat = stat
|
||||
self.serial = serial
|
||||
self.uri = uri
|
||||
|
||||
class AllnetDownloadOrderResponseLite:
|
||||
def __init__(self, result: int = 1, uri: str = "null") -> None:
|
||||
self.result = result
|
||||
self.uri = uri
|
||||
|
||||
class TraceDataType(Enum):
|
||||
CHARGE = 0
|
||||
EVENT = 1
|
||||
@ -1068,7 +1206,9 @@ app_billing = Starlette(
|
||||
allnet = AllnetServlet(cfg, cfg_dir)
|
||||
route_lst = [
|
||||
Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]),
|
||||
Route("/net/initialize", allnet.handle_poweron, methods=["GET", "POST"]),
|
||||
Route("/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"]),
|
||||
Route("/net/delivery/instruction", 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),
|
||||
|
11
core/app.py
11
core/app.py
@ -11,6 +11,7 @@ from typing import List
|
||||
|
||||
from core import CoreConfig, TitleServlet, MuchaServlet
|
||||
from core.allnet import AllnetServlet, BillingServlet
|
||||
from core.chimedb import ChimeServlet
|
||||
from core.frontend import FrontendServlet
|
||||
|
||||
async def dummy_rt(request: Request):
|
||||
@ -75,7 +76,9 @@ if not cfg.allnet.standalone:
|
||||
allnet = AllnetServlet(cfg, cfg_dir)
|
||||
route_lst += [
|
||||
Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]),
|
||||
Route("/net/initialize", allnet.handle_poweron, methods=["GET", "POST"]),
|
||||
Route("/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"]),
|
||||
Route("/net/delivery/instruction", 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),
|
||||
@ -87,6 +90,14 @@ if not cfg.allnet.standalone:
|
||||
Route("/dl/ini/{file:str}", allnet.handle_dlorder_ini),
|
||||
]
|
||||
|
||||
if cfg.chimedb.enable:
|
||||
chimedb = ChimeServlet(cfg, cfg_dir)
|
||||
route_lst += [
|
||||
Route("/wc_aime/api/alive_check", chimedb.handle_qr_alive, methods=["POST"]),
|
||||
Route("/qrcode/api/alive_check", chimedb.handle_qr_alive, methods=["POST"]),
|
||||
Route("/wc_aime/api/get_data", chimedb.handle_qr_lookup, methods=["POST"])
|
||||
]
|
||||
|
||||
for code, game in title.title_registry.items():
|
||||
route_lst += game.get_routes()
|
||||
|
||||
|
139
core/chimedb.py
Normal file
139
core/chimedb.py
Normal file
@ -0,0 +1,139 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from enum import Enum
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
import coloredlogs
|
||||
from starlette.responses import PlainTextResponse
|
||||
from starlette.requests import Request
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.data import Data
|
||||
|
||||
class ChimeDBStatus(Enum):
|
||||
NONE = 0
|
||||
READER_SETUP_FAIL = 1
|
||||
READER_ACCESS_FAIL = 2
|
||||
READER_INCOMPATIBLE = 3
|
||||
DB_RESOLVE_FAIL = 4
|
||||
DB_ACCESS_TIMEOUT = 5
|
||||
DB_ACCESS_FAIL = 6
|
||||
AIME_ID_INVALID = 7
|
||||
NO_BOARD_INFO = 8
|
||||
LOCK_BAN_SYSTEM_USER = 9
|
||||
LOCK_BAN_SYSTEM = 10
|
||||
LOCK_BAN_USER = 11
|
||||
LOCK_BAN = 12
|
||||
LOCK_SYSTEM_USER = 13
|
||||
LOCK_SYSTEM = 14
|
||||
LOCK_USER = 15
|
||||
|
||||
class ChimeServlet:
|
||||
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("chimedb")
|
||||
if not hasattr(self.logger, "initted"):
|
||||
log_fmt_str = "[%(asctime)s] Chimedb | %(levelname)s | %(message)s"
|
||||
log_fmt = logging.Formatter(log_fmt_str)
|
||||
|
||||
fileHandler = TimedRotatingFileHandler(
|
||||
"{0}/{1}.log".format(self.config.server.log_dir, "chimedb"),
|
||||
when="d",
|
||||
backupCount=10,
|
||||
)
|
||||
fileHandler.setFormatter(log_fmt)
|
||||
|
||||
consoleHandler = logging.StreamHandler()
|
||||
consoleHandler.setFormatter(log_fmt)
|
||||
|
||||
self.logger.addHandler(fileHandler)
|
||||
self.logger.addHandler(consoleHandler)
|
||||
|
||||
self.logger.setLevel(self.config.aimedb.loglevel)
|
||||
coloredlogs.install(
|
||||
level=core_cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||
)
|
||||
self.logger.initted = True
|
||||
|
||||
if not core_cfg.chimedb.key:
|
||||
self.logger.error("!!!KEY NOT SET!!!")
|
||||
exit(1)
|
||||
|
||||
self.logger.info("Serving")
|
||||
|
||||
async def handle_qr_alive(self, request: Request):
|
||||
return PlainTextResponse("alive")
|
||||
|
||||
async def handle_qr_lookup(self, request: Request) -> bytes:
|
||||
req = json.loads(await request.body())
|
||||
access_code = req["qrCode"][-20:]
|
||||
timestamp = req["timestamp"]
|
||||
|
||||
try:
|
||||
userId = await self._lookup(access_code)
|
||||
data = json.dumps({
|
||||
"userID": userId,
|
||||
"errorID": 0,
|
||||
"timestamp": timestamp,
|
||||
"key": self._hash_key(userId, timestamp)
|
||||
})
|
||||
except Exception as e:
|
||||
|
||||
self.logger.error(e.with_traceback(None))
|
||||
|
||||
data = json.dumps({
|
||||
"userID": -1,
|
||||
"errorID": ChimeDBStatus.DB_ACCESS_FAIL,
|
||||
"timestamp": timestamp,
|
||||
"key": self._hash_key(-1, timestamp)
|
||||
})
|
||||
|
||||
return PlainTextResponse(data)
|
||||
|
||||
def _hash_key(self, chip_id, timestamp):
|
||||
input_string = f"{chip_id}{timestamp}{self.config.chimedb.key}"
|
||||
hash_object = hashlib.sha256(input_string.encode('utf-8'))
|
||||
hex_dig = hash_object.hexdigest()
|
||||
|
||||
formatted_hex = format(int(hex_dig, 16), '064x').upper()
|
||||
|
||||
return formatted_hex
|
||||
|
||||
async def _lookup(self, access_code):
|
||||
user_id = await self.data.card.get_user_id_from_card(access_code)
|
||||
|
||||
self.logger.info(f"access_code {access_code} -> user_id {user_id}")
|
||||
|
||||
if not user_id or user_id <= 0:
|
||||
user_id = await self._register(access_code)
|
||||
|
||||
return user_id
|
||||
|
||||
async def _register(self, access_code):
|
||||
user_id = -1
|
||||
|
||||
if self.config.server.allow_user_registration:
|
||||
user_id = await self.data.user.create_user()
|
||||
|
||||
if user_id is None:
|
||||
self.logger.error("Failed to register user!")
|
||||
user_id = -1
|
||||
else:
|
||||
card_id = await self.data.card.create_card(user_id, access_code)
|
||||
|
||||
if card_id is None:
|
||||
self.logger.error("Failed to register card!")
|
||||
user_id = -1
|
||||
|
||||
self.logger.info(
|
||||
f"Register access code {access_code} -> user_id {user_id}"
|
||||
)
|
||||
else:
|
||||
self.logger.info(f"Registration blocked!: access code {access_code}")
|
||||
|
||||
return user_id
|
@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
import ssl
|
||||
from typing import Any, Union
|
||||
from typing import Any, Union, Dict
|
||||
|
||||
from typing_extensions import Optional
|
||||
|
||||
@ -378,6 +378,11 @@ class AllnetConfig:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "allnet", "save_billing", default=False
|
||||
)
|
||||
@property
|
||||
def allnet_lite_keys(self) -> Dict:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "allnet", "allnet_lite_keys", default={}
|
||||
)
|
||||
|
||||
class BillingConfig:
|
||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||
@ -469,6 +474,28 @@ class AimedbConfig:
|
||||
self.__config, "core", "aimedb", "id_lifetime_seconds", default=86400
|
||||
)
|
||||
|
||||
class ChimedbConfig:
|
||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||
self.__config = parent_config
|
||||
|
||||
@property
|
||||
def enable(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "chimedb", "enable", default=True
|
||||
)
|
||||
@property
|
||||
def loglevel(self) -> int:
|
||||
return CoreConfig.str_to_loglevel(
|
||||
CoreConfig.get_config_field(
|
||||
self.__config, "core", "chimedb", "loglevel", default="info"
|
||||
)
|
||||
)
|
||||
@property
|
||||
def key(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "chimedb", "key", default=""
|
||||
)
|
||||
|
||||
class MuchaConfig:
|
||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||
self.__config = parent_config
|
||||
@ -490,6 +517,7 @@ class CoreConfig(dict):
|
||||
self.allnet = AllnetConfig(self)
|
||||
self.billing = BillingConfig(self)
|
||||
self.aimedb = AimedbConfig(self)
|
||||
self.chimedb = ChimedbConfig(self)
|
||||
self.mucha = MuchaConfig(self)
|
||||
|
||||
@classmethod
|
||||
|
@ -6,6 +6,7 @@ services:
|
||||
volumes:
|
||||
- ./aime:/app/aime
|
||||
- ./configs/config:/app/config
|
||||
- ./cert:/app/cert
|
||||
|
||||
environment:
|
||||
CFG_DEV: 1
|
||||
@ -14,7 +15,8 @@ services:
|
||||
CFG_CORE_MEMCACHED_HOSTNAME: ma.memcached
|
||||
CFG_CORE_AIMEDB_KEY: <INSERT AIMEDB KEY HERE>
|
||||
CFG_CHUNI_SERVER_LOGLEVEL: debug
|
||||
|
||||
|
||||
##Note: comment 80 and 8443 when you plan to use with nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8443:8443"
|
||||
@ -64,3 +66,18 @@ services:
|
||||
ports:
|
||||
- "9090:8080"
|
||||
|
||||
##Note: uncomment to allow use nginx with artemis, don't forget to comment 80 and 8443 ports on artemis
|
||||
#nginx:
|
||||
# hostname: ma.nginx
|
||||
# image: nginx:latest
|
||||
# ports:
|
||||
# - "80:80"
|
||||
# - "443:443"
|
||||
# - "8443:8443"
|
||||
# volumes:
|
||||
##Note: copy example_config/example_nginx.conf to configs/nginx folder, edit it and rename to nginx.conf
|
||||
# - ./configs/nginx:/etc/nginx/conf.d
|
||||
# - ./cert:/etc/nginx/cert
|
||||
# - ./logs/nginx:/var/log/nginx
|
||||
# depends_on:
|
||||
# - app
|
@ -41,6 +41,13 @@
|
||||
- `loglevel`: Logging level for the allnet server. Default `info`
|
||||
- `allow_online_updates`: Allow allnet to distribute online updates via DownloadOrders. This system is currently non-functional, so leave it disabled. Default `False`
|
||||
- `update_cfg_folder`: Folder where delivery INI files will be checked for. Ignored if `allow_online_updates` is `False`. Default `""`
|
||||
- `allnet_lite_keys:` Allnet Lite (Chinese Allnet) PowerOn/DownloadOrder unique keys. Default ` `
|
||||
```yaml
|
||||
allnet_lite_keys:
|
||||
"SDJJ": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
|
||||
"SDHJ": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
|
||||
"SDGB": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
|
||||
```
|
||||
## Billing
|
||||
- `standalone`: Whether the billing server should launch it's own servlet on it's own port, or be part of the main servlet on the default port. Setting this to `True` requires that you have `ssl_key` and `ssl_cert` set. Default `False`
|
||||
- `loglevel`: Logging level for the billing server. Default `info`
|
||||
@ -56,3 +63,8 @@
|
||||
- `key`: Key to encrypt/decrypt aimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle aimedb requests. Default `""`
|
||||
- `id_secret`: Base64-encoded JWT secret for Sega Auth IDs. Leaving this blank disables this feature. Default `""`
|
||||
- `id_lifetime_seconds`: Number of secons a JWT generated should be valid for. Default `86400` (1 day)
|
||||
## Chimedb
|
||||
- `enable`: Whether or not chimedb should run. Default `False`
|
||||
- `loglevel`: Logging level for the chimedb server. Default `info`
|
||||
- `key`: Key to hash chimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle chimedb requests. Default `""`
|
||||
|
||||
|
@ -108,6 +108,7 @@ crypto:
|
||||
keys:
|
||||
13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
|
||||
"13_int": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000", 42]
|
||||
"13_chn": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000", 8]
|
||||
```
|
||||
|
||||
### Database upgrade
|
||||
@ -257,6 +258,31 @@ python dbutils.py upgrade
|
||||
|
||||
Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code!
|
||||
|
||||
### Config
|
||||
|
||||
Config file is located in `config/mai2.yaml`.
|
||||
|
||||
| Option | Info |
|
||||
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `crypto` | This option is used to enable the TLS Encryption |
|
||||
|
||||
|
||||
If you would like to use network encryption, add the keys to the `keys` section under `crypto`, where the key
|
||||
is the version ID for Japanese (SDEZ) versions and `"{versionID}_int"` for Export (SDGA) versions, and the value
|
||||
is an array containing `[key, iv, salt]` in order.
|
||||
|
||||
Just copy your salt in here, no need to convert anything.
|
||||
|
||||
|
||||
```yaml
|
||||
crypto:
|
||||
encrypted_only: False
|
||||
keys:
|
||||
23: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
|
||||
"23_int": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
|
||||
"23_chn": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
|
||||
```
|
||||
|
||||
## Hatsune Miku Project Diva
|
||||
|
||||
### SBZV
|
||||
|
@ -2,6 +2,7 @@ server:
|
||||
enable: True
|
||||
loglevel: "info"
|
||||
news_msg: ""
|
||||
use_https: False # for CRYSTAL PLUS and later or SUPERSTAR and later
|
||||
|
||||
team:
|
||||
name: ARTEMiS # If this is set, all players that are not on a team will use this one by default.
|
||||
|
@ -46,6 +46,7 @@ allnet:
|
||||
allow_online_updates: False
|
||||
update_cfg_folder: ""
|
||||
save_billing: True
|
||||
allnet_lite_keys: []
|
||||
|
||||
billing:
|
||||
standalone: True
|
||||
@ -64,5 +65,10 @@ aimedb:
|
||||
id_secret: ""
|
||||
id_lifetime_seconds: 86400
|
||||
|
||||
chimedb:
|
||||
enable: False
|
||||
loglevel: "info"
|
||||
key: ""
|
||||
|
||||
mucha:
|
||||
loglevel: "info"
|
||||
|
@ -1,6 +1,7 @@
|
||||
server:
|
||||
enable: True
|
||||
loglevel: "info"
|
||||
use_https: False # for DX and later
|
||||
|
||||
deliver:
|
||||
enable: False
|
||||
|
@ -66,6 +66,52 @@ server {
|
||||
}
|
||||
}
|
||||
|
||||
# WAHLAP Billing, they use 443 port
|
||||
# comment this out if running billing standalone
|
||||
# still not work for some reason, please set
|
||||
# billing=127.0.0.1 in segatools.ini for now and looking for fix
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name bl.sys-all.cn;
|
||||
|
||||
ssl_certificate /path/to/cert/server.pem;
|
||||
ssl_certificate_key /path/to/cert/server.key;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:MozSSL:10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers "ALL:@SECLEVEL=0";
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name bl.sys-allnet.cn;
|
||||
|
||||
ssl_certificate /path/to/cert/server.pem;
|
||||
ssl_certificate_key /path/to/cert/server.key;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:MozSSL:10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers "ALL:@SECLEVEL=0";
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
}
|
||||
}
|
||||
|
||||
# Frontend, set to redirect to HTTPS. Comment out if you don't intend to use the frontend
|
||||
server {
|
||||
listen 80;
|
||||
|
29
readme.md
29
readme.md
@ -8,6 +8,11 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
||||
+ 1.30
|
||||
+ 1.35
|
||||
|
||||
+ CHUNITHM CHINA
|
||||
+ NEW
|
||||
+ 2024 (NEW)
|
||||
+ 2024 (LUMINOUS)
|
||||
|
||||
+ CHUNITHM INTL
|
||||
+ SUPERSTAR
|
||||
+ SUPERSTAR PLUS
|
||||
@ -15,6 +20,8 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
||||
+ NEW PLUS
|
||||
+ SUN
|
||||
+ SUN PLUS
|
||||
+ LUMINOUS
|
||||
+ LUMINOUS PLUS
|
||||
|
||||
+ CHUNITHM JP
|
||||
+ AIR
|
||||
@ -43,7 +50,29 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
||||
+ Initial D THE ARCADE
|
||||
+ Season 2
|
||||
|
||||
+ maimai DX CHINA
|
||||
+ DX (Muji)
|
||||
+ 2021 (Muji)
|
||||
+ 2022 (Muji)
|
||||
+ 2023 (FESTiVAL)
|
||||
+ 2024 (BUDDiES)
|
||||
|
||||
+ maimai DX INTL
|
||||
+ DX
|
||||
+ DX Plus
|
||||
+ Splash
|
||||
+ Splash Plus
|
||||
+ UNiVERSE
|
||||
+ UNiVERSE PLUS
|
||||
+ FESTiVAL
|
||||
+ FESTiVAL PLUS
|
||||
+ BUDDiES
|
||||
+ BUDDiES PLUS
|
||||
+ PRiSM
|
||||
|
||||
+ maimai DX
|
||||
+ DX
|
||||
+ DX Plus
|
||||
+ Splash
|
||||
+ Splash Plus
|
||||
+ UNiVERSE
|
||||
|
@ -8,4 +8,4 @@ index = ChuniServlet
|
||||
database = ChuniData
|
||||
reader = ChuniReader
|
||||
frontend = ChuniFrontend
|
||||
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW, ChuniConstants.GAME_CODE_INT]
|
||||
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW, ChuniConstants.GAME_CODE_INT, ChuniConstants.GAME_CODE_CHN]
|
||||
|
@ -25,6 +25,12 @@ class ChuniServerConfig:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "chuni", "server", "news_msg", default=""
|
||||
)
|
||||
|
||||
@property
|
||||
def use_https(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "chuni", "server", "use_https", default=False
|
||||
)
|
||||
|
||||
|
||||
class ChuniTeamConfig:
|
||||
|
@ -6,6 +6,7 @@ class ChuniConstants:
|
||||
GAME_CODE = "SDBT"
|
||||
GAME_CODE_NEW = "SDHD"
|
||||
GAME_CODE_INT = "SDGS"
|
||||
GAME_CODE_CHN = "SDHJ"
|
||||
|
||||
CONFIG_NAME = "chuni.yaml"
|
||||
|
||||
|
@ -101,14 +101,17 @@ class ChuniServlet(BaseServlet):
|
||||
f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 51, # SUPERSTAR PLUS
|
||||
ChuniConstants.VER_CHUNITHM_NEW: 54,
|
||||
f"{ChuniConstants.VER_CHUNITHM_NEW}_int": 49,
|
||||
f"{ChuniConstants.VER_CHUNITHM_NEW}_chn": 37,
|
||||
ChuniConstants.VER_CHUNITHM_NEW_PLUS: 25,
|
||||
f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_int": 31,
|
||||
f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_chn": 35, # NEW
|
||||
ChuniConstants.VER_CHUNITHM_SUN: 70,
|
||||
f"{ChuniConstants.VER_CHUNITHM_SUN}_int": 35,
|
||||
ChuniConstants.VER_CHUNITHM_SUN_PLUS: 36,
|
||||
f"{ChuniConstants.VER_CHUNITHM_SUN_PLUS}_int": 36,
|
||||
ChuniConstants.VER_CHUNITHM_LUMINOUS: 8,
|
||||
f"{ChuniConstants.VER_CHUNITHM_LUMINOUS}_int": 8,
|
||||
f"{ChuniConstants.VER_CHUNITHM_LUMINOUS}_chn": 8,
|
||||
ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS: 56,
|
||||
}
|
||||
|
||||
@ -150,6 +153,11 @@ class ChuniServlet(BaseServlet):
|
||||
and version_idx >= ChuniConstants.VER_CHUNITHM_NEW
|
||||
):
|
||||
method_fixed += "C3Exp"
|
||||
elif (
|
||||
isinstance(version, str)
|
||||
and version.endswith("_chn")
|
||||
):
|
||||
method_fixed += "Chn"
|
||||
|
||||
hash = PBKDF2(
|
||||
method_fixed,
|
||||
@ -182,10 +190,26 @@ class ChuniServlet(BaseServlet):
|
||||
return True
|
||||
|
||||
def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]:
|
||||
if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80:
|
||||
return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", self.core_cfg.server.hostname)
|
||||
title_port_int = Utils.get_title_port(self.core_cfg)
|
||||
title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg)
|
||||
|
||||
return (f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", self.core_cfg.server.hostname)
|
||||
if self.game_cfg.server.use_https and (
|
||||
(game_code == "SDBT" and game_ver >= 145) or # JP use TLS from CRYSTAL PLUS
|
||||
game_code != "SDBT" # SDGS and SDHJ all version can use TLS
|
||||
):
|
||||
proto = "https"
|
||||
else:
|
||||
proto = "http"
|
||||
|
||||
if proto == "https":
|
||||
t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else ""
|
||||
else:
|
||||
t_port = f":{title_port_int}" if title_port_int != 80 else ""
|
||||
|
||||
return (
|
||||
f"{proto}://{self.core_cfg.server.hostname}{t_port}/{game_code}/{game_ver}/",
|
||||
f"{self.core_cfg.server.hostname}",
|
||||
)
|
||||
|
||||
def get_routes(self) -> List[Route]:
|
||||
return [
|
||||
@ -259,6 +283,13 @@ class ChuniServlet(BaseServlet):
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||
elif version >= 135: # LUMINOUS PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS_PLUS
|
||||
elif game_code == "SDHJ": # Chn
|
||||
if version < 110: # NEW
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
||||
elif version >= 110 and version < 120: # NEW *Cursed but needed due to different encryption key
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||
elif version >= 120: # LUMINOUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||
|
||||
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
||||
# If we get a 32 character long hex string, it's a hash and we're
|
||||
@ -268,6 +299,9 @@ class ChuniServlet(BaseServlet):
|
||||
if game_code == "SDGS":
|
||||
crypto_cfg_key = f"{internal_ver}_int"
|
||||
hash_table_key = f"{internal_ver}_int"
|
||||
elif game_code == "SDHJ":
|
||||
crypto_cfg_key = f"{internal_ver}_chn"
|
||||
hash_table_key = f"{internal_ver}_chn"
|
||||
else:
|
||||
crypto_cfg_key = internal_ver
|
||||
hash_table_key = internal_ver
|
||||
@ -337,6 +371,8 @@ class ChuniServlet(BaseServlet):
|
||||
endpoint = endpoint.replace("C3Exp", "")
|
||||
elif game_code == "SDGS" and version < 110:
|
||||
endpoint = endpoint.replace("Exp", "")
|
||||
elif game_code == "SDHJ":
|
||||
endpoint = endpoint.replace("Chn", "")
|
||||
else:
|
||||
endpoint = endpoint
|
||||
|
||||
|
@ -18,4 +18,5 @@ game_codes = [
|
||||
Mai2Constants.GAME_CODE_GREEN,
|
||||
Mai2Constants.GAME_CODE,
|
||||
Mai2Constants.GAME_CODE_DX_INT,
|
||||
Mai2Constants.GAME_CODE_DX_CHN,
|
||||
]
|
||||
|
@ -139,6 +139,9 @@ class Mai2Base:
|
||||
async def handle_get_game_ng_music_id_api_request(self, data: Dict) -> Dict:
|
||||
return {"length": 0, "musicIdList": []}
|
||||
|
||||
async def handle_get_game_ng_word_list_api_request(self, data: Dict) -> Dict:
|
||||
return {"ngWordExactMatchLength": 0, "ngWordExactMatchList": [], "ngWordPartialMatchLength": 0, "ngWordPartialMatchList": []}
|
||||
|
||||
async def handle_get_game_charge_api_request(self, data: Dict) -> Dict:
|
||||
game_charge_list = await self.data.static.get_enabled_tickets(self.version, 1)
|
||||
if game_charge_list is None:
|
||||
|
@ -20,6 +20,12 @@ class Mai2ServerConfig:
|
||||
self.__config, "mai2", "server", "loglevel", default="info"
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def use_https(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "mai2", "server", "use_https", default=False
|
||||
)
|
||||
|
||||
class Mai2DeliverConfig:
|
||||
def __init__(self, parent: "Mai2Config") -> None:
|
||||
|
@ -32,6 +32,7 @@ class Mai2Constants:
|
||||
GAME_CODE_FINALE = "SDEY"
|
||||
GAME_CODE_DX = "SDEZ"
|
||||
GAME_CODE_DX_INT = "SDGA"
|
||||
GAME_CODE_DX_CHN = "SDGB"
|
||||
|
||||
CONFIG_NAME = "mai2.yaml"
|
||||
|
||||
|
@ -39,7 +39,7 @@ class Mai2Servlet(BaseServlet):
|
||||
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||
super().__init__(core_cfg, cfg_dir)
|
||||
self.game_cfg = Mai2Config()
|
||||
self.hash_table: Dict[int, Dict[str, str]] = {}
|
||||
self.hash_table: Dict[str, Dict[str, str]] = {}
|
||||
if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"):
|
||||
self.game_cfg.update(
|
||||
yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))
|
||||
@ -99,16 +99,21 @@ class Mai2Servlet(BaseServlet):
|
||||
self.logger.initted = True
|
||||
|
||||
for version, keys in self.game_cfg.crypto.keys.items():
|
||||
if version < Mai2Constants.VER_MAIMAI_DX:
|
||||
if int(str(version).split('_')[0]) < Mai2Constants.VER_MAIMAI_DX:
|
||||
continue
|
||||
|
||||
if len(keys) < 3:
|
||||
continue
|
||||
|
||||
if isinstance(version, int):
|
||||
version_idx = version
|
||||
else:
|
||||
version_idx = int(version.split("_")[0])
|
||||
|
||||
self.hash_table[version] = {}
|
||||
method_list = [
|
||||
method
|
||||
for method in dir(self.versions[version])
|
||||
for method in dir(self.versions[version_idx])
|
||||
if not method.startswith("__")
|
||||
]
|
||||
|
||||
@ -117,6 +122,21 @@ class Mai2Servlet(BaseServlet):
|
||||
# remove the first 6 chars and the final 7 chars to get the canonical
|
||||
# endpoint name.
|
||||
method_fixed = inflection.camelize(method)[6:-7]
|
||||
|
||||
# This only applies for maimai DX International and later for some reason.
|
||||
if (
|
||||
isinstance(version, str)
|
||||
and version.endswith("_int")
|
||||
and version_idx >= Mai2Constants.VER_MAIMAI_DX_UNIVERSE
|
||||
):
|
||||
method_fixed += "MaimaiExp"
|
||||
elif (
|
||||
isinstance(version, str)
|
||||
and version.endswith("_chn")
|
||||
and version_idx >= Mai2Constants.VER_MAIMAI_DX_UNIVERSE # 1.00, 1.11 and 1.20 all use DX, but they add MaimaiChn in 1.20, we set 1.20 to use UNIVERSE code
|
||||
):
|
||||
method_fixed += "MaimaiChn"
|
||||
|
||||
hash = MD5.new((method_fixed + keys[2]).encode())
|
||||
|
||||
# truncate unused bytes like the game does
|
||||
@ -159,14 +179,29 @@ class Mai2Servlet(BaseServlet):
|
||||
]
|
||||
|
||||
def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]:
|
||||
if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80:
|
||||
return (
|
||||
f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/",
|
||||
f"{self.core_cfg.server.hostname}",
|
||||
)
|
||||
title_port_int = Utils.get_title_port(self.core_cfg)
|
||||
title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg)
|
||||
|
||||
if self.game_cfg.server.use_https:
|
||||
if (game_code == "SDEZ" and game_ver >= 114) or (game_code == "SDGA" and game_ver >= 110): # SDEZ and SDGA use tls from Splash version
|
||||
proto = "" # game will auto add https:// in uri with original code
|
||||
elif game_code == "SDGB" and game_ver >= 130: # SDGB use tls from 1.30
|
||||
# game will check if uri start with "http:", if yes, set IsHttpConnection = true
|
||||
# so we can return https://example.com or http://example.com, all will work
|
||||
proto = "https://"
|
||||
else:
|
||||
# "maimai", SDEZ 1.00 ~ 1.13, SDGA 1.00 ~ 1.06 and SDGB 1.01, 1.20 use http://
|
||||
proto = "http://"
|
||||
else:
|
||||
proto = "http://"
|
||||
|
||||
if proto == "" or proto == "https://":
|
||||
t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else ""
|
||||
else:
|
||||
t_port = f":{title_port_int}" if title_port_int != 80 else ""
|
||||
|
||||
return (
|
||||
f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/",
|
||||
f"{proto}{self.core_cfg.server.hostname}{t_port}/{game_code}/{game_ver}/",
|
||||
f"{self.core_cfg.server.hostname}",
|
||||
)
|
||||
|
||||
@ -343,30 +378,56 @@ class Mai2Servlet(BaseServlet):
|
||||
elif version >= 155:
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS
|
||||
|
||||
elif game_code == "SDGB": # Chn
|
||||
if version < 110: # Muji
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX
|
||||
elif version >= 110 and version < 120: # Muji
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH # still DX, but need Splash to set encryption key
|
||||
elif version >= 120 and version < 130: # Muji (LMAO)
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE # still DX, but need UNIVERSE to set encryption key
|
||||
elif version >= 130 and version < 140: # FESTiVAL
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL
|
||||
elif version >= 140 and version < 150: # BUDDiES
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES
|
||||
elif version >= 150: # PRiSM
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM
|
||||
|
||||
|
||||
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
||||
|
||||
if game_code == "SDGA":
|
||||
crypto_cfg_key = f"{internal_ver}_int"
|
||||
hash_table_key = f"{internal_ver}_int"
|
||||
elif game_code == "SDGB":
|
||||
crypto_cfg_key = f"{internal_ver}_chn"
|
||||
hash_table_key = f"{internal_ver}_chn"
|
||||
else:
|
||||
crypto_cfg_key = internal_ver
|
||||
hash_table_key = internal_ver
|
||||
|
||||
# If we get a 32 character long hex string, it's a hash and we're
|
||||
# dealing with an encrypted request. False positives shouldn't happen
|
||||
# as long as requests are suffixed with `Api`.
|
||||
if internal_ver not in self.hash_table:
|
||||
if hash_table_key not in self.hash_table:
|
||||
self.logger.error(
|
||||
"v%s does not support encryption or no keys entered",
|
||||
version,
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
elif endpoint.lower() not in self.hash_table[internal_ver]:
|
||||
elif endpoint.lower() not in self.hash_table[hash_table_key]:
|
||||
self.logger.error(
|
||||
"No hash found for v%s endpoint %s",
|
||||
version, endpoint
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
|
||||
endpoint = self.hash_table[internal_ver][endpoint.lower()]
|
||||
endpoint = self.hash_table[hash_table_key][endpoint.lower()]
|
||||
|
||||
try:
|
||||
crypt = AES.new(
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]),
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][0]),
|
||||
AES.MODE_CBC,
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][1]),
|
||||
)
|
||||
|
||||
req_raw = crypt.decrypt(req_raw)
|
||||
@ -384,7 +445,10 @@ class Mai2Servlet(BaseServlet):
|
||||
if (
|
||||
not encrypted
|
||||
and self.game_cfg.crypto.encrypted_only
|
||||
and version >= 110
|
||||
and (
|
||||
# SDEZ start from 1.10, SDGA and SDGB keep use encryption from 1.00
|
||||
internal_ver >= Mai2Constants.VER_MAIMAI_DX_PLUS or (game_code == "SDGA" or game_code == "SDGB")
|
||||
)
|
||||
):
|
||||
self.logger.error(
|
||||
"Unencrypted v%s %s request, but config is set to encrypted only: %r",
|
||||
@ -408,7 +472,9 @@ class Mai2Servlet(BaseServlet):
|
||||
|
||||
endpoint = (
|
||||
endpoint.replace("MaimaiExp", "")
|
||||
if game_code == Mai2Constants.GAME_CODE_DX_INT
|
||||
if game_code == Mai2Constants.GAME_CODE_DX_INT and version >= 120
|
||||
else endpoint.replace("MaimaiChn", "")
|
||||
if game_code == Mai2Constants.GAME_CODE_DX_CHN and version >= 120
|
||||
else endpoint
|
||||
)
|
||||
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
||||
@ -434,15 +500,17 @@ class Mai2Servlet(BaseServlet):
|
||||
|
||||
zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
|
||||
|
||||
if not encrypted or version < 110:
|
||||
if not encrypted or (
|
||||
internal_ver < Mai2Constants.VER_MAIMAI_DX_PLUS and game_code == "SDEZ"
|
||||
):
|
||||
return Response(zipped)
|
||||
|
||||
padded = pad(zipped, 16)
|
||||
|
||||
crypt = AES.new(
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]),
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][0]),
|
||||
AES.MODE_CBC,
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][1]),
|
||||
)
|
||||
|
||||
return Response(crypt.encrypt(padded))
|
||||
|
Reference in New Issue
Block a user