Compare commits
205
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c51103aaf5 | ||
|
|
08927db100 | ||
|
|
238e39f415 | ||
|
|
5499d38bb4 | ||
|
|
3a6cfedcca | ||
|
|
7a6272dcc5 | ||
|
|
136e47d1e6 | ||
|
|
dac655b4ae | ||
|
|
37e2da2051 | ||
|
|
9d74d60c14 | ||
|
|
d4ea3bc12a | ||
|
|
d8b0e2ea2a | ||
|
|
984949d902 | ||
|
|
2e8d99e5fa | ||
|
|
fd6cadf2da | ||
|
|
71489c1272 | ||
|
|
3773c57de1 | ||
|
|
8ea82ffe1a | ||
|
|
904ea10920 | ||
|
|
8a8c0e023e | ||
|
|
cf7cc0997a | ||
|
|
92567504f4 | ||
|
|
fd50a7ee68 | ||
|
|
9e3a51a57a | ||
|
|
4744e8cf5f | ||
|
|
88a1462304 | ||
|
|
2e277e7791 | ||
|
|
757fdc5c57 | ||
|
|
23bcb5cc13 | ||
|
|
9f0c181593 | ||
|
|
5a4baba102 | ||
|
|
156b4e4ede | ||
|
|
6c89a97fe3 | ||
|
|
b943807904 | ||
|
|
f417be671b | ||
|
|
20335aaebe | ||
|
|
097181008b | ||
|
|
63d81a2704 | ||
|
|
718229b267 | ||
|
|
7c78975431 | ||
|
|
14a315a673 | ||
|
|
343fe4357c | ||
|
|
d0e43140ba | ||
|
|
859bf4bf5d | ||
|
|
c6e7100f51 | ||
|
|
b0ca37815b | ||
|
|
f39317301b | ||
|
|
389784ce82 | ||
|
|
2f13596885 | ||
|
|
6a41dac46c | ||
|
|
85b73e634d | ||
|
|
09c4f8cda4 | ||
|
|
36d338e618 | ||
|
|
03cf535ff6 | ||
|
|
6c155a5e48 | ||
|
|
737312ca3d | ||
|
|
1edec7dba2 | ||
|
|
d60f827000 | ||
|
|
da422e602b | ||
|
|
d276ac8598 | ||
|
|
84e880e94f | ||
|
|
432177957a | ||
|
|
a89247cdd6 | ||
|
|
f279adb894 | ||
|
|
a680699939 | ||
|
|
d204954447 | ||
|
|
042440c76e | ||
|
|
c4c0566cd5 | ||
|
|
3e9cec3a20 | ||
|
|
8f9584c3d2 | ||
|
|
b29cb0fbaa | ||
|
|
d9a92f5865 | ||
|
|
9859ab4fdb | ||
|
|
d89eb61e62 | ||
|
|
dc8c27046e | ||
|
|
3e461f4d71 | ||
|
|
2c6902a546 | ||
|
|
318b73dd57 | ||
|
|
9d33091bb8 | ||
|
|
8b43d554fc | ||
|
|
610ef70bad | ||
|
|
60b3bc7750 | ||
|
|
4ea83f6025 | ||
|
|
20389011e9 | ||
|
|
e446816b9a | ||
|
|
9dd2b4d524 | ||
|
|
b60cf6258d | ||
|
|
127e6f8aa8 | ||
|
|
5155353360 | ||
|
|
e3d38dacde | ||
|
|
0c6d9a36ce | ||
|
|
b1968fe320 | ||
|
|
03f91d18c9 | ||
|
|
17508f09b2 | ||
|
|
aae4afe7b8 | ||
|
|
514f786e2d | ||
|
|
ec9ad1ebb0 | ||
|
|
08ebb5c907 | ||
|
|
571b92d0cd | ||
|
|
01b5282899 | ||
|
|
391edd3354 | ||
|
|
d5bff0e891 | ||
|
|
402e753469 | ||
|
|
154ccbdae5 | ||
|
|
858b101a36 | ||
|
|
1d10e798a5 | ||
|
|
3c385f505b | ||
|
|
b12938bcd8 | ||
|
|
1b2f5e3709 | ||
|
|
65686fb615 | ||
|
|
f56332141e | ||
|
|
5a35b1c823 | ||
|
|
5ca16f2067 | ||
|
|
a0b25e2b7b | ||
|
|
84fc002cdb | ||
|
|
3bd03c592e | ||
|
|
cf6cfdbd3b | ||
|
|
db77e61b79 | ||
|
|
ac9e71ee2f | ||
|
|
20865dc495 | ||
|
|
37d24b3b4d | ||
|
|
2418abacce | ||
|
|
5c3f812caf | ||
|
|
4854bcfcad | ||
|
|
bf6d126f8a | ||
|
|
e466ddce55 | ||
|
|
ad820ed091 | ||
|
|
960a0e3fd9 | ||
|
|
a2fe11d654 | ||
|
|
2b4ac06389 | ||
|
|
d8af7be4a4 | ||
|
|
84cb786bde | ||
|
|
05dee87a9a | ||
|
|
049dc40a8b | ||
|
|
cab1d6814a | ||
|
|
72594fef31 | ||
|
|
7ed294e9f7 | ||
|
|
b9fd4f294d | ||
|
|
5ddfb88182 | ||
|
|
4da8622977 | ||
|
|
97892d6a7d | ||
|
|
02078080a8 | ||
|
|
61e3a2c930 | ||
|
|
8ae0aba89c | ||
|
|
49166c1a7b | ||
|
|
013e83420b | ||
|
|
0dce7e7849 | ||
|
|
f959236af0 | ||
|
|
b85a65204f | ||
|
|
0ab539173a | ||
|
|
42ed222095 | ||
|
|
d172e5582b | ||
|
|
9766e3ab78 | ||
|
|
b34b441ba8 | ||
|
|
8149f09a40 | ||
|
|
cad523dfce | ||
|
|
8b9771b5af | ||
|
|
989c080657 | ||
|
|
dcff8adbab | ||
|
|
e3b1addce6 | ||
|
|
b6f43d887a | ||
|
|
efd8f86e48 | ||
|
|
d0242b456d | ||
|
|
7bb8c2c80c | ||
|
|
6d1855a6bc | ||
|
|
8d94d25893 | ||
|
|
ae6dcb68df | ||
|
|
3b6fc6618c | ||
|
|
deeac1d8db | ||
|
|
6ad5194bb8 | ||
|
|
a0793aa13a | ||
|
|
7364181de1 | ||
|
|
9d8762d3da | ||
|
|
238d437519 | ||
|
|
9d23d59e43 | ||
|
|
f4ee4238d9 | ||
|
|
b498e82bf8 | ||
|
|
0668488ccf | ||
|
|
47f4aaddf8 | ||
|
|
26c4bcb466 | ||
|
|
d8c3ed5c01 | ||
|
|
58a088b9a4 | ||
|
|
190c41e03e | ||
|
|
a30967e8d7 | ||
|
|
00b127361b | ||
|
|
241f29e29c | ||
|
|
4d6afd757f | ||
|
|
68b0894e47 | ||
|
|
017ef1e224 | ||
|
|
15433b681c | ||
|
|
b0042bc776 | ||
|
|
469ead7a84 | ||
|
|
0dc96f33e1 | ||
|
|
4102ba21fc | ||
|
|
83d2151b6b | ||
|
|
9895068125 | ||
|
|
4419310086 | ||
|
|
a416fb09e1 | ||
|
|
baa885f674 | ||
|
|
0d5567c990 | ||
|
|
b1f9be0121 | ||
|
|
dc3e3e1fb3 | ||
|
|
71c43a4a57 | ||
|
|
bd356af272 | ||
|
|
68e25b9c5e |
@@ -158,5 +158,6 @@ cert/*
|
||||
!cert/server.pem
|
||||
config/*
|
||||
deliver/*
|
||||
*.gz
|
||||
|
||||
dbdump-*.json
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
FROM python:3.9.15-slim-bullseye
|
||||
|
||||
RUN apt update && apt install default-libmysqlclient-dev build-essential libtk nodejs npm -y
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip3 install -r requirements.txt
|
||||
RUN npm i -g nodemon
|
||||
|
||||
COPY entrypoint.sh entrypoint.sh
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
COPY index.py index.py
|
||||
COPY dbutils.py dbutils.py
|
||||
ADD core core
|
||||
ADD titles titles
|
||||
ADD config config
|
||||
ADD log log
|
||||
ADD cert cert
|
||||
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
@@ -0,0 +1,99 @@
|
||||
# Changelog
|
||||
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
|
||||
|
||||
## 20230716
|
||||
### General
|
||||
+ Docker files added (#19)
|
||||
+ Added support for threading
|
||||
+ This comes with the caviat that enabling it will not allow you to use Ctrl + C to stop the server.
|
||||
|
||||
### Webui
|
||||
+ Small improvements
|
||||
+ Add card display
|
||||
|
||||
### Allnet
|
||||
+ Billing format validation
|
||||
+ Fix naomitest.html endpoint
|
||||
+ Add event logging for auths and billing
|
||||
+ LoaderStateRecorder endpoint handler added
|
||||
|
||||
### Mucha
|
||||
+ Fixed log level always being "Info"
|
||||
+ Add stub handler for DownloadState
|
||||
|
||||
### Sword Art Online
|
||||
+ Support added
|
||||
|
||||
### Crossbeats
|
||||
+ Added threading to profile loading
|
||||
+ This should cause a noticeable speed-up
|
||||
|
||||
### Card Maker
|
||||
+ DX Passes fixed
|
||||
+ Various improvements
|
||||
|
||||
### Diva
|
||||
+ Added clear status calculation
|
||||
+ Various minor fixes and improvements
|
||||
|
||||
### Maimai
|
||||
+ Added support for memorial photo uploads
|
||||
+ Added support for the following versions
|
||||
+ Festival
|
||||
+ FiNALE
|
||||
+ Various bug fixes and improvements
|
||||
|
||||
### Wacca
|
||||
+ Fixed an error that sometimes occoured when trying to unlock songs (#22)
|
||||
|
||||
### Pokken
|
||||
+ Profile saving added (loading TBA)
|
||||
+ Use external STUN server for matching by default
|
||||
+ Matching still not working
|
||||
|
||||
## 2023042300
|
||||
### Wacca
|
||||
+ Time free now works properly
|
||||
+ Fix reverse gate mission causing a fatal error
|
||||
+ Other misc. fixes
|
||||
+ Latest DB: 5
|
||||
|
||||
### Pokken
|
||||
+ Added preliminary support
|
||||
+ Nothing saves currently, but the game will boot and function properly.
|
||||
|
||||
### Initial D Zero
|
||||
+ Added preliminary support
|
||||
+ Nothing saves currently, but the game will boot and function for the most part.
|
||||
|
||||
### Mai2
|
||||
+ Added support for Festival
|
||||
+ Lasted DB Version: 4
|
||||
|
||||
### Ongeki
|
||||
+ Misc fixes
|
||||
+ Lasted DB Version: 4
|
||||
|
||||
### Diva
|
||||
+ Misc fixes
|
||||
+ Lasted DB Version: 4
|
||||
|
||||
### Chuni
|
||||
+ Fix network encryption
|
||||
+ Add `handle_remove_token_api_request` for event mode
|
||||
|
||||
### Allnet
|
||||
+ Added download order support
|
||||
+ It is up to the sysop to provide the INI file, and host the files.
|
||||
+ ONLY for use with cabs. It's not checked currently, which it's why it's default disabled
|
||||
+ YMMV, use at your own risk
|
||||
+ When running develop mode, games that are not recognised will still be able to authenticate.
|
||||
|
||||
### Database
|
||||
+ Add autoupgrade command
|
||||
+ Invoke to automatically upgrade all schemas to their latest versions
|
||||
|
||||
+ `version` arg no longer required, leave it blank to update the game schema to latest if it isn't already
|
||||
|
||||
### Misc
|
||||
+ Update example nginx config file
|
||||
@@ -0,0 +1,6 @@
|
||||
from .base import ADBBaseRequest, ADBBaseResponse, ADBHeader, ADBHeaderException, PortalRegStatus, LogStatus, ADBStatus
|
||||
from .base import CompanyCodes, ReaderFwVer, CMD_CODE_GOODBYE, HEADER_SIZE
|
||||
from .lookup import ADBLookupRequest, ADBLookupResponse, ADBLookupExResponse
|
||||
from .campaign import ADBCampaignClearRequest, ADBCampaignClearResponse, ADBCampaignResponse, ADBOldCampaignRequest, ADBOldCampaignResponse
|
||||
from .felica import ADBFelicaLookupRequest, ADBFelicaLookupResponse, ADBFelicaLookup2Request, ADBFelicaLookup2Response
|
||||
from .log import ADBLogExRequest, ADBLogRequest, ADBStatusLogRequest, ADBLogExResponse
|
||||
@@ -0,0 +1,170 @@
|
||||
import struct
|
||||
from construct import Struct, Int16ul, Int32ul, PaddedString
|
||||
from enum import Enum
|
||||
import re
|
||||
from typing import Union, Final
|
||||
|
||||
class LogStatus(Enum):
|
||||
NONE = 0
|
||||
START = 1
|
||||
CONTINUE = 2
|
||||
END = 3
|
||||
OTHER = 4
|
||||
|
||||
class PortalRegStatus(Enum):
|
||||
NO_REG = 0
|
||||
PORTAL = 1
|
||||
SEGA_ID = 2
|
||||
|
||||
class ADBStatus(Enum):
|
||||
UNKNOWN = 0
|
||||
GOOD = 1
|
||||
BAD_AMIE_ID = 2
|
||||
ALREADY_REG = 3
|
||||
BAN_SYS_USER = 4
|
||||
BAN_SYS = 5
|
||||
BAN_USER = 6
|
||||
BAN_GEN = 7
|
||||
LOCK_SYS_USER = 8
|
||||
LOCK_SYS = 9
|
||||
LOCK_USER = 10
|
||||
|
||||
class CompanyCodes(Enum):
|
||||
NONE = 0
|
||||
SEGA = 1
|
||||
BAMCO = 2
|
||||
KONAMI = 3
|
||||
TAITO = 4
|
||||
|
||||
class ReaderFwVer(Enum): # Newer readers use a singly byte value
|
||||
NONE = 0
|
||||
TN32_10 = 1
|
||||
TN32_12 = 2
|
||||
OTHER = 9
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self == self.TN32_10:
|
||||
return "TN32MSEC003S F/W Ver1.0"
|
||||
elif self == self.TN32_12:
|
||||
return "TN32MSEC003S F/W Ver1.2"
|
||||
elif self == self.NONE:
|
||||
return "Not Specified"
|
||||
elif self == self.OTHER:
|
||||
return "Unknown/Other"
|
||||
else:
|
||||
raise ValueError(f"Bad ReaderFwVer value {self.value}")
|
||||
|
||||
@classmethod
|
||||
def from_byte(self, byte: bytes) -> Union["ReaderFwVer", int]:
|
||||
try:
|
||||
i = int.from_bytes(byte, 'little')
|
||||
try:
|
||||
return ReaderFwVer(i)
|
||||
except ValueError:
|
||||
return i
|
||||
except TypeError:
|
||||
return 0
|
||||
|
||||
class ADBHeaderException(Exception):
|
||||
pass
|
||||
|
||||
HEADER_SIZE: Final[int] = 0x20
|
||||
CMD_CODE_GOODBYE: Final[int] = 0x66
|
||||
|
||||
# everything is LE
|
||||
class ADBHeader:
|
||||
def __init__(self, magic: int, protocol_ver: int, cmd: int, length: int, status: int, game_id: Union[str, bytes], store_id: int, keychip_id: Union[str, bytes]) -> None:
|
||||
self.magic = magic # u16
|
||||
self.protocol_ver = protocol_ver # u16
|
||||
self.cmd = cmd # u16
|
||||
self.length = length # u16
|
||||
try:
|
||||
self.status = ADBStatus(status) # u16
|
||||
except ValueError as e:
|
||||
raise ADBHeaderException(f"Status is incorrect! {e}")
|
||||
self.game_id = game_id # 4 char + \x00
|
||||
self.store_id = store_id # u32
|
||||
self.keychip_id = keychip_id# 11 char + \x00
|
||||
|
||||
if type(self.game_id) == bytes:
|
||||
self.game_id = self.game_id.decode()
|
||||
|
||||
if type(self.keychip_id) == bytes:
|
||||
self.keychip_id = self.keychip_id.decode()
|
||||
|
||||
self.game_id = self.game_id.replace("\0", "")
|
||||
self.keychip_id = self.keychip_id.replace("\0", "")
|
||||
if self.cmd != CMD_CODE_GOODBYE: # Games for some reason send no data with goodbye
|
||||
self.validate()
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: bytes) -> "ADBHeader":
|
||||
magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id = struct.unpack_from("<5H6sI12s", data)
|
||||
head = cls(magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id)
|
||||
|
||||
if head.length != len(data):
|
||||
raise ADBHeaderException(f"Length is incorrect! Expect {head.length}, got {len(data)}")
|
||||
|
||||
return head
|
||||
|
||||
def validate(self) -> bool:
|
||||
if self.magic != 0xa13e:
|
||||
raise ADBHeaderException(f"Magic {self.magic} != 0xa13e")
|
||||
|
||||
if self.protocol_ver < 0x1000:
|
||||
raise ADBHeaderException(f"Protocol version {hex(self.protocol_ver)} is invalid!")
|
||||
|
||||
if re.fullmatch(r"^S[0-9A-Z]{3}[P]?$", self.game_id) is None:
|
||||
raise ADBHeaderException(f"Game ID {self.game_id} is invalid!")
|
||||
|
||||
if self.store_id == 0:
|
||||
raise ADBHeaderException(f"Store ID cannot be 0!")
|
||||
|
||||
if re.fullmatch(r"^A[0-9]{2}[E|X][0-9]{2}[A-HJ-NP-Z][0-9]{4}$", self.keychip_id) is None:
|
||||
raise ADBHeaderException(f"Keychip ID {self.keychip_id} is invalid!")
|
||||
|
||||
return True
|
||||
|
||||
def make(self) -> bytes:
|
||||
resp_struct = Struct(
|
||||
"magic" / Int16ul,
|
||||
"unknown" / Int16ul,
|
||||
"response_code" / Int16ul,
|
||||
"length" / Int16ul,
|
||||
"status" / Int16ul,
|
||||
"game_id" / PaddedString(6, 'utf_8'),
|
||||
"store_id" / Int32ul,
|
||||
"keychip_id" / PaddedString(12, 'utf_8'),
|
||||
)
|
||||
|
||||
return resp_struct.build(dict(
|
||||
magic=self.magic,
|
||||
unknown=self.protocol_ver,
|
||||
response_code=self.cmd,
|
||||
length=self.length,
|
||||
status=self.status.value,
|
||||
game_id = self.game_id,
|
||||
store_id = self.store_id,
|
||||
keychip_id = self.keychip_id,
|
||||
))
|
||||
|
||||
class ADBBaseRequest:
|
||||
def __init__(self, data: bytes) -> None:
|
||||
self.head = ADBHeader.from_data(data)
|
||||
|
||||
class ADBBaseResponse:
|
||||
def __init__(self, code: int = 0, length: int = 0x20, status: int = 1, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", protocol_ver: int = 0x3087) -> None:
|
||||
self.head = ADBHeader(0xa13e, protocol_ver, code, length, status, game_id, store_id, keychip_id)
|
||||
|
||||
@classmethod
|
||||
def from_req(cls, req: ADBHeader, cmd: int, length: int = 0x20, status: int = 1) -> "ADBBaseResponse":
|
||||
return cls(cmd, length, status, req.game_id, req.store_id, req.keychip_id, req.protocol_ver)
|
||||
|
||||
def append_padding(self, data: bytes):
|
||||
"""Appends 0s to the end of the data until it's at the correct size"""
|
||||
padding_size = self.head.length - len(data)
|
||||
data += bytes(padding_size)
|
||||
return data
|
||||
|
||||
def make(self) -> bytes:
|
||||
return self.head.make()
|
||||
@@ -0,0 +1,132 @@
|
||||
from construct import Struct, Int16ul, Padding, Bytes, Int32ul, Int32sl
|
||||
|
||||
from .base import *
|
||||
|
||||
class Campaign:
|
||||
def __init__(self) -> None:
|
||||
self.id = 0
|
||||
self.name = ""
|
||||
self.announce_date = 0
|
||||
self.start_date = 0
|
||||
self.end_date = 0
|
||||
self.distrib_start_date = 0
|
||||
self.distrib_end_date = 0
|
||||
|
||||
def make(self) -> bytes:
|
||||
name_padding = bytes(128 - len(self.name))
|
||||
return Struct(
|
||||
"id" / Int32ul,
|
||||
"name" / Bytes(128),
|
||||
"announce_date" / Int32ul,
|
||||
"start_date" / Int32ul,
|
||||
"end_date" / Int32ul,
|
||||
"distrib_start_date" / Int32ul,
|
||||
"distrib_end_date" / Int32ul,
|
||||
Padding(8),
|
||||
).build(dict(
|
||||
id = self.id,
|
||||
name = self.name.encode() + name_padding,
|
||||
announce_date = self.announce_date,
|
||||
start_date = self.start_date,
|
||||
end_date = self.end_date,
|
||||
distrib_start_date = self.distrib_start_date,
|
||||
distrib_end_date = self.distrib_end_date,
|
||||
))
|
||||
|
||||
class CampaignClear:
|
||||
def __init__(self) -> None:
|
||||
self.id = 0
|
||||
self.entry_flag = 0
|
||||
self.clear_flag = 0
|
||||
|
||||
def make(self) -> bytes:
|
||||
return Struct(
|
||||
"id" / Int32ul,
|
||||
"entry_flag" / Int32ul,
|
||||
"clear_flag" / Int32ul,
|
||||
Padding(4),
|
||||
).build(dict(
|
||||
id = self.id,
|
||||
entry_flag = self.entry_flag,
|
||||
clear_flag = self.clear_flag,
|
||||
))
|
||||
|
||||
class ADBCampaignResponse(ADBBaseResponse):
|
||||
def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x0C, length: int = 0x200, status: int = 1) -> None:
|
||||
super().__init__(code, length, status, game_id, store_id, keychip_id)
|
||||
self.campaigns = [Campaign(), Campaign(), Campaign()]
|
||||
|
||||
@classmethod
|
||||
def from_req(cls, req: ADBHeader) -> "ADBCampaignResponse":
|
||||
c = cls(req.game_id, req.store_id, req.keychip_id)
|
||||
c.head.protocol_ver = req.protocol_ver
|
||||
return c
|
||||
|
||||
def make(self) -> bytes:
|
||||
body = b""
|
||||
|
||||
for c in self.campaigns:
|
||||
body += c.make()
|
||||
|
||||
self.head.length = HEADER_SIZE + len(body)
|
||||
return self.head.make() + body
|
||||
|
||||
class ADBOldCampaignRequest(ADBBaseRequest):
|
||||
def __init__(self, data: bytes) -> None:
|
||||
super().__init__(data)
|
||||
self.campaign_id = struct.unpack_from("<I", data, 0x20)
|
||||
|
||||
class ADBOldCampaignResponse(ADBBaseResponse):
|
||||
def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x0C, length: int = 0x30, status: int = 1) -> None:
|
||||
super().__init__(code, length, status, game_id, store_id, keychip_id)
|
||||
self.info0 = 0
|
||||
self.info1 = 0
|
||||
self.info2 = 0
|
||||
self.info3 = 0
|
||||
|
||||
@classmethod
|
||||
def from_req(cls, req: ADBHeader) -> "ADBCampaignResponse":
|
||||
c = cls(req.game_id, req.store_id, req.keychip_id)
|
||||
c.head.protocol_ver = req.protocol_ver
|
||||
return c
|
||||
|
||||
def make(self) -> bytes:
|
||||
resp_struct = Struct(
|
||||
"info0" / Int32sl,
|
||||
"info1" / Int32sl,
|
||||
"info2" / Int32sl,
|
||||
"info3" / Int32sl,
|
||||
).build(
|
||||
info0 = self.info0,
|
||||
info1 = self.info1,
|
||||
info2 = self.info2,
|
||||
info3 = self.info3,
|
||||
)
|
||||
|
||||
self.head.length = HEADER_SIZE + len(resp_struct)
|
||||
return self.head.make() + resp_struct
|
||||
|
||||
class ADBCampaignClearRequest(ADBBaseRequest):
|
||||
def __init__(self, data: bytes) -> None:
|
||||
super().__init__(data)
|
||||
self.aime_id = struct.unpack_from("<i", data, 0x20)
|
||||
|
||||
class ADBCampaignClearResponse(ADBBaseResponse):
|
||||
def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x0E, length: int = 0x50, status: int = 1) -> None:
|
||||
super().__init__(code, length, status, game_id, store_id, keychip_id)
|
||||
self.campaign_clear_status = [CampaignClear(), CampaignClear(), CampaignClear()]
|
||||
|
||||
@classmethod
|
||||
def from_req(cls, req: ADBHeader) -> "ADBCampaignResponse":
|
||||
c = cls(req.game_id, req.store_id, req.keychip_id)
|
||||
c.head.protocol_ver = req.protocol_ver
|
||||
return c
|
||||
|
||||
def make(self) -> bytes:
|
||||
body = b""
|
||||
|
||||
for c in self.campaign_clear_status:
|
||||
body += c.make()
|
||||
|
||||
self.head.length = HEADER_SIZE + len(body)
|
||||
return self.head.make() + body
|
||||
@@ -0,0 +1,84 @@
|
||||
from construct import Struct, Int32sl, Padding, Int8ub, Int16sl
|
||||
from typing import Union
|
||||
from .base import *
|
||||
|
||||
class ADBFelicaLookupRequest(ADBBaseRequest):
|
||||
def __init__(self, data: bytes) -> None:
|
||||
super().__init__(data)
|
||||
idm, pmm = struct.unpack_from(">QQ", data, 0x20)
|
||||
self.idm = hex(idm)[2:].upper()
|
||||
self.pmm = hex(pmm)[2:].upper()
|
||||
|
||||
class ADBFelicaLookupResponse(ADBBaseResponse):
|
||||
def __init__(self, access_code: str = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x03, length: int = 0x30, status: int = 1) -> None:
|
||||
super().__init__(code, length, status, game_id, store_id, keychip_id)
|
||||
self.access_code = access_code if access_code is not None else "00000000000000000000"
|
||||
|
||||
@classmethod
|
||||
def from_req(cls, req: ADBHeader, access_code: str = None) -> "ADBFelicaLookupResponse":
|
||||
c = cls(access_code, req.game_id, req.store_id, req.keychip_id)
|
||||
c.head.protocol_ver = req.protocol_ver
|
||||
return c
|
||||
|
||||
def make(self) -> bytes:
|
||||
resp_struct = Struct(
|
||||
"felica_idx" / Int32ul,
|
||||
"access_code" / Int8ub[10],
|
||||
Padding(2)
|
||||
).build(dict(
|
||||
felica_idx = 0,
|
||||
access_code = bytes.fromhex(self.access_code)
|
||||
))
|
||||
|
||||
self.head.length = HEADER_SIZE + len(resp_struct)
|
||||
|
||||
return self.head.make() + resp_struct
|
||||
|
||||
class ADBFelicaLookup2Request(ADBBaseRequest):
|
||||
def __init__(self, data: bytes) -> None:
|
||||
super().__init__(data)
|
||||
self.random = struct.unpack_from("<16s", data, 0x20)[0]
|
||||
idm, pmm = struct.unpack_from(">QQ", data, 0x30)
|
||||
self.card_key_ver, self.write_ct, self.maca, company, fw_ver, self.dfc = struct.unpack_from("<16s16sQccH", data, 0x40)
|
||||
self.idm = hex(idm)[2:].upper()
|
||||
self.pmm = hex(pmm)[2:].upper()
|
||||
self.company = CompanyCodes(int.from_bytes(company, 'little'))
|
||||
self.fw_ver = ReaderFwVer.from_byte(fw_ver)
|
||||
|
||||
class ADBFelicaLookup2Response(ADBBaseResponse):
|
||||
def __init__(self, user_id: Union[int, None] = None, access_code: Union[str, None] = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x12, length: int = 0x130, status: int = 1) -> None:
|
||||
super().__init__(code, length, status, game_id, store_id, keychip_id)
|
||||
self.user_id = user_id if user_id is not None else -1
|
||||
self.access_code = access_code if access_code is not None else "00000000000000000000"
|
||||
self.company = CompanyCodes.SEGA
|
||||
self.portal_status = PortalRegStatus.NO_REG
|
||||
|
||||
@classmethod
|
||||
def from_req(cls, req: ADBHeader, user_id: Union[int, None] = None, access_code: Union[str, None] = None) -> "ADBFelicaLookup2Response":
|
||||
c = cls(user_id, access_code, req.game_id, req.store_id, req.keychip_id)
|
||||
c.head.protocol_ver = req.protocol_ver
|
||||
return c
|
||||
|
||||
def make(self) -> bytes:
|
||||
resp_struct = Struct(
|
||||
"user_id" / Int32sl,
|
||||
"relation1" / Int32sl,
|
||||
"relation2" / Int32sl,
|
||||
"access_code" / Int8ub[10],
|
||||
"portal_status" / Int8ub,
|
||||
"company_code" / Int8ub,
|
||||
Padding(8),
|
||||
"auth_key" / Int8ub[256],
|
||||
).build(dict(
|
||||
user_id = self.user_id,
|
||||
relation1 = -1, # Unsupported
|
||||
relation2 = -1, # Unsupported
|
||||
access_code = bytes.fromhex(self.access_code),
|
||||
portal_status = self.portal_status.value,
|
||||
company_code = self.company.value,
|
||||
auth_key = [0] * 256 # Unsupported
|
||||
))
|
||||
|
||||
self.head.length = HEADER_SIZE + len(resp_struct)
|
||||
|
||||
return self.head.make() + resp_struct
|
||||
@@ -0,0 +1,56 @@
|
||||
from construct import Struct, Padding, Int8sl
|
||||
from typing import Final, List
|
||||
|
||||
from .base import *
|
||||
NUM_LOGS: Final[int] = 20
|
||||
NUM_LEN_LOG_EX: Final[int] = 48
|
||||
|
||||
class AmLogEx:
|
||||
def __init__(self, data: bytes) -> None:
|
||||
self.aime_id, status, self.user_id, self.credit_ct, self.bet_ct, self.won_ct, self.local_time, \
|
||||
self.tseq, self.place_id = struct.unpack("<IIQiii4xQiI", data)
|
||||
self.status = LogStatus(status)
|
||||
|
||||
class ADBStatusLogRequest(ADBBaseRequest):
|
||||
def __init__(self, data: bytes) -> None:
|
||||
super().__init__(data)
|
||||
self.aime_id, status = struct.unpack_from("<II", data, 0x20)
|
||||
self.status = LogStatus(status)
|
||||
|
||||
class ADBLogRequest(ADBBaseRequest):
|
||||
def __init__(self, data: bytes) -> None:
|
||||
super().__init__(data)
|
||||
self.aime_id, status, self.user_id, self.credit_ct, self.bet_ct, self.won_ct = struct.unpack_from("<IIQiii", data, 0x20)
|
||||
self.status = LogStatus(status)
|
||||
|
||||
class ADBLogExRequest(ADBBaseRequest):
|
||||
def __init__(self, data: bytes) -> None:
|
||||
super().__init__(data)
|
||||
self.logs: List[AmLogEx] = []
|
||||
|
||||
for x in range(NUM_LOGS):
|
||||
self.logs.append(AmLogEx(data[0x20 + (NUM_LEN_LOG_EX * x): 0x50 + (NUM_LEN_LOG_EX * x)]))
|
||||
|
||||
self.num_logs = struct.unpack_from("<I", data, 0x03E0)[0]
|
||||
|
||||
class ADBLogExResponse(ADBBaseResponse):
|
||||
def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", protocol_ver: int = 12423, code: int = 20, length: int = 64, status: int = 1) -> None:
|
||||
super().__init__(code, length, status, game_id, store_id, keychip_id, protocol_ver)
|
||||
|
||||
@classmethod
|
||||
def from_req(cls, req: ADBHeader) -> "ADBLogExResponse":
|
||||
c = cls(req.game_id, req.store_id, req.keychip_id, req.protocol_ver)
|
||||
return c
|
||||
|
||||
def make(self) -> bytes:
|
||||
resp_struct = Struct(
|
||||
"log_result" / Int8sl[NUM_LOGS],
|
||||
Padding(12)
|
||||
)
|
||||
|
||||
body = resp_struct.build(dict(
|
||||
log_result = [1] * NUM_LOGS
|
||||
))
|
||||
|
||||
self.head.length = HEADER_SIZE + len(body)
|
||||
return self.head.make() + body
|
||||
@@ -0,0 +1,81 @@
|
||||
from construct import Struct, Int32sl, Padding, Int8sl
|
||||
from typing import Union
|
||||
|
||||
from .base import *
|
||||
|
||||
class ADBLookupException(Exception):
|
||||
pass
|
||||
|
||||
class ADBLookupRequest(ADBBaseRequest):
|
||||
def __init__(self, data: bytes) -> None:
|
||||
super().__init__(data)
|
||||
self.access_code = data[0x20:0x2A].hex()
|
||||
company_code, fw_version, self.serial_number = struct.unpack_from("<bbI", data, 0x2A)
|
||||
|
||||
try:
|
||||
self.company_code = CompanyCodes(company_code)
|
||||
except ValueError as e:
|
||||
raise ADBLookupException(f"Invalid company code - {e}")
|
||||
|
||||
self.fw_version = ReaderFwVer.from_byte(fw_version)
|
||||
|
||||
class ADBLookupResponse(ADBBaseResponse):
|
||||
def __init__(self, user_id: Union[int, None], game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x06, length: int = 0x30, status: int = 1) -> None:
|
||||
super().__init__(code, length, status, game_id, store_id, keychip_id)
|
||||
self.user_id = user_id if user_id is not None else -1
|
||||
self.portal_reg = PortalRegStatus.NO_REG
|
||||
|
||||
@classmethod
|
||||
def from_req(cls, req: ADBHeader, user_id: Union[int, None]) -> "ADBLookupResponse":
|
||||
c = cls(user_id, req.game_id, req.store_id, req.keychip_id)
|
||||
c.head.protocol_ver = req.protocol_ver
|
||||
return c
|
||||
|
||||
def make(self):
|
||||
resp_struct = Struct(
|
||||
"user_id" / Int32sl,
|
||||
"portal_reg" / Int8sl,
|
||||
Padding(11)
|
||||
)
|
||||
|
||||
body = resp_struct.build(dict(
|
||||
user_id = self.user_id,
|
||||
portal_reg = self.portal_reg.value
|
||||
))
|
||||
|
||||
self.head.length = HEADER_SIZE + len(body)
|
||||
return self.head.make() + body
|
||||
|
||||
class ADBLookupExResponse(ADBBaseResponse):
|
||||
def __init__(self, user_id: Union[int, None], game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888",
|
||||
code: int = 0x10, length: int = 0x130, status: int = 1) -> None:
|
||||
super().__init__(code, length, status, game_id, store_id, keychip_id)
|
||||
self.user_id = user_id if user_id is not None else -1
|
||||
self.portal_reg = PortalRegStatus.NO_REG
|
||||
|
||||
@classmethod
|
||||
def from_req(cls, req: ADBHeader, user_id: Union[int, None]) -> "ADBLookupExResponse":
|
||||
c = cls(user_id, req.game_id, req.store_id, req.keychip_id)
|
||||
c.head.protocol_ver = req.protocol_ver
|
||||
return c
|
||||
|
||||
def make(self):
|
||||
resp_struct = Struct(
|
||||
"user_id" / Int32sl,
|
||||
"portal_reg" / Int8sl,
|
||||
Padding(3),
|
||||
"auth_key" / Int8sl[256],
|
||||
"relation1" / Int32sl,
|
||||
"relation2" / Int32sl,
|
||||
)
|
||||
|
||||
body = resp_struct.build(dict(
|
||||
user_id = self.user_id,
|
||||
portal_reg = self.portal_reg.value,
|
||||
auth_key = [0] * 256,
|
||||
relation1 = -1,
|
||||
relation2 = -1
|
||||
))
|
||||
|
||||
self.head.length = HEADER_SIZE + len(body)
|
||||
return self.head.make() + body
|
||||
+192
-160
@@ -2,27 +2,17 @@ from twisted.internet.protocol import Factory, Protocol
|
||||
import logging, coloredlogs
|
||||
from Crypto.Cipher import AES
|
||||
import struct
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Tuple, Callable, Union
|
||||
from typing_extensions import Final
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.data import Data
|
||||
from .adb_handlers import *
|
||||
|
||||
|
||||
class AimedbProtocol(Protocol):
|
||||
AIMEDB_RESPONSE_CODES = {
|
||||
"felica_lookup": 0x03,
|
||||
"lookup": 0x06,
|
||||
"log": 0x0A,
|
||||
"campaign": 0x0C,
|
||||
"touch": 0x0E,
|
||||
"lookup2": 0x10,
|
||||
"felica_lookup2": 0x12,
|
||||
"log2": 0x14,
|
||||
"hello": 0x65,
|
||||
}
|
||||
|
||||
request_list: Dict[int, Any] = {}
|
||||
request_list: Dict[int, Tuple[Callable[[bytes, int], Union[ADBBaseResponse, bytes]], int, str]] = {}
|
||||
|
||||
def __init__(self, core_cfg: CoreConfig) -> None:
|
||||
self.logger = logging.getLogger("aimedb")
|
||||
@@ -32,16 +22,27 @@ class AimedbProtocol(Protocol):
|
||||
self.logger.error("!!!KEY NOT SET!!!")
|
||||
exit(1)
|
||||
|
||||
self.request_list[0x01] = self.handle_felica_lookup
|
||||
self.request_list[0x04] = self.handle_lookup
|
||||
self.request_list[0x05] = self.handle_register
|
||||
self.request_list[0x09] = self.handle_log
|
||||
self.request_list[0x0B] = self.handle_campaign
|
||||
self.request_list[0x0D] = self.handle_touch
|
||||
self.request_list[0x0F] = self.handle_lookup2
|
||||
self.request_list[0x11] = self.handle_felica_lookup2
|
||||
self.request_list[0x13] = self.handle_log2
|
||||
self.request_list[0x64] = self.handle_hello
|
||||
self.register_handler(0x01, 0x03, self.handle_felica_lookup, 'felica_lookup')
|
||||
self.register_handler(0x02, 0x03, self.handle_felica_register, 'felica_register')
|
||||
|
||||
self.register_handler(0x04, 0x06, self.handle_lookup, 'lookup')
|
||||
self.register_handler(0x05, 0x06, self.handle_register, 'register')
|
||||
|
||||
self.register_handler(0x07, 0x08, self.handle_status_log, 'status_log')
|
||||
self.register_handler(0x09, 0x0A, self.handle_log, 'aime_log')
|
||||
|
||||
self.register_handler(0x0B, 0x0C, self.handle_campaign, 'campaign')
|
||||
self.register_handler(0x0D, 0x0E, self.handle_campaign_clear, 'campaign_clear')
|
||||
|
||||
self.register_handler(0x0F, 0x10, self.handle_lookup_ex, 'lookup_ex')
|
||||
self.register_handler(0x11, 0x12, self.handle_felica_lookup_ex, 'felica_lookup_ex')
|
||||
|
||||
self.register_handler(0x13, 0x14, self.handle_log_ex, 'aime_log_ex')
|
||||
self.register_handler(0x64, 0x65, self.handle_hello, 'hello')
|
||||
self.register_handler(0x66, 0, self.handle_goodbye, 'goodbye')
|
||||
|
||||
def register_handler(self, cmd: int, resp:int, handler: Callable[[bytes, int], Union[ADBBaseResponse, bytes]], name: str) -> None:
|
||||
self.request_list[cmd] = (handler, resp, name)
|
||||
|
||||
def append_padding(self, data: bytes):
|
||||
"""Appends 0s to the end of the data until it's at the correct size"""
|
||||
@@ -63,202 +64,233 @@ class AimedbProtocol(Protocol):
|
||||
|
||||
try:
|
||||
decrypted = cipher.decrypt(data)
|
||||
except:
|
||||
self.logger.error(f"Failed to decrypt {data.hex()}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to decrypt {data.hex()} because {e}")
|
||||
return None
|
||||
|
||||
self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}")
|
||||
|
||||
if not decrypted[1] == 0xA1 and not decrypted[0] == 0x3E:
|
||||
self.logger.error(f"Bad magic")
|
||||
return None
|
||||
|
||||
req_code = decrypted[4]
|
||||
|
||||
if req_code == 0x66:
|
||||
self.logger.info(f"goodbye from {self.transport.getPeer().host}")
|
||||
self.transport.loseConnection()
|
||||
return
|
||||
|
||||
try:
|
||||
resp = self.request_list[req_code](decrypted)
|
||||
encrypted = cipher.encrypt(resp)
|
||||
self.logger.debug(f"Response {resp.hex()}")
|
||||
head = ADBHeader.from_data(decrypted)
|
||||
|
||||
except ADBHeaderException as e:
|
||||
self.logger.error(f"Error parsing ADB header: {e}")
|
||||
try:
|
||||
encrypted = cipher.encrypt(ADBBaseResponse().make())
|
||||
self.transport.write(encrypted)
|
||||
|
||||
except KeyError:
|
||||
self.logger.error(f"Unknown command code {hex(req_code)}")
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to encrypt default response because {e}")
|
||||
|
||||
except ValueError as e:
|
||||
self.logger.error(f"Failed to encrypt {resp.hex()} because {e}")
|
||||
return None
|
||||
return
|
||||
|
||||
def handle_campaign(self, data: bytes) -> bytes:
|
||||
self.logger.info(f"campaign from {self.transport.getPeer().host}")
|
||||
ret = struct.pack(
|
||||
"<5H",
|
||||
0xA13E,
|
||||
0x3087,
|
||||
self.AIMEDB_RESPONSE_CODES["campaign"],
|
||||
0x0200,
|
||||
0x0001,
|
||||
if head.keychip_id == "ABCD1234567" or head.store_id == 0xfff0:
|
||||
self.logger.warning(f"Request from uninitialized AMLib: {vars(head)}")
|
||||
|
||||
handler, resp_code, name = self.request_list.get(head.cmd, (self.handle_default, None, 'default'))
|
||||
|
||||
if resp_code is None:
|
||||
self.logger.warning(f"No handler for cmd {hex(head.cmd)}")
|
||||
|
||||
elif resp_code > 0:
|
||||
self.logger.info(f"{name} from {head.keychip_id} ({head.game_id}) @ {self.transport.getPeer().host}")
|
||||
|
||||
resp = handler(decrypted, resp_code)
|
||||
|
||||
if type(resp) == ADBBaseResponse or issubclass(type(resp), ADBBaseResponse):
|
||||
resp_bytes = resp.make()
|
||||
if len(resp_bytes) != resp.head.length:
|
||||
resp_bytes = self.append_padding(resp_bytes)
|
||||
|
||||
elif type(resp) == bytes:
|
||||
resp_bytes = resp
|
||||
|
||||
elif resp is None: # Nothing to send, probably a goodbye
|
||||
return
|
||||
|
||||
else:
|
||||
raise TypeError(f"Unsupported type returned by ADB handler for {name}: {type(resp)}")
|
||||
|
||||
try:
|
||||
encrypted = cipher.encrypt(resp_bytes)
|
||||
self.logger.debug(f"Response {resp_bytes.hex()}")
|
||||
self.transport.write(encrypted)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to encrypt {resp_bytes.hex()} because {e}")
|
||||
|
||||
def handle_default(self, data: bytes, resp_code: int, length: int = 0x20) -> ADBBaseResponse:
|
||||
req = ADBHeader.from_data(data)
|
||||
return ADBBaseResponse(resp_code, length, 1, req.game_id, req.store_id, req.keychip_id, req.protocol_ver)
|
||||
|
||||
def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
return self.handle_default(data, resp_code)
|
||||
|
||||
def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
h = ADBHeader.from_data(data)
|
||||
if h.protocol_ver >= 0x3030:
|
||||
req = h
|
||||
resp = ADBCampaignResponse.from_req(req)
|
||||
|
||||
else:
|
||||
req = ADBOldCampaignRequest(data)
|
||||
|
||||
self.logger.info(f"Legacy campaign request for campaign {req.campaign_id} (protocol version {hex(h.protocol_ver)})")
|
||||
resp = ADBOldCampaignResponse.from_req(req.head)
|
||||
|
||||
# We don't currently support campaigns
|
||||
return resp
|
||||
|
||||
def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
req = ADBLookupRequest(data)
|
||||
user_id = self.data.card.get_user_id_from_card(req.access_code)
|
||||
|
||||
ret = ADBLookupResponse.from_req(req.head, user_id)
|
||||
|
||||
self.logger.info(
|
||||
f"access_code {req.access_code} -> user_id {ret.user_id}"
|
||||
)
|
||||
return self.append_padding(ret)
|
||||
return ret
|
||||
|
||||
def handle_hello(self, data: bytes) -> bytes:
|
||||
self.logger.info(f"hello from {self.transport.getPeer().host}")
|
||||
ret = struct.pack(
|
||||
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["hello"], 0x0020, 0x0001
|
||||
def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
req = ADBLookupRequest(data)
|
||||
user_id = self.data.card.get_user_id_from_card(req.access_code)
|
||||
|
||||
ret = ADBLookupExResponse.from_req(req.head, user_id)
|
||||
|
||||
self.logger.info(
|
||||
f"access_code {req.access_code} -> user_id {ret.user_id}"
|
||||
)
|
||||
return self.append_padding(ret)
|
||||
return ret
|
||||
|
||||
def handle_lookup(self, data: bytes) -> bytes:
|
||||
luid = data[0x20:0x2A].hex()
|
||||
user_id = self.data.card.get_user_id_from_card(access_code=luid)
|
||||
def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes:
|
||||
"""
|
||||
On official, I think a card has to be registered for this to actually work, but
|
||||
I'm making the executive decision to not implement that and just kick back our
|
||||
faux generated access code. The real felica IDm -> access code conversion is done
|
||||
on the ADB server, which we do not and will not ever have access to. Because we can
|
||||
assure that all IDms will be unique, this basic 0-padded hex -> int conversion will
|
||||
be fine.
|
||||
"""
|
||||
req = ADBFelicaLookupRequest(data)
|
||||
ac = self.data.card.to_access_code(req.idm)
|
||||
self.logger.info(
|
||||
f"idm {req.idm} ipm {req.pmm} -> access_code {ac}"
|
||||
)
|
||||
return ADBFelicaLookupResponse.from_req(req.head, ac)
|
||||
|
||||
def handle_felica_register(self, data: bytes, resp_code: int) -> bytes:
|
||||
"""
|
||||
I've never seen this used.
|
||||
"""
|
||||
req = ADBFelicaLookupRequest(data)
|
||||
ac = self.data.card.to_access_code(req.idm)
|
||||
|
||||
if self.config.server.allow_user_registration:
|
||||
user_id = self.data.user.create_user()
|
||||
|
||||
if user_id is None:
|
||||
self.logger.error("Failed to register user!")
|
||||
user_id = -1
|
||||
|
||||
else:
|
||||
card_id = self.data.card.create_card(user_id, ac)
|
||||
|
||||
if card_id is None:
|
||||
self.logger.error("Failed to register card!")
|
||||
user_id = -1
|
||||
|
||||
self.logger.info(
|
||||
f"lookup from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}"
|
||||
f"Register access code {ac} (IDm: {req.idm} PMm: {req.pmm}) -> user_id {user_id}"
|
||||
)
|
||||
|
||||
ret = struct.pack(
|
||||
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["lookup"], 0x0130, 0x0001
|
||||
)
|
||||
ret += bytes(0x20 - len(ret))
|
||||
|
||||
if user_id is None:
|
||||
ret += struct.pack("<iH", -1, 0)
|
||||
else:
|
||||
ret += struct.pack("<l", user_id)
|
||||
return self.append_padding(ret)
|
||||
|
||||
def handle_lookup2(self, data: bytes) -> bytes:
|
||||
self.logger.info(f"lookup2")
|
||||
|
||||
ret = bytearray(self.handle_lookup(data))
|
||||
ret[4] = self.AIMEDB_RESPONSE_CODES["lookup2"]
|
||||
|
||||
return bytes(ret)
|
||||
|
||||
def handle_felica_lookup(self, data: bytes) -> bytes:
|
||||
idm = data[0x20:0x28].hex()
|
||||
pmm = data[0x28:0x30].hex()
|
||||
access_code = self.data.card.to_access_code(idm)
|
||||
self.logger.info(
|
||||
f"felica_lookup from {self.transport.getPeer().host}: idm {idm} pmm {pmm} -> access_code {access_code}"
|
||||
f"Registration blocked!: access code {ac} (IDm: {req.idm} PMm: {req.pmm})"
|
||||
)
|
||||
|
||||
ret = struct.pack(
|
||||
"<5H",
|
||||
0xA13E,
|
||||
0x3087,
|
||||
self.AIMEDB_RESPONSE_CODES["felica_lookup"],
|
||||
0x0030,
|
||||
0x0001,
|
||||
)
|
||||
ret += bytes(26)
|
||||
ret += bytes.fromhex(access_code)
|
||||
return ADBFelicaLookupResponse.from_req(req.head, ac)
|
||||
|
||||
return self.append_padding(ret)
|
||||
|
||||
def handle_felica_lookup2(self, data: bytes) -> bytes:
|
||||
idm = data[0x30:0x38].hex()
|
||||
pmm = data[0x38:0x40].hex()
|
||||
access_code = self.data.card.to_access_code(idm)
|
||||
def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes:
|
||||
req = ADBFelicaLookup2Request(data)
|
||||
access_code = self.data.card.to_access_code(req.idm)
|
||||
user_id = self.data.card.get_user_id_from_card(access_code=access_code)
|
||||
|
||||
if user_id is None:
|
||||
user_id = -1
|
||||
|
||||
self.logger.info(
|
||||
f"felica_lookup2 from {self.transport.getPeer().host}: idm {idm} ipm {pmm} -> access_code {access_code} user_id {user_id}"
|
||||
f"idm {req.idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}"
|
||||
)
|
||||
|
||||
ret = struct.pack(
|
||||
"<5H",
|
||||
0xA13E,
|
||||
0x3087,
|
||||
self.AIMEDB_RESPONSE_CODES["felica_lookup2"],
|
||||
0x0140,
|
||||
0x0001,
|
||||
)
|
||||
ret += bytes(22)
|
||||
ret += struct.pack("<lq", user_id, -1) # first -1 is ext_id, 3rd is access code
|
||||
ret += bytes.fromhex(access_code)
|
||||
ret += struct.pack("<l", 1)
|
||||
return ADBFelicaLookup2Response.from_req(req.head, user_id, access_code)
|
||||
|
||||
return self.append_padding(ret)
|
||||
def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
req = ADBCampaignClearRequest(data)
|
||||
|
||||
def handle_touch(self, data: bytes) -> bytes:
|
||||
self.logger.info(f"touch from {self.transport.getPeer().host}")
|
||||
ret = struct.pack(
|
||||
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["touch"], 0x0050, 0x0001
|
||||
)
|
||||
ret += bytes(5)
|
||||
ret += struct.pack("<3H", 0x6F, 0, 1)
|
||||
resp = ADBCampaignClearResponse.from_req(req.head)
|
||||
|
||||
return self.append_padding(ret)
|
||||
# We don't support campaign stuff
|
||||
return resp
|
||||
|
||||
def handle_register(self, data: bytes, resp_code: int) -> bytes:
|
||||
req = ADBLookupRequest(data)
|
||||
user_id = -1
|
||||
|
||||
def handle_register(self, data: bytes) -> bytes:
|
||||
luid = data[0x20:0x2A].hex()
|
||||
if self.config.server.allow_user_registration:
|
||||
user_id = self.data.user.create_user()
|
||||
|
||||
if user_id is None:
|
||||
user_id = -1
|
||||
self.logger.error("Failed to register user!")
|
||||
user_id = -1
|
||||
|
||||
else:
|
||||
card_id = self.data.card.create_card(user_id, luid)
|
||||
card_id = self.data.card.create_card(user_id, req.access_code)
|
||||
|
||||
if card_id is None:
|
||||
user_id = -1
|
||||
self.logger.error("Failed to register card!")
|
||||
user_id = -1
|
||||
|
||||
self.logger.info(
|
||||
f"register from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}"
|
||||
f"Register access code {req.access_code} -> user_id {user_id}"
|
||||
)
|
||||
|
||||
else:
|
||||
self.logger.info(
|
||||
f"register from {self.transport.getPeer().host} blocked!: luid {luid}"
|
||||
f"Registration blocked!: access code {req.access_code}"
|
||||
)
|
||||
user_id = -1
|
||||
|
||||
ret = struct.pack(
|
||||
"<5H",
|
||||
0xA13E,
|
||||
0x3087,
|
||||
self.AIMEDB_RESPONSE_CODES["lookup"],
|
||||
0x0030,
|
||||
0x0001 if user_id > -1 else 0,
|
||||
)
|
||||
ret += bytes(0x20 - len(ret))
|
||||
ret += struct.pack("<l", user_id)
|
||||
resp = ADBLookupResponse.from_req(req.head, user_id)
|
||||
if resp.user_id <= 0:
|
||||
resp.head.status = ADBStatus.BAN_SYS # Closest we can get to a "You cannot register"
|
||||
|
||||
return self.append_padding(ret)
|
||||
return resp
|
||||
|
||||
def handle_log(self, data: bytes) -> bytes:
|
||||
# TODO: Save aimedb logs
|
||||
self.logger.info(f"log from {self.transport.getPeer().host}")
|
||||
ret = struct.pack(
|
||||
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["log"], 0x0020, 0x0001
|
||||
)
|
||||
return self.append_padding(ret)
|
||||
# TODO: Save these in some capacity, as deemed relevant
|
||||
def handle_status_log(self, data: bytes, resp_code: int) -> bytes:
|
||||
req = ADBStatusLogRequest(data)
|
||||
self.logger.info(f"User {req.aime_id} logged {req.status.name} event")
|
||||
return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver)
|
||||
|
||||
def handle_log2(self, data: bytes) -> bytes:
|
||||
self.logger.info(f"log2 from {self.transport.getPeer().host}")
|
||||
ret = struct.pack(
|
||||
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["log2"], 0x0040, 0x0001
|
||||
)
|
||||
ret += bytes(22)
|
||||
ret += struct.pack("H", 1)
|
||||
def handle_log(self, data: bytes, resp_code: int) -> bytes:
|
||||
req = ADBLogRequest(data)
|
||||
self.logger.info(f"User {req.aime_id} logged {req.status.name} event, credit_ct: {req.credit_ct} bet_ct: {req.bet_ct} won_ct: {req.won_ct}")
|
||||
return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver)
|
||||
|
||||
return self.append_padding(ret)
|
||||
def handle_log_ex(self, data: bytes, resp_code: int) -> bytes:
|
||||
req = ADBLogExRequest(data)
|
||||
strs = []
|
||||
self.logger.info(f"Recieved {req.num_logs} or {len(req.logs)} logs")
|
||||
|
||||
for x in range(req.num_logs):
|
||||
self.logger.debug(f"User {req.logs[x].aime_id} logged {req.logs[x].status.name} event, credit_ct: {req.logs[x].credit_ct} bet_ct: {req.logs[x].bet_ct} won_ct: {req.logs[x].won_ct}")
|
||||
return ADBLogExResponse.from_req(req.head)
|
||||
|
||||
def handle_goodbye(self, data: bytes, resp_code: int) -> None:
|
||||
self.logger.info(f"goodbye from {self.transport.getPeer().host}")
|
||||
self.transport.loseConnection()
|
||||
return
|
||||
|
||||
class AimedbFactory(Factory):
|
||||
protocol = AimedbProtocol
|
||||
|
||||
+522
-165
@@ -1,4 +1,4 @@
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from typing import Dict, List, Any, Optional, Tuple, Union, Final
|
||||
import logging, coloredlogs
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from twisted.web.http import Request
|
||||
@@ -6,16 +6,32 @@ from datetime import datetime
|
||||
import pytz
|
||||
import base64
|
||||
import zlib
|
||||
import json
|
||||
from enum import Enum
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Hash import SHA
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from time import strptime
|
||||
from os import path
|
||||
import urllib.parse
|
||||
import math
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.utils import Utils
|
||||
from core.data import Data
|
||||
from core.const import *
|
||||
|
||||
BILLING_DT_FORMAT: Final[str] = "%Y%m%d%H%M%S"
|
||||
|
||||
class DLIMG_TYPE(Enum):
|
||||
app = 0
|
||||
opt = 1
|
||||
|
||||
class ALLNET_STAT(Enum):
|
||||
ok = 0
|
||||
bad_game = -1
|
||||
bad_machine = -2
|
||||
bad_shop = -3
|
||||
|
||||
class AllnetServlet:
|
||||
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
||||
@@ -55,7 +71,7 @@ class AllnetServlet:
|
||||
self.logger.error("No games detected!")
|
||||
|
||||
for _, mod in plugins.items():
|
||||
if hasattr(mod.index, "get_allnet_info"):
|
||||
if hasattr(mod, "index") and hasattr(mod.index, "get_allnet_info"):
|
||||
for code in mod.game_codes:
|
||||
enabled, uri, host = mod.index.get_allnet_info(
|
||||
code, self.config, self.config_folder
|
||||
@@ -70,15 +86,23 @@ class AllnetServlet:
|
||||
|
||||
def handle_poweron(self, request: Request, _: Dict):
|
||||
request_ip = Utils.get_ip_addr(request)
|
||||
pragma_header = request.getHeader('Pragma')
|
||||
is_dfi = pragma_header is not None and pragma_header == "DFI"
|
||||
|
||||
try:
|
||||
req_dict = self.allnet_req_to_dict(request.content.getvalue())
|
||||
if is_dfi:
|
||||
req_urlencode = self.from_dfi(request.content.getvalue())
|
||||
else:
|
||||
req_urlencode = request.content.getvalue().decode()
|
||||
|
||||
req_dict = self.allnet_req_to_dict(req_urlencode)
|
||||
if req_dict is None:
|
||||
raise AllnetRequestException()
|
||||
|
||||
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:
|
||||
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)}"
|
||||
)
|
||||
@@ -88,30 +112,14 @@ class AllnetServlet:
|
||||
self.logger.error(e)
|
||||
return b""
|
||||
|
||||
if req.format_ver == "3":
|
||||
if req.format_ver == 3:
|
||||
resp = AllnetPowerOnResponse3(req.token)
|
||||
else:
|
||||
elif req.format_ver == 2:
|
||||
resp = AllnetPowerOnResponse2()
|
||||
else:
|
||||
resp = AllnetPowerOnResponse()
|
||||
|
||||
self.logger.debug(f"Allnet request: {vars(req)}")
|
||||
if req.game_id not in self.uri_registry:
|
||||
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)
|
||||
|
||||
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)])
|
||||
|
||||
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:
|
||||
@@ -119,13 +127,38 @@ class AllnetServlet:
|
||||
self.data.base.log_event(
|
||||
"allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg
|
||||
)
|
||||
self.logger.warn(msg)
|
||||
self.logger.warning(msg)
|
||||
|
||||
resp.stat = 0
|
||||
return self.dict_to_http_form_string([vars(resp)])
|
||||
resp.stat = ALLNET_STAT.bad_machine.value
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
|
||||
|
||||
if machine is not None:
|
||||
arcade = 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:
|
||||
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})."
|
||||
self.data.base.log_event(
|
||||
"allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg
|
||||
)
|
||||
self.logger.warning(msg)
|
||||
|
||||
resp.stat = ALLNET_STAT.bad_shop.value
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
|
||||
|
||||
elif not arcade["ip"] or arcade["ip"] is None and self.config.server.strict_ip_checking:
|
||||
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)."
|
||||
self.data.base.log_event(
|
||||
"allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg
|
||||
)
|
||||
self.logger.warning(msg)
|
||||
|
||||
resp.stat = ALLNET_STAT.bad_shop.value
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
country = (
|
||||
arcade["country"] if machine["country"] is None else machine["country"]
|
||||
)
|
||||
@@ -157,6 +190,33 @@ class AllnetServlet:
|
||||
arcade["timezone"] if arcade["timezone"] is not None else "+0900"
|
||||
)
|
||||
|
||||
if req.game_id not in self.uri_registry:
|
||||
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.warning(msg)
|
||||
|
||||
resp.stat = ALLNET_STAT.bad_game.value
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
|
||||
|
||||
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}"
|
||||
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict))
|
||||
|
||||
self.logger.debug(f"Allnet response: {resp_str}")
|
||||
return (resp_str + "\n").encode("utf-8")
|
||||
|
||||
resp.uri, resp.host = self.uri_registry[req.game_id]
|
||||
|
||||
int_ver = req.ver.replace(".", "")
|
||||
resp.uri = resp.uri.replace("$v", int_ver)
|
||||
resp.host = resp.host.replace("$v", int_ver)
|
||||
@@ -164,14 +224,30 @@ class AllnetServlet:
|
||||
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)
|
||||
self.logger.debug(f"Allnet response: {vars(resp)}")
|
||||
|
||||
return self.dict_to_http_form_string([vars(resp)]).encode("utf-8")
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict))
|
||||
self.logger.debug(f"Allnet response: {resp_dict}")
|
||||
resp_str += "\n"
|
||||
|
||||
"""if is_dfi:
|
||||
request.responseHeaders.addRawHeader('Pragma', 'DFI')
|
||||
return self.to_dfi(resp_str)"""
|
||||
|
||||
return resp_str.encode("utf-8")
|
||||
|
||||
def handle_dlorder(self, request: Request, _: Dict):
|
||||
request_ip = Utils.get_ip_addr(request)
|
||||
pragma_header = request.getHeader('Pragma')
|
||||
is_dfi = pragma_header is not None and pragma_header == "DFI"
|
||||
|
||||
try:
|
||||
req_dict = self.allnet_req_to_dict(request.content.getvalue())
|
||||
if is_dfi:
|
||||
req_urlencode = self.from_dfi(request.content.getvalue())
|
||||
else:
|
||||
req_urlencode = request.content.getvalue().decode()
|
||||
|
||||
req_dict = self.allnet_req_to_dict(req_urlencode)
|
||||
if req_dict is None:
|
||||
raise AllnetRequestException()
|
||||
|
||||
@@ -188,18 +264,115 @@ class AllnetServlet:
|
||||
self.logger.error(e)
|
||||
return b""
|
||||
|
||||
self.logger.info(f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}")
|
||||
resp = AllnetDownloadOrderResponse()
|
||||
self.logger.info(
|
||||
f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}"
|
||||
)
|
||||
resp = AllnetDownloadOrderResponse(serial=req.serial)
|
||||
|
||||
if not self.config.allnet.allow_online_updates:
|
||||
return self.dict_to_http_form_string([vars(resp)])
|
||||
if (
|
||||
not self.config.allnet.allow_online_updates
|
||||
or not self.config.allnet.update_cfg_folder
|
||||
):
|
||||
return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n"
|
||||
|
||||
else: # TODO: Actual dlorder response
|
||||
return self.dict_to_http_form_string([vars(resp)])
|
||||
else: # TODO: Keychip check
|
||||
if path.exists(
|
||||
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
|
||||
):
|
||||
resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
|
||||
|
||||
if path.exists(
|
||||
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
|
||||
):
|
||||
resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
|
||||
|
||||
self.logger.debug(f"Sending download uri {resp.uri}")
|
||||
self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}")
|
||||
|
||||
res_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n"
|
||||
"""if is_dfi:
|
||||
request.responseHeaders.addRawHeader('Pragma', 'DFI')
|
||||
return self.to_dfi(res_str)"""
|
||||
|
||||
return res_str
|
||||
|
||||
|
||||
def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes:
|
||||
if "file" not in match:
|
||||
return b""
|
||||
|
||||
req_file = match["file"].replace("%0A", "")
|
||||
|
||||
if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"):
|
||||
self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful")
|
||||
self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}")
|
||||
|
||||
return open(
|
||||
f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb"
|
||||
).read()
|
||||
|
||||
self.logger.info(f"DL INI File {req_file} not found")
|
||||
return b""
|
||||
|
||||
def handle_dlorder_report(self, request: Request, match: Dict) -> bytes:
|
||||
req_raw = request.content.getvalue()
|
||||
try:
|
||||
req_dict: Dict = json.loads(req_raw)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to parse DL Report: {e}")
|
||||
return "NG"
|
||||
|
||||
dl_data_type = DLIMG_TYPE.app
|
||||
dl_data = req_dict.get("appimage", {})
|
||||
|
||||
if dl_data is None or not dl_data:
|
||||
dl_data_type = DLIMG_TYPE.opt
|
||||
dl_data = req_dict.get("optimage", {})
|
||||
|
||||
if dl_data is None or not dl_data:
|
||||
self.logger.warning(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage")
|
||||
return "NG"
|
||||
|
||||
dl_report_data = DLReport(dl_data, dl_data_type)
|
||||
|
||||
if not dl_report_data.validate():
|
||||
self.logger.warning(f"Failed to parse DL Report: Invalid format - {dl_report_data.err}")
|
||||
return "NG"
|
||||
|
||||
return "OK"
|
||||
|
||||
def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes:
|
||||
req_data = request.content.getvalue()
|
||||
sections = req_data.decode("utf-8").split("\r\n")
|
||||
|
||||
req_dict = dict(urllib.parse.parse_qsl(sections[0]))
|
||||
|
||||
serial: Union[str, None] = req_dict.get("serial", None)
|
||||
num_files_to_dl: Union[str, None] = req_dict.get("nb_ftd", None)
|
||||
num_files_dld: Union[str, None] = req_dict.get("nb_dld", None)
|
||||
dl_state: Union[str, None] = req_dict.get("dld_st", None)
|
||||
ip = Utils.get_ip_addr(request)
|
||||
|
||||
if serial is None or num_files_dld is None or num_files_to_dl is None or dl_state is None:
|
||||
return "NG".encode()
|
||||
|
||||
self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})")
|
||||
return "OK".encode()
|
||||
|
||||
def handle_alive(self, request: Request, match: Dict) -> bytes:
|
||||
return "OK".encode()
|
||||
|
||||
def handle_billing_request(self, request: Request, _: Dict):
|
||||
req_dict = self.billing_req_to_dict(request.content.getvalue())
|
||||
req_raw = request.content.getvalue()
|
||||
|
||||
if request.getHeader('Content-Type') == "application/octet-stream":
|
||||
req_unzip = zlib.decompressobj(-zlib.MAX_WBITS).decompress(req_raw)
|
||||
else:
|
||||
req_unzip = req_raw
|
||||
|
||||
req_dict = self.billing_req_to_dict(req_unzip)
|
||||
request_ip = Utils.get_ip_addr(request)
|
||||
|
||||
if req_dict is None:
|
||||
self.logger.error(f"Failed to parse request {request.content.getvalue()}")
|
||||
return b""
|
||||
@@ -209,41 +382,60 @@ class AllnetServlet:
|
||||
rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read())
|
||||
signer = PKCS1_v1_5.new(rsa)
|
||||
digest = SHA.new()
|
||||
traces: List[TraceData] = []
|
||||
|
||||
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()
|
||||
try:
|
||||
for x in range(len(req_dict)):
|
||||
if not req_dict[x]:
|
||||
continue
|
||||
|
||||
machine = self.data.arcade.get_machine(kc_serial)
|
||||
if x == 0:
|
||||
req = BillingInfo(req_dict[x])
|
||||
continue
|
||||
|
||||
tmp = TraceData(req_dict[x])
|
||||
if tmp.trace_type == TraceDataType.CHARGE:
|
||||
tmp = TraceDataCharge(req_dict[x])
|
||||
elif tmp.trace_type == TraceDataType.EVENT:
|
||||
tmp = TraceDataEvent(req_dict[x])
|
||||
elif tmp.trace_type == TraceDataType.CREDIT:
|
||||
tmp = TraceDataCredit(req_dict[x])
|
||||
|
||||
traces.append(tmp)
|
||||
|
||||
kc_serial_bytes = req.keychipid.encode()
|
||||
|
||||
except KeyError as e:
|
||||
self.logger.error(f"Billing request failed to parse: {e}")
|
||||
return f"result=5&linelimit=&message=field is missing or formatting is incorrect\r\n".encode()
|
||||
|
||||
machine = self.data.arcade.get_machine(req.keychipid)
|
||||
if machine is None and not self.config.server.allow_unregistered_serials:
|
||||
msg = f"Unrecognised serial {kc_serial} attempted billing checkin from {request_ip} for game {kc_game}."
|
||||
msg = f"Unrecognised serial {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}."
|
||||
self.data.base.log_event(
|
||||
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg
|
||||
)
|
||||
self.logger.warn(msg)
|
||||
self.logger.warning(msg)
|
||||
|
||||
resp = BillingResponse("", "", "", "")
|
||||
resp.result = "1"
|
||||
return self.dict_to_http_form_string([vars(resp)])
|
||||
return f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n".encode()
|
||||
|
||||
msg = (
|
||||
f"Billing checkin from {request_ip}: game {kc_game} keychip {kc_serial} playcount "
|
||||
f"{kc_playcount} billing_type {kc_billigtype} nearfull {kc_nearfull} playlimit {kc_playlimit}"
|
||||
f"Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount "
|
||||
f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}"
|
||||
)
|
||||
self.logger.info(msg)
|
||||
self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, msg)
|
||||
if req.traceleft > 0:
|
||||
self.logger.warn(f"{req.traceleft} unsent tracelogs")
|
||||
kc_playlimit = req.playlimit
|
||||
kc_nearfull = req.nearfull
|
||||
|
||||
while kc_playcount > kc_playlimit:
|
||||
while req.playcnt > req.playlimit:
|
||||
kc_playlimit += 1024
|
||||
kc_nearfull += 1024
|
||||
|
||||
playlimit = kc_playlimit
|
||||
nearfull = kc_nearfull + (kc_billigtype * 0x00010000)
|
||||
nearfull = kc_nearfull + (req.billingtype.value * 0x00010000)
|
||||
|
||||
digest.update(playlimit.to_bytes(4, "little") + kc_serial_bytes)
|
||||
playlimit_sig = signer.sign(digest).hex()
|
||||
@@ -254,44 +446,33 @@ class AllnetServlet:
|
||||
|
||||
# TODO: playhistory
|
||||
|
||||
resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig)
|
||||
#resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig)
|
||||
resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig, req.requestno, req.protocolver)
|
||||
|
||||
resp_str = self.dict_to_http_form_string([vars(resp)], True)
|
||||
if resp_str is None:
|
||||
self.logger.error(f"Failed to parse response {vars(resp)}")
|
||||
resp_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n"
|
||||
|
||||
self.logger.debug(f"response {vars(resp)}")
|
||||
if req.traceleft > 0:
|
||||
self.logger.info(f"Requesting 20 more of {req.traceleft} unsent tracelogs")
|
||||
return f"result=6&waittime=0&linelimit=20\r\n".encode()
|
||||
|
||||
return resp_str.encode("utf-8")
|
||||
|
||||
def handle_naomitest(self, request: Request, _: Dict) -> bytes:
|
||||
self.logger.info(f"Ping from {Utils.get_ip_addr(request)}")
|
||||
return b"naomi ok"
|
||||
|
||||
def kvp_to_dict(self, kvp: List[str]) -> List[Dict[str, Any]]:
|
||||
ret: List[Dict[str, Any]] = []
|
||||
for x in kvp:
|
||||
items = x.split("&")
|
||||
tmp = {}
|
||||
|
||||
for item in items:
|
||||
kvp = item.split("=")
|
||||
if len(kvp) == 2:
|
||||
tmp[kvp[0]] = kvp[1]
|
||||
|
||||
ret.append(tmp)
|
||||
|
||||
return ret
|
||||
|
||||
def billing_req_to_dict(self, data: bytes):
|
||||
"""
|
||||
Parses an billing request string into a python dictionary
|
||||
"""
|
||||
try:
|
||||
decomp = zlib.decompressobj(-zlib.MAX_WBITS)
|
||||
unzipped = decomp.decompress(data)
|
||||
sections = unzipped.decode("ascii").split("\r\n")
|
||||
sections = data.decode("ascii").split("\r\n")
|
||||
|
||||
return self.kvp_to_dict(sections)
|
||||
ret = []
|
||||
for x in sections:
|
||||
ret.append(dict(urllib.parse.parse_qsl(x)))
|
||||
return ret
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"billing_req_to_dict: {e} while parsing {data}")
|
||||
@@ -302,111 +483,94 @@ class AllnetServlet:
|
||||
Parses an allnet request string into a python dictionary
|
||||
"""
|
||||
try:
|
||||
zipped = base64.b64decode(data)
|
||||
unzipped = zlib.decompress(zipped)
|
||||
sections = unzipped.decode("utf-8").split("\r\n")
|
||||
sections = data.split("\r\n")
|
||||
|
||||
return self.kvp_to_dict(sections)
|
||||
ret = []
|
||||
for x in sections:
|
||||
ret.append(dict(urllib.parse.parse_qsl(x)))
|
||||
return ret
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}")
|
||||
return None
|
||||
|
||||
def dict_to_http_form_string(
|
||||
self,
|
||||
data: List[Dict[str, Any]],
|
||||
crlf: bool = False,
|
||||
trailing_newline: bool = True,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Takes a python dictionary and parses it into an allnet response string
|
||||
"""
|
||||
try:
|
||||
urlencode = ""
|
||||
for item in data:
|
||||
for k, v in item.items():
|
||||
urlencode += f"{k}={v}&"
|
||||
def from_dfi(self, data: bytes) -> str:
|
||||
zipped = base64.b64decode(data)
|
||||
unzipped = zlib.decompress(zipped)
|
||||
return unzipped.decode("utf-8")
|
||||
|
||||
if crlf:
|
||||
urlencode = urlencode[:-1] + "\r\n"
|
||||
else:
|
||||
urlencode = urlencode[:-1] + "\n"
|
||||
|
||||
if not trailing_newline:
|
||||
if crlf:
|
||||
urlencode = urlencode[:-2]
|
||||
else:
|
||||
urlencode = urlencode[:-1]
|
||||
|
||||
return urlencode
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"dict_to_http_form_string: {e} while parsing {data}")
|
||||
return None
|
||||
def to_dfi(self, data: str) -> bytes:
|
||||
unzipped = data.encode('utf-8')
|
||||
zipped = zlib.compress(unzipped)
|
||||
return base64.b64encode(zipped)
|
||||
|
||||
|
||||
class AllnetPowerOnRequest:
|
||||
def __init__(self, req: Dict) -> None:
|
||||
if req is None:
|
||||
raise AllnetRequestException("Request processing failed")
|
||||
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"))
|
||||
self.format_ver = req.get("format_ver", "2")
|
||||
self.token = int(req.get("token", "0"))
|
||||
self.game_id: str = req.get("game_id", None)
|
||||
self.ver: str = req.get("ver", None)
|
||||
self.serial: str = req.get("serial", None)
|
||||
self.ip: str = req.get("ip", None)
|
||||
self.firm_ver: str = req.get("firm_ver", None)
|
||||
self.boot_ver: str = req.get("boot_ver", None)
|
||||
self.encode: str = req.get("encode", "EUC-JP")
|
||||
self.hops = int(req.get("hops", "-1"))
|
||||
self.format_ver = float(req.get("format_ver", "1.00"))
|
||||
self.token: str = req.get("token", "0")
|
||||
|
||||
|
||||
class AllnetPowerOnResponse3:
|
||||
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"
|
||||
self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ"
|
||||
)
|
||||
self.setting = "1"
|
||||
self.res_ver = "3"
|
||||
self.token = str(token)
|
||||
|
||||
|
||||
class AllnetPowerOnResponse2:
|
||||
class AllnetPowerOnResponse:
|
||||
def __init__(self) -> None:
|
||||
self.stat = 1
|
||||
self.uri = ""
|
||||
self.host = ""
|
||||
self.place_id = "123"
|
||||
self.name = "Test"
|
||||
self.nickname = "Test123"
|
||||
self.name = "ARTEMiS"
|
||||
self.nickname = "ARTEMiS"
|
||||
self.region0 = "1"
|
||||
self.region_name0 = "W"
|
||||
self.region_name1 = "X"
|
||||
self.region_name2 = "Y"
|
||||
self.region_name3 = "Z"
|
||||
self.country = "JPN"
|
||||
self.region_name1 = ""
|
||||
self.region_name2 = ""
|
||||
self.region_name3 = ""
|
||||
self.setting = "1"
|
||||
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"
|
||||
|
||||
class AllnetPowerOnResponse3(AllnetPowerOnResponse):
|
||||
def __init__(self, token) -> None:
|
||||
super().__init__()
|
||||
|
||||
# Added in v3
|
||||
self.country = "JPN"
|
||||
self.allnet_id = "123"
|
||||
self.client_timezone = "+0900"
|
||||
self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ"
|
||||
)
|
||||
self.res_ver = "3"
|
||||
self.token = token
|
||||
|
||||
# Removed in v3
|
||||
self.year = None
|
||||
self.month = None
|
||||
self.day = None
|
||||
self.hour = None
|
||||
self.minute = None
|
||||
self.second = None
|
||||
|
||||
|
||||
class AllnetPowerOnResponse2(AllnetPowerOnResponse):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
# Added in v2
|
||||
self.country = "JPN"
|
||||
self.timezone = "+09:00"
|
||||
self.res_class = "PowerOnResponseV2"
|
||||
|
||||
|
||||
@@ -419,11 +583,119 @@ class AllnetDownloadOrderRequest:
|
||||
|
||||
|
||||
class AllnetDownloadOrderResponse:
|
||||
def __init__(self, stat: int = 1, serial: str = "", uri: str = "null") -> None:
|
||||
def __init__(self, stat: int = 1, serial: str = "", uri: str = "") -> None:
|
||||
self.stat = stat
|
||||
self.serial = serial
|
||||
self.uri = uri
|
||||
|
||||
class TraceDataType(Enum):
|
||||
CHARGE = 0
|
||||
EVENT = 1
|
||||
CREDIT = 2
|
||||
|
||||
class BillingType(Enum):
|
||||
A = 1
|
||||
B = 0
|
||||
|
||||
class float5:
|
||||
def __init__(self, n: str = "0") -> None:
|
||||
nf = float(n)
|
||||
if nf > 999.9 or nf < 0:
|
||||
raise ValueError('float5 must be between 0.000 and 999.9 inclusive')
|
||||
|
||||
return nf
|
||||
|
||||
@classmethod
|
||||
def to_str(cls, f: float):
|
||||
return f"%.{2 - int(math.log10(f))+1}f" % f
|
||||
|
||||
class BillingInfo:
|
||||
def __init__(self, data: Dict) -> None:
|
||||
try:
|
||||
self.keychipid = str(data.get("keychipid", None))
|
||||
self.functype = int(data.get("functype", None))
|
||||
self.gameid = str(data.get("gameid", None))
|
||||
self.gamever = float(data.get("gamever", None))
|
||||
self.boardid = str(data.get("boardid", None))
|
||||
self.tenpoip = str(data.get("tenpoip", None))
|
||||
self.libalibver = float(data.get("libalibver", None))
|
||||
self.datamax = int(data.get("datamax", None))
|
||||
self.billingtype = BillingType(int(data.get("billingtype", None)))
|
||||
self.protocolver = float(data.get("protocolver", None))
|
||||
self.operatingfix = bool(data.get("operatingfix", None))
|
||||
self.traceleft = int(data.get("traceleft", None))
|
||||
self.requestno = int(data.get("requestno", None))
|
||||
self.datesync = bool(data.get("datesync", None))
|
||||
self.timezone = str(data.get("timezone", None))
|
||||
self.date = datetime.strptime(data.get("date", None), BILLING_DT_FORMAT)
|
||||
self.crcerrcnt = int(data.get("crcerrcnt", None))
|
||||
self.memrepair = bool(data.get("memrepair", None))
|
||||
self.playcnt = int(data.get("playcnt", None))
|
||||
self.playlimit = int(data.get("playlimit", None))
|
||||
self.nearfull = int(data.get("nearfull", None))
|
||||
except Exception as e:
|
||||
raise KeyError(e)
|
||||
|
||||
class TraceData:
|
||||
def __init__(self, data: Dict) -> None:
|
||||
try:
|
||||
self.crc_err_flg = bool(data.get("cs", None))
|
||||
self.record_number = int(data.get("rn", None))
|
||||
self.seq_number = int(data.get("sn", None))
|
||||
self.trace_type = TraceDataType(int(data.get("tt", None)))
|
||||
self.date_sync_flg = bool(data.get("ds", None))
|
||||
self.date = datetime.strptime(data.get("dt", None), BILLING_DT_FORMAT)
|
||||
self.keychip = str(data.get("kn", None))
|
||||
self.lib_ver = float(data.get("alib", None))
|
||||
except Exception as e:
|
||||
raise KeyError(e)
|
||||
|
||||
class TraceDataCharge(TraceData):
|
||||
def __init__(self, data: Dict) -> None:
|
||||
super().__init__(data)
|
||||
try:
|
||||
self.game_id = str(data.get("gi", None))
|
||||
self.game_version = float(data.get("gv", None))
|
||||
self.board_serial = str(data.get("bn", None))
|
||||
self.shop_ip = str(data.get("ti", None))
|
||||
self.play_count = int(data.get("pc", None))
|
||||
self.play_limit = int(data.get("pl", None))
|
||||
self.product_code = int(data.get("ic", None))
|
||||
self.product_count = int(data.get("in", None))
|
||||
self.func_type = int(data.get("kk", None))
|
||||
self.player_number = int(data.get("playerno", None))
|
||||
except Exception as e:
|
||||
raise KeyError(e)
|
||||
|
||||
class TraceDataEvent(TraceData):
|
||||
def __init__(self, data: Dict) -> None:
|
||||
super().__init__(data)
|
||||
try:
|
||||
self.message = str(data.get("me", None))
|
||||
except Exception as e:
|
||||
raise KeyError(e)
|
||||
|
||||
class TraceDataCredit(TraceData):
|
||||
def __init__(self, data: Dict) -> None:
|
||||
super().__init__(data)
|
||||
try:
|
||||
self.chute_type = int(data.get("cct", None))
|
||||
self.service_type = int(data.get("cst", None))
|
||||
self.operation_type = int(data.get("cop", None))
|
||||
self.coin_rate0 = int(data.get("cr0", None))
|
||||
self.coin_rate1 = int(data.get("cr1", None))
|
||||
self.bonus_addition = int(data.get("cba", None))
|
||||
self.credit_rate = int(data.get("ccr", None))
|
||||
self.credit0 = int(data.get("cc0", None))
|
||||
self.credit1 = int(data.get("cc1", None))
|
||||
self.credit2 = int(data.get("cc2", None))
|
||||
self.credit3 = int(data.get("cc3", None))
|
||||
self.credit4 = int(data.get("cc4", None))
|
||||
self.credit5 = int(data.get("cc5", None))
|
||||
self.credit6 = int(data.get("cc6", None))
|
||||
self.credit7 = int(data.get("cc7", None))
|
||||
except Exception as e:
|
||||
raise KeyError(e)
|
||||
|
||||
class BillingResponse:
|
||||
def __init__(
|
||||
@@ -432,20 +704,22 @@ class BillingResponse:
|
||||
playlimit_sig: str = "",
|
||||
nearfull: str = "",
|
||||
nearfull_sig: str = "",
|
||||
request_num: int = 1,
|
||||
protocol_ver: float = 1.000,
|
||||
playhistory: str = "000000/0:000000/0:000000/0",
|
||||
) -> None:
|
||||
self.result = "0"
|
||||
self.waitime = "100"
|
||||
self.linelimit = "1"
|
||||
self.message = ""
|
||||
self.result = 0
|
||||
self.requestno = request_num
|
||||
self.traceerase = 1
|
||||
self.fixinterval = 120
|
||||
self.fixlogcnt = 100
|
||||
self.playlimit = playlimit
|
||||
self.playlimitsig = playlimit_sig
|
||||
self.protocolver = "1.000"
|
||||
self.playhistory = playhistory
|
||||
self.nearfull = nearfull
|
||||
self.nearfullsig = nearfull_sig
|
||||
self.fixlogincnt = "0"
|
||||
self.fixinterval = "5"
|
||||
self.playhistory = playhistory
|
||||
self.linelimit = 100
|
||||
self.protocolver = float5.to_str(protocol_ver)
|
||||
# playhistory -> YYYYMM/C:...
|
||||
# YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period
|
||||
|
||||
@@ -454,3 +728,86 @@ class AllnetRequestException(Exception):
|
||||
def __init__(self, message="") -> None:
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
class DLReport:
|
||||
def __init__(self, data: Dict, report_type: DLIMG_TYPE) -> None:
|
||||
self.serial = data.get("serial")
|
||||
self.dfl = data.get("dfl")
|
||||
self.wfl = data.get("wfl")
|
||||
self.tsc = data.get("tsc")
|
||||
self.tdsc = data.get("tdsc")
|
||||
self.at = data.get("at")
|
||||
self.ot = data.get("ot")
|
||||
self.rt = data.get("rt")
|
||||
self.as_ = data.get("as")
|
||||
self.rf_state = data.get("rf_state")
|
||||
self.gd = data.get("gd")
|
||||
self.dav = data.get("dav")
|
||||
self.wdav = data.get("wdav") # app only
|
||||
self.dov = data.get("dov")
|
||||
self.wdov = data.get("wdov") # app only
|
||||
self.__type = report_type
|
||||
self.err = ""
|
||||
|
||||
def validate(self) -> bool:
|
||||
if self.serial is None:
|
||||
self.err = "serial not provided"
|
||||
return False
|
||||
|
||||
if self.dfl is None:
|
||||
self.err = "dfl not provided"
|
||||
return False
|
||||
|
||||
if self.wfl is None:
|
||||
self.err = "wfl not provided"
|
||||
return False
|
||||
|
||||
if self.tsc is None:
|
||||
self.err = "tsc not provided"
|
||||
return False
|
||||
|
||||
if self.tdsc is None:
|
||||
self.err = "tdsc not provided"
|
||||
return False
|
||||
|
||||
if self.at is None:
|
||||
self.err = "at not provided"
|
||||
return False
|
||||
|
||||
if self.ot is None:
|
||||
self.err = "ot not provided"
|
||||
return False
|
||||
|
||||
if self.rt is None:
|
||||
self.err = "rt not provided"
|
||||
return False
|
||||
|
||||
if self.as_ is None:
|
||||
self.err = "as not provided"
|
||||
return False
|
||||
|
||||
if self.rf_state is None:
|
||||
self.err = "rf_state not provided"
|
||||
return False
|
||||
|
||||
if self.gd is None:
|
||||
self.err = "gd not provided"
|
||||
return False
|
||||
|
||||
if self.dav is None:
|
||||
self.err = "dav not provided"
|
||||
return False
|
||||
|
||||
if self.dov is None:
|
||||
self.err = "dov not provided"
|
||||
return False
|
||||
|
||||
if (self.wdav is None or self.wdov is None) and self.__type == DLIMG_TYPE.app:
|
||||
self.err = "wdav or wdov not provided in app image"
|
||||
return False
|
||||
|
||||
if (self.wdav is not None or self.wdov is not None) and self.__type == DLIMG_TYPE.opt:
|
||||
self.err = "wdav or wdov provided in opt image"
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -36,12 +36,30 @@ class ServerConfig:
|
||||
self.__config, "core", "server", "is_develop", default=True
|
||||
)
|
||||
|
||||
@property
|
||||
def threading(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "server", "threading", default=False
|
||||
)
|
||||
|
||||
@property
|
||||
def log_dir(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "server", "log_dir", default="logs"
|
||||
)
|
||||
|
||||
@property
|
||||
def check_arcade_ip(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "server", "check_arcade_ip", default=False
|
||||
)
|
||||
|
||||
@property
|
||||
def strict_ip_checking(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "server", "strict_ip_checking", default=False
|
||||
)
|
||||
|
||||
|
||||
class TitleConfig:
|
||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||
@@ -182,12 +200,24 @@ class AllnetConfig:
|
||||
self.__config, "core", "allnet", "port", default=80
|
||||
)
|
||||
|
||||
@property
|
||||
def ip_check(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "allnet", "ip_check", default=False
|
||||
)
|
||||
|
||||
@property
|
||||
def allow_online_updates(self) -> int:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "allnet", "allow_online_updates", default=False
|
||||
)
|
||||
|
||||
@property
|
||||
def update_cfg_folder(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "allnet", "update_cfg_folder", default=""
|
||||
)
|
||||
|
||||
|
||||
class BillingConfig:
|
||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||
|
||||
+109
-31
@@ -1,5 +1,5 @@
|
||||
import logging, coloredlogs
|
||||
from typing import Optional
|
||||
from typing import Optional, Dict, List
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy import create_engine
|
||||
@@ -15,31 +15,48 @@ from core.utils import Utils
|
||||
|
||||
|
||||
class Data:
|
||||
current_schema_version = 6
|
||||
engine = None
|
||||
session = None
|
||||
user = None
|
||||
arcade = None
|
||||
card = None
|
||||
base = None
|
||||
def __init__(self, cfg: CoreConfig) -> None:
|
||||
self.config = cfg
|
||||
|
||||
if self.config.database.sha2_password:
|
||||
passwd = sha256(self.config.database.password.encode()).digest()
|
||||
self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{passwd.hex()}@{self.config.database.host}/{self.config.database.name}?charset=utf8mb4"
|
||||
self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{passwd.hex()}@{self.config.database.host}:{self.config.database.port}/{self.config.database.name}?charset=utf8mb4"
|
||||
else:
|
||||
self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{self.config.database.password}@{self.config.database.host}/{self.config.database.name}?charset=utf8mb4"
|
||||
self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{self.config.database.password}@{self.config.database.host}:{self.config.database.port}/{self.config.database.name}?charset=utf8mb4"
|
||||
|
||||
self.__engine = create_engine(self.__url, pool_recycle=3600)
|
||||
session = sessionmaker(bind=self.__engine, autoflush=True, autocommit=True)
|
||||
self.session = scoped_session(session)
|
||||
if Data.engine is None:
|
||||
Data.engine = create_engine(self.__url, pool_recycle=3600)
|
||||
self.__engine = Data.engine
|
||||
|
||||
self.user = UserData(self.config, self.session)
|
||||
self.arcade = ArcadeData(self.config, self.session)
|
||||
self.card = CardData(self.config, self.session)
|
||||
self.base = BaseData(self.config, self.session)
|
||||
self.schema_ver_latest = 4
|
||||
if Data.session is None:
|
||||
s = sessionmaker(bind=Data.engine, autoflush=True, autocommit=True)
|
||||
Data.session = scoped_session(s)
|
||||
|
||||
if Data.user is None:
|
||||
Data.user = UserData(self.config, self.session)
|
||||
|
||||
if Data.arcade is None:
|
||||
Data.arcade = ArcadeData(self.config, self.session)
|
||||
|
||||
if Data.card is None:
|
||||
Data.card = CardData(self.config, self.session)
|
||||
|
||||
if Data.base is None:
|
||||
Data.base = BaseData(self.config, self.session)
|
||||
|
||||
log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s"
|
||||
log_fmt = logging.Formatter(log_fmt_str)
|
||||
self.logger = logging.getLogger("database")
|
||||
|
||||
# Prevent the logger from adding handlers multiple times
|
||||
if not getattr(self.logger, "handler_set", None):
|
||||
log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s"
|
||||
log_fmt = logging.Formatter(log_fmt_str)
|
||||
fileHandler = TimedRotatingFileHandler(
|
||||
"{0}/{1}.log".format(self.config.server.log_dir, "db"),
|
||||
encoding="utf-8",
|
||||
@@ -71,11 +88,13 @@ class Data:
|
||||
games = Utils.get_all_titles()
|
||||
for game_dir, game_mod in games.items():
|
||||
try:
|
||||
if hasattr(game_mod, "database") and hasattr(game_mod, "current_schema_version"):
|
||||
if hasattr(game_mod, "database") and hasattr(
|
||||
game_mod, "current_schema_version"
|
||||
):
|
||||
game_mod.database(self.config)
|
||||
metadata.create_all(self.__engine.connect())
|
||||
|
||||
self.base.set_schema_ver(
|
||||
self.base.touch_schema_ver(
|
||||
game_mod.current_schema_version, game_mod.game_codes[0]
|
||||
)
|
||||
|
||||
@@ -84,8 +103,8 @@ class Data:
|
||||
f"Could not load database schema from {game_dir} - {e}"
|
||||
)
|
||||
|
||||
self.logger.info(f"Setting base_schema_ver to {self.schema_ver_latest}")
|
||||
self.base.set_schema_ver(self.schema_ver_latest)
|
||||
self.logger.info(f"Setting base_schema_ver to {self.current_schema_version}")
|
||||
self.base.set_schema_ver(self.current_schema_version)
|
||||
|
||||
self.logger.info(
|
||||
f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}"
|
||||
@@ -129,9 +148,32 @@ class Data:
|
||||
|
||||
self.create_database()
|
||||
|
||||
def migrate_database(self, game: str, version: int, action: str) -> None:
|
||||
def migrate_database(self, game: str, version: Optional[int], action: str) -> None:
|
||||
old_ver = self.base.get_schema_ver(game)
|
||||
sql = ""
|
||||
if version is None:
|
||||
if not game == "CORE":
|
||||
titles = Utils.get_all_titles()
|
||||
|
||||
for folder, mod in titles.items():
|
||||
if not mod.game_codes[0] == game:
|
||||
continue
|
||||
|
||||
if hasattr(mod, "current_schema_version"):
|
||||
version = mod.current_schema_version
|
||||
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"current_schema_version not found for {folder}"
|
||||
)
|
||||
|
||||
else:
|
||||
version = self.current_schema_version
|
||||
|
||||
if version is None:
|
||||
self.logger.warning(
|
||||
f"Could not determine latest version for {game}, please specify --version"
|
||||
)
|
||||
|
||||
if old_ver is None:
|
||||
self.logger.error(
|
||||
@@ -212,7 +254,7 @@ class Data:
|
||||
self.logger.error(f"Failed to create card for owner with id {user_id}")
|
||||
return
|
||||
|
||||
self.logger.warn(
|
||||
self.logger.warning(
|
||||
f"Successfully created owner with email {email}, access code 00000000000000000000, and password {pw} Make sure to change this password and assign a real card ASAP!"
|
||||
)
|
||||
|
||||
@@ -227,7 +269,7 @@ class Data:
|
||||
return
|
||||
|
||||
if not should_force:
|
||||
self.logger.warn(
|
||||
self.logger.warning(
|
||||
f"Card already exists for access code {new_ac} (id {new_card['id']}). If you wish to continue, rerun with the '--force' flag."
|
||||
f" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
|
||||
)
|
||||
@@ -263,17 +305,53 @@ class Data:
|
||||
self.user.delete_user(user["id"])
|
||||
|
||||
def autoupgrade(self) -> None:
|
||||
all_games = self.base.get_all_schema_vers()
|
||||
if all_games is None:
|
||||
self.logger.warn("Failed to get schema versions")
|
||||
all_game_versions = self.base.get_all_schema_vers()
|
||||
if all_game_versions is None:
|
||||
self.logger.warning("Failed to get schema versions")
|
||||
return
|
||||
|
||||
for x in all_games:
|
||||
all_games = Utils.get_all_titles()
|
||||
all_games_list: Dict[str, int] = {}
|
||||
for _, mod in all_games.items():
|
||||
if hasattr(mod, "current_schema_version"):
|
||||
all_games_list[mod.game_codes[0]] = mod.current_schema_version
|
||||
|
||||
for x in all_game_versions:
|
||||
failed = False
|
||||
game = x["game"].upper()
|
||||
update_ver = 1
|
||||
for y in range(2, 100):
|
||||
if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"):
|
||||
update_ver = y
|
||||
else:
|
||||
break
|
||||
update_ver = int(x["version"])
|
||||
latest_ver = all_games_list.get(game, 1)
|
||||
if game == "CORE":
|
||||
latest_ver = self.current_schema_version
|
||||
|
||||
self.migrate_database(game, update_ver, "upgrade")
|
||||
if update_ver == latest_ver:
|
||||
self.logger.info(f"{game} is already latest version")
|
||||
continue
|
||||
|
||||
for y in range(update_ver + 1, latest_ver + 1):
|
||||
if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"):
|
||||
with open(
|
||||
f"core/data/schema/versions/{game}_{y}_upgrade.sql",
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
sql = f.read()
|
||||
|
||||
result = self.base.execute(sql)
|
||||
if result is None:
|
||||
self.logger.error(
|
||||
f"Error execuing sql script for game {game} v{y}!"
|
||||
)
|
||||
failed = True
|
||||
break
|
||||
else:
|
||||
self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql")
|
||||
failed = True
|
||||
|
||||
if not failed:
|
||||
self.base.set_schema_ver(latest_ver, game)
|
||||
|
||||
def show_versions(self) -> None:
|
||||
all_game_versions = self.base.get_all_schema_vers()
|
||||
for ver in all_game_versions:
|
||||
self.logger.info(f"{ver['game']} -> v{ver['version']}")
|
||||
|
||||
+40
-33
@@ -1,9 +1,10 @@
|
||||
from typing import Optional, Dict
|
||||
from sqlalchemy import Table, Column
|
||||
from typing import Optional, Dict, List
|
||||
from sqlalchemy import Table, Column, and_, or_
|
||||
from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint
|
||||
from sqlalchemy.types import Integer, String, Boolean
|
||||
from sqlalchemy.types import Integer, String, Boolean, JSON
|
||||
from sqlalchemy.sql import func, select
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from sqlalchemy.engine import Row
|
||||
import re
|
||||
|
||||
from core.data.schema.base import BaseData, metadata
|
||||
@@ -21,6 +22,7 @@ arcade = Table(
|
||||
Column("city", String(255)),
|
||||
Column("region_id", Integer),
|
||||
Column("timezone", String(255)),
|
||||
Column("ip", String(39)),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
@@ -40,6 +42,9 @@ machine = Table(
|
||||
Column("timezone", String(255)),
|
||||
Column("ota_enable", Boolean),
|
||||
Column("is_cab", Boolean),
|
||||
Column("memo", String(255)),
|
||||
Column("is_cab", Boolean),
|
||||
Column("data", JSON),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
@@ -65,7 +70,7 @@ arcade_owner = Table(
|
||||
|
||||
|
||||
class ArcadeData(BaseData):
|
||||
def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]:
|
||||
def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]:
|
||||
if serial is not None:
|
||||
serial = serial.replace("-", "")
|
||||
if len(serial) == 11:
|
||||
@@ -130,13 +135,20 @@ class ArcadeData(BaseData):
|
||||
f"Failed to update board id for machine {machine_id} -> {boardid}"
|
||||
)
|
||||
|
||||
def get_arcade(self, id: int) -> Optional[Dict]:
|
||||
def get_arcade(self, id: int) -> Optional[Row]:
|
||||
sql = arcade.select(arcade.c.id == id)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
def get_arcade_machines(self, id: int) -> Optional[List[Row]]:
|
||||
sql = machine.select(machine.c.arcade == id)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
def put_arcade(
|
||||
self,
|
||||
name: str,
|
||||
@@ -165,7 +177,21 @@ class ArcadeData(BaseData):
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]:
|
||||
def get_arcades_managed_by_user(self, user_id: int) -> Optional[List[Row]]:
|
||||
sql = select(arcade).join(arcade_owner, arcade_owner.c.arcade == arcade.c.id).where(arcade_owner.c.user == user_id)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return False
|
||||
return result.fetchall()
|
||||
|
||||
def get_manager_permissions(self, user_id: int, arcade_id: int) -> Optional[int]:
|
||||
sql = select(arcade_owner.c.permissions).where(and_(arcade_owner.c.user == user_id, arcade_owner.c.arcade == arcade_id))
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return False
|
||||
return result.fetchone()
|
||||
|
||||
def get_arcade_owners(self, arcade_id: int) -> Optional[Row]:
|
||||
sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id)
|
||||
|
||||
result = self.execute(sql)
|
||||
@@ -187,33 +213,14 @@ class ArcadeData(BaseData):
|
||||
return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
|
||||
|
||||
def validate_keychip_format(self, serial: str) -> bool:
|
||||
serial = serial.replace("-", "")
|
||||
if len(serial) != 11 or len(serial) != 15:
|
||||
self.logger.error(
|
||||
f"Serial validate failed: Incorrect length for {serial} (len {len(serial)})"
|
||||
)
|
||||
return False
|
||||
|
||||
platform_code = serial[:4]
|
||||
platform_rev = serial[4:6]
|
||||
const_a = serial[6]
|
||||
num = serial[7:11]
|
||||
append = serial[11:15]
|
||||
|
||||
if re.match("A[7|6]\d[E|X][0|1][0|1|2]A\d{4,8}", serial) is None:
|
||||
self.logger.error(f"Serial validate failed: {serial} failed regex")
|
||||
return False
|
||||
|
||||
if len(append) != 0 or len(append) != 4:
|
||||
self.logger.error(
|
||||
f"Serial validate failed: {serial} had malformed append {append}"
|
||||
)
|
||||
return False
|
||||
|
||||
if len(num) != 4:
|
||||
self.logger.error(
|
||||
f"Serial validate failed: {serial} had malformed number {num}"
|
||||
)
|
||||
if re.fullmatch(r"^A[0-9]{2}[E|X][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def find_arcade_by_name(self, name: str) -> List[Row]:
|
||||
sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%")))
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return False
|
||||
return result.fetchall()
|
||||
|
||||
@@ -47,7 +47,7 @@ class BaseData:
|
||||
res = None
|
||||
|
||||
try:
|
||||
self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())} || {opts}")
|
||||
self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())}")
|
||||
res = self.conn.execute(text(sql), opts)
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
@@ -58,7 +58,7 @@ class BaseData:
|
||||
self.logger.error(f"UnicodeEncodeError error {e}")
|
||||
return None
|
||||
|
||||
except:
|
||||
except Exception:
|
||||
try:
|
||||
res = self.conn.execute(sql, opts)
|
||||
|
||||
@@ -70,7 +70,7 @@ class BaseData:
|
||||
self.logger.error(f"UnicodeEncodeError error {e}")
|
||||
return None
|
||||
|
||||
except:
|
||||
except Exception:
|
||||
self.logger.error(f"Unknown error")
|
||||
raise
|
||||
|
||||
@@ -103,6 +103,18 @@ class BaseData:
|
||||
|
||||
return row["version"]
|
||||
|
||||
def touch_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]:
|
||||
sql = insert(schema_ver).values(game=game, version=ver)
|
||||
conflict = sql.on_duplicate_key_update(version=schema_ver.c.version)
|
||||
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
self.logger.error(
|
||||
f"Failed to update schema version for game {game} (v{ver})"
|
||||
)
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]:
|
||||
sql = insert(schema_ver).values(game=game, version=ver)
|
||||
conflict = sql.on_duplicate_key_update(version=ver)
|
||||
|
||||
@@ -80,6 +80,9 @@ class UserData(BaseData):
|
||||
if usr["password"] is None:
|
||||
return False
|
||||
|
||||
if passwd is None or not passwd:
|
||||
return False
|
||||
|
||||
return bcrypt.checkpw(passwd, usr["password"].encode())
|
||||
|
||||
def reset_autoincrement(self, ai_value: int) -> None:
|
||||
@@ -104,3 +107,17 @@ class UserData(BaseData):
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
def find_user_by_email(self, email: str) -> Row:
|
||||
sql = select(aime_user).where(aime_user.c.email == email)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return False
|
||||
return result.fetchone()
|
||||
|
||||
def find_user_by_username(self, username: str) -> List[Row]:
|
||||
sql = aime_user.select(aime_user.c.username.like(f"%{username}%"))
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return False
|
||||
return result.fetchall()
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE machine DROP COLUMN memo;
|
||||
ALTER TABLE machine DROP COLUMN is_blacklisted;
|
||||
ALTER TABLE machine DROP COLUMN `data`;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE arcade DROP COLUMN 'ip';
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE machine ADD memo varchar(255) NULL;
|
||||
ALTER TABLE machine ADD is_blacklisted tinyint(1) NULL;
|
||||
ALTER TABLE machine ADD `data` longtext NULL;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE arcade ADD ip varchar(39) NULL;
|
||||
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE diva_profile
|
||||
DROP cnp_cid,
|
||||
DROP cnp_val,
|
||||
DROP cnp_rr,
|
||||
DROP cnp_sp,
|
||||
DROP btn_se_eqp,
|
||||
DROP sld_se_eqp,
|
||||
DROP chn_sld_se_eqp,
|
||||
DROP sldr_tch_se_eqp;
|
||||
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE diva_profile
|
||||
ADD cnp_cid INT NOT NULL DEFAULT -1,
|
||||
ADD cnp_val INT NOT NULL DEFAULT -1,
|
||||
ADD cnp_rr INT NOT NULL DEFAULT -1,
|
||||
ADD cnp_sp VARCHAR(255) NOT NULL DEFAULT "",
|
||||
ADD btn_se_eqp INT NOT NULL DEFAULT -1,
|
||||
ADD sld_se_eqp INT NOT NULL DEFAULT -1,
|
||||
ADD chn_sld_se_eqp INT NOT NULL DEFAULT -1,
|
||||
ADD sldr_tch_se_eqp INT NOT NULL DEFAULT -1;
|
||||
@@ -0,0 +1,30 @@
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
ALTER TABLE chuni_score_playlog
|
||||
DROP COLUMN regionId,
|
||||
DROP COLUMN machineType;
|
||||
|
||||
ALTER TABLE chuni_static_events
|
||||
DROP COLUMN startDate;
|
||||
|
||||
ALTER TABLE chuni_profile_data
|
||||
DROP COLUMN rankUpChallengeResults;
|
||||
|
||||
ALTER TABLE chuni_static_login_bonus
|
||||
DROP FOREIGN KEY chuni_static_login_bonus_ibfk_1;
|
||||
|
||||
ALTER TABLE chuni_static_login_bonus_preset
|
||||
DROP PRIMARY KEY;
|
||||
|
||||
ALTER TABLE chuni_static_login_bonus_preset
|
||||
CHANGE COLUMN presetId id INT NOT NULL;
|
||||
ALTER TABLE chuni_static_login_bonus_preset
|
||||
ADD PRIMARY KEY(id);
|
||||
ALTER TABLE chuni_static_login_bonus_preset
|
||||
ADD CONSTRAINT chuni_static_login_bonus_preset_uk UNIQUE(id, version);
|
||||
|
||||
ALTER TABLE chuni_static_login_bonus
|
||||
ADD CONSTRAINT chuni_static_login_bonus_ibfk_1 FOREIGN KEY(presetId)
|
||||
REFERENCES chuni_static_login_bonus_preset(id) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
@@ -0,0 +1,29 @@
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
ALTER TABLE chuni_score_playlog
|
||||
ADD COLUMN regionId INT,
|
||||
ADD COLUMN machineType INT;
|
||||
|
||||
ALTER TABLE chuni_static_events
|
||||
ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp();
|
||||
|
||||
ALTER TABLE chuni_profile_data
|
||||
ADD COLUMN rankUpChallengeResults JSON;
|
||||
|
||||
ALTER TABLE chuni_static_login_bonus
|
||||
DROP FOREIGN KEY chuni_static_login_bonus_ibfk_1;
|
||||
|
||||
ALTER TABLE chuni_static_login_bonus_preset
|
||||
CHANGE COLUMN id presetId INT NOT NULL;
|
||||
ALTER TABLE chuni_static_login_bonus_preset
|
||||
DROP PRIMARY KEY;
|
||||
ALTER TABLE chuni_static_login_bonus_preset
|
||||
DROP INDEX chuni_static_login_bonus_preset_uk;
|
||||
ALTER TABLE chuni_static_login_bonus_preset
|
||||
ADD CONSTRAINT chuni_static_login_bonus_preset_pk PRIMARY KEY (presetId, version);
|
||||
|
||||
ALTER TABLE chuni_static_login_bonus
|
||||
ADD CONSTRAINT chuni_static_login_bonus_ibfk_1 FOREIGN KEY (presetId, version)
|
||||
REFERENCES chuni_static_login_bonus_preset(presetId, version) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE ongeki_static_events
|
||||
DROP COLUMN startDate;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE ongeki_static_events
|
||||
ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp();
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE mai2_item_card
|
||||
CHANGE COLUMN startDate startDate TIMESTAMP DEFAULT "2018-01-01 00:00:00.0",
|
||||
CHANGE COLUMN endDate endDate TIMESTAMP DEFAULT "2038-01-01 00:00:00.0";
|
||||
@@ -0,0 +1,78 @@
|
||||
DELETE FROM mai2_static_event WHERE version < 13;
|
||||
UPDATE mai2_static_event SET version = version - 13 WHERE version >= 13;
|
||||
|
||||
DELETE FROM mai2_static_music WHERE version < 13;
|
||||
UPDATE mai2_static_music SET version = version - 13 WHERE version >= 13;
|
||||
|
||||
DELETE FROM mai2_static_ticket WHERE version < 13;
|
||||
UPDATE mai2_static_ticket SET version = version - 13 WHERE version >= 13;
|
||||
|
||||
DELETE FROM mai2_static_cards WHERE version < 13;
|
||||
UPDATE mai2_static_cards SET version = version - 13 WHERE version >= 13;
|
||||
|
||||
DELETE FROM mai2_profile_detail WHERE version < 13;
|
||||
UPDATE mai2_profile_detail SET version = version - 13 WHERE version >= 13;
|
||||
|
||||
DELETE FROM mai2_profile_extend WHERE version < 13;
|
||||
UPDATE mai2_profile_extend SET version = version - 13 WHERE version >= 13;
|
||||
|
||||
DELETE FROM mai2_profile_option WHERE version < 13;
|
||||
UPDATE mai2_profile_option SET version = version - 13 WHERE version >= 13;
|
||||
|
||||
DELETE FROM mai2_profile_ghost WHERE version < 13;
|
||||
UPDATE mai2_profile_ghost SET version = version - 13 WHERE version >= 13;
|
||||
|
||||
DELETE FROM mai2_profile_rating WHERE version < 13;
|
||||
UPDATE mai2_profile_rating SET version = version - 13 WHERE version >= 13;
|
||||
|
||||
DROP TABLE maimai_score_best;
|
||||
DROP TABLE maimai_playlog;
|
||||
DROP TABLE maimai_profile_detail;
|
||||
DROP TABLE maimai_profile_option;
|
||||
DROP TABLE maimai_profile_web_option;
|
||||
DROP TABLE maimai_profile_grade_status;
|
||||
|
||||
ALTER TABLE mai2_item_character DROP COLUMN point;
|
||||
|
||||
ALTER TABLE mai2_item_card MODIFY COLUMN cardId int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_card MODIFY COLUMN cardTypeId int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_card MODIFY COLUMN charaId int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_card MODIFY COLUMN mapId int(11) NOT NULL;
|
||||
|
||||
ALTER TABLE mai2_item_character MODIFY COLUMN characterId int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_character MODIFY COLUMN level int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_character MODIFY COLUMN awakening int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_character MODIFY COLUMN useCount int(11) NOT NULL;
|
||||
|
||||
ALTER TABLE mai2_item_charge MODIFY COLUMN chargeId int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_charge MODIFY COLUMN stock int(11) NOT NULL;
|
||||
|
||||
ALTER TABLE mai2_item_favorite MODIFY COLUMN itemKind int(11) NOT NULL;
|
||||
|
||||
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN seasonId int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN point int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN `rank` int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rewardGet tinyint(1) NOT NULL;
|
||||
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN userName varchar(8) NOT NULL;
|
||||
|
||||
ALTER TABLE mai2_item_item MODIFY COLUMN itemId int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_item MODIFY COLUMN itemKind int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_item MODIFY COLUMN stock int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_item MODIFY COLUMN isValid tinyint(1) NOT NULL;
|
||||
|
||||
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN bonusId int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN point int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isCurrent tinyint(1) NOT NULL;
|
||||
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isComplete tinyint(1) NOT NULL;
|
||||
|
||||
ALTER TABLE mai2_item_map MODIFY COLUMN mapId int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_map MODIFY COLUMN distance int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_map MODIFY COLUMN isLock tinyint(1) NOT NULL;
|
||||
ALTER TABLE mai2_item_map MODIFY COLUMN isClear tinyint(1) NOT NULL;
|
||||
ALTER TABLE mai2_item_map MODIFY COLUMN isComplete tinyint(1) NOT NULL;
|
||||
|
||||
ALTER TABLE mai2_item_print_detail MODIFY COLUMN printDate timestamp DEFAULT current_timestamp() NOT NULL;
|
||||
ALTER TABLE mai2_item_print_detail MODIFY COLUMN serialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;
|
||||
ALTER TABLE mai2_item_print_detail MODIFY COLUMN placeId int(11) NOT NULL;
|
||||
ALTER TABLE mai2_item_print_detail MODIFY COLUMN clientId varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;
|
||||
ALTER TABLE mai2_item_print_detail MODIFY COLUMN printerSerialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE mai2_item_card
|
||||
CHANGE COLUMN startDate startDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHANGE COLUMN endDate endDate TIMESTAMP NOT NULL;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE aime.mai2_profile_consec_logins;
|
||||
@@ -0,0 +1,62 @@
|
||||
UPDATE mai2_static_event SET version = version + 13 WHERE version < 1000;
|
||||
|
||||
UPDATE mai2_static_music SET version = version + 13 WHERE version < 1000;
|
||||
|
||||
UPDATE mai2_static_ticket SET version = version + 13 WHERE version < 1000;
|
||||
|
||||
UPDATE mai2_static_cards SET version = version + 13 WHERE version < 1000;
|
||||
|
||||
UPDATE mai2_profile_detail SET version = version + 13 WHERE version < 1000;
|
||||
|
||||
UPDATE mai2_profile_extend SET version = version + 13 WHERE version < 1000;
|
||||
|
||||
UPDATE mai2_profile_option SET version = version + 13 WHERE version < 1000;
|
||||
|
||||
UPDATE mai2_profile_ghost SET version = version + 13 WHERE version < 1000;
|
||||
|
||||
UPDATE mai2_profile_rating SET version = version + 13 WHERE version < 1000;
|
||||
|
||||
ALTER TABLE mai2_item_character ADD point int(11) NULL;
|
||||
|
||||
ALTER TABLE mai2_item_card MODIFY COLUMN cardId int(11) NULL;
|
||||
ALTER TABLE mai2_item_card MODIFY COLUMN cardTypeId int(11) NULL;
|
||||
ALTER TABLE mai2_item_card MODIFY COLUMN charaId int(11) NULL;
|
||||
ALTER TABLE mai2_item_card MODIFY COLUMN mapId int(11) NULL;
|
||||
|
||||
ALTER TABLE mai2_item_character MODIFY COLUMN characterId int(11) NULL;
|
||||
ALTER TABLE mai2_item_character MODIFY COLUMN level int(11) NULL;
|
||||
ALTER TABLE mai2_item_character MODIFY COLUMN awakening int(11) NULL;
|
||||
ALTER TABLE mai2_item_character MODIFY COLUMN useCount int(11) NULL;
|
||||
|
||||
ALTER TABLE mai2_item_charge MODIFY COLUMN chargeId int(11) NULL;
|
||||
ALTER TABLE mai2_item_charge MODIFY COLUMN stock int(11) NULL;
|
||||
|
||||
ALTER TABLE mai2_item_favorite MODIFY COLUMN itemKind int(11) NULL;
|
||||
|
||||
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN seasonId int(11) NULL;
|
||||
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN point int(11) NULL;
|
||||
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN `rank` int(11) NULL;
|
||||
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rewardGet tinyint(1) NULL;
|
||||
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN userName varchar(8) NULL;
|
||||
|
||||
ALTER TABLE mai2_item_item MODIFY COLUMN itemId int(11) NULL;
|
||||
ALTER TABLE mai2_item_item MODIFY COLUMN itemKind int(11) NULL;
|
||||
ALTER TABLE mai2_item_item MODIFY COLUMN stock int(11) NULL;
|
||||
ALTER TABLE mai2_item_item MODIFY COLUMN isValid tinyint(1) NULL;
|
||||
|
||||
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN bonusId int(11) NULL;
|
||||
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN point int(11) NULL;
|
||||
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isCurrent tinyint(1) NULL;
|
||||
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isComplete tinyint(1) NULL;
|
||||
|
||||
ALTER TABLE mai2_item_map MODIFY COLUMN mapId int(11) NULL;
|
||||
ALTER TABLE mai2_item_map MODIFY COLUMN distance int(11) NULL;
|
||||
ALTER TABLE mai2_item_map MODIFY COLUMN isLock tinyint(1) NULL;
|
||||
ALTER TABLE mai2_item_map MODIFY COLUMN isClear tinyint(1) NULL;
|
||||
ALTER TABLE mai2_item_map MODIFY COLUMN isComplete tinyint(1) NULL;
|
||||
|
||||
ALTER TABLE mai2_item_print_detail MODIFY COLUMN printDate timestamp DEFAULT current_timestamp() NULL;
|
||||
ALTER TABLE mai2_item_print_detail MODIFY COLUMN serialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL;
|
||||
ALTER TABLE mai2_item_print_detail MODIFY COLUMN placeId int(11) NULL;
|
||||
ALTER TABLE mai2_item_print_detail MODIFY COLUMN clientId varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL;
|
||||
ALTER TABLE mai2_item_print_detail MODIFY COLUMN printerSerialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL;
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE `mai2_profile_consec_logins` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user` int(11) NOT NULL,
|
||||
`version` int(11) NOT NULL,
|
||||
`logins` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `mai2_profile_consec_logins_uk` (`user`,`version`),
|
||||
CONSTRAINT `mai2_profile_consec_logins_ibfk_1` FOREIGN KEY (`user`) REFERENCES `aime_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
@@ -0,0 +1,2 @@
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE wacca_profile DROP COLUMN playcount_time_free;
|
||||
@@ -0,0 +1 @@
|
||||
DELETE FROM wacca_item WHERE type=17 AND item_id=312002;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE wacca_profile ADD playcount_time_free int(11) DEFAULT 0 NULL AFTER playcount_stageup;
|
||||
+245
-14
@@ -9,21 +9,35 @@ from zope.interface import Interface, Attribute, implementer
|
||||
from twisted.python.components import registerAdapter
|
||||
import jinja2
|
||||
import bcrypt
|
||||
import re
|
||||
from enum import Enum
|
||||
from urllib import parse
|
||||
|
||||
from core import CoreConfig, Utils
|
||||
from core.data import Data
|
||||
|
||||
|
||||
class IUserSession(Interface):
|
||||
userId = Attribute("User's ID")
|
||||
user_id = Attribute("User's ID")
|
||||
username = Attribute("User's username")
|
||||
current_ip = Attribute("User's current ip address")
|
||||
permissions = Attribute("User's permission level")
|
||||
|
||||
|
||||
class PermissionOffset(Enum):
|
||||
USER = 0 # Regular user
|
||||
USERMOD = 1 # Can moderate other users
|
||||
ACMOD = 2 # Can add arcades and cabs
|
||||
SYSADMIN = 3 # Can change settings
|
||||
# 4 - 6 reserved for future use
|
||||
OWNER = 7 # Can do anything
|
||||
|
||||
|
||||
@implementer(IUserSession)
|
||||
class UserSession(object):
|
||||
def __init__(self, session):
|
||||
self.userId = 0
|
||||
self.user_id = 0
|
||||
self.username = None
|
||||
self.current_ip = "0.0.0.0"
|
||||
self.permissions = 0
|
||||
|
||||
@@ -71,12 +85,18 @@ class FrontendServlet(resource.Resource):
|
||||
game_fe = game_mod.frontend(cfg, self.environment, config_dir)
|
||||
self.game_list.append({"url": game_dir, "name": game_fe.nav_name})
|
||||
fe_game.putChild(game_dir.encode(), game_fe)
|
||||
except:
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Failed to import frontend from {game_dir} because {e}"
|
||||
)
|
||||
|
||||
self.environment.globals["game_list"] = self.game_list
|
||||
self.putChild(b"gate", FE_Gate(cfg, self.environment))
|
||||
self.putChild(b"user", FE_User(cfg, self.environment))
|
||||
self.putChild(b"sys", FE_System(cfg, self.environment))
|
||||
self.putChild(b"arcade", FE_Arcade(cfg, self.environment))
|
||||
self.putChild(b"cab", FE_Machine(cfg, self.environment))
|
||||
self.putChild(b"game", fe_game)
|
||||
|
||||
self.logger.info(
|
||||
@@ -90,6 +110,7 @@ class FrontendServlet(resource.Resource):
|
||||
server_name=self.config.server.name,
|
||||
title=self.config.server.name,
|
||||
game_list=self.game_list,
|
||||
active_page="/",
|
||||
sesh=vars(IUserSession(request.getSession())),
|
||||
).encode("utf-16")
|
||||
|
||||
@@ -118,7 +139,7 @@ class FE_Gate(FE_Base):
|
||||
|
||||
sesh = request.getSession()
|
||||
usr_sesh = IUserSession(sesh)
|
||||
if usr_sesh.userId > 0:
|
||||
if usr_sesh.user_id > 0:
|
||||
return redirectTo(b"/user", request)
|
||||
|
||||
if uri.startswith("/gate/create"):
|
||||
@@ -127,7 +148,7 @@ class FE_Gate(FE_Base):
|
||||
if b"e" in request.args:
|
||||
try:
|
||||
err = int(request.args[b"e"][0].decode())
|
||||
except:
|
||||
except Exception:
|
||||
err = 0
|
||||
|
||||
else:
|
||||
@@ -137,6 +158,7 @@ class FE_Gate(FE_Base):
|
||||
return template.render(
|
||||
title=f"{self.core_config.server.name} | Login Gate",
|
||||
error=err,
|
||||
active_page="gate",
|
||||
sesh=vars(usr_sesh),
|
||||
).encode("utf-16")
|
||||
|
||||
@@ -145,12 +167,13 @@ class FE_Gate(FE_Base):
|
||||
ip = Utils.get_ip_addr(request)
|
||||
|
||||
if uri == "/gate/gate.login":
|
||||
access_code: str = request.args[b"access_code"][0].decode()
|
||||
access_code: str = request.args[b"access-code"][0].decode()
|
||||
passwd: bytes = request.args[b"passwd"][0]
|
||||
if passwd == b"":
|
||||
passwd = None
|
||||
|
||||
uid = self.data.card.get_user_id_from_card(access_code)
|
||||
user = self.data.user.get_user(uid)
|
||||
if uid is None:
|
||||
return redirectTo(b"/gate?e=1", request)
|
||||
|
||||
@@ -170,8 +193,10 @@ class FE_Gate(FE_Base):
|
||||
|
||||
sesh = request.getSession()
|
||||
usr_sesh = IUserSession(sesh)
|
||||
usr_sesh.userId = uid
|
||||
usr_sesh.user_id = uid
|
||||
usr_sesh.username = user["username"]
|
||||
usr_sesh.current_ip = ip
|
||||
usr_sesh.permissions = user["permissions"]
|
||||
|
||||
return redirectTo(b"/user", request)
|
||||
|
||||
@@ -179,7 +204,7 @@ class FE_Gate(FE_Base):
|
||||
access_code: str = request.args[b"access_code"][0].decode()
|
||||
username: str = request.args[b"username"][0]
|
||||
email: str = request.args[b"email"][0].decode()
|
||||
passwd: str = request.args[b"passwd"][0]
|
||||
passwd: bytes = request.args[b"passwd"][0]
|
||||
|
||||
uid = self.data.card.get_user_id_from_card(access_code)
|
||||
if uid is None:
|
||||
@@ -189,12 +214,12 @@ class FE_Gate(FE_Base):
|
||||
hashed = bcrypt.hashpw(passwd, salt)
|
||||
|
||||
result = self.data.user.create_user(
|
||||
uid, username, email, hashed.decode(), 1
|
||||
uid, username, email.lower(), hashed.decode(), 1
|
||||
)
|
||||
if result is None:
|
||||
return redirectTo(b"/gate?e=3", request)
|
||||
|
||||
if not self.data.user.check_password(uid, passwd.encode()):
|
||||
if not self.data.user.check_password(uid, passwd):
|
||||
return redirectTo(b"/gate", request)
|
||||
|
||||
return redirectTo(b"/user", request)
|
||||
@@ -207,26 +232,179 @@ class FE_Gate(FE_Base):
|
||||
return redirectTo(b"/gate?e=2", request)
|
||||
|
||||
ac = request.args[b"ac"][0].decode()
|
||||
card = self.data.card.get_card_by_access_code(ac)
|
||||
if card is None:
|
||||
return redirectTo(b"/gate?e=1", request)
|
||||
|
||||
user = self.data.user.get_user(card["user"])
|
||||
if user is None:
|
||||
self.logger.warning(
|
||||
f"Card {ac} exists with no/invalid associated user ID {card['user']}"
|
||||
)
|
||||
return redirectTo(b"/gate?e=0", request)
|
||||
|
||||
if user["password"] is not None:
|
||||
return redirectTo(b"/gate?e=1", request)
|
||||
|
||||
template = self.environment.get_template("core/frontend/gate/create.jinja")
|
||||
return template.render(
|
||||
title=f"{self.core_config.server.name} | Create User",
|
||||
code=ac,
|
||||
sesh={"userId": 0},
|
||||
active_page="gate",
|
||||
sesh={"user_id": 0, "permissions": 0},
|
||||
).encode("utf-16")
|
||||
|
||||
|
||||
class FE_User(FE_Base):
|
||||
def render_GET(self, request: Request):
|
||||
uri = request.uri.decode()
|
||||
template = self.environment.get_template("core/frontend/user/index.jinja")
|
||||
|
||||
sesh: Session = request.getSession()
|
||||
usr_sesh = IUserSession(sesh)
|
||||
if usr_sesh.userId == 0:
|
||||
if usr_sesh.user_id == 0:
|
||||
return redirectTo(b"/gate", request)
|
||||
|
||||
m = re.match("\/user\/(\d*)", uri)
|
||||
|
||||
if m is not None:
|
||||
usrid = m.group(1)
|
||||
if (
|
||||
usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value
|
||||
or not usrid == usr_sesh.user_id
|
||||
):
|
||||
return redirectTo(b"/user", request)
|
||||
|
||||
else:
|
||||
usrid = usr_sesh.user_id
|
||||
|
||||
user = self.data.user.get_user(usrid)
|
||||
if user is None:
|
||||
return redirectTo(b"/user", request)
|
||||
|
||||
cards = self.data.card.get_user_cards(usrid)
|
||||
arcades = self.data.arcade.get_arcades_managed_by_user(usrid)
|
||||
|
||||
card_data = []
|
||||
arcade_data = []
|
||||
|
||||
for i, c in enumerate(cards):
|
||||
if c["is_locked"]:
|
||||
status = "Locked"
|
||||
elif c["is_banned"]:
|
||||
status = "Banned"
|
||||
else:
|
||||
status = "Active"
|
||||
|
||||
card_data.append({"index": i+1, "access_code": c["access_code"], "status": status})
|
||||
|
||||
for i, a in enumerate(arcades):
|
||||
arcade_data.append({"index": i+1, "id": a["id"], "name": a["name"]})
|
||||
|
||||
return template.render(
|
||||
title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh)
|
||||
title=f"{self.core_config.server.name} | Account",
|
||||
sesh=vars(usr_sesh),
|
||||
cards=card_data,
|
||||
arcades=arcade_data,
|
||||
active_page="user",
|
||||
).encode("utf-16")
|
||||
|
||||
def render_POST(self, request: Request):
|
||||
pass
|
||||
|
||||
|
||||
class FE_System(FE_Base):
|
||||
def render_GET(self, request: Request):
|
||||
uri = request.uri.decode()
|
||||
template = self.environment.get_template("core/frontend/sys/index.jinja")
|
||||
usrlist = []
|
||||
aclist = []
|
||||
cablist = []
|
||||
|
||||
sesh: Session = request.getSession()
|
||||
usr_sesh = IUserSession(sesh)
|
||||
if (
|
||||
usr_sesh.user_id == 0
|
||||
or usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value
|
||||
):
|
||||
return redirectTo(b"/gate", request)
|
||||
|
||||
if uri.startswith("/sys/lookup.user?"):
|
||||
uri_parse = parse.parse_qs(
|
||||
uri.replace("/sys/lookup.user?", "")
|
||||
) # lop off the first bit
|
||||
uid_search = uri_parse.get("usrId")
|
||||
email_search = uri_parse.get("usrEmail")
|
||||
uname_search = uri_parse.get("usrName")
|
||||
|
||||
if uid_search is not None:
|
||||
u = self.data.user.get_user(uid_search[0])
|
||||
if u is not None:
|
||||
usrlist.append(u._asdict())
|
||||
|
||||
elif email_search is not None:
|
||||
u = self.data.user.find_user_by_email(email_search[0])
|
||||
if u is not None:
|
||||
usrlist.append(u._asdict())
|
||||
|
||||
elif uname_search is not None:
|
||||
ul = self.data.user.find_user_by_username(uname_search[0])
|
||||
for u in ul:
|
||||
usrlist.append(u._asdict())
|
||||
|
||||
elif uri.startswith("/sys/lookup.arcade?"):
|
||||
uri_parse = parse.parse_qs(
|
||||
uri.replace("/sys/lookup.arcade?", "")
|
||||
) # lop off the first bit
|
||||
ac_id_search = uri_parse.get("arcadeId")
|
||||
ac_name_search = uri_parse.get("arcadeName")
|
||||
ac_user_search = uri_parse.get("arcadeUser")
|
||||
|
||||
if ac_id_search is not None:
|
||||
u = self.data.arcade.get_arcade(ac_id_search[0])
|
||||
if u is not None:
|
||||
aclist.append(u._asdict())
|
||||
|
||||
elif ac_name_search is not None:
|
||||
ul = self.data.arcade.find_arcade_by_name(ac_name_search[0])
|
||||
for u in ul:
|
||||
aclist.append(u._asdict())
|
||||
|
||||
elif ac_user_search is not None:
|
||||
ul = self.data.arcade.get_arcades_managed_by_user(ac_user_search[0])
|
||||
for u in ul:
|
||||
aclist.append(u._asdict())
|
||||
|
||||
elif uri.startswith("/sys/lookup.cab?"):
|
||||
uri_parse = parse.parse_qs(
|
||||
uri.replace("/sys/lookup.cab?", "")
|
||||
) # lop off the first bit
|
||||
cab_id_search = uri_parse.get("cabId")
|
||||
cab_serial_search = uri_parse.get("cabSerial")
|
||||
cab_acid_search = uri_parse.get("cabAcId")
|
||||
|
||||
if cab_id_search is not None:
|
||||
u = self.data.arcade.get_machine(id=cab_id_search[0])
|
||||
if u is not None:
|
||||
cablist.append(u._asdict())
|
||||
|
||||
elif cab_serial_search is not None:
|
||||
u = self.data.arcade.get_machine(serial=cab_serial_search[0])
|
||||
if u is not None:
|
||||
cablist.append(u._asdict())
|
||||
|
||||
elif cab_acid_search is not None:
|
||||
ul = self.data.arcade.get_arcade_machines(cab_acid_search[0])
|
||||
for u in ul:
|
||||
cablist.append(u._asdict())
|
||||
|
||||
return template.render(
|
||||
title=f"{self.core_config.server.name} | System",
|
||||
sesh=vars(usr_sesh),
|
||||
usrlist=usrlist,
|
||||
aclist=aclist,
|
||||
cablist=cablist,
|
||||
active_page="sys",
|
||||
).encode("utf-16")
|
||||
|
||||
|
||||
@@ -241,3 +419,56 @@ class FE_Game(FE_Base):
|
||||
|
||||
def render_GET(self, request: Request) -> bytes:
|
||||
return redirectTo(b"/user", request)
|
||||
|
||||
|
||||
class FE_Arcade(FE_Base):
|
||||
def render_GET(self, request: Request):
|
||||
uri = request.uri.decode()
|
||||
template = self.environment.get_template("core/frontend/arcade/index.jinja")
|
||||
managed = []
|
||||
|
||||
sesh: Session = request.getSession()
|
||||
usr_sesh = IUserSession(sesh)
|
||||
if usr_sesh.user_id == 0:
|
||||
return redirectTo(b"/gate", request)
|
||||
|
||||
m = re.match("\/arcade\/(\d*)", uri)
|
||||
|
||||
if m is not None:
|
||||
arcadeid = m.group(1)
|
||||
perms = self.data.arcade.get_manager_permissions(usr_sesh.user_id, arcadeid)
|
||||
arcade = self.data.arcade.get_arcade(arcadeid)
|
||||
|
||||
if perms is None:
|
||||
perms = 0
|
||||
|
||||
else:
|
||||
return redirectTo(b"/user", request)
|
||||
|
||||
return template.render(
|
||||
title=f"{self.core_config.server.name} | Arcade",
|
||||
sesh=vars(usr_sesh),
|
||||
error=0,
|
||||
perms=perms,
|
||||
arcade=arcade._asdict(),
|
||||
active_page="arcade",
|
||||
).encode("utf-16")
|
||||
|
||||
|
||||
class FE_Machine(FE_Base):
|
||||
def render_GET(self, request: Request):
|
||||
uri = request.uri.decode()
|
||||
template = self.environment.get_template("core/frontend/machine/index.jinja")
|
||||
|
||||
sesh: Session = request.getSession()
|
||||
usr_sesh = IUserSession(sesh)
|
||||
if usr_sesh.user_id == 0:
|
||||
return redirectTo(b"/gate", request)
|
||||
|
||||
return template.render(
|
||||
title=f"{self.core_config.server.name} | Machine",
|
||||
sesh=vars(usr_sesh),
|
||||
arcade={},
|
||||
error=0,
|
||||
active_page="machine",
|
||||
).encode("utf-16")
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{% extends "core/frontend/index.jinja" %}
|
||||
{% block content %}
|
||||
<h1>{{ arcade.name }}</h1>
|
||||
{% endblock content %}
|
||||
@@ -1,24 +1,44 @@
|
||||
{% extends "core/frontend/index.jinja" %}
|
||||
{% block content %}
|
||||
<h1>Create User</h1>
|
||||
<form id="create" style="max-width: 240px; min-width: 10%;" action="/gate/gate.create" method="post">
|
||||
<div class="form-group row">
|
||||
<label for="access_code">Card Access Code</label><br>
|
||||
<input class="form-control" name="access_code" id="access_code" type="text" placeholder="00000000000000000000" value={{ code }} maxlength="20" readonly>
|
||||
<div class="row justify-content-md-center form-signin">
|
||||
<div class="col col-lg-4">
|
||||
<form id="create" action="/gate/gate.create" method="post">
|
||||
<h1 class="h3 mb-3 fw-normal">Sign-up</h1>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input class="form-control" name="access-code" id="access-code" type="text"
|
||||
placeholder="00000000000000000000" value={{ code }} maxlength="20" required>
|
||||
<label for="access-code">Access Code*</label>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="username">Username</label><br>
|
||||
<input id="username" class="form-control" name="username" type="text" placeholder="username">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control" name="username" id="username" placeholder="username">
|
||||
<label for="username">Username*</label>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="email">Email</label><br>
|
||||
<input id="email" class="form-control" name="email" type="email" placeholder="example@example.com">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="email" class="form-control" name="email" id="email" placeholder="example@example.com">
|
||||
<label for="email">E-Mail*</label>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="passwd">Password</label><br>
|
||||
<input id="passwd" class="form-control" name="passwd" type="password" placeholder="password">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="password" class="form-control" name="passwd" id="passwd" placeholder="Password">
|
||||
<label for="passwd">Password*</label>
|
||||
</div>
|
||||
<p></p>
|
||||
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" type="submit" value="Create">
|
||||
</form>
|
||||
|
||||
<!--
|
||||
<div class="form-check text-start my-3">
|
||||
<input class="form-check-input" type="checkbox" value="remember-me" id="remember-me">
|
||||
<label class="form-check-label" for="remember-me">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
If you have not registered a card with this server, you cannot create a WebUI account!
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-100 py-2" type="submit">Sign-up</button>
|
||||
<!-- <p class="mt-5 mb-3 text-body-secondary">© 2023 ARTEMiS</p>-->
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -1,32 +1,52 @@
|
||||
{% extends "core/frontend/index.jinja" %}
|
||||
{% block content %}
|
||||
<h1>Gate</h1>
|
||||
{% include "core/frontend/widgets/err_banner.jinja" %}
|
||||
<style>
|
||||
/* Chrome, Safari, Edge, Opera */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
.form-signin input[type="text"] {
|
||||
margin-bottom: -1px;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
.form-signin input[type="password"] {
|
||||
margin-bottom: 10px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
</style>
|
||||
<form id="login" style="max-width: 240px; min-width: 10%;" action="/gate/gate.login" method="post">
|
||||
<div class="form-group row">
|
||||
<label for="access_code">Card Access Code</label><br>
|
||||
<input form="login" class="form-control" name="access_code" id="access_code" type="number" placeholder="00000000000000000000" maxlength="20" required>
|
||||
<div class="row justify-content-md-center form-signin">
|
||||
<div class="col col-lg-4">
|
||||
<form id="login" action="/gate/gate.login" method="post">
|
||||
<h1 class="h3 mb-3 fw-normal">Login</h1>
|
||||
|
||||
{% include "core/frontend/widgets/err_banner.jinja" %}
|
||||
|
||||
<div class="form-floating">
|
||||
<input form="login" class="form-control" name="access-code" id="access-code" type="text"
|
||||
placeholder="00000000000000000000" maxlength="20" required>
|
||||
<label for="access-code">Access Code</label>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="passwd">Password</label><br>
|
||||
<input id="passwd" class="form-control" name="passwd" type="password" placeholder="password">
|
||||
<div class="form-floating">
|
||||
<input form="login" type="password" class="form-control" name="passwd" id="passwd" placeholder="Password">
|
||||
<label for="passwd">Password</label>
|
||||
</div>
|
||||
<p></p>
|
||||
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" form="login" type="submit" value="Login">
|
||||
</form>
|
||||
<h6>*To register for the webui, type in the access code of your card, as shown in a game, and leave the password field blank.</h6>
|
||||
<h6>*If you have not registered a card with this server, you cannot create a webui account.</h6>
|
||||
|
||||
<!--
|
||||
<div class="form-check text-start my-3">
|
||||
<input class="form-check-input" type="checkbox" value="remember-me" id="remember-me">
|
||||
<label class="form-check-label" for="remember-me">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div class="alert alert-info" role="alert">
|
||||
To register for the WebUI, type in the <strong>access code</strong> of your card, as shown in a game, and leave the
|
||||
password field blank.
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-100 py-2" type="submit">Sign in</button>
|
||||
<!-- <p class="mt-5 mb-3 text-body-secondary">© 2023 ARTEMiS</p>-->
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
+18
-81
@@ -1,88 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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">
|
||||
<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>
|
||||
<style>
|
||||
html {
|
||||
background-color: #181a1b !important;
|
||||
margin: 10px;
|
||||
}
|
||||
html {
|
||||
color-scheme: dark !important;
|
||||
}
|
||||
html, body, input, textarea, select, button, dialog {
|
||||
background-color: #181a1b;
|
||||
}
|
||||
html, body, input, textarea, select, button {
|
||||
border-color: #736b5e;
|
||||
color: #e8e6e3;
|
||||
}
|
||||
a {
|
||||
color: #3391ff;
|
||||
}
|
||||
table {
|
||||
border-color: #545b5e;
|
||||
}
|
||||
::placeholder {
|
||||
color: #b2aba1;
|
||||
}
|
||||
input:-webkit-autofill,
|
||||
textarea:-webkit-autofill,
|
||||
select:-webkit-autofill {
|
||||
background-color: #404400 !important;
|
||||
color: #e8e6e3 !important;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
background-color: #202324;
|
||||
color: #aba499;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #454a4d;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #575e62;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background-color: #484e51;
|
||||
}
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: #181a1b;
|
||||
}
|
||||
* {
|
||||
scrollbar-color: #454a4d #202324;
|
||||
}
|
||||
::selection {
|
||||
background-color: #004daa !important;
|
||||
color: #e8e6e3 !important;
|
||||
}
|
||||
::-moz-selection {
|
||||
background-color: #004daa !important;
|
||||
color: #e8e6e3 !important;
|
||||
}
|
||||
input[type="text"], input[type="text"]:focus, input[type="password"], input[type="password"]:focus, input[type="email"], input[type="email"]:focus {
|
||||
background-color: #202324 !important;
|
||||
color: #e8e6e3;
|
||||
}
|
||||
form {
|
||||
outline: 1px solid grey;
|
||||
padding: 20px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.err-banner {
|
||||
background-color: #AA0000;
|
||||
padding: 20px;
|
||||
margin-bottom: 10px;
|
||||
width: 15%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% include "core/frontend/widgets/topbar.jinja" %}
|
||||
<div class="container">
|
||||
{% block content %}
|
||||
<h1>{{ server_name }}</h1>
|
||||
{% endblock content %}
|
||||
</body>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm"
|
||||
crossorigin="anonymous"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
{% extends "core/frontend/index.jinja" %}
|
||||
{% block content %}
|
||||
{% include "core/frontend/widgets/err_banner.jinja" %}
|
||||
<h1>Machine Management</h1>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,98 @@
|
||||
{% extends "core/frontend/index.jinja" %}
|
||||
{% block content %}
|
||||
<h1>System Management</h1>
|
||||
|
||||
<div class="row" id="rowForm">
|
||||
{% if sesh.permissions >= 2 %}
|
||||
<div class="col-sm-6" style="max-width: 25%;">
|
||||
<form id="usrLookup" name="usrLookup" action="/sys/lookup.user" class="form-inline">
|
||||
<h3>User Search</h3>
|
||||
<div class="form-group">
|
||||
<label for="usrEmail">Email address</label>
|
||||
<input type="email" class="form-control" id="usrEmail" name="usrEmail" aria-describedby="emailHelp">
|
||||
</div>
|
||||
OR
|
||||
<div class="form-group">
|
||||
<label for="usrName">Username</label>
|
||||
<input type="text" class="form-control" id="usrName" name="usrName">
|
||||
</div>
|
||||
OR
|
||||
<div class="form-group">
|
||||
<label for="usrId">User ID</label>
|
||||
<input type="number" class="form-control" id="usrId" name="usrId">
|
||||
</div>
|
||||
<br />
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if sesh.permissions >= 4 %}
|
||||
<div class="col-sm-6" style="max-width: 25%;">
|
||||
<form id="arcadeLookup" name="arcadeLookup" action="/sys/lookup.arcade" class="form-inline" >
|
||||
<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">
|
||||
<label for="arcadeId">Arcade ID</label>
|
||||
<input type="number" class="form-control" id="arcadeId" name="arcadeId">
|
||||
</div>
|
||||
OR
|
||||
<div class="form-group">
|
||||
<label for="arcadeUser">Owner User ID</label>
|
||||
<input type="number" class="form-control" id="arcadeUser" name="arcadeUser">
|
||||
</div>
|
||||
<br />
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-sm-6" style="max-width: 25%;">
|
||||
<form id="cabLookup" name="cabLookup" action="/sys/lookup.cab" class="form-inline" >
|
||||
<h3>Machine Search</h3>
|
||||
<div class="form-group">
|
||||
<label for="cabSerial">Machine Serial</label>
|
||||
<input type="text" class="form-control" id="cabSerial" name="cabSerial">
|
||||
</div>
|
||||
OR
|
||||
<div class="form-group">
|
||||
<label for="cabId">Machine ID</label>
|
||||
<input type="number" class="form-control" id="cabId" name="cabId">
|
||||
</div>
|
||||
OR
|
||||
<div class="form-group">
|
||||
<label for="cabAcId">Arcade ID</label>
|
||||
<input type="number" class="form-control" id="cabAcId" name="cabAcId">
|
||||
</div>
|
||||
<br />
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row" id="rowResult" style="margin: 10px;">
|
||||
{% if sesh.permissions >= 2 %}
|
||||
<div id="userSearchResult" class="col-sm-6" style="max-width: 25%;">
|
||||
{% for usr in usrlist %}
|
||||
<pre><a href=/user/{{ usr.id }}>{{ usr.id }} | {{ usr.username }}</a></pre>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if sesh.permissions >= 4 %}
|
||||
<div id="arcadeSearchResult" class="col-sm-6" style="max-width: 25%;">
|
||||
{% for ac in aclist %}
|
||||
<pre><a href=/arcade/{{ ac.id }}>{{ ac.id }} | {{ ac.name }}</a></pre>
|
||||
{% endfor %}
|
||||
</div
|
||||
><div id="cabSearchResult" class="col-sm-6" style="max-width: 25%;">
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row" id="rowAdd">
|
||||
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -1,4 +1,126 @@
|
||||
{% extends "core/frontend/index.jinja" %}
|
||||
{% block content %}
|
||||
<h1>testing</h1>
|
||||
<h1>{{ sesh["username"] }}'s Account</h1>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Cards</h3>
|
||||
<!--<h4 class="card-subtitle mb-2 text-body-secondary">Card subtitle</h4>-->
|
||||
<p class="card-text">All aime cards are listed here for the given user.</p>
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target="#card-add" class="btn btn-primary mb-3">Add Card</a>
|
||||
{% if cards is defined and cards|length > 0 %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Access Code</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in cards %}
|
||||
<tr class="align-middle">
|
||||
<th scope="row">{{ c.index }}</th>
|
||||
<td>{{ c.access_code }}</td>
|
||||
<td>
|
||||
{% if c.status == 'Active'%}
|
||||
<span class="badge rounded-pill text-bg-success">Active</span>
|
||||
{% elif c.status == 'Locked' %}
|
||||
<span class="badge rounded-pill text-bg-warning">Locked</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.status == 'Active'%}
|
||||
<button type="button" class="btn btn-warning btn-sm">Lock</i></button>
|
||||
{% elif c.status == 'Locked' %}
|
||||
<button type="button" class="btn btn-success btn-sm">Unlock</i></button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-danger btn-sm">Delete</i></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
You have no cards registered to your account. Please add one.
|
||||
</div>
|
||||
{% endif %}
|
||||
<!--<a href="#" data-bs-toggle="modal" data-bs-target="#card-add" class="card-link">Add Card</a>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Arcades</h3>
|
||||
<!--<h4 class="card-subtitle mb-2 text-body-secondary">Card subtitle</h4>-->
|
||||
<p class="card-text">All arcades connected to the given account are listed here.</p>
|
||||
{% if arcades is defined and arcades|length > 0 %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in arcades %}
|
||||
<tr class="align-middle clickable-row" data-href=/arcade/{{ a.id }}>
|
||||
<th scope="row">{{ a.index }}</th>
|
||||
<td>{{ a.name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
You have no arcades connected to your account.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="card-add" tabindex="-1" aria-labelledby="card-add-label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="card-add-label">Add Card</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h3 class="fs-5">How to:</h3>
|
||||
Scan your card on any networked game and press the "View Access Code" button (varies by game) and enter
|
||||
the 20
|
||||
digit code below.
|
||||
<hr>
|
||||
FOR AMUSE IC CARDS: DO NOT ENTER THE CODE SHOWN ON THE BACK OF THE CARD ITSELF OR IT WILL NOT WORK!
|
||||
<form>
|
||||
<div class="form-floating mt-3">
|
||||
<label for="access-code" class="col-form-label">Access Code</label>
|
||||
<input maxlength="20" type="text" required pattern="[0-9]{20}" class="form-control"
|
||||
id="access-code">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
@@ -1,12 +1,15 @@
|
||||
{% if error > 0 %}
|
||||
<div class="err-banner">
|
||||
<h3>Error</h3>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{% if error == 1 %}
|
||||
Card not registered, or wrong password
|
||||
{% elif error == 2 %}
|
||||
Missing or malformed access code
|
||||
{% elif error == 3 %}
|
||||
Failed to create user
|
||||
{% elif error == 4 %}
|
||||
Arcade not found
|
||||
{% elif error == 5 %}
|
||||
Machine not found
|
||||
{% else %}
|
||||
An unknown error occoured
|
||||
{% endif %}
|
||||
|
||||
@@ -1,18 +1,56 @@
|
||||
<div style="background: #333; color: #f9f9f9; width: 10%; height: 50px; line-height: 50px; text-align: center; float: left;">
|
||||
Navigation
|
||||
</div>
|
||||
<div style="background: #333; color: #f9f9f9; width: 80%; height: 50px; line-height: 50px; padding-left: 10px; float: left;">
|
||||
<a href=/><button class="btn btn-primary">Home</button></a>
|
||||
{% for game in game_list %}
|
||||
<a href=game/{{ game.url }}><button class="btn btn-success">{{ game.name }}</button></a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div style="background: #333; color: #f9f9f9; width: 10%; height: 50px; line-height: 50px; text-align: center; float: left;">
|
||||
{% if sesh is defined and sesh["userId"] > 0 %}
|
||||
<a href="/user"><button class="btn btn-primary">Account</button></a>
|
||||
{% else %}
|
||||
<a href="/gate"><button class="btn btn-primary">Gate</button></a>
|
||||
{% endif %}
|
||||
<nav class="p-3 mb-3 navbar navbar-expand-lg bg-body-tertiary" aria-label="Thirteenth navbar example">
|
||||
<div class="container">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarsExample11"
|
||||
aria-controls="navbarsExample11" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div class="collapse navbar-collapse d-lg-flex" id="navbarsExample11">
|
||||
<a class="navbar-brand col-lg-3 me-0" href= />ARTEMiS</a>
|
||||
<ul class="navbar-nav col-lg-6 justify-content-lg-center nav-pills">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == '/' %}active{% endif %}" aria-current="page" href= />Home</a>
|
||||
</li>
|
||||
{% for game in game_list %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == game.url %}active{% endif %}" href=/game/{{ game.url }}>{{ game.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<!--
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">Dropdown</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#">Action</a></li>
|
||||
<li><a class="dropdown-item" href="#">Another action</a></li>
|
||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
-->
|
||||
</ul>
|
||||
<div class="d-lg-flex col-lg-3 justify-content-lg-end">
|
||||
{% if sesh is defined and sesh["user_id"] > 0 %}
|
||||
<div class="btn-group dropdown">
|
||||
<a href="#" class="d-block link-body-emphasis text-decoration-none dropdown-toggle"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ sesh["username"] }}
|
||||
</a>
|
||||
<ul class="dropdown-menu text-small">
|
||||
{% if sesh["permissions"] >= 2 %}
|
||||
<li><a class="dropdown-item {% if active_page == 'sys' %}active{% endif %}" href="/sys">System</a></li>
|
||||
{% endif %}
|
||||
<li><a class="dropdown-item" href="#">Settings</a></li>
|
||||
<li><a class="dropdown-item {% if active_page == 'user' %}active{% endif %}" href="/user">Account</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="#">Sign out</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="/gate"><button class="btn {% if active_page == 'gate' %}btn-primary{% else %}btn-outline-primary{% endif %}">Login</button></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
+142
-28
@@ -4,6 +4,7 @@ from logging.handlers import TimedRotatingFileHandler
|
||||
from twisted.web import resource
|
||||
from twisted.web.http import Request
|
||||
from datetime import datetime
|
||||
from Crypto.Cipher import Blowfish
|
||||
import pytz
|
||||
|
||||
from core import CoreConfig
|
||||
@@ -33,8 +34,8 @@ class MuchaServlet:
|
||||
self.logger.addHandler(fileHandler)
|
||||
self.logger.addHandler(consoleHandler)
|
||||
|
||||
self.logger.setLevel(logging.INFO)
|
||||
coloredlogs.install(level=logging.INFO, logger=self.logger, fmt=log_fmt_str)
|
||||
self.logger.setLevel(cfg.mucha.loglevel)
|
||||
coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str)
|
||||
|
||||
all_titles = Utils.get_all_titles()
|
||||
|
||||
@@ -46,9 +47,7 @@ class MuchaServlet:
|
||||
if enabled:
|
||||
self.mucha_registry.append(game_cd)
|
||||
|
||||
self.logger.info(
|
||||
f"Serving {len(self.mucha_registry)} games"
|
||||
)
|
||||
self.logger.info(f"Serving {len(self.mucha_registry)} games")
|
||||
|
||||
def handle_boardauth(self, request: Request, _: Dict) -> bytes:
|
||||
req_dict = self.mucha_preprocess(request.content.getvalue())
|
||||
@@ -58,19 +57,24 @@ class MuchaServlet:
|
||||
self.logger.error(
|
||||
f"Error processing mucha request {request.content.getvalue()}"
|
||||
)
|
||||
return b""
|
||||
return b"RESULTS=000"
|
||||
|
||||
req = MuchaAuthRequest(req_dict)
|
||||
self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}")
|
||||
self.logger.debug(f"Mucha request {vars(req)}")
|
||||
self.logger.info(
|
||||
f"Boardauth request from {client_ip} for {req.gameVer}"
|
||||
)
|
||||
|
||||
if req.gameCd not in self.mucha_registry:
|
||||
self.logger.warn(f"Unknown gameCd {req.gameCd}")
|
||||
return b""
|
||||
self.logger.warning(f"Unknown gameCd {req.gameCd}")
|
||||
return b"RESULTS=000"
|
||||
|
||||
# TODO: Decrypt S/N
|
||||
b_key = b""
|
||||
for x in range(8):
|
||||
b_key += req.sendDate[(x - 1) & 7].encode()
|
||||
|
||||
cipher = Blowfish.new(b_key, Blowfish.MODE_ECB)
|
||||
sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum))
|
||||
self.logger.debug(f"Decrypt SN to {sn_decrypt.hex()}")
|
||||
|
||||
resp = MuchaAuthResponse(
|
||||
f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}"
|
||||
@@ -88,24 +92,37 @@ class MuchaServlet:
|
||||
self.logger.error(
|
||||
f"Error processing mucha request {request.content.getvalue()}"
|
||||
)
|
||||
return b""
|
||||
return b"RESULTS=000"
|
||||
|
||||
req = MuchaUpdateRequest(req_dict)
|
||||
self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}")
|
||||
self.logger.debug(f"Mucha request {vars(req)}")
|
||||
self.logger.info(
|
||||
f"Updatecheck request from {client_ip} for {req.gameVer}"
|
||||
)
|
||||
|
||||
if req.gameCd not in self.mucha_registry:
|
||||
self.logger.warn(f"Unknown gameCd {req.gameCd}")
|
||||
return b""
|
||||
self.logger.warning(f"Unknown gameCd {req.gameCd}")
|
||||
return b"RESULTS=000"
|
||||
|
||||
resp = MuchaUpdateResponseStub(req.gameVer)
|
||||
resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}")
|
||||
|
||||
self.logger.debug(f"Mucha response {vars(resp)}")
|
||||
|
||||
return self.mucha_postprocess(vars(resp))
|
||||
|
||||
def handle_dlstate(self, request: Request, _: Dict) -> bytes:
|
||||
req_dict = self.mucha_preprocess(request.content.getvalue())
|
||||
client_ip = Utils.get_ip_addr(request)
|
||||
|
||||
if req_dict is None:
|
||||
self.logger.error(
|
||||
f"Error processing mucha request {request.content.getvalue()}"
|
||||
)
|
||||
return b""
|
||||
|
||||
req = MuchaDownloadStateRequest(req_dict)
|
||||
self.logger.info(f"DownloadState request from {client_ip} for {req.gameCd} -> {req.updateVer}")
|
||||
self.logger.debug(f"request {vars(req)}")
|
||||
return b"RESULTS=001"
|
||||
|
||||
def mucha_preprocess(self, data: bytes) -> Optional[Dict]:
|
||||
try:
|
||||
ret: Dict[str, Any] = {}
|
||||
@@ -117,19 +134,17 @@ class MuchaServlet:
|
||||
|
||||
return ret
|
||||
|
||||
except:
|
||||
except Exception:
|
||||
self.logger.error(f"Error processing mucha request {data}")
|
||||
return None
|
||||
|
||||
def mucha_postprocess(self, data: dict) -> Optional[bytes]:
|
||||
try:
|
||||
urlencode = ""
|
||||
for k, v in data.items():
|
||||
urlencode += f"{k}={v}&"
|
||||
urlencode = "&".join(f"{k}={v}" for k, v in data.items())
|
||||
|
||||
return urlencode.encode()
|
||||
|
||||
except:
|
||||
except Exception:
|
||||
self.logger.error("Error processing mucha response")
|
||||
return None
|
||||
|
||||
@@ -209,21 +224,120 @@ class MuchaUpdateRequest:
|
||||
class MuchaUpdateResponse:
|
||||
def __init__(self, game_ver: str, mucha_url: str) -> None:
|
||||
self.RESULTS = "001"
|
||||
self.EXE_VER = game_ver
|
||||
|
||||
self.UPDATE_VER_1 = game_ver
|
||||
self.UPDATE_URL_1 = f"https://{mucha_url}/updUrl1/"
|
||||
self.UPDATE_SIZE_1 = "0"
|
||||
self.UPDATE_CRC_1 = "0000000000000000"
|
||||
self.CHECK_URL_1 = f"https://{mucha_url}/checkUrl/"
|
||||
self.EXE_VER_1 = game_ver
|
||||
self.UPDATE_URL_1 = f"http://{mucha_url}/updUrl1/"
|
||||
self.UPDATE_SIZE_1 = "20"
|
||||
|
||||
self.CHECK_CRC_1 = "0000000000000000"
|
||||
self.CHECK_URL_1 = f"http://{mucha_url}/checkUrl/"
|
||||
self.CHECK_SIZE_1 = "20"
|
||||
|
||||
self.INFO_SIZE_1 = "0"
|
||||
self.COM_SIZE_1 = "0"
|
||||
self.COM_TIME_1 = "0"
|
||||
self.LAN_INFO_SIZE_1 = "0"
|
||||
|
||||
self.USER_ID = ""
|
||||
self.PASSWORD = ""
|
||||
|
||||
"""
|
||||
RESULTS
|
||||
EXE_VER
|
||||
|
||||
UPDATE_VER_%d
|
||||
UPDATE_URL_%d
|
||||
UPDATE_SIZE_%d
|
||||
|
||||
CHECK_CRC_%d
|
||||
CHECK_URL_%d
|
||||
CHECK_SIZE_%d
|
||||
|
||||
INFO_SIZE_1
|
||||
COM_SIZE_1
|
||||
COM_TIME_1
|
||||
LAN_INFO_SIZE_1
|
||||
|
||||
USER_ID
|
||||
PASSWORD
|
||||
"""
|
||||
class MuchaUpdateResponseStub:
|
||||
def __init__(self, game_ver: str) -> None:
|
||||
self.RESULTS = "001"
|
||||
self.UPDATE_VER_1 = game_ver
|
||||
|
||||
class MuchaDownloadStateRequest:
|
||||
def __init__(self, request: Dict) -> None:
|
||||
self.gameCd = request.get("gameCd", "")
|
||||
self.updateVer = request.get("updateVer", "")
|
||||
self.serialNum = request.get("serialNum", "")
|
||||
self.fileSize = request.get("fileSize", "")
|
||||
self.compFileSize = request.get("compFileSize", "")
|
||||
self.boardId = request.get("boardId", "")
|
||||
self.placeId = request.get("placeId", "")
|
||||
self.storeRouterIp = request.get("storeRouterIp", "")
|
||||
|
||||
class MuchaDownloadErrorRequest:
|
||||
def __init__(self, request: Dict) -> None:
|
||||
self.gameCd = request.get("gameCd", "")
|
||||
self.updateVer = request.get("updateVer", "")
|
||||
self.serialNum = request.get("serialNum", "")
|
||||
self.downloadUrl = request.get("downloadUrl", "")
|
||||
self.errCd = request.get("errCd", "")
|
||||
self.errMessage = request.get("errMessage", "")
|
||||
self.boardId = request.get("boardId", "")
|
||||
self.placeId = request.get("placeId", "")
|
||||
self.storeRouterIp = request.get("storeRouterIp", "")
|
||||
|
||||
class MuchaRegiAuthRequest:
|
||||
def __init__(self, request: Dict) -> None:
|
||||
self.gameCd = request.get("gameCd", "")
|
||||
self.serialNum = request.get("serialNum", "") # Encrypted
|
||||
self.countryCd = request.get("countryCd", "")
|
||||
self.registrationCd = request.get("registrationCd", "")
|
||||
self.sendDate = request.get("sendDate", "")
|
||||
self.useToken = request.get("useToken", "")
|
||||
self.allToken = request.get("allToken", "")
|
||||
self.placeId = request.get("placeId", "")
|
||||
self.storeRouterIp = request.get("storeRouterIp", "")
|
||||
|
||||
class MuchaRegiAuthResponse:
|
||||
def __init__(self) -> None:
|
||||
self.RESULTS = "001" # 001 = success, 099, 098, 097 = fail, others = fail
|
||||
self.ALL_TOKEN = "0" # Encrypted
|
||||
self.ADD_TOKEN = "0" # Encrypted
|
||||
|
||||
class MuchaTokenStateRequest:
|
||||
def __init__(self, request: Dict) -> None:
|
||||
self.gameCd = request.get("gameCd", "")
|
||||
self.serialNum = request.get("serialNum", "")
|
||||
self.countryCd = request.get("countryCd", "")
|
||||
self.useToken = request.get("useToken", "")
|
||||
self.allToken = request.get("allToken", "")
|
||||
self.placeId = request.get("placeId", "")
|
||||
self.storeRouterIp = request.get("storeRouterIp", "")
|
||||
|
||||
class MuchaTokenStateResponse:
|
||||
def __init__(self) -> None:
|
||||
self.RESULTS = "001"
|
||||
|
||||
class MuchaTokenMarginStateRequest:
|
||||
def __init__(self, request: Dict) -> None:
|
||||
self.gameCd = request.get("gameCd", "")
|
||||
self.serialNum = request.get("serialNum", "")
|
||||
self.countryCd = request.get("countryCd", "")
|
||||
self.placeId = request.get("placeId", "")
|
||||
self.limitLowerToken = request.get("limitLowerToken", 0)
|
||||
self.limitUpperToken = request.get("limitUpperToken", 0)
|
||||
self.settlementMonth = request.get("settlementMonth", 0)
|
||||
|
||||
class MuchaTokenMarginStateResponse:
|
||||
def __init__(self) -> None:
|
||||
self.RESULTS = "001"
|
||||
self.LIMIT_LOWER_TOKEN = 0
|
||||
self.LIMIT_UPPER_TOKEN = 0
|
||||
self.LAST_SETTLEMENT_MONTH = 0
|
||||
self.LAST_LIMIT_LOWER_TOKEN = 0
|
||||
self.LAST_LIMIT_UPPER_TOKEN = 0
|
||||
self.SETTLEMENT_MONTH = 0
|
||||
|
||||
+6
-6
@@ -62,7 +62,7 @@ class TitleServlet:
|
||||
self.title_registry[code] = handler_cls
|
||||
|
||||
else:
|
||||
self.logger.warn(f"Game {folder} has no get_allnet_info")
|
||||
self.logger.warning(f"Game {folder} has no get_allnet_info")
|
||||
|
||||
else:
|
||||
self.logger.error(f"{folder} missing game_code or index in __init__.py")
|
||||
@@ -74,28 +74,28 @@ class TitleServlet:
|
||||
def render_GET(self, request: Request, endpoints: dict) -> bytes:
|
||||
code = endpoints["game"]
|
||||
if code not in self.title_registry:
|
||||
self.logger.warn(f"Unknown game code {code}")
|
||||
self.logger.warning(f"Unknown game code {code}")
|
||||
request.setResponseCode(404)
|
||||
return b""
|
||||
|
||||
index = self.title_registry[code]
|
||||
if not hasattr(index, "render_GET"):
|
||||
self.logger.warn(f"{code} does not dispatch GET")
|
||||
self.logger.warning(f"{code} does not dispatch GET")
|
||||
request.setResponseCode(405)
|
||||
return b""
|
||||
|
||||
return index.render_GET(request, endpoints["version"], endpoints["endpoint"])
|
||||
return index.render_GET(request, int(endpoints["version"]), endpoints["endpoint"])
|
||||
|
||||
def render_POST(self, request: Request, endpoints: dict) -> bytes:
|
||||
code = endpoints["game"]
|
||||
if code not in self.title_registry:
|
||||
self.logger.warn(f"Unknown game code {code}")
|
||||
self.logger.warning(f"Unknown game code {code}")
|
||||
request.setResponseCode(404)
|
||||
return b""
|
||||
|
||||
index = self.title_registry[code]
|
||||
if not hasattr(index, "render_POST"):
|
||||
self.logger.warn(f"{code} does not dispatch POST")
|
||||
self.logger.warning(f"{code} does not dispatch POST")
|
||||
request.setResponseCode(405)
|
||||
return b""
|
||||
|
||||
|
||||
+8
-1
@@ -16,6 +16,9 @@ class Utils:
|
||||
if not dir.startswith("__"):
|
||||
try:
|
||||
mod = importlib.import_module(f"titles.{dir}")
|
||||
if hasattr(mod, "game_codes") and hasattr(
|
||||
mod, "index"
|
||||
): # Minimum required to function
|
||||
ret[dir] = mod
|
||||
|
||||
except ImportError as e:
|
||||
@@ -25,4 +28,8 @@ class Utils:
|
||||
|
||||
@classmethod
|
||||
def get_ip_addr(cls, req: Request) -> str:
|
||||
return req.getAllHeaders()[b"x-forwarded-for"].decode() if b"x-forwarded-for" in req.getAllHeaders() else req.getClientAddress().host
|
||||
return (
|
||||
req.getAllHeaders()[b"x-forwarded-for"].decode()
|
||||
if b"x-forwarded-for" in req.getAllHeaders()
|
||||
else req.getClientAddress().host
|
||||
)
|
||||
|
||||
+19
-7
@@ -1,5 +1,6 @@
|
||||
import yaml
|
||||
import argparse
|
||||
import logging
|
||||
from core.config import CoreConfig
|
||||
from core.data import Data
|
||||
from os import path, mkdir, access, W_OK
|
||||
@@ -32,7 +33,9 @@ if __name__ == "__main__":
|
||||
|
||||
cfg = CoreConfig()
|
||||
if path.exists(f"{args.config}/core.yaml"):
|
||||
cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
|
||||
cfg_dict = yaml.safe_load(open(f"{args.config}/core.yaml"))
|
||||
cfg_dict.get("database", {})["loglevel"] = "info"
|
||||
cfg.update(cfg_dict)
|
||||
|
||||
if not path.exists(cfg.server.log_dir):
|
||||
mkdir(cfg.server.log_dir)
|
||||
@@ -45,7 +48,6 @@ if __name__ == "__main__":
|
||||
|
||||
data = Data(cfg)
|
||||
|
||||
|
||||
if args.action == "create":
|
||||
data.create_database()
|
||||
|
||||
@@ -54,15 +56,22 @@ if __name__ == "__main__":
|
||||
|
||||
elif args.action == "upgrade" or args.action == "rollback":
|
||||
if args.version is None:
|
||||
data.logger.error("Must set game and version to migrate to")
|
||||
exit(0)
|
||||
data.logger.warning("No version set, upgrading to latest")
|
||||
|
||||
if args.game is None:
|
||||
data.logger.info("No game set, upgrading core schema")
|
||||
data.migrate_database("CORE", int(args.version), args.action)
|
||||
data.logger.warning("No game set, upgrading core schema")
|
||||
data.migrate_database(
|
||||
"CORE",
|
||||
int(args.version) if args.version is not None else None,
|
||||
args.action,
|
||||
)
|
||||
|
||||
else:
|
||||
data.migrate_database(args.game, int(args.version), args.action)
|
||||
data.migrate_database(
|
||||
args.game,
|
||||
int(args.version) if args.version is not None else None,
|
||||
args.action,
|
||||
)
|
||||
|
||||
elif args.action == "autoupgrade":
|
||||
data.autoupgrade()
|
||||
@@ -76,4 +85,7 @@ if __name__ == "__main__":
|
||||
elif args.action == "cleanup":
|
||||
data.delete_hanging_users()
|
||||
|
||||
elif args.action == "version":
|
||||
data.show_versions()
|
||||
|
||||
data.logger.info("Done")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user