Merge branch 'develop' into fork_develop

This commit is contained in:
Dniel97 2023-09-25 22:48:53 +02:00
commit 38c1c31cf5
Signed by untrusted user: Dniel97
GPG Key ID: 6180B3C768FB2E08
15 changed files with 415 additions and 69 deletions

View File

@ -176,18 +176,18 @@ class AllnetServlet:
else AllnetJapanRegionId.AICHI.value else AllnetJapanRegionId.AICHI.value
) )
resp.region_name0 = ( resp.region_name0 = (
arcade["country"]
if arcade["country"] is not None
else AllnetCountryCode.JAPAN.value
)
resp.region_name1 = (
arcade["state"] arcade["state"]
if arcade["state"] is not None if arcade["state"] is not None
else AllnetJapanRegionId.AICHI.name else AllnetJapanRegionId.AICHI.name
) )
resp.region_name1 = (
arcade["country"]
if arcade["country"] is not None
else AllnetCountryCode.JAPAN.value
)
resp.region_name2 = arcade["city"] if arcade["city"] is not None else "" resp.region_name2 = arcade["city"] if arcade["city"] is not None else ""
resp.client_timezone = ( resp.client_timezone = ( # lmao
arcade["timezone"] if arcade["timezone"] is not None else "+0900" arcade["timezone"] if arcade["timezone"] is not None else "+0900" if req.format_ver == 3 else "+09:00"
) )
if req.game_id not in self.uri_registry: if req.game_id not in self.uri_registry:
@ -296,7 +296,6 @@ class AllnetServlet:
return res_str return res_str
def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes:
if "file" not in match: if "file" not in match:
return b"" return b""

View File

@ -218,9 +218,16 @@ class ArcadeData(BaseData):
return True return True
def find_arcade_by_name(self, name: str) -> List[Row]: def get_arcade_by_name(self, name: str) -> Optional[List[Row]]:
sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%"))) sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%")))
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
return False return None
return result.fetchall()
def get_arcades_by_ip(self, ip: str) -> Optional[List[Row]]:
sql = arcade.select().where(arcade.c.ip == ip)
result = self.execute(sql)
if result is None:
return None
return result.fetchall() return result.fetchall()

View File

@ -21,6 +21,7 @@ class IUserSession(Interface):
userId = Attribute("User's ID") userId = Attribute("User's ID")
current_ip = Attribute("User's current ip address") current_ip = Attribute("User's current ip address")
permissions = Attribute("User's permission level") permissions = Attribute("User's permission level")
ongeki_version = Attribute("User's selected Ongeki Version")
class PermissionOffset(Enum): class PermissionOffset(Enum):
USER = 0 # Regular user USER = 0 # Regular user
@ -36,6 +37,7 @@ class UserSession(object):
self.userId = 0 self.userId = 0
self.current_ip = "0.0.0.0" self.current_ip = "0.0.0.0"
self.permissions = 0 self.permissions = 0
self.ongeki_version = 7
class FrontendServlet(resource.Resource): class FrontendServlet(resource.Resource):
@ -304,9 +306,9 @@ class FE_System(FE_Base):
def render_GET(self, request: Request): def render_GET(self, request: Request):
uri = request.uri.decode() uri = request.uri.decode()
template = self.environment.get_template("core/frontend/sys/index.jinja") template = self.environment.get_template("core/frontend/sys/index.jinja")
usrlist = [] usrlist: List[Dict] = []
aclist = [] aclist: List[Dict] = []
cablist = [] cablist: List[Dict] = []
sesh: Session = request.getSession() sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh) usr_sesh = IUserSession(sesh)
@ -339,6 +341,7 @@ class FE_System(FE_Base):
ac_id_search = uri_parse.get("arcadeId") ac_id_search = uri_parse.get("arcadeId")
ac_name_search = uri_parse.get("arcadeName") ac_name_search = uri_parse.get("arcadeName")
ac_user_search = uri_parse.get("arcadeUser") ac_user_search = uri_parse.get("arcadeUser")
ac_ip_search = uri_parse.get("arcadeIp")
if ac_id_search is not None: if ac_id_search is not None:
u = self.data.arcade.get_arcade(ac_id_search[0]) u = self.data.arcade.get_arcade(ac_id_search[0])
@ -346,12 +349,20 @@ class FE_System(FE_Base):
aclist.append(u._asdict()) aclist.append(u._asdict())
elif ac_name_search is not None: elif ac_name_search is not None:
ul = self.data.arcade.find_arcade_by_name(ac_name_search[0]) ul = self.data.arcade.get_arcade_by_name(ac_name_search[0])
if ul is not None:
for u in ul: for u in ul:
aclist.append(u._asdict()) aclist.append(u._asdict())
elif ac_user_search is not None: elif ac_user_search is not None:
ul = self.data.arcade.get_arcades_managed_by_user(ac_user_search[0]) ul = self.data.arcade.get_arcades_managed_by_user(ac_user_search[0])
if ul is not None:
for u in ul:
aclist.append(u._asdict())
elif ac_ip_search is not None:
ul = self.data.arcade.get_arcades_by_ip(ac_ip_search[0])
if ul is not None:
for u in ul: for u in ul:
aclist.append(u._asdict()) aclist.append(u._asdict())

