mirror of
https://github.com/hykilpikonna/AquaDX.git
synced 2026-09-13 20:06:01 -05:00
feat: Title Server Encryption (#216)
i still have a few things i'd like to do but i want to get this merged in and deployed asap
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -84,3 +84,6 @@ src/main/resources/meta/*/*.json
|
||||
test-diff
|
||||
htmlReport
|
||||
docs/logs
|
||||
|
||||
# Sensitive keys
|
||||
game_encryption.json
|
||||
@@ -144,10 +144,9 @@ export const EN_REF_SETUP = {
|
||||
'setup.welcome': `Welcome! If you have a game set up, please follow the instructions below to set up the connection with AquaDX.`,
|
||||
'setup.keychip-warning': `Your keychip(s) are linked to your account and should be kept secure.`,
|
||||
'setup.steps.one': `Pick a method of setting up network communications. Some browsers may not be able to do automatic setup.`,
|
||||
'setup.steps.two': `Ensure your game has encryption disabled as AquaDX does not support encryption. This may be via a patch or setting.`,
|
||||
'setup.steps.three': `Link your Aime card to your AquaDX account using the <a href="/cards">Cards</a> page via it's access code or serial number.`,
|
||||
'setup.steps.four': `Start the game. Upon reaching the title screen, the network icon in the corner should now show green instead of grey.`,
|
||||
'setup.support-info': `If you need assistance, feel free to make an inquiry in a <a href="/support">support channel</a>.`,
|
||||
'setup.steps.two': `Link your Aime card to your AquaDX account using the <a href="/cards">Cards</a> page via it's access code or serial number.`,
|
||||
'setup.steps.three': `Start the game. Upon reaching the title screen, the network icon in the corner should now show green instead of grey.`,
|
||||
'setup.support-info': `If you need additional assistance, feel free to make an inquiry in a <a href="/support">support channel</a>.`,
|
||||
'setup.reveal-keychip': `Reveal keychip`,
|
||||
'setup.type.automatic': `Automatic Setup`,
|
||||
'setup.type.manual': `Manual Setup`,
|
||||
@@ -156,6 +155,15 @@ export const EN_REF_SETUP = {
|
||||
'setup.automatic.success': `Success, data has been updated`,
|
||||
'setup.automatic.failure': `Failed to update information, please ensure access is not blocked and try again.`,
|
||||
'setup.automatic.select': `Pick file`,
|
||||
'setup.troubleshooting.header': 'Setup Troubleshooting (FAQ)',
|
||||
'setup.troubleshooting.items.one': `» It displays DNS BAD on Network Test<br>
|
||||
You likely misconfigured your segatools.ini. Ensure it's pointing to the DNS shown above (under Manual Setup).`,
|
||||
'setup.troubleshooting.items.two': `» It displays タイトル サーバー BAD (Title Server BAD) on Network Test<br>
|
||||
You should check your segatools.ini to ensure your keychip is set correctly.<br>
|
||||
Additionally, Card Maker users and players using a game version higher than what's listed may need to disable encryption
|
||||
<a target="_blank" href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/SDED#patching-assemblycsharpdll">(instructions for Card Maker)</a>`,
|
||||
'setup.troubleshooting.items.three': `» I can't scan my card<br>
|
||||
The built-in emulated card reader, by default, is configured to use the Enter / Return key to scan in (hold the key).`,
|
||||
'setup.keychip': 'Keychip Management',
|
||||
'setup.keychip.warning': 'Keychips can be manually set here for cabinet owners with physical keychips trying to connect to AquaDX. Selecting a keychip will autofill it for the setup below.',
|
||||
'setup.keychip.add': `Add keychip`,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
EN_REF_LEADERBOARD,
|
||||
EN_REF_MAI_PHOTO,
|
||||
EN_REF_SETTINGS,
|
||||
EN_REF_SETUP,
|
||||
EN_REF_USER,
|
||||
EN_REF_USERBOX,
|
||||
type EN_REF_Welcome
|
||||
@@ -59,15 +60,15 @@ const zhWelcome: typeof EN_REF_Welcome = {
|
||||
'welcome.username-missing': '用户名/邮箱必须填哦',
|
||||
'welcome.email-password-missing': '邮箱和密码必须填哦',
|
||||
'welcome.waiting-turnstile': '正在验证网络环境…',
|
||||
'welcome.turnstile-error': '验证网络环境出错了,请关闭 VPN 后重试',
|
||||
'welcome.turnstile-timeout': '验证网络环境超时了,请重试',
|
||||
'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱',
|
||||
'welcome.reset-password-sent': '重置邮件已发送至 ${email},请翻翻收件箱',
|
||||
'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱',
|
||||
'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件,所以我们不会再发送了,请翻翻收件箱',
|
||||
'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱',
|
||||
'welcome.reset-state-0': '重置邮件刚刚发送到你的邮箱啦,请翻翻收件箱!',
|
||||
'welcome.reset-state-1': '邮件发送次数过多,暂时不会再发送新的重置邮件了',
|
||||
'welcome.turnstile-error': '验证网络环境出错了,请关闭 VPN 后重试',
|
||||
'welcome.turnstile-timeout': '验证网络环境超时了,请重试',
|
||||
'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱',
|
||||
'welcome.reset-password-sent': '重置邮件已发送至 ${email},请翻翻收件箱',
|
||||
'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱',
|
||||
'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件,所以我们不会再发送了,请翻翻收件箱',
|
||||
'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱',
|
||||
'welcome.reset-state-0': '重置邮件刚刚发送到你的邮箱啦,请翻翻收件箱!',
|
||||
'welcome.reset-state-1': '邮件发送次数过多,暂时不会再发送新的重置邮件了',
|
||||
'welcome.verifying': '正在验证邮箱…请稍等',
|
||||
'welcome.verified': '您的邮箱已经验证成功!您现在可以登录了',
|
||||
'welcome.verification-failed': '验证失败:${message}。请重试',
|
||||
@@ -90,7 +91,7 @@ const zhGeneral: typeof EN_REF_GENERAL = {
|
||||
'game.ongeki': "音击",
|
||||
'game.wacca': "华卡",
|
||||
"status.error": "发生错误",
|
||||
"status.error.hint": "出了一些问题,请稍后刷新重试或者",
|
||||
"status.error.hint": "出了一些问题,请稍后刷新重试或者",
|
||||
"status.error.hint.link": "加我们的 Discord 群问一问",
|
||||
"status.detail": "详细信息:${detail}",
|
||||
"action.refresh": "刷新",
|
||||
@@ -112,21 +113,21 @@ const zhHome: typeof EN_REF_HOME = {
|
||||
'home.link-card': '绑定游戏卡',
|
||||
'home.link-cards-description':'绑定游戏数据卡(Amusement IC 或 Aime 卡)后才可以访问游戏存档哦',
|
||||
'home.join-community': '加入群组',
|
||||
'home.join-community-description': '加入我们的聊天群组,与其他玩家聊天、获取帮助',
|
||||
'home.join-community-description': '加入我们的聊天群组,与其他玩家聊天、获取帮助',
|
||||
'home.setup': '连接到 AquaDX',
|
||||
'home.setup-description': '如果您有街机框体或者手台,点击这里设置服务器的连接',
|
||||
'home.setup-description': '如果您有街机框体或者手台,点击这里设置服务器的连接',
|
||||
'home.import': '导入玩家数据',
|
||||
'home.import-description': '如果你来自其他在线服,可以点击这里导入从其他服务器导出的数据',
|
||||
'home.import-description': '如果你来自其他在线服,可以点击这里导入从其他服务器导出的数据',
|
||||
'home.linkcard.cards': "已绑卡片",
|
||||
'home.linkcard.description': "这些是您绑定到帐户的卡",
|
||||
'home.linkcard.account-card': "账户卡",
|
||||
'home.linkcard.registered': "注册于",
|
||||
'home.linkcard.lastused': "上次使用",
|
||||
'home.linkcard.enter-info': "请输入以下信息,或将 aime.txt / felica.txt 文件拖放到此区域",
|
||||
'home.linkcard.access-code': "卡背面的 20 位卡号(如果提示找不到卡,请尝试使用游戏内置的显示卡号功能,输入游戏读取到的卡号)",
|
||||
'home.linkcard.enter-info': "请输入以下信息,或将 aime.txt / felica.txt 文件拖放到此区域",
|
||||
'home.linkcard.access-code': "卡背面的 20 位卡号(如果提示找不到卡,请尝试使用游戏内置的显示卡号功能,输入游戏读取到的卡号)",
|
||||
'home.linkcard.enter-sn1': "在您的手机",
|
||||
'home.linkcard.enter-sn2': "上下载 NFC Tools 并扫描您的卡。然后输入显示的 SN 号。",
|
||||
'home.linkcard.kdx-notice': "如果你在玩 KanadeDX,请在这里输入虚拟卡号(可以在设置 > 卡号中找到卡号)",
|
||||
'home.linkcard.kdx-notice': "如果你在玩 KanadeDX,请在这里输入虚拟卡号(可以在设置 > 卡号中找到卡号)",
|
||||
'home.linkcard.link': "绑定",
|
||||
'home.linkcard.data-conflict': "卡号冲突",
|
||||
'home.linkcard.name': "名称",
|
||||
@@ -137,15 +138,15 @@ const zhHome: typeof EN_REF_HOME = {
|
||||
'home.linkcard.notfound': "找不到卡",
|
||||
'home.linkcard.unlink': "取消链接",
|
||||
'home.linkcard.unlink-notice': "你确定要取消此卡的链接吗?",
|
||||
'home.linkcard.felica-ac-warning': "该 Access Code 是一张 FeliCa AIC 卡。\n如果你使用实体卡(而非 aime.txt 模拟)刷卡登录游戏,与官方服务器不同,你需要绑定该卡的 FeliCa SN(或与之对应的,游戏界面中查看得到的 00 开头的卡号)而非此号码。\n如果你使用 aime.txt 模拟登录,请忽略本警告继续绑定。",
|
||||
'home.setup.welcome': "欢迎! 如果您有街机框体或者手台,请按照以下说明设置以连接到 AquaDX。",
|
||||
'home.setup.blockquote': "我们假设您已经拥有所需的文件,并且可以启动机台或手台附带的游戏(例如 ROM 和 segatools)。如果没有,请联系您设备的卖家以获取所需的文件,因为出于版权原因,我们不会提供这些文件。",
|
||||
'home.linkcard.felica-ac-warning': "该 Access Code 是一张 FeliCa AIC 卡。\n如果你使用实体卡(而非 aime.txt 模拟)刷卡登录游戏,与官方服务器不同,你需要绑定该卡的 FeliCa SN(或与之对应的,游戏界面中查看得到的 00 开头的卡号)而非此号码。\n如果你使用 aime.txt 模拟登录,请忽略本警告继续绑定。",
|
||||
'home.setup.welcome': "欢迎! 如果您有街机框体或者手台,请按照以下说明设置以连接到 AquaDX。",
|
||||
'home.setup.blockquote': "我们假设您已经拥有所需的文件,并且可以启动机台或手台附带的游戏(例如 ROM 和 segatools)。如果没有,请联系您设备的卖家以获取所需的文件,因为出于版权原因,我们不会提供这些文件。",
|
||||
'home.setup.get': "开始",
|
||||
'home.setup.edit': "请打开您的 segatools.ini 文件并修改以下行",
|
||||
'home.setup.test': "在您重新启动游戏后,应该能够连接到 AquaDX。可以验证测试菜单中的网络测试测试连接是否全部良好。",
|
||||
'home.setup.ask': "如果您有任何问题,请加入我们的",
|
||||
'home.setup.test': "在您重新启动游戏后,应该能够连接到 AquaDX。可以验证测试菜单中的网络测试测试连接是否全部良好。",
|
||||
'home.setup.ask': "如果您有任何问题,请加入我们的",
|
||||
'home.setup.support': "以获取支持",
|
||||
'home.setup.keychip-tips': "这是你的狗号,不要与任何人分享",
|
||||
'home.setup.keychip-tips': "这是你的狗号,不要与任何人分享",
|
||||
'home.community.discord': 'Discord',
|
||||
'home.community.telegram': 'Telegram (中文)',
|
||||
'home.community.qq': 'QQ (中文)',
|
||||
@@ -154,6 +155,37 @@ const zhHome: typeof EN_REF_HOME = {
|
||||
'home.import.data-conflict': '继续导入将覆盖现有数据',
|
||||
}
|
||||
|
||||
const zhSetup: typeof EN_REF_SETUP = {
|
||||
'setup.welcome': `欢迎!如果您已准备好游戏,请按照以下说明设置与 AquaDX 的连接。`,
|
||||
'setup.keychip-warning': `您的密钥芯片已与您的账户绑定,请妥善保管。`,
|
||||
'setup.steps.one': `选择一种网络通信设置方法。某些浏览器可能无法进行自动设置。`,
|
||||
'setup.steps.two': `请通过 <a href="/cards">卡片</a> 页面,使用访问码或序列号将您的 Aime 卡与 AquaDX 账户绑定。`,
|
||||
'setup.steps.three': `启动游戏。进入标题画面后,角落处的网络图标应变为绿色,而非灰色。`,
|
||||
'setup.support-info': `如需进一步协助,欢迎在 <a href="/support">支持频道</a> 中咨询。`,
|
||||
'setup.reveal-keychip': `显示密钥芯片`,
|
||||
'setup.type.automatic': `自动设置`,
|
||||
'setup.type.manual': `手动设置`,
|
||||
'setup.manual': `请使用以下信息修改您的 segatools.ini 文件。`,
|
||||
'setup.automatic': `请在下方的 segatools.ini 文件中选择,以自动填写网络信息。`,
|
||||
'setup.automatic.select': `选择文件`,
|
||||
'setup.automatic.success': `成功,数据已更新`,
|
||||
'setup.automatic.failure': `更新信息失败,请确保访问未被阻止,然后重试。`,
|
||||
'setup.troubleshooting.header': '设置故障排除(常见问题解答)',
|
||||
'setup.troubleshooting.items.one': `» 网络测试显示 DNS BAD<br>
|
||||
您可能配置了错误的 segatools.ini。请确保其指向上述(手动设置部分)显示的 DNS。`,
|
||||
'setup.troubleshooting.items.two': `» 网络测试显示 タイトル サーバー BAD (Title Server BAD)<br>
|
||||
请检查您的 segatools.ini 文件,确保密钥芯片设置正确。<br>
|
||||
此外, Card Maker 用户以及使用高于列表中版本的游戏版本的玩家,可能需要禁用加密
|
||||
<a target="_blank" href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/SDED#patching-assemblycsharpdll">(Card Maker 操作指南)</a>`,
|
||||
'setup.troubleshooting.items.three': `» 无法扫描卡片<br>
|
||||
内置的模拟读卡器默认配置为使用回车键进行扫描(长按该键)。`,
|
||||
'setup.keychip': '钥匙芯片管理',
|
||||
'setup.keychip.warning': '对于拥有实体钥匙芯片并试图连接到 AquaDX 的机柜所有者,可在此手动设置钥匙芯片。选择钥匙芯片后,下方设置项将自动填充。',
|
||||
'setup.keychip.add': `添加钥匙芯片`,
|
||||
'setup.keychip.delete': `删除`,
|
||||
'setup.keychip.placeholder': `新钥匙芯片 ID`
|
||||
}
|
||||
|
||||
const zhSettings: typeof EN_REF_SETTINGS = {
|
||||
'settings.title': '用户设置',
|
||||
'settings.tabs.profile': '个人资料',
|
||||
@@ -163,7 +195,7 @@ const zhSettings: typeof EN_REF_SETTINGS = {
|
||||
'settings.tabs.ongeki': '音击',
|
||||
'settings.tabs.wacca': '华卡',
|
||||
|
||||
"settings.options.all.preset-warning": "您正在使用预设选项配置,部分选项可能显示不正确。",
|
||||
"settings.options.all.preset-warning": "您正在使用预设选项配置,部分选项可能显示不正确。",
|
||||
"settings.options.all.personal-best": "记录",
|
||||
"settings.options.all.rival-score": "对手",
|
||||
"settings.options.all.life": "生命值",
|
||||
@@ -220,11 +252,11 @@ const zhSettings: typeof EN_REF_SETTINGS = {
|
||||
'settings.fields.optOutOfLeaderboard.name': '不参与排行榜',
|
||||
'settings.fields.optOutOfLeaderboard.desc': '登录之后还是可以在排行榜上看到自己',
|
||||
'settings.fields.enableMusicRank.name': '在你的机台上启用「推荐乐曲排行榜」',
|
||||
'settings.fields.enableMusicRank.desc': '如果你自己设计了排行榜的话,可以关闭这个(会影响你自己的机器)。',
|
||||
'settings.fields.enableMusicRank.desc': '如果你自己设计了排行榜的话,可以关闭这个(会影响你自己的机器)。',
|
||||
'settings.mai2.name': '玩家名字',
|
||||
'settings.profile.picture': '头像',
|
||||
'settings.profile.upload-new': '上传',
|
||||
'settings.profile.bad-format': '无效的图片格式,支持的格式有 PNG、JPG、JPEG、WEBP 和 GIF。',
|
||||
'settings.profile.bad-format': '无效的图片格式,支持的格式有 PNG、JPG、JPEG、WEBP 和 GIF。',
|
||||
'settings.profile.save': '保存',
|
||||
'settings.profile.name': '昵称',
|
||||
'settings.profile.username': '用户名',
|
||||
@@ -237,7 +269,7 @@ const zhSettings: typeof EN_REF_SETTINGS = {
|
||||
'settings.profile.unchanged': '未更改',
|
||||
'settings.export': '导出玩家数据',
|
||||
'settings.batchManualExport': "导出 Batch Manual 格式(用于 Tachi)",
|
||||
'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
|
||||
'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
|
||||
// AI
|
||||
'settings.siteNotice': "这些设置仅适用于网站。",
|
||||
// AI
|
||||
@@ -284,7 +316,7 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
|
||||
'userbox.matching.symbolChat.default': '默认',
|
||||
|
||||
'userbox.new.name': 'AquaBox',
|
||||
'userbox.new.setup': '将中二(Lumi 或更高版本)的游戏文件夹拖放到下方区域,以显示带有名牌和头像的 UserBox。所有文件都在浏览器中处理。',
|
||||
'userbox.new.setup': '将中二(Lumi 或更高版本)的游戏文件夹拖放到下方区域,以显示带有名牌和头像的 UserBox。所有文件都在浏览器中处理。',
|
||||
'userbox.new.setup.notice': '选择包含游戏数据的最外层文件夹。',
|
||||
'userbox.new.setup.processing_file': '正在处理文件',
|
||||
'userbox.new.setup.finalizing': '正在保存到内部存储',
|
||||
@@ -298,20 +330,20 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
|
||||
'userbox.new.activate_desc': '启用后可显示带有名牌和头像的 UserBox',
|
||||
'userbox.new.activate_profile': '在用户页面启用 AquaBox',
|
||||
'userbox.new.activate_profile_desc': '启用后可在个人页面显示带有名牌和头像的 UserBox',
|
||||
'userbox.new.error.invalidFolder': '所选文件夹无效。请确认游戏版本为 Lumi 或更新,并且包含 “A000” 选项包。',
|
||||
'userbox.new.error.invalidFolder': '所选文件夹无效。请确认游戏版本为 Lumi 或更新,并且包含 “A000” 选项包。',
|
||||
'userbox.new.error.invalidUrl': '输入的 URL 无效。'
|
||||
};
|
||||
|
||||
export const zhMaiPhoto: typeof EN_REF_MAI_PHOTO = {
|
||||
'maiphoto.title': 'Mai 纪念照片库',
|
||||
'maiphoto.url_warning': '注意:如果想与朋友分享图片的话,请先保存照片再发出去。不要复制图片 URL,因为 URL 中包含 AquaDX 账号信息。',
|
||||
'maiphoto.url_warning': '注意:如果想与朋友分享图片的话,请先保存照片再发出去。不要复制图片 URL,因为 URL 中包含 AquaDX 账号信息。',
|
||||
'maiphoto.none': '还没有图片哦~ 可以在每次游戏结束的时候点击上传来上传照片。',
|
||||
}
|
||||
|
||||
export const zhAquaTrans: typeof EN_REF_AQUATRANS = {
|
||||
'trans.title': '🏳️⚧️ AquaTrans™ 数据迁移工具',
|
||||
'trans.confirm.unbackuped.title': '确认迁移',
|
||||
'trans.confirm.unbackuped.msg': '似乎还没有备份目标服务器的数据,真的要继续吗?(推荐先备份一下,因为迁移的时候会覆盖数据)',
|
||||
'trans.confirm.unbackuped.msg': '似乎还没有备份目标服务器的数据,真的要继续吗?(推荐先备份一下,因为迁移的时候会覆盖数据)',
|
||||
'trans.confirm.untested.title': '不太聪明喵',
|
||||
'trans.confirm.untested.msg': '在两个服务器上都测试完连接之后才能进行数据迁移哦!',
|
||||
'trans.confirm.done.title': '完成!',
|
||||
@@ -319,8 +351,8 @@ export const zhAquaTrans: typeof EN_REF_AQUATRANS = {
|
||||
'trans.alert.in-progress': '在迁移了在迁移了',
|
||||
'trans.prompt-html': `
|
||||
<p>👋 欢迎使用 AquaTrans™ 服务器游玩数据迁移工具!</p>
|
||||
<p>这个工具可以导出任意服务器的数据,并使用连接凭证(卡号、服务器地址和 Keychip ID)将数据导入任何其他服务器。</p>
|
||||
<p>我将模拟游戏客户端,从源服务器拉取游戏数据并推送到目标服务器。</p>
|
||||
<p>这个工具可以导出任意服务器的数据,并使用连接凭证(卡号、服务器地址和 Keychip ID)将数据导入任何其他服务器。</p>
|
||||
<p>我将模拟游戏客户端,从源服务器拉取游戏数据并推送到目标服务器。</p>
|
||||
<p>填写下面的表格开始迁移吧!</p>
|
||||
`,
|
||||
'trans.error.empty': '请填写所有字段。',
|
||||
@@ -336,11 +368,11 @@ export const zhAquaTrans: typeof EN_REF_AQUATRANS = {
|
||||
'trans.btn.test': '测试连接',
|
||||
'trans.btn.export': '导出数据',
|
||||
'trans.btn.import': '导入数据',
|
||||
'trans.blacklist': "这个服务器的服主把这个导出工具 ban 了,所以不能从这里导出",
|
||||
'trans.blacklist': "这个服务器的服主把这个导出工具 ban 了,所以不能从这里导出",
|
||||
}
|
||||
|
||||
|
||||
export const ZH = { ...zhUser, ...zhWelcome, ...zhGeneral,
|
||||
...zhLeaderboard, ...zhHome, ...zhSettings, ...zhUserbox, ...zhMaiPhoto,
|
||||
...zhAquaTrans
|
||||
...zhAquaTrans, ...zhSetup
|
||||
}
|
||||
|
||||
@@ -201,10 +201,18 @@ ${keychipLines}`.trim(), {
|
||||
<div class="setup-step">
|
||||
3. <div>{@html t('setup.steps.three')}</div>
|
||||
</div>
|
||||
<div class="setup-step">
|
||||
4. <div>{@html t('setup.steps.four')}</div>
|
||||
</div>
|
||||
|
||||
<h2>{t('setup.troubleshooting.header')}</h2>
|
||||
<ul>
|
||||
<li>
|
||||
{@html t('setup.troubleshooting.items.one')}
|
||||
</li>
|
||||
<li>
|
||||
{@html t('setup.troubleshooting.items.two')}
|
||||
</li>
|
||||
<li>
|
||||
{@html t('setup.troubleshooting.items.three')}
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
{@html t('setup.support-info')}
|
||||
</p>
|
||||
@@ -217,6 +225,9 @@ ${keychipLines}`.trim(), {
|
||||
.code
|
||||
overflow-x: auto
|
||||
|
||||
ul
|
||||
li
|
||||
margin: 0.75em 0
|
||||
.divider
|
||||
width: 90%
|
||||
height: 1px
|
||||
|
||||
19
README.md
19
README.md
@@ -32,21 +32,22 @@ Multipurpose game server for ALL.Net games.
|
||||
> [!WARNING]
|
||||
> CHUNITHM pre-NEW!! and maimai pre-DX are no longer supported after February 6th, 2026 and all associated data will be removed.
|
||||
|
||||
| Game | Latest Ver. | Initial Ver. | Notes | Web UI | Data Import |
|
||||
|------------------------|---------------------|---------------------|-------------------------------------------------------------|--------|-------------|
|
||||
| SDHD: CHUNITHM | 2.45 (X-VERSE-X) | 2.00 (NEW) | | ✅ | ❗ |
|
||||
| SDEZ: maimai DX | 1.65 (CiRCLE PLUS) | 1.00 (DX) | Missing circle (teams) support | ✅ | ✅ |
|
||||
| SDGA: maimai DX (Intl) | 1.60 (CiRCLE) | 1.00 (DX) | Thanks [@Clansty](https://github.com/clansty) | ✅ | ✅ |
|
||||
| SDED: Card Maker | 1.39 | N/A | Thanks [@Becods](https://github.com/Becods) | ❌ | ❌ |
|
||||
| SDDT: O.N.G.E.K.I. | 1.50 (Re:Fresh) | N/A | Thanks [@PenguinCaptain](https://github.com/PenguinCaptain) | ✅ | ❌ |
|
||||
| SBZV: Project DIVA | 7.10 | N/A | Minimal support | ❌ | ❌ |
|
||||
| SDFE: Wacca | 3.07 (Reverse) | N/A | Later versions are EOS patches, network will not work | ✅ | ❌ |
|
||||
| Game | Latest Ver. | Initial Ver. | Notes | Web UI | Import | Encrypt |
|
||||
|------------------------|---------------------|---------------------|-------------------------------------------------------------|--------|-------------|---------|
|
||||
| SDHD: CHUNITHM | 2.45 (X-VERSE-X) | 2.00 (NEW) | | ✅ | ❗ | ✅ |
|
||||
| SDEZ: maimai DX | 1.65 (CiRCLE PLUS) | 1.00 (DX) | Missing circle (teams) support | ✅ | ✅ | ✅ |
|
||||
| SDGA: maimai DX (Intl) | 1.60 (CiRCLE) | 1.00 (DX) | Thanks [@Clansty](https://github.com/clansty) | ✅ | ✅ | ✅ |
|
||||
| SDED: Card Maker | 1.39 | N/A | Thanks [@Becods](https://github.com/Becods) | ❌ | ❌ | ❌ |
|
||||
| SDDT: O.N.G.E.K.I. | 1.50 (Re:Fresh) | N/A | Thanks [@PenguinCaptain](https://github.com/PenguinCaptain) | ✅ | ❌ | ✅ |
|
||||
| SBZV: Project DIVA | 7.10 | N/A | Minimal support | ❌ | ❌ | 🔓 |
|
||||
| SDFE: Wacca | 3.07 (REVERSE) | N/A | Later versions are EOS patches | ✅ | ❌ | 🔓 |
|
||||
|
||||
<!-- A majority of them have been left as N/A for initial version as they do not appear to have restrictions -->
|
||||
|
||||
Check out these docs for more information.
|
||||
* [Game specific notes](docs/game_specific_notes.md)
|
||||
* [Frequently asked questions](docs/frequently_asked_questions.md)
|
||||
* [Encryption notes](docs/encryption.md)
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -142,4 +142,4 @@ aqua-net.bot.enabled=true
|
||||
aqua-net.bot.secret=hunter2
|
||||
|
||||
## OpenAI Settings (For content moderation)
|
||||
aqua-net.openai.api-key=sk-1234567890abcdef1234567890abcdef
|
||||
aqua-net.openai.api-key=sk-1234567890abcdef1234567890abcdef
|
||||
35
docs/encryption.md
Normal file
35
docs/encryption.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Encryption Support
|
||||
|
||||
AquaDX supports encryption for Chunithm, Maimai and Ongeki.<br>
|
||||
It is not required to connect and keys are NOT provided by default.
|
||||
|
||||
Card Maker has encryption features in the client, but they are not supported due to it's complexity.
|
||||
|
||||
## Adding new keys to AquaDX
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Ensure you're using AquaDX from source. Docker will work, as long as you use `docker-compose up --build`
|
||||
|
||||
1. Create a new file at `src/main/resources/data/game/<game name>/game_encryption.json`
|
||||
2. Fill it out using the following schema:
|
||||
```json
|
||||
{
|
||||
"code": "SDHD",
|
||||
"versions": [215, 216],
|
||||
"key": "DECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBAD",
|
||||
"iv": "DECAFBADDECAFBADDECAFBADDECAFBAD",
|
||||
"salt": "DECAFBADDECAFBAD",
|
||||
"iterations": 36
|
||||
},
|
||||
```
|
||||
|
||||
**`key`, `salt`, and `iv` MUST be hex strings. Do NOT insert raw ASCII strings.**<br>
|
||||
|
||||
Replace `code` with the correct code for your game (`SDHD` for Chunithm JP, `SDEZ` for Maimai JP, etc.).<br>
|
||||
Set `versions` to be an array of game versions without the decimal (215 for 2.15, 150 for 1.50, etc.).<br>
|
||||
You MUST include rom patch versions in this array or it will not encrypt correctly.
|
||||
|
||||
- Maimai DX does NOT use iterations, you do not need to include it
|
||||
- Ongeki always uses 64 iterations (you must include it)
|
||||
- If using a specific Python script to rip Chunithm keys, focus on the `=>` entries.
|
||||
- `key` is 32 bytes long. `iv` is 16 bytes long. `salt` is 8 bytes long. (hex strings should be twice as long as they are in bytes)
|
||||
@@ -185,20 +185,7 @@ class AllNet(
|
||||
// If keychip authentication is enabled, the game URLs will be set to /gs/{token}/{game}/...
|
||||
val base = if (session != null) "gs/$session" else "g"
|
||||
|
||||
return "http://$addr/$base/" + when (gameId) {
|
||||
"SDBT" -> "chu2/$ver/$session/"
|
||||
"SDHD" -> "chu3/$ver/"
|
||||
"SDGS" -> "chu3/$ver/" // International (c3exp)
|
||||
"SBZV" -> "diva/"
|
||||
"SDDT" -> "ongeki/$ver/"
|
||||
"SDEY" -> "mai/"
|
||||
"SDGA" -> "mai2/" // International (Exp)
|
||||
"SDGB" -> "mai2/" // International (China) - TODO: Test it
|
||||
"SDEZ" -> "mai2/"
|
||||
"SDFE" -> "wacca" // Note: Wacca must not end with a trailing slash
|
||||
"SDED" -> "card/"
|
||||
else -> ""
|
||||
}
|
||||
return "http://$addr/$base/$gameId/$ver/"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -19,7 +19,7 @@ import java.time.format.DateTimeFormatter
|
||||
* @author samnyan (privateamusement@protonmail.com)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/g/card")
|
||||
@RequestMapping("/g/SDED/{version}")
|
||||
class CardMakerController(
|
||||
val mapper: BasicMapper,
|
||||
@param:Value("\${allnet.server.host:}") val ALLNET_HOST: String,
|
||||
|
||||
@@ -17,13 +17,18 @@ import icu.samnyan.aqua.spring.Metrics
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
|
||||
/**
|
||||
* @author samnyan (privateamusement@protonmail.com)
|
||||
*/
|
||||
@Suppress("unused")
|
||||
@RestController
|
||||
@API(value = ["/g/chu3/{version}/ChuniServlet", "/g/chu3/{version}"])
|
||||
@API(value = [
|
||||
"/g/SDHD/{version}/ChuniServlet", "/g/SDHD/{version}",
|
||||
"/g/SDGS/{version}/ChuniServlet", "/g/SDGS/{version}",
|
||||
])
|
||||
class ChusanController(
|
||||
val mapper: StringMapper,
|
||||
val cmMapper: BasicMapper,
|
||||
@@ -37,14 +42,19 @@ class ChusanController(
|
||||
): MeowApi({ api, resp ->
|
||||
if (resp is String) resp
|
||||
else (if ("CM" in api || api == "GetUserItemApi" ) cmMapper else mapper).write(resp)
|
||||
},
|
||||
@kotlin.ExperimentalStdlibApi
|
||||
{ endpoint: String ->
|
||||
db.gameData.chu3GameEncryption.map{
|
||||
val suffix = when (it.code) { "SDGS" -> "C3Exp" else -> "" }
|
||||
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
.generateSecret(
|
||||
PBEKeySpec((endpoint + suffix).toCharArray(), it.salt!!.hexToByteArray(), it.iterations, 128)
|
||||
).encoded.toHexString().substring(0, 32)
|
||||
}
|
||||
}) {
|
||||
val log = LoggerFactory.getLogger(ChusanController::class.java)
|
||||
|
||||
val noopEndpoint = setOf("UpsertClientBookkeepingApi", "UpsertClientDevelopApi", "UpsertClientErrorApi",
|
||||
"UpsertClientSettingApi", "UpsertClientTestmodeApi", "CreateTokenApi", "RemoveTokenApi", "UpsertClientUploadApi",
|
||||
"PrinterLoginApi", "PrinterLogoutApi", "Ping", "GameLogoutApi", "RemoveMatchingMemberApi",
|
||||
"UpsertClientPlayTimeApi", "UpsertClientGameStartApi", "UpsertClientGameEndApi")
|
||||
|
||||
init { chusanInit() }
|
||||
val handlers = initH
|
||||
|
||||
@@ -60,28 +70,27 @@ class ChusanController(
|
||||
data["c3exp"] = true
|
||||
}
|
||||
|
||||
val apiFriendlyName = if (endpoint.lowercase() == endpoint && endpoint.length == 32)
|
||||
hashes.filter { it.value.contains(api) }.keys.first().str + " (encrypt)" else api
|
||||
|
||||
if (api.startsWith("CM") && api !in handlers) api = api.removePrefix("CM")
|
||||
val token = TokenChecker.tokenShort()
|
||||
log.info("$token : $api < ${data.toJson()}")
|
||||
log.info("$token : $apiFriendlyName < ${data.toJson()}")
|
||||
|
||||
val noop = """{"returnCode":"1","apiName":"$api"}"""
|
||||
if (api !in noopEndpoint && !handlers.containsKey(api)) {
|
||||
log.warn("$token : $api > not found")
|
||||
if (!handlers.containsKey(api)) {
|
||||
log.warn("$token : $apiFriendlyName > not found")
|
||||
return noop
|
||||
}
|
||||
|
||||
// Only record the counter metrics if the API is known.
|
||||
Metrics.counter("aquadx_chusan_api_call", "api" to api).increment()
|
||||
if (api in noopEndpoint) {
|
||||
log.info("$token : $api > no-op")
|
||||
return noop
|
||||
}
|
||||
|
||||
return try {
|
||||
Metrics.timer("aquadx_chusan_api_latency", "api" to api).recordCallable {
|
||||
serialize(api, handlers[api]!!(ctx) ?: noop).also {
|
||||
if (api !in setOf("GetUserItemApi", "GetGameEventApi"))
|
||||
log.info("$token : $api > ${it.truncate(500)}")
|
||||
log.info("$token : $apiFriendlyName > ${it.truncate(500)}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -431,5 +431,23 @@ fun ChusanController.chusanInit() {
|
||||
// process()
|
||||
|
||||
"""{"returnCode":"1"}"""
|
||||
|
||||
// NOTE: no-op APIs moved to respect encryption
|
||||
"UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"UpsertClientBookkeepingApi"}""" }
|
||||
"UpsertClientDevelop" static { """{returnCode":1, "apiName":"UpsertClientDevelopApi"}""" }
|
||||
"UpsertClientError" static { """{returnCode":1, "apiName":"UpsertClientErrorApi"}""" }
|
||||
"UpsertClientSetting" static { """{returnCode":1, "apiName":"UpsertClientSettingApi"}""" }
|
||||
"UpsertClientTestmode" static { """{returnCode":1, "apiName":"UpsertClientTestmodeApi"}""" }
|
||||
"CreateToken" static { """{returnCode":1, "apiName":"CreateTokenApi"}""" }
|
||||
"RemoveToken" static { """{returnCode":1, "apiName":"RemoveTokenApi"}""" }
|
||||
"UpsertClientUpload" static { """{returnCode":1, "apiName":"UpsertClientUploadApi"}""" }
|
||||
"PrinterLogin" static { """{returnCode":1, "apiName":"PrinterLoginApi"}""" }
|
||||
"PrinterLogout" static { """{returnCode":1, "apiName":"PrinterLogoutApi"}""" }
|
||||
"Ping" static { """{returnCode":1, "apiName":"Ping"}""" }
|
||||
"GameLogout" static { """{returnCode":1, "apiName":"GameLogoutApi"}""" }
|
||||
"RemoveMatchingMember" static { """{returnCode":1, "apiName":"RemoveMatchingMemberApi"}""" }
|
||||
"UpsertClientPlayTime" static { """{returnCode":1, "apiName":"UpsertClientPlayTimeApi"}""" }
|
||||
"UpsertClientGameStart" static { """{returnCode":1, "apiName":"UpsertClientGameStartApi"}""" }
|
||||
"UpsertClientGameEnd" static { """{returnCode":1, "apiName":"UpsertClientGameEndApi"}""" }
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ val DIVA_INIT = mapOf("db_close" to "0,0", "retry_time" to "FFFF")
|
||||
* @author samnyan (privateamusement@protonmail.com)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/g/diva")
|
||||
@RequestMapping("/g/SBZV/{version}")
|
||||
class DivaController(
|
||||
val attendHandler: AttendHandler,
|
||||
val cardProcedureHandler: CardProcedureHandler,
|
||||
|
||||
@@ -31,11 +31,23 @@ typealias PagePost = (MutJDict) -> Unit
|
||||
data class PagedProcessor(val add: JDict?, val fn: PagedHandler, var post: PagePost? = null)
|
||||
|
||||
// A very :3 way of declaring APIs
|
||||
abstract class MeowApi(val serialize: (String, Any) -> String) {
|
||||
abstract class MeowApi(
|
||||
val serialize: (String, Any) -> String,
|
||||
val endpointHash: ((String) -> List<String>)? = null
|
||||
) {
|
||||
val initH = mutableMapOf<String, SpecialHandler>()
|
||||
val hashes = mutableMapOf<String, MutableList<String>>()
|
||||
infix operator fun String.invoke(fn: SpecialHandler) {
|
||||
if (initH.containsKey("${this}Api")) error("Duplicate API $this found! Someone is not smart 👀")
|
||||
initH["${this}Api"] = fn
|
||||
if (endpointHash != null) {
|
||||
// Keep track of hashes for logging; they're not necessary for anything else
|
||||
hashes["${this}Api"] = mutableListOf()
|
||||
for (hashedEndpoint in endpointHash("${this}Api")) {
|
||||
initH[hashedEndpoint] = fn
|
||||
hashes["${this}Api"]?.add(hashedEndpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
infix fun String.static(fn: () -> Any) = serialize(this, fn()).let { resp -> this { resp } }
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import ext.logger
|
||||
import ext.toJson
|
||||
import icu.samnyan.aqua.net.components.GeoIP
|
||||
import icu.samnyan.aqua.sega.allnet.TokenChecker
|
||||
import icu.samnyan.aqua.sega.general.model.GameEncryptionKey
|
||||
import icu.samnyan.aqua.sega.util.GameDataService
|
||||
import icu.samnyan.aqua.sega.util.ZLib
|
||||
import jakarta.servlet.FilterChain
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
@@ -14,6 +16,9 @@ import org.springframework.stereotype.Component
|
||||
import org.springframework.web.filter.OncePerRequestFilter
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
|
||||
/**
|
||||
@@ -21,21 +26,46 @@ import java.util.*
|
||||
*/
|
||||
@Component
|
||||
class CompressionFilter(
|
||||
val geoip: GeoIP
|
||||
val geoip: GeoIP,
|
||||
val gameData: GameDataService
|
||||
) : OncePerRequestFilter() {
|
||||
companion object {
|
||||
val log = logger()
|
||||
val b64d = Base64.getMimeDecoder()
|
||||
val b64e = Base64.getMimeEncoder()
|
||||
|
||||
}
|
||||
|
||||
var keys = gameData.chu3GameEncryption + gameData.mai2GameEncryption + gameData.ogkGameEncryption
|
||||
fun getEncryptionKeys(path: String): GameEncryptionKey? {
|
||||
val endpoint = path.split("/").last()
|
||||
if (endpoint.lowercase() != endpoint || endpoint.length != 32) return null
|
||||
|
||||
val game = path.split("/")[2]
|
||||
val version = path.split("/")[3].filterNot { it == '.' }.toInt()
|
||||
return keys.find { it.versions.contains(version) && it.code == game }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
override fun doFilterInternal(req: HttpServletRequest, resp: HttpServletResponse, chain: FilterChain) {
|
||||
val isDeflate = req.getHeader("content-encoding") == "deflate"
|
||||
val isDfi = req.getHeader("pragma") == "DFI"
|
||||
|
||||
val keys = getEncryptionKeys(req.servletPath)
|
||||
|
||||
// Decode input
|
||||
val reqSrc = try {
|
||||
req.inputStream.readAllBytes().let {
|
||||
if (keys != null) {
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(
|
||||
Cipher.DECRYPT_MODE,
|
||||
SecretKeySpec(keys.key!!.hexToByteArray(), "AES"),
|
||||
IvParameterSpec(keys.iv!!.hexToByteArray())
|
||||
)
|
||||
cipher.doFinal(it)
|
||||
} else it
|
||||
}.let {
|
||||
if (isDeflate) ZLib.decompress(it)
|
||||
else if (isDfi) ZLib.decompress(b64d.decode(it))
|
||||
else it
|
||||
@@ -50,7 +80,19 @@ class CompressionFilter(
|
||||
val respW = ContentCachingResponseWrapper(resp)
|
||||
val result = try {
|
||||
chain.doFilter(CompressRequestWrapper(req, reqSrc), respW)
|
||||
ZLib.compress(respW.contentAsByteArray).let { if (isDfi) b64e.encode(it) else it }
|
||||
ZLib.compress(respW.contentAsByteArray)
|
||||
.let { if (isDfi) b64e.encode(it) else it }
|
||||
.let {
|
||||
if (keys != null) {
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(
|
||||
Cipher.ENCRYPT_MODE,
|
||||
SecretKeySpec(keys.key!!.hexToByteArray(), "AES"),
|
||||
IvParameterSpec(keys.iv!!.hexToByteArray())
|
||||
)
|
||||
cipher.doFinal(it)
|
||||
} else it
|
||||
}
|
||||
} finally {
|
||||
if (respW.status != 200) {
|
||||
val details = mapOf(
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package icu.samnyan.aqua.sega.general.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class GameEncryptionKey {
|
||||
var code: String? = null
|
||||
var versions: List<Int> = listOf()
|
||||
var key: String? = null
|
||||
var salt: String? = null
|
||||
var iv: String? = null
|
||||
var iterations = 0
|
||||
}
|
||||
@@ -424,8 +424,20 @@ fun Maimai2ServletController.initApis() {
|
||||
)
|
||||
) }
|
||||
|
||||
"UpsertUserPlaceCircleRegist" static { mapOf(
|
||||
"returnCode" to 0,
|
||||
"apiName" to "UpsertUserPlaceCircleRegistApi"
|
||||
) }
|
||||
// NOTE: no-op APIs moved to respect encryption
|
||||
"UpsertUserPlaceCircleRegist" static { mapOf( "returnCode" to 0, "apiName" to "com.sega.maimai2servlet.api.UpsertUserPlaceCircleRegistApi") }
|
||||
"GetUserScoreRanking" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.GetUserScoreRankingApi"}""" }
|
||||
"UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientBookkeepingApi"}""" }
|
||||
"UpsertClientSetting" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientSettingApi"}""" }
|
||||
"UpsertClientTestmode" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientTestmodeApi"}""" }
|
||||
"UpsertClientUpload" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientUploadApi"}""" }
|
||||
"Ping" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.Ping"}""" }
|
||||
"RemoveToken" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.RemoveTokenApi"}""" }
|
||||
"CMLogin" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMLoginApi"}""" }
|
||||
"CMLogout" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMLogoutApi"}""" }
|
||||
"CMUpsertBuyCard" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMUpsertBuyCardApi"}""" }
|
||||
"UserLogout" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UserLogoutApi"}""" }
|
||||
"GetGameMapAreaCondition" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.GetGameMapAreaConditionApi"}""" }
|
||||
"UpsertUserChargelog" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertUserChargelogApi"}""" }
|
||||
"UpsertClientPlayTime" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientPlayTimeApi"}""" }
|
||||
}
|
||||
|
||||
@@ -11,12 +11,14 @@ import icu.samnyan.aqua.sega.maimai2.handler.*
|
||||
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
|
||||
import icu.samnyan.aqua.spring.Metrics
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import kotlinx.io.bytestring.hexToByteString
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.security.MessageDigest
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.reflect.full.declaredMemberProperties
|
||||
|
||||
@@ -25,7 +27,11 @@ import kotlin.reflect.full.declaredMemberProperties
|
||||
*/
|
||||
@Suppress("unused")
|
||||
@RestController
|
||||
@RequestMapping(path = ["/g/mai2/Maimai2Servlet/", "/g/mai2/"])
|
||||
@RequestMapping(path = [
|
||||
"/g/SDGA/{version}/Maimai2Servlet/", "/g/SDGA/{version}",
|
||||
"/g/SDEZ/{version}/Maimai2Servlet/", "/g/SDEZ/{version}",
|
||||
//"/g/SDGB/{version}/Maimai2Servlet/", "/g/SDGB/{version}",
|
||||
])
|
||||
class Maimai2ServletController(
|
||||
val upsertUserAll: UpsertUserAllHandler,
|
||||
val getUserItem: GetUserItemHandler,
|
||||
@@ -40,7 +46,18 @@ class Maimai2ServletController(
|
||||
val getGameRanking: GetGameRankingHandler,
|
||||
val db: Mai2Repos,
|
||||
val net: Maimai2,
|
||||
): MeowApi(serialize = { _, resp -> if (resp is String) resp else resp.toJson() }) {
|
||||
): MeowApi(
|
||||
serialize = { _, resp -> if (resp is String) resp else resp.toJson() },
|
||||
@kotlin.ExperimentalStdlibApi
|
||||
{ endpoint: String ->
|
||||
db.gameData.mai2GameEncryption.map{
|
||||
val suffix = when (it.code) { "SDGA" -> "MaimaiExp" "SDGB" -> "MaimaiChn" else -> "" }
|
||||
MessageDigest.getInstance("MD5")
|
||||
.digest((endpoint + suffix).toByteArray() + it.salt!!.hexToByteArray())
|
||||
.toHexString()
|
||||
}
|
||||
}
|
||||
) {
|
||||
companion object {
|
||||
private val log = logger()
|
||||
private val empty = listOf<Any>()
|
||||
@@ -55,11 +72,6 @@ class Maimai2ServletController(
|
||||
"CMGetUserCardApi","CMGetUserCardPrintErrorApi","CMGetUserDataApi","CMGetUserItemApi","CMUpsertUserPrintApi",
|
||||
"GetUserFavoriteItemApi")
|
||||
|
||||
val noopEndpoint = setOf("GetUserScoreRankingApi", "UpsertClientBookkeepingApi",
|
||||
"UpsertClientSettingApi", "UpsertClientTestmodeApi", "UpsertClientUploadApi", "Ping", "RemoveTokenApi",
|
||||
"CMLoginApi", "CMLogoutApi", "CMUpsertBuyCardApi", "UserLogoutApi", "GetGameMapAreaConditionApi",
|
||||
"UpsertUserChargelogApi","UpsertClientPlayTimeApi")
|
||||
|
||||
val members = this::class.declaredMemberProperties
|
||||
val handlers: Map<String, SpecialHandler> = initH + endpointList.associateWith { api ->
|
||||
val name = api.replace("Api", "").lowercase()
|
||||
@@ -72,27 +84,25 @@ class Maimai2ServletController(
|
||||
@API("/{api}")
|
||||
fun handle(@PathVariable api: String, @RequestBody data: Map<String, Any>, req: HttpServletRequest): Any {
|
||||
val token = TokenChecker.tokenShort()
|
||||
log.info("$token : $api < ${data.toJson()}")
|
||||
|
||||
val apiFriendlyName = if (api.lowercase() == api && api.length == 32)
|
||||
((hashes.filter { it.value.contains(api) }.keys.firstOrNull()?.str + " (encrypt)") ?: api) else "$api (encrypt)"
|
||||
log.info("$token : $apiFriendlyName < ${data.toJson()}")
|
||||
|
||||
val noop = """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.$api"}"""
|
||||
if (api !in noopEndpoint && !handlers.containsKey(api)) {
|
||||
log.warn("$token : $api > not found")
|
||||
if (!handlers.containsKey(api)) {
|
||||
log.warn("$token : $apiFriendlyName > not found")
|
||||
return noop
|
||||
}
|
||||
|
||||
// Only record the counter metrics if the API is known.
|
||||
Metrics.counter("aquadx_maimai2_api_call", "api" to api).increment()
|
||||
|
||||
if (api in noopEndpoint) {
|
||||
log.info("$token : $api > no-op")
|
||||
return noop
|
||||
}
|
||||
|
||||
return try {
|
||||
Metrics.timer("aquadx_maimai2_api_latency", "api" to api).recordCallable {
|
||||
val ctx = RequestContext(req, data.mut)
|
||||
serialize(api, handlers[api]!!(ctx) ?: noop).also {
|
||||
log.info("$token : $api > ${it.truncate(500)}")
|
||||
log.info("$token : $apiFriendlyName > ${it.truncate(500)}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -147,7 +147,7 @@ class Mai2Repos(
|
||||
val userKaleidx: MAi2UserKaleidxRepo,
|
||||
val userIntimate: MAi2UserIntimateRepo,
|
||||
val userRegions: Mai2UserRegionsRepo,
|
||||
gameData: GameDataService
|
||||
val gameData: GameDataService
|
||||
) {
|
||||
val gameCharge = StaticRepo(gameData.mai2Charges) { it.orderId }
|
||||
val gameEvent = StaticRepo(gameData.mai2Events) { it.id }
|
||||
|
||||
@@ -70,4 +70,16 @@ fun OngekiController.ongekiInit() {
|
||||
"GetClientTestmode" {
|
||||
empty.staticLst("clientTestmodeList") + mapOf("placeId" to data["placeId"])
|
||||
}
|
||||
|
||||
// NOTE: no-op APIs moved to respect encryption
|
||||
"ExtendLockTime" static { """{returnCode":1, "apiName":"ExtendLockTimeApi"}""" }
|
||||
"GameLogout" static { """{returnCode":1, "apiName":"GameLogoutApi"}""" }
|
||||
"RegisterPromotionCard" static { """{returnCode":1, "apiName":"RegisterPromotionCardApi"}""" }
|
||||
"UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"UpsertClientBookkeepingApi"}""" }
|
||||
"UpsertClientDevelop" static { """{returnCode":1, "apiName":"UpsertClientDevelopApi"}""" }
|
||||
"UpsertClientError" static { """{returnCode":1, "apiName":"UpsertClientErrorApi"}""" }
|
||||
"UpsertClientSetting" static { """{returnCode":1, "apiName":"UpsertClientSettingApi"}""" }
|
||||
"UpsertClientTestmode" static { """{returnCode":1, "apiName":"UpsertClientTestmodeApi"}""" }
|
||||
"UpsertUserGplog" static { """{returnCode":1, "apiName":"UpsertUserGplogApi"}""" }
|
||||
"Ping" static { """{returnCode":1, "apiName":"Ping"}""" }
|
||||
}
|
||||
@@ -8,14 +8,18 @@ import icu.samnyan.aqua.sega.general.GameMusicPopularity
|
||||
import icu.samnyan.aqua.sega.general.MeowApi
|
||||
import icu.samnyan.aqua.sega.general.RequestContext
|
||||
import icu.samnyan.aqua.sega.general.service.CardService
|
||||
import icu.samnyan.aqua.sega.maimai2.Maimai2ServletController
|
||||
import icu.samnyan.aqua.sega.util.BasicMapper
|
||||
import icu.samnyan.aqua.spring.Metrics
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
|
||||
|
||||
@Suppress("unused")
|
||||
@RestController
|
||||
@API("/g/ongeki/{version}", "/g/ongeki")
|
||||
@API("/g/SDDT/{version}", "/g/SDDT")
|
||||
class OngekiController(
|
||||
val mapper: BasicMapper,
|
||||
val db: OngekiUserRepos,
|
||||
@@ -23,14 +27,23 @@ class OngekiController(
|
||||
val us: AquaUserServices,
|
||||
val pop: GameMusicPopularity,
|
||||
val cardService: CardService,
|
||||
): MeowApi({ _, resp -> if (resp is String) resp else mapper.write(resp) }) {
|
||||
): MeowApi(
|
||||
{ _, resp -> if (resp is String) resp else mapper.write(resp) },
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
{ endpoint: String ->
|
||||
gdb.gameData.ogkGameEncryption.map{
|
||||
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
.generateSecret(PBEKeySpec(
|
||||
endpoint.toCharArray(),
|
||||
it.salt!!.hexToByteArray(),
|
||||
it.iterations, 128
|
||||
)).encoded.toHexString().substring(0, 32)
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
val log = logger()
|
||||
|
||||
val noopEndpoint = setOf("ExtendLockTimeApi", "GameLogoutApi", "RegisterPromotionCardApi",
|
||||
"UpsertClientBookkeepingApi", "UpsertClientDevelopApi", "UpsertClientErrorApi", "UpsertClientSettingApi",
|
||||
"UpsertClientTestmodeApi", "UpsertUserGplogApi", "Ping")
|
||||
|
||||
init { ongekiInit() }
|
||||
val handlers = initH
|
||||
|
||||
@@ -40,26 +53,25 @@ class OngekiController(
|
||||
version?.let { data["version"] = it }
|
||||
|
||||
val token = TokenChecker.tokenShort()
|
||||
log.info("$token : $api < ${data.toJson()}")
|
||||
|
||||
val apiFriendlyName = if (api.lowercase() == api && api.length == 32)
|
||||
((hashes.filter { it.value.contains(api) }.keys.firstOrNull()?.str + " (encrypt)") ?: api) else "$api (encrypt)"
|
||||
log.info("$token : $apiFriendlyName < ${data.toJson()}")
|
||||
|
||||
val noop = """{"returnCode":1,"apiName":"${api.substringBefore("Api").firstCharLower()}"}"""
|
||||
if (api !in noopEndpoint && !handlers.containsKey(api)) {
|
||||
log.warn("$token : $api > not found")
|
||||
if (!handlers.containsKey(api)) {
|
||||
log.warn("$token : $apiFriendlyName > not found")
|
||||
return noop
|
||||
}
|
||||
|
||||
// Only record the counter metrics if the API is known.
|
||||
Metrics.counter("aquadx_ongeki_api_call", "api" to api).increment()
|
||||
if (api in noopEndpoint) {
|
||||
log.info("$token : $api > no-op")
|
||||
return noop
|
||||
}
|
||||
|
||||
return try {
|
||||
Metrics.timer("aquadx_ongeki_api_latency", "api" to api).recordCallable {
|
||||
serialize(api, handlers[api]!!(ctx) ?: noop).also {
|
||||
if (api !in setOf("GetUserItemApi", "GetGameEventApi"))
|
||||
log.info("$token : $api > ${it.truncate(500)}")
|
||||
log.info("$token : $apiFriendlyName > ${it.truncate(500)}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package icu.samnyan.aqua.sega.util
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import ext.logger
|
||||
import ext.toJson
|
||||
import icu.samnyan.aqua.sega.general.model.GameEncryptionKey
|
||||
import icu.samnyan.aqua.sega.chusan.model.GameCharge as Chu3GameCharge
|
||||
import icu.samnyan.aqua.sega.chusan.model.GameEvent as Chu3GameEvent
|
||||
import icu.samnyan.aqua.sega.chusan.model.GameGacha as Chu3GameGacha
|
||||
@@ -45,7 +46,8 @@ class GameDataService() {
|
||||
return mapper.read(resStream.bufferedReader().use { it.readText() }, object : TypeReference<List<T>>() {})
|
||||
}
|
||||
|
||||
log.warn("Game data file $f or resource $resPath not found, using empty list")
|
||||
if (file != "game_encryption.json")
|
||||
log.warn("Game data file $f or resource $resPath not found, using empty list")
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
@@ -54,6 +56,7 @@ class GameDataService() {
|
||||
lateinit var mai2Events: List<Mai2GameEvent>
|
||||
lateinit var mai2Charges: List<Mai2GameCharge>
|
||||
lateinit var mai2SellingCards: List<Mai2GameSellingCard>
|
||||
lateinit var mai2GameEncryption: List<GameEncryptionKey>
|
||||
|
||||
// chusan
|
||||
lateinit var chu3GameLinkedVerses: List<Chu3GameLinkedVerse>
|
||||
@@ -63,6 +66,7 @@ class GameDataService() {
|
||||
lateinit var chu3GameGachas: List<Chu3GameGacha>
|
||||
lateinit var chu3GameLoginBonusPresets: List<Chu3GameLoginBonusPreset>
|
||||
lateinit var chu3GameLoginBonuses: List<Chu3GameLoginBonus>
|
||||
lateinit var chu3GameEncryption: List<GameEncryptionKey>
|
||||
|
||||
// ongeki
|
||||
lateinit var ogkGameCards: List<OgkGameCard>
|
||||
@@ -75,6 +79,7 @@ class GameDataService() {
|
||||
lateinit var ogkGameSkills: List<OgkGameSkill>
|
||||
lateinit var ogkGameGachaCards: List<OgkGameGachaCard>
|
||||
lateinit var ogkGameGachas: List<OgkGameGacha>
|
||||
lateinit var ogkGameEncryption: List<GameEncryptionKey>
|
||||
|
||||
init {
|
||||
load()
|
||||
@@ -84,6 +89,7 @@ class GameDataService() {
|
||||
mai2Events = load("maimai2", "game_event.json")
|
||||
mai2Charges = load("maimai2", "game_charge.json")
|
||||
mai2SellingCards = load("maimai2", "game_selling_card.json")
|
||||
mai2GameEncryption = load(game = "maimai2", file = "game_encryption.json")
|
||||
|
||||
chu3GameLinkedVerses = load("chusan", "game_linked_verse.json")
|
||||
chu3GameCharges = load("chusan", "game_charge.json")
|
||||
@@ -92,6 +98,7 @@ class GameDataService() {
|
||||
chu3GameGachas = load("chusan", "game_gacha.json")
|
||||
chu3GameLoginBonusPresets = load("chusan", "game_login_bonus_preset.json")
|
||||
chu3GameLoginBonuses = load("chusan", "game_login_bonus.json")
|
||||
chu3GameEncryption = load(game = "chusan", file = "game_encryption.json")
|
||||
|
||||
ogkGameCards = load("ongeki", "game_card.json")
|
||||
ogkGameCharas = load("ongeki", "game_chara.json")
|
||||
@@ -103,6 +110,7 @@ class GameDataService() {
|
||||
ogkGameSkills = load("ongeki", "game_skill.json")
|
||||
ogkGameGachaCards = load("ongeki", "game_gacha_card.json")
|
||||
ogkGameGachas = load("ongeki", "game_gacha.json")
|
||||
ogkGameEncryption = load(game = "ongeki", file = "game_encryption.json")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@RestController
|
||||
@API("/g/wacca/")
|
||||
@API("/g/SDFE/{version}")
|
||||
class WaccaServer {
|
||||
// These are lateinit autowired instead of constructor injection because the tests depend on creating
|
||||
// an instance of this class with no arguments.
|
||||
|
||||
Reference in New Issue
Block a user