Compare commits

...

2 Commits

Author SHA1 Message Date
Remik1r3n
47cbd5b09a Merge branch 'master' of https://github.com/Remik1r3n/maimaiDX-Api 2025-02-21 17:37:30 +08:00
Remik1r3n
7570fbc8f4 AuthLite实现,ApiHash草稿,各种小改进 2025-02-18 14:53:27 +08:00
8 changed files with 227 additions and 12 deletions

125
API_AuthLite.py Normal file
View File

@ -0,0 +1,125 @@
# All.Net AuthLite 更新获取
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import httpx
from loguru import logger
from urllib.parse import parse_qs
import configparser as ini
def enc(key, iv, data):
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(data)
return encrypted
def dec(key, iv ,data):
de_cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = de_cipher.decrypt(data)
return decrypted
def getRawDelivery():
key = bytes([47, 63, 106, 111, 43, 34, 76, 38, 92, 67, 114, 57, 40, 61, 107, 71])
iv = bytes.fromhex('00000000000000000000000000000000')
content = bytes([0] * 16) + b'title_id=SDGB&title_ver=1.40&client_id=A63E01C2805'
header = bytes.fromhex('00000000000000000000000000000000')
bytes_data = pad(header + content, 16)
encrypted = enc(key, iv, bytes_data)
r = httpx.post(
'http://at.sys-allnet.cn/net/delivery/instruction',
data = encrypted,
headers = {
'User-Agent': "SDGB;Windows/Lite",
'Pragma': 'DFI'
}
)
resp_data = r.content
decrypted = dec(key, resp_data[:16], resp_data)
decrypted_str = decrypted[16:].decode('UTF-8').strip()
# 过滤所有控制字符
decrypted_str = ''.join([i for i in decrypted_str if 31 < ord(i) < 127])
logger.info(f"RAW Response: {decrypted_str}")
return decrypted_str
def parseRawDelivery(deliveryStr):
"""解析 RAW 的 Delivery 字符串,返回其中的有效的 instruction URL 的列表"""
parsedResponseDict = {key: value[0] for key, value in parse_qs(deliveryStr).items()}
urlList = parsedResponseDict['uri'].split('|')
# 过滤掉空字符串和内容为 null 的情况
urlList = [url for url in urlList if url and url != 'null']
logger.info(f"Parsed URL List: {urlList}")
validURLs = []
for url in urlList:
# 检查是否是 HTTPS 的 URL以及是否是 txt 文件,否则忽略
if not url.startswith('https://') or not url.endswith('.txt'):
logger.warning(f"Invalid URL will be ignored: {url}")
continue
validURLs.append(url)
logger.info(f"Verified Valid URLs: {validURLs}")
return validURLs
def getUpdateIniFromURL(url):
# 发送请求
response = httpx.get(url, headers={
'User-Agent': 'SDGB;Windows/Lite',
'Pragma': 'DFI'
})
logger.info(f"成功自 {url} 获取更新信息")
return response.text
def parseUpdateIni(iniText):
# 解析配置
config = ini.ConfigParser(allow_no_value=True)
config.read_string(iniText)
logger.info(f"成功解析配置文件,包含的节有:{config.sections()}")
# 获取 COMMON 节的配置
common = config['COMMON']
# 初始化消息列表
message = []
# 获取游戏描述并去除引号
game_desc = common['GAME_DESC'].strip('"')
# 根据前缀选择消息模板和图标
prefix_icons = {
'PATCH': ('💾 游戏程序更新 ', 'PATCH_'),
'OPTION': ('📚 游戏内容更新 ', 'OPTION_')
}
icon, prefix = prefix_icons.get(game_desc.split('_')[0], ('📦 游戏更新 ', ''))
# 构建消息标题
game_title = game_desc.replace(prefix, '', 1)
message.append(f"{icon}{game_title}")
# 添加主文件的下载链接
main_file = common['INSTALL1']
main_file_name = main_file.split('/')[-1]
message.append(f"下载: \n- [{main_file_name}]({main_file})")
# 添加可选文件的下载链接(如果有)
if 'OPTIONAL' in config:
message.append("其它文件:")
optional_files = [f"- [{url.split('/')[-1]}]({url})" for _, url in config.items('OPTIONAL')]
message.extend(optional_files)
# 添加发布时间信息
release_time = common['RELEASE_TIME'].replace('T', ' ')
message.append(f"将于 {release_time} 发布。\n")
# 构建最终的消息字符串
final_message = '\n'.join(message)
logger.info(f"消息构建完成,最终的消息为:\n{final_message}")
return final_message
if __name__ == '__main__':
urlList = parseRawDelivery(getRawDelivery())
for url in urlList:
iniText = getUpdateIniFromURL(url)
message = parseUpdateIni(iniText)