View File

@ -4,6 +4,7 @@
<title>{{ title }}</title> <title>{{ title }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
<style> <style>
html { html {
background-color: #181a1b !important; background-color: #181a1b !important;
@ -77,6 +78,9 @@
margin-bottom: 10px; margin-bottom: 10px;
width: 15%; width: 15%;
} }
.modal-content {
background-color: #181a1b;
}
</style> </style>
</head> </head>
<body> <body>

View File

@ -8,8 +8,8 @@
<form id="usrLookup" name="usrLookup" action="/sys/lookup.user" class="form-inline"> <form id="usrLookup" name="usrLookup" action="/sys/lookup.user" class="form-inline">
<h3>User Search</h3> <h3>User Search</h3>
<div class="form-group"> <div class="form-group">
<label for="usrEmail">Email address</label> <label for="usrId">User ID</label>
<input type="email" class="form-control" id="usrEmail" name="usrEmail" aria-describedby="emailHelp"> <input type="number" class="form-control" id="usrId" name="usrId">
</div> </div>
OR OR
<div class="form-group"> <div class="form-group">
@ -18,8 +18,8 @@
</div> </div>
OR OR
<div class="form-group"> <div class="form-group">
<label for="usrId">User ID</label> <label for="usrEmail">Email address</label>
<input type="number" class="form-control" id="usrId" name="usrId"> <input type="email" class="form-control" id="usrEmail" name="usrEmail" aria-describedby="emailHelp">
</div> </div>
<br /> <br />
<button type="submit" class="btn btn-primary">Search</button> <button type="submit" class="btn btn-primary">Search</button>
@ -30,20 +30,25 @@
<div class="col-sm-6" style="max-width: 25%;"> <div class="col-sm-6" style="max-width: 25%;">
<form id="arcadeLookup" name="arcadeLookup" action="/sys/lookup.arcade" class="form-inline" > <form id="arcadeLookup" name="arcadeLookup" action="/sys/lookup.arcade" class="form-inline" >
<h3>Arcade Search</h3> <h3>Arcade Search</h3>
<div class="form-group">
<label for="arcadeName">Arcade Name</label>
<input type="text" class="form-control" id="arcadeName" name="arcadeName">
</div>
OR
<div class="form-group"> <div class="form-group">
<label for="arcadeId">Arcade ID</label> <label for="arcadeId">Arcade ID</label>
<input type="number" class="form-control" id="arcadeId" name="arcadeId"> <input type="number" class="form-control" id="arcadeId" name="arcadeId">
</div> </div>
OR OR
<div class="form-group">
<label for="arcadeName">Arcade Name</label>
<input type="text" class="form-control" id="arcadeName" name="arcadeName">
</div>
OR
<div class="form-group"> <div class="form-group">
<label for="arcadeUser">Owner User ID</label> <label for="arcadeUser">Owner User ID</label>
<input type="number" class="form-control" id="arcadeUser" name="arcadeUser"> <input type="number" class="form-control" id="arcadeUser" name="arcadeUser">
</div> </div>
OR
<div class="form-group">
<label for="arcadeIp">Assigned IP Address</label>
<input type="text" class="form-control" id="arcadeIp" name="arcadeIp">
</div>
<br /> <br />
<button type="submit" class="btn btn-primary">Search</button> <button type="submit" class="btn btn-primary">Search</button>
</form> </form>
@ -52,13 +57,13 @@
<form id="cabLookup" name="cabLookup" action="/sys/lookup.cab" class="form-inline" > <form id="cabLookup" name="cabLookup" action="/sys/lookup.cab" class="form-inline" >
<h3>Machine Search</h3> <h3>Machine Search</h3>
<div class="form-group"> <div class="form-group">
<label for="cabSerial">Machine Serial</label> <label for="cabId">Machine ID</label>
<input type="text" class="form-control" id="cabSerial" name="cabSerial"> <input type="number" class="form-control" id="cabId" name="cabId">
</div> </div>
OR OR
<div class="form-group"> <div class="form-group">
<label for="cabId">Machine ID</label> <label for="cabSerial">Machine Serial</label>
<input type="number" class="form-control" id="cabId" name="cabId"> <input type="text" class="form-control" id="cabSerial" name="cabSerial">
</div> </div>
OR OR
<div class="form-group"> <div class="form-group">
@ -75,19 +80,19 @@
{% if sesh.permissions >= 2 %} {% if sesh.permissions >= 2 %}
<div id="userSearchResult" class="col-sm-6" style="max-width: 25%;"> <div id="userSearchResult" class="col-sm-6" style="max-width: 25%;">
{% for usr in usrlist %} {% for usr in usrlist %}
<pre><a href=/user/{{ usr.id }}>{{ usr.id }} | {{ usr.username }}</a></pre> <a href=/user/{{ usr.id }}><pre>{{ usr.id }} | {{ usr.username if usr.username != None else "<i>No Name Set</i>"}}</pre></a>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
{% if sesh.permissions >= 4 %} {% if sesh.permissions >= 4 %}
<div id="arcadeSearchResult" class="col-sm-6" style="max-width: 25%;"> <div id="arcadeSearchResult" class="col-sm-6" style="max-width: 25%;">
{% for ac in aclist %} {% for ac in aclist %}
<pre><a href=/arcade/{{ ac.id }}>{{ ac.id }} | {{ ac.name }}</a></pre> <pre><a href=/arcade/{{ ac.id }}>{{ ac.id }} | {{ ac.name if ac.name != None else "<i>No Name Set</i>" }} | {{ ac.ip if ac.ip != None else "<i>No IP Assigned</i>"}}</pre></a>
{% endfor %} {% endfor %}
</div </div
><div id="cabSearchResult" class="col-sm-6" style="max-width: 25%;"> ><div id="cabSearchResult" class="col-sm-6" style="max-width: 25%;">
{% for cab in cablist %} {% for cab in cablist %}
<a href=/cab/{{ cab.id }}><pre>{{ cab.id }} | {{ cab.game if cab.game is defined else "ANY " }} | {{ cab.serial }}</pre></a> <a href=/cab/{{ cab.id }}><pre>{{ cab.id }} | {{ cab.game if cab.game != None else "<i>ANY </i>" }} | {{ cab.serial }}</pre></a>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}

View File

@ -35,3 +35,6 @@ version:
card_maker: 1.30.01 card_maker: 1.30.01
7: 7:
card_maker: 1.35.03 card_maker: 1.35.03
crypto:
encrypted_only: False

View File

@ -169,8 +169,10 @@ class ChuniReader(BaseReader):
fumen_path = MusicFumenData.find("file").find("path") fumen_path = MusicFumenData.find("file").find("path")
if fumen_path is not None: if fumen_path is not None:
chart_id = MusicFumenData.find("type").find("id").text chart_type = MusicFumenData.find("type")
if chart_id == "4": chart_id = chart_type.find("id").text
chart_diff = chart_type.find("str").text
if chart_diff == "WorldsEnd" and (chart_id == "4" or chart_id == "5"): # 4 in SDBT, 5 in SDHD
level = float(xml_root.find("starDifType").text) level = float(xml_root.find("starDifType").text)
we_chara = ( we_chara = (
xml_root.find("worldsEndTagName") xml_root.find("worldsEndTagName")

View File

@ -2,9 +2,11 @@ from titles.ongeki.index import OngekiServlet
from titles.ongeki.const import OngekiConstants from titles.ongeki.const import OngekiConstants
from titles.ongeki.database import OngekiData from titles.ongeki.database import OngekiData
from titles.ongeki.read import OngekiReader from titles.ongeki.read import OngekiReader
from titles.ongeki.frontend import OngekiFrontend
index = OngekiServlet index = OngekiServlet
database = OngekiData database = OngekiData
reader = OngekiReader reader = OngekiReader
frontend = OngekiFrontend
game_codes = [OngekiConstants.GAME_CODE] game_codes = [OngekiConstants.GAME_CODE]
current_schema_version = 5 current_schema_version = 5

View File

@ -978,35 +978,38 @@ class OngekiBase:
""" """
Added in Bright Added in Bright
""" """
rival_list = self.data.profile.get_rivals(data["userId"])
if rival_list is None or len(rival_list) < 1: rival_list = []
user_rivals = self.data.profile.get_rivals(data["userId"])
for rival in user_rivals:
tmp = {}
tmp["rivalUserId"] = rival[0]
rival_list.append(tmp)
if user_rivals is None or len(rival_list) < 1:
return { return {
"userId": data["userId"], "userId": data["userId"],
"length": 0, "length": 0,
"userRivalList": [], "userRivalList": [],
} }
return { return {
"userId": data["userId"], "userId": data["userId"],
"length": len(rival_list), "length": len(rival_list),
"userRivalList": rival_list._asdict(), "userRivalList": rival_list,
} }
def handle_get_user_rival_data_api_reqiest(self, data: Dict) -> Dict: def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict:
""" """
Added in Bright Added in Bright
""" """
rivals = [] rivals = []
for rival in data["userRivalList"]: for rival in data["userRivalList"]:
name = self.data.profile.get_profile_name( name = self.data.profile.get_profile_name(
rival["rivalUserId"], self.version rival["rivalUserId"], self.version
) )
if name is None: if name is None:
continue continue
rivals.append({"rivalUserId": rival["rivalUserId"], "rivalUserName": name})
rivals.append({"rivalUserId": rival["rival"], "rivalUserName": name})
return { return {
"userId": data["userId"], "userId": data["userId"],
"length": len(rivals), "length": len(rivals),
@ -1027,7 +1030,6 @@ class OngekiBase:
for song in music["userMusicList"]: for song in music["userMusicList"]:
song["userRivalMusicDetailList"] = song["userMusicDetailList"] song["userRivalMusicDetailList"] = song["userMusicDetailList"]
song.pop("userMusicDetailList") song.pop("userMusicDetailList")
return { return {
"userId": data["userId"], "userId": data["userId"],
"rivalUserId": rival_id, "rivalUserId": rival_id,

View File

@ -48,9 +48,30 @@ class OngekiCardMakerVersionConfig:
self.__config, "ongeki", "version", default={} self.__config, "ongeki", "version", default={}
).get(version) ).get(version)
class OngekiCryptoConfig:
def __init__(self, parent_config: "OngekiConfig") -> None:
self.__config = parent_config
@property
def keys(self) -> Dict:
"""
in the form of:
internal_version: [key, iv]
all values are hex strings
"""
return CoreConfig.get_config_field(
self.__config, "ongeki", "crypto", "keys", default={}
)
@property
def encrypted_only(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "ongeki", "crypto", "encrypted_only", default=False
)
class OngekiConfig(dict): class OngekiConfig(dict):
def __init__(self) -> None: def __init__(self) -> None:
self.server = OngekiServerConfig(self) self.server = OngekiServerConfig(self)
self.gachas = OngekiGachaConfig(self) self.gachas = OngekiGachaConfig(self)
self.version = OngekiCardMakerVersionConfig(self) self.version = OngekiCardMakerVersionConfig(self)
self.crypto = OngekiCryptoConfig(self)

87
titles/ongeki/frontend.py Normal file
View File

@ -0,0 +1,87 @@
import yaml
import jinja2
from twisted.web.http import Request
from os import path
from twisted.web.util import redirectTo
from twisted.web.server import Session
from core.frontend import FE_Base, IUserSession
from core.config import CoreConfig
from titles.ongeki.config import OngekiConfig
from titles.ongeki.const import OngekiConstants
from titles.ongeki.database import OngekiData
from titles.ongeki.base import OngekiBase
class OngekiFrontend(FE_Base):
def __init__(
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
) -> None:
super().__init__(cfg, environment)
self.data = OngekiData(cfg)
self.game_cfg = OngekiConfig()
if path.exists(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"):
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"))
)
self.nav_name = "O.N.G.E.K.I."
self.version_list = OngekiConstants.VERSION_NAMES
def render_GET(self, request: Request) -> bytes:
template = self.environment.get_template(
"titles/ongeki/frontend/ongeki_index.jinja"
)
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
self.version = usr_sesh.ongeki_version
if getattr(usr_sesh, "userId", 0) != 0:
profile_data =self.data.profile.get_profile_data(usr_sesh.userId, self.version)
rival_list = self.data.profile.get_rivals(usr_sesh.userId)
rival_data = {
"userRivalList": rival_list,
"userId": usr_sesh.userId
}
rival_info = OngekiBase.handle_get_user_rival_data_api_request(self, rival_data)
return template.render(
data=self.data.profile,
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
gachas=self.game_cfg.gachas.enabled_gachas,
profile_data=profile_data,
rival_info=rival_info["userRivalDataList"],
version_list=self.version_list,
version=self.version,
sesh=vars(usr_sesh)
).encode("utf-16")
else:
return redirectTo(b"/gate/", request)
def render_POST(self, request: Request):
uri = request.uri.decode()
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
if hasattr(usr_sesh, "userId"):
if uri == "/game/ongeki/rival.add":
rival_id = request.args[b"rivalUserId"][0].decode()
self.data.profile.put_rival(usr_sesh.userId, rival_id)
# self.logger.info(f"{usr_sesh.userId} added a rival")
return redirectTo(b"/game/ongeki/", request)
elif uri == "/game/ongeki/rival.delete":
rival_id = request.args[b"rivalUserId"][0].decode()
self.data.profile.delete_rival(usr_sesh.userId, rival_id)
# self.logger.info(f"{response}")
return redirectTo(b"/game/ongeki/", request)
elif uri == "/game/ongeki/version.change":
ongeki_version=request.args[b"version"][0].decode()
if(ongeki_version.isdigit()):
usr_sesh.ongeki_version=int(ongeki_version)
return redirectTo(b"/game/ongeki/", request)
else:
return b"Something went wrong"
else:
return b"User is not logged in"

View File

@ -0,0 +1,24 @@
function deleteRival(rivalUserId){
$(document).ready(function () {
$.post("/game/ongeki/rival.delete",
{
rivalUserId
},
function(data,status){
window.location.replace("/game/ongeki/")
})
});
}
function changeVersion(sel){
$(document).ready(function () {
$.post("/game/ongeki/version.change",
{
version: sel.value
},
function(data,status){
window.location.replace("/game/ongeki/")
})
});
}

View File

@ -0,0 +1,83 @@
{% extends "core/frontend/index.jinja" %}
{% block content %}
{% if sesh is defined and sesh["userId"] > 0 %}
<br>
<br>
<br>
<div class="container">
<div class="row">
<h2> Profile </h2>
<h3>Version:
<select name="version" id="version" onChange="changeVersion(this)">
{% for ver in version_list %}
<option value={{loop.index0}} {{ "selected" if loop.index0==version else "" }} >{{ver}}</option>
{% endfor %}
</select>
</h3>
<hr>
</div>
<div class="row">
<div class="col">
<h2> Name: {{ profile_data.userName if profile_data.userName is defined else "Profile not found" }}</h2>
</div>
<div class="col">
<h4> ID: {{ profile_data.user if profile_data.user is defined else 'Profile not found' }}</h4>
</div>
</div>
<hr>
<div class="row">
<h2> Rivals <button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#rival_add">Add</button></h2>
</div>
<div class="row">
<table class="table table-dark table-hover">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Delete</th>
</tr>
</thead>
<tbody>
{% for rival in rival_info%}
<tr id="{{rival.rivalUserId}}">
<td>{{rival.rivalUserId}}</td>
<td>{{rival.rivalUserName}}</td>
<td><button class="btn-danger btn btn-sm" onclick="deleteRival({{rival.rivalUserId}})">Delete</button></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="modal fade" id="rival_add" tabindex="-1" aria-labelledby="card_add_label" data-bs-theme="dark" aria-hidden="true">
<form id="rival" action="/game/ongeki/rival.add" method="post">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Note:<br>
Please use the ID show next to your name in the profile page.
<br>
<label for="rivalUserId">ID:&nbsp;</label><input form="rival" id="rivalUserId" name="rivalUserId" maxlength="5" type="number" required>
</div>
<div class="modal-footer">
<input type=submit class="btn btn-primary" type="button" form="rival" value="Add">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</form>
</div>
</div>
<script>
{% include 'titles/ongeki/frontend/js/ongeki_scripts.js' %}
</script>
{% else %}
<h2>Not Currently Logged In</h2>
{% endif %}
{% endblock content %}

View File

@ -7,6 +7,10 @@ import logging
import coloredlogs import coloredlogs
import zlib import zlib
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Hash import SHA1
from os import path from os import path
from typing import Tuple from typing import Tuple
@ -28,6 +32,7 @@ class OngekiServlet:
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
self.core_cfg = core_cfg self.core_cfg = core_cfg
self.game_cfg = OngekiConfig() self.game_cfg = OngekiConfig()
self.hash_table: Dict[Dict[str, str]] = {}
if path.exists(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"): if path.exists(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"):
self.game_cfg.update( self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}")) yaml.safe_load(open(f"{cfg_dir}/{OngekiConstants.CONFIG_NAME}"))
@ -45,6 +50,8 @@ class OngekiServlet:
] ]
self.logger = logging.getLogger("ongeki") self.logger = logging.getLogger("ongeki")
if not hasattr(self.logger, "inited"):
log_fmt_str = "[%(asctime)s] Ongeki | %(levelname)s | %(message)s" log_fmt_str = "[%(asctime)s] Ongeki | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str) log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler( fileHandler = TimedRotatingFileHandler(
@ -66,6 +73,37 @@ class OngekiServlet:
coloredlogs.install( coloredlogs.install(
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
) )
self.logger.inited = True
for version, keys in self.game_cfg.crypto.keys.items():
if len(keys) < 3:
continue
self.hash_table[version] = {}
method_list = [
method
for method in dir(self.versions[version])
if not method.startswith("__")
]
for method in method_list:
method_fixed = inflection.camelize(method)[6:-7]
# number of iterations is 64 on Bright Memory
iter_count = 64
hash = PBKDF2(
method_fixed,
bytes.fromhex(keys[2]),
128,
count=iter_count,
hmac_hash_module=SHA1,
)
hashed_name = hash.hex()[:32] # truncate unused bytes like the game does
self.hash_table[version][hashed_name] = method_fixed
self.logger.debug(
f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()[:32]}"
)
@classmethod @classmethod
def get_allnet_info( def get_allnet_info(
@ -100,6 +138,7 @@ class OngekiServlet:
req_raw = request.content.getvalue() req_raw = request.content.getvalue()
url_split = url_path.split("/") url_split = url_path.split("/")
encrtped = False
internal_ver = 0 internal_ver = 0
endpoint = url_split[len(url_split) - 1] endpoint = url_split[len(url_split) - 1]
client_ip = Utils.get_ip_addr(request) client_ip = Utils.get_ip_addr(request)
@ -125,8 +164,45 @@ class OngekiServlet:
# If we get a 32 character long hex string, it's a hash and we're # If we get a 32 character long hex string, it's a hash and we're
# doing encrypted. The likelyhood of false positives is low but # doing encrypted. The likelyhood of false positives is low but
# technically not 0 # technically not 0
self.logger.error("Encryption not supported at this time") if internal_ver not in self.hash_table:
return b"" self.logger.error(
f"v{version} does not support encryption or no keys entered"
)
return zlib.compress(b'{"stat": "0"}')
elif endpoint.lower() not in self.hash_table[internal_ver]:
self.logger.error(
f"No hash found for v{version} endpoint {endpoint}"
)
return zlib.compress(b'{"stat": "0"}')
endpoint = self.hash_table[internal_ver][endpoint.lower()]
try:
crypt = AES.new(
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]),
AES.MODE_CBC,
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
)
req_raw = crypt.decrypt(req_raw)
except Exception as e:
self.logger.error(
f"Failed to decrypt v{version} request to {endpoint} -> {e}"
)
return zlib.compress(b'{"stat": "0"}')
encrtped = True
if (
not encrtped
and self.game_cfg.crypto.encrypted_only
):
self.logger.error(
f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}"
)
return zlib.compress(b'{"stat": "0"}')
try: try:
unzip = zlib.decompress(req_raw) unzip = zlib.decompress(req_raw)
@ -163,4 +239,17 @@ class OngekiServlet:
self.logger.debug(f"Response {resp}") self.logger.debug(f"Response {resp}")
return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
if not encrtped:
return zipped
padded = pad(zipped, 16)
crypt = AES.new(
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]),
AES.MODE_CBC,
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
)
return crypt.encrypt(padded)