View File

@ -95,7 +95,8 @@ def apiSDGB(data:str, targetApi:str, userAgentExtraData:str, noLog:bool=False, t
"Expect": "100-continue"
},
content=reqData_deflated,
# verify=certifi.where(),
# 经测试,加 Verify 之后速度慢好多,因此建议选择性开
#verify=certifi.where(),
verify=False,
timeout=timeout
)
@ -145,11 +146,11 @@ def apiSDGB(data:str, targetApi:str, userAgentExtraData:str, noLog:bool=False, t
raise SDGBApiError("重试多次仍然无法成功请求服务器")
def calcSpecialNumber():
def calcPlaySpecial():
"""使用 c_int32 实现的 SpecialNumber 算法"""
rng = random.SystemRandom()
num2 = rng.randint(1, 1037933) * 2069
num2 += 0x400
num2 += 1024 #GameManager.CalcSpecialNum()
num2 = c_int32(num2).value
result = c_int32(0)
for _ in range(32):
@ -158,8 +159,9 @@ def calcSpecialNumber():
num2 >>= 1
return c_int32(result.value).value
"""
DEPRECATED: 旧的 SpecialNumber 算法
def calcSpecialNumber2():
"""实验性替代 SpecialNumber 算法"""
max = 1037933
num2 = random.randint(1, max) * 2069
@ -170,4 +172,5 @@ def calcSpecialNumber2():
num3 += num2 % 2
num2 >>= 1
return num3
return num3
"""

View File

@ -144,3 +144,11 @@ def generateLoginBonusList(UserLoginBonusList, generateMode=1):
logger.debug(f"ログインボーナスリスト: {bonusList}")
return bonusList
if __name__ == "__main__":
# ログインボーナスデータをアップロードする
userId = testUid
currentLoginTimestamp = generateTimestamp()
currentLoginResult = apiLogin(currentLoginTimestamp, userId)
implLoginBonus(userId, currentLoginTimestamp, currentLoginResult, 2)
apiLogout(currentLoginTimestamp, userId)

View File

@ -0,0 +1,43 @@
GetUserCardApi
GetUserCharacterApi
GetUserChargeApi
GetUserCourseApi
GetUserDataApi
GetUserExtendApi
GetUserFavoriteApi
GetUserFriendSeasonRankingApi
GetUserGhostApi
GetUserItemApi
GetGameChargeApi
GetUserLoginBonusApi
GetGameEventApi
GetUserMapApi
GetGameNgMusicIdApi
GetUserMusicApi
GetGameRankingApi
GetGameSettingApi
GetUserOptionApi
GetUserPortraitApi
GetGameTournamentInfoApi
UserLogoutApi
GetUserPreviewApi
GetTransferFriendApi
GetUserRatingApi
GetUserActivityApi
GetUserRecommendRateMusicApi
GetUserRecommendSelectMusicApi
GetUserRegionApi
GetUserScoreRankingApi
UploadUserPhotoApi
UploadUserPlaylogApi
UploadUserPortraitApi
UpsertClientBookkeepingApi
UpsertClientSettingApi
UpsertClientTestmodeApi
UpsertClientUploadApi
UpsertUserAllApi
UpsertUserChargelogApi
UserLoginApi
Ping
GetUserFavoriteItemApi
GetGameNgWordListApi

View File

@ -0,0 +1,28 @@
import os
import re
pattern = re.compile(r'Maimai2Servlet/(.*?)MaimaiChn')
# 获取目录
dir = 'C:/Users/remik1r3n/Workspace/maimaiDX-Api/HashEntertainment'
known_hashes = []
for filename in os.listdir(dir):
# 只处理.txt文件
if filename.endswith('.txt'):
file_path = os.path.join(dir, filename)
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
# 搜索匹配的模式
matches = pattern.findall(content)
# 输出每个匹配中的不定内容
for match in matches:
known_hashes.append(match)
# 去重
known_hashes = list(set(known_hashes))
# 输出
for hash in known_hashes:
print(hash)

View File

@ -63,9 +63,9 @@ def implFullPlayAction(userId: int, currentLoginTimestamp:int, currentLoginResul
retries = 0
while retries < 3:
# 计算一个特殊数
currentSpecialNumber = calcSpecialNumber()
currentPlaySpecial = calcPlaySpecial()
# 生成出 UserAll
currentUserAll = generateFullUserAll(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentSpecialNumber)
currentUserAll = generateFullUserAll(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentPlaySpecial)
# 应用参数里的补丁
applyUserAllPatches(currentUserAll, userAllPatches)

View File

@ -4,6 +4,7 @@
import rapidjson as json
import time
from loguru import logger
import random
from Config import *
from API_TitleServer import apiSDGB
@ -41,12 +42,19 @@ def apiLogout(timestamp:int, userId:int, noLog:bool=False) -> dict:
logger.info("登出:结果:"+ str(logout_result))
return logout_result
def generateTimestamp() -> int:
def generateTimestampLegacy() -> int:
"""生成一个凑合用的时间戳"""
timestamp = int(time.time()) - 60
logger.info(f"生成时间戳: {timestamp}")
return timestamp
def generateTimestamp() -> int:
"""生成一个今天早上 10:00 随机偏移的时间戳"""
timestamp = int(time.mktime(time.strptime(time.strftime("%Y-%m-%d 10:00:00"), "%Y-%m-%d %H:%M:%S"))) + random.randint(-600, 600)
logger.info(f"生成时间戳: {timestamp}")
logger.info(f"此时间戳对应的时间为: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp))}")
return timestamp
if __name__ == "__main__":
print("强制登出 CLI")
uid = testUid

View File

@ -5,11 +5,11 @@ from datetime import datetime
from Config import *
from HelperGetUserThing import implGetUser_
def generateFullUserAll(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentSpecialNumber):
def generateFullUserAll(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentPlaySpecial):
"""从服务器取得必要的数据并构建一个比较完整的 UserAll"""
# 先构建一个基础 UserAll
currentUserAll = generateUserAllData(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentSpecialNumber)
currentUserAll = generateUserAllData(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentPlaySpecial)
# 然后从服务器取得必要的数据
currentUserExtend = implGetUser_("Extend", userId, True)
@ -29,7 +29,7 @@ def generateFullUserAll(userId, currentLoginResult, currentLoginTimestamp, curre
return currentUserAll
def generateUserAllData(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentSpecialNumber):
def generateUserAllData(userId, currentLoginResult, currentLoginTimestamp, currentUserData2, currentPlaySpecial):
"""构建一个非常基础的 UserAll 数据,必须手动填充一些数据"""
data = {
@ -149,7 +149,7 @@ def generateUserAllData(userId, currentLoginResult, currentLoginTimestamp, curre
"isEventMode": False,
"isNewFree": False,
"playCount": currentUserData2['playCount'],
"playSpecial": currentSpecialNumber,
"playSpecial": currentPlaySpecial,
"playOtherUserId": 0
}
],