View File

@ -3,7 +3,7 @@ from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, an
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger
from sqlalchemy.engine.base import Connection from sqlalchemy.engine.base import Connection
from sqlalchemy.schema import ForeignKey from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select from sqlalchemy.sql import func, select, delete
from sqlalchemy.engine import Row from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
@ -499,7 +499,7 @@ class OngekiProfileData(BaseData):
def put_rival(self, aime_id: int, rival_id: int) -> Optional[int]: def put_rival(self, aime_id: int, rival_id: int) -> Optional[int]:
sql = insert(rival).values(user=aime_id, rivalUserId=rival_id) sql = insert(rival).values(user=aime_id, rivalUserId=rival_id)
conflict = sql.on_duplicate_key_update(rival=rival_id) conflict = sql.on_duplicate_key_update(rivalUserId=rival_id)
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
@ -508,3 +508,10 @@ class OngekiProfileData(BaseData):
) )
return None return None
return result.lastrowid return result.lastrowid
def delete_rival(self, aime_id: int, rival_id: int) -> Optional[int]:
sql = delete(rival).where(rival.c.user==aime_id, rival.c.rivalUserId==rival_id)
result = self.execute(sql)
if result is None:
self.logger.error(f"delete_rival: failed to delete! aime_id: {aime_id}, rival_id: {rival_id}")
else:
return result.rowcount