diff --git a/SCHALE.Common/Database/dbs.cs b/SCHALE.Common/Database/dbs.cs index be4147b..2fb03d5 100644 --- a/SCHALE.Common/Database/dbs.cs +++ b/SCHALE.Common/Database/dbs.cs @@ -759,20 +759,20 @@ namespace SCHALE.Common.Database [Key] public long ServerId { get; set; } public long UniqueId { get; set; } - public int StarGrade { get; set; } - public int Level { get; set; } - public long Exp { get; set; } - public int FavorRank { get; set; } - public long FavorExp { get; set; } - public int PublicSkillLevel { get; set; } - public int ExSkillLevel { get; set; } - public int PassiveSkillLevel { get; set; } - public int ExtraPassiveSkillLevel { get; set; } - public int LeaderSkillLevel { get; set; } + public int StarGrade { get; set; } = 1; + public int Level { get; set; } = 1; + public long Exp { get; set; } = 0; + public int FavorRank { get; set; } = 1; + public long FavorExp { get; set; } = 0; + public int PublicSkillLevel { get; set; } = 1; + public int ExSkillLevel { get; set; } = 1; + public int PassiveSkillLevel { get; set; } = 1; + public int ExtraPassiveSkillLevel { get; set; } = 1; + public int LeaderSkillLevel { get; set; } = 1; [NotMapped] - public bool IsNew { get; set; } - public bool IsLocked { get; set; } + public bool IsNew { get; set; } = true; + public bool IsLocked { get; set; } = true; public bool IsFavorite { get; set; } public List EquipmentServerIds { get; set; } = []; public Dictionary PotentialStats { get; set; } = []; diff --git a/SCHALE.Common/NetworkProtocol/protos.cs b/SCHALE.Common/NetworkProtocol/protos.cs index 33a691b..a2979ac 100644 --- a/SCHALE.Common/NetworkProtocol/protos.cs +++ b/SCHALE.Common/NetworkProtocol/protos.cs @@ -9025,8 +9025,8 @@ namespace SCHALE.Common.NetworkProtocol public class GachaResult { public long CharacterId { get; set; } - public CharacterDB Character { get; set; } - public ItemDB Stone { get; set; } + public CharacterDB? Character { get; set; } + public ItemDB? Stone { get; set; } public GachaResult(long id) { this.CharacterId = id; diff --git a/SCHALE.Common/Utils/AccDict.cs b/SCHALE.Common/Utils/AccDict.cs new file mode 100644 index 0000000..4451bfe --- /dev/null +++ b/SCHALE.Common/Utils/AccDict.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SCHALE.Common.Utils +{ + public class AccDict : Dictionary where T : notnull + { + public new int this[T key] + { + get => TryGetValue(key, out var value) ? value : 0; + set => base[key] = value; + } + } +} diff --git a/SCHALE.Common/Utils/CharacterExcelTExt.cs b/SCHALE.Common/Utils/CharacterExcelTExt.cs new file mode 100644 index 0000000..161bcc5 --- /dev/null +++ b/SCHALE.Common/Utils/CharacterExcelTExt.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SCHALE.Common.FlatData; + +namespace SCHALE.Common.Utils +{ + public enum StudentType + { + Normal, + Unique, + Event, + } + + public static class CharacterExcelTExt + { + public static StudentType GetStudentType(this CharacterExcelT ch) + { + if (!ch.CollectionVisibleEndDate.StartsWith("2099")) + { + return StudentType.Unique; + } + else if (ch.CombineRecipeId == 0) + { + return StudentType.Event; + } + + return StudentType.Normal; + } + } +} diff --git a/SCHALE.GameServer/Controllers/Api/ProtocolHandlers/Shop.cs b/SCHALE.GameServer/Controllers/Api/ProtocolHandlers/Shop.cs index a4c41d9..2a5b6ae 100644 --- a/SCHALE.GameServer/Controllers/Api/ProtocolHandlers/Shop.cs +++ b/SCHALE.GameServer/Controllers/Api/ProtocolHandlers/Shop.cs @@ -1,21 +1,29 @@ -using SCHALE.Common.Database; +using Microsoft.EntityFrameworkCore; +using SCHALE.Common.Database; +using SCHALE.Common.Database.ModelExtensions; using SCHALE.Common.NetworkProtocol; +using SCHALE.Common.Utils; using SCHALE.GameServer.Services; +using SCHALE.GameServer.Utils; namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers { public class Shop : ProtocolHandlerBase { - private readonly ISessionKeyService sessionKeyService; - private readonly SCHALEContext context; + private readonly ISessionKeyService _sessionKeyService; + private readonly SCHALEContext _context; + private readonly SharedDataCacheService _sharedData; + private readonly ILogger _logger; // TODO: temp storage until gacha management public List SavedGachaResults { get; set; } = []; - public Shop(IProtocolHandlerFactory protocolHandlerFactory, ISessionKeyService _sessionKeyService, SCHALEContext _context) : base(protocolHandlerFactory) + public Shop(IProtocolHandlerFactory protocolHandlerFactory, ISessionKeyService sessionKeyService, SCHALEContext context, SharedDataCacheService sharedData, ILogger logger) : base(protocolHandlerFactory) { - sessionKeyService = _sessionKeyService; - context = _context; + _sessionKeyService = sessionKeyService; + _context = context; + _sharedData = sharedData; + _logger = logger; } [ProtocolHandler(Protocol.Shop_BeforehandGachaGet)] @@ -57,7 +65,7 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers [ProtocolHandler(Protocol.Shop_BeforehandGachaPick)] public ResponsePacket BeforehandGachaPickHandler(ShopBeforehandGachaPickRequest req) { - var account = sessionKeyService.GetAccount(req.SessionKey); + var account = _sessionKeyService.GetAccount(req.SessionKey); var GachaResults = new List(); foreach (var charId in SavedGachaResults) @@ -92,39 +100,214 @@ namespace SCHALE.GameServer.Controllers.Api.ProtocolHandlers [ProtocolHandler(Protocol.Shop_BuyGacha3)] public ResponsePacket ShopBuyGacha3ResponseHandler(ShopBuyGacha3Request req) { - var gachaResults = new List(); + var account = _sessionKeyService.GetAccount(req.SessionKey); + var accountChSet = account.Characters.Select(x => x.UniqueId).ToHashSet(); - for (int i = 0; i < 10; i++) + // TODO: Implement FES Gacha + // TODO: Check Gacha currency + // TODO: SR pickup + // TODO: pickup stone count + // TODO: even more... + // Type Rate Acc.R + // ------------------------- + // Current SSR 0.7% 0.7% + // Other SSR 2.3% 3.0%ServerId + // SR 18.5% 21.5% + // R 78.5% 100.% + + const int gpStoneID = 90070086; + const int chUniStoneID = 23; + + var rateUpChId = 10094; // 10094, 10095 + var rateUpIsNormalStudent = false; + var gachaList = new List(10); + var itemDict = new AccDict(); + bool shouldDoGuaranteedSR = true; + // itemDict[gpStoneID] = 10; + + for (int i = 0; i < 10; ++i) { - long id = 10000 + new Random().Next(0, 94); - - gachaResults.Add(new(id) + var randomNumber = Random.Shared.NextInt64(1000); + if (randomNumber < 7) { - Character = new() // hardcoded util proper db + // always 3 star + shouldDoGuaranteedSR = false; + var isNew = accountChSet.Add(rateUpChId); + gachaList.Add(new(rateUpChId) { - ServerId = req.AccountId, - UniqueId = id, - StarGrade = 3, - Level = 1, - FavorRank = 1, - PublicSkillLevel = 1, - ExSkillLevel = 1, - PassiveSkillLevel = 1, - ExtraPassiveSkillLevel = 1, - LeaderSkillLevel = 1, - IsNew = true, - IsLocked = true + Character = !isNew ? null : new() + { + AccountServerId = account.ServerId, + UniqueId = rateUpChId, + StarGrade = 3, + }, + Stone = isNew ? null : new() + { + UniqueId = chUniStoneID, + StackCount = 50, + } + }); + if (!isNew) + { + itemDict[chUniStoneID] += 50; + itemDict[rateUpChId] += 100; } - }); + } + else if (randomNumber < 30) + { + shouldDoGuaranteedSR = false; + var normalSSRList = _sharedData.CharaListSSRNormal; + var poolSize = normalSSRList.Count; + if (rateUpIsNormalStudent) poolSize--; + + var randomPoolIdx = (int)Random.Shared.NextInt64(poolSize); + if (normalSSRList[randomPoolIdx].Id == rateUpChId) randomPoolIdx++; + + var chId = normalSSRList[randomPoolIdx].Id; + var isNew = accountChSet.Add(chId); + gachaList.Add(new(chId) + { + Character = !isNew ? null : new() + { + AccountServerId = account.ServerId, + UniqueId = chId, + StarGrade = 3, + }, + Stone = isNew ? null : new() + { + UniqueId = chUniStoneID, + StackCount = 50, + } + }); + if (!isNew) + { + itemDict[chUniStoneID] += 50; + itemDict[chId] += 30; + } + } + else if (randomNumber < 215 || (i == 9 && shouldDoGuaranteedSR)) + { + shouldDoGuaranteedSR = false; + var normalSRList = _sharedData.CharaListSRNormal; + var randomPoolIdx = (int)Random.Shared.NextInt64(normalSRList.Count); + var chId = normalSRList[randomPoolIdx].Id; + var isNew = accountChSet.Add(chId); + + gachaList.Add(new(chId) + { + Character = !isNew ? null : new() + { + AccountServerId = account.ServerId, + UniqueId = chId, + StarGrade = 2, + }, + Stone = isNew ? null : new() + { + UniqueId = chUniStoneID, + StackCount = 10, + } + }); + if (!isNew) + { + itemDict[chUniStoneID] += 10; + itemDict[chId] += 5; + } + } + else + { + var normalRList = _sharedData.CharaListRNormal; + var randomPoolIdx = (int)Random.Shared.NextInt64(normalRList.Count); + var chId = normalRList[randomPoolIdx].Id; + var isNew = accountChSet.Add(chId); + + gachaList.Add(new(chId) + { + Character = !isNew ? null : new() + { + AccountServerId = account.ServerId, + UniqueId = chId, + StarGrade = 1, + }, + Stone = isNew ? null : new() + { + UniqueId = chUniStoneID, + StackCount = 1, + } + }); + if (!isNew) + { + itemDict[chUniStoneID] += 1; + itemDict[chId] += 1; + } + } + } + + using var transaction = _context.Database.BeginTransaction(); + + try + { + // add characters + _context.Characters.AddRange( + gachaList.Where(x => x.Character != null) + .Select(x => x.Character)!); + + // create if item does not exist + foreach (var id in itemDict.Keys) + { + var itemExists = _context.Items + .Any(x => x.AccountServerId == account.ServerId && x.UniqueId == id); + if (!itemExists) + { + _context.Items.Add(new ItemDB() + { + IsNew = true, + UniqueId = id, + StackCount = 0, + AccountServerId = account.ServerId, + }); + } + } + _context.SaveChanges(); + + // perform item count update + foreach (var (id, count) in itemDict) + { + _context.Items + .Where(x => x.AccountServerId == account.ServerId && x.UniqueId == id) + .ExecuteUpdate(setters => setters.SetProperty( + item => item.StackCount, item => item.StackCount + count)); + } + + _context.SaveChanges(); + + transaction.Commit(); + + _context.Entry(account).Collection(x => x.Items).Reload(); + } + catch (Exception ex) + { + _logger.LogError("Transaction failed: {Message}", ex.Message); + throw; + } + + var itemDbList = itemDict.Keys + .Select(id => _context.Items.AsNoTracking().First(x => x.AccountServerId == account.ServerId && x.UniqueId == id)) + .ToList(); + foreach (var gacha in gachaList) + { + if (gacha.Stone != null) + { + gacha.Stone.ServerId = itemDbList.First(x => x.UniqueId == gacha.Stone.UniqueId).ServerId; + } } return new ShopBuyGacha3Response() { - GachaResults = gachaResults, + GachaResults = gachaList, UpdateTime = DateTime.UtcNow, - GemBonusRemain = long.MaxValue, + GemBonusRemain = int.MaxValue, ConsumedItems = [], - AcquiredItems = [], + AcquiredItems = itemDbList, MissionProgressDBs = [], }; } diff --git a/SCHALE.GameServer/GameServer.cs b/SCHALE.GameServer/GameServer.cs index 99a54c4..070fa52 100644 --- a/SCHALE.GameServer/GameServer.cs +++ b/SCHALE.GameServer/GameServer.cs @@ -87,6 +87,7 @@ namespace SCHALE.GameServer builder.Services.AddMemorySessionKeyService(); builder.Services.AddExcelTableService(); builder.Services.AddIrcService(); + builder.Services.AddSharedDataCache(); // Add all Handler Groups var handlerGroups = Assembly diff --git a/SCHALE.GameServer/Services/ExcelTableService.cs b/SCHALE.GameServer/Services/ExcelTableService.cs index 83738a7..cbf0ad5 100644 --- a/SCHALE.GameServer/Services/ExcelTableService.cs +++ b/SCHALE.GameServer/Services/ExcelTableService.cs @@ -17,11 +17,13 @@ namespace SCHALE.GameServer.Services private readonly ILogger logger = _logger; private readonly Dictionary caches = []; - public static async Task LoadExcels() + public static async Task LoadExcels(string excelDirectory = "") { var excelZipUrl = $"https://prod-clientpatch.bluearchiveyostar.com/{Config.Instance.VersionId}/TableBundles/Excel.zip"; - var excelDir = $"{Path.GetDirectoryName(AppContext.BaseDirectory)}/Resources/excel/"; + var excelDir = string.IsNullOrWhiteSpace(excelDirectory) + ? Path.Join(Path.GetDirectoryName(AppContext.BaseDirectory), "Resources/excel") + : excelDirectory; var excelZipPath = Path.Combine(excelDir, "Excel.zip"); if (Directory.Exists(excelDir)) @@ -53,14 +55,17 @@ namespace SCHALE.GameServer.Services /// /// /// - public T GetTable() where T : IFlatbufferObject + public T GetTable(bool bypassCache = false, string excelDirectory = "") where T : IFlatbufferObject { var type = typeof(T); - if (caches.TryGetValue(type, out var cache)) + if (!bypassCache && caches.TryGetValue(type, out var cache)) return (T)cache; - var bytesFilePath = Path.Join(Path.GetDirectoryName(AppContext.BaseDirectory), "Resources/excel/", $"{type.Name.ToLower()}.bytes"); + var excelDir = string.IsNullOrWhiteSpace(excelDirectory) + ? Path.Join(Path.GetDirectoryName(AppContext.BaseDirectory), "Resources/excel") + : excelDirectory; + var bytesFilePath = Path.Join(excelDir, $"{type.Name.ToLower()}.bytes"); if (!File.Exists(bytesFilePath)) { throw new FileNotFoundException($"bytes files for {type.Name} not found"); diff --git a/SCHALE.GameServer/Services/SharedDataCacheService.cs b/SCHALE.GameServer/Services/SharedDataCacheService.cs new file mode 100644 index 0000000..6d03aab --- /dev/null +++ b/SCHALE.GameServer/Services/SharedDataCacheService.cs @@ -0,0 +1,66 @@ +using SCHALE.Common.FlatData; +using SCHALE.Common.Utils; + +namespace SCHALE.GameServer.Services +{ + public class SharedDataCacheService + { + private readonly ILogger _logger; + private readonly ExcelTableService _excelTable; + private readonly List _charaList; + private readonly List _charaListR; + private readonly List _charaListSR; + private readonly List _charaListSSR; + private readonly List _charaListRNormal; + private readonly List _charaListSRNormal; + private readonly List _charaListSSRNormal; + private readonly List _charaListUnique; + private readonly List _charaListEvent; + + public IReadOnlyList CharaList => _charaList; + public IReadOnlyList CharaListR => _charaListR; + public IReadOnlyList CharaListSR => _charaListSR; + public IReadOnlyList CharaListSSR => _charaListSSR; + public IReadOnlyList CharaListRNormal=> _charaListRNormal; + public IReadOnlyList CharaListSRNormal=> _charaListSRNormal; + public IReadOnlyList CharaListSSRNormal=> _charaListSSRNormal; + public IReadOnlyList CharaListUnique => _charaListUnique; + public IReadOnlyList CharaListEvent => _charaListEvent; + + public SharedDataCacheService(ILogger logger, ExcelTableService excelTable) + { + _logger = logger; + _excelTable = excelTable; + + _charaList = excelTable + .GetTable() + .UnPack() + .DataList! + .Where(x => x is + { + IsPlayable: true, + IsPlayableCharacter: true, + IsDummy: false, + IsNPC: false, + ProductionStep: ProductionStep.Release, + }) + .ToList(); + _charaListR = _charaList.Where(x => x.Rarity == Rarity.R).ToList(); + _charaListSR = _charaList.Where(x => x.Rarity == Rarity.SR).ToList(); + _charaListSSR = _charaList.Where(x => x.Rarity == Rarity.SSR).ToList(); + _charaListRNormal = _charaListR.Where(x => x.GetStudentType() == StudentType.Normal).ToList(); + _charaListSRNormal = _charaListSR.Where(x => x.GetStudentType() == StudentType.Normal).ToList(); + _charaListSSRNormal = _charaListSSR.Where(x => x.GetStudentType() == StudentType.Normal).ToList(); + _charaListUnique = _charaListR.Where(x => x.GetStudentType() == StudentType.Unique).ToList(); + _charaListEvent = _charaListR.Where(x => x.GetStudentType() == StudentType.Event).ToList(); + } + } + + internal static class DataCacheServiceExtensions + { + public static void AddSharedDataCache(this IServiceCollection services) + { + services.AddSingleton(); + } + } +} diff --git a/SCHALE.GameServer/Utils/CollectionEntryExt.cs b/SCHALE.GameServer/Utils/CollectionEntryExt.cs new file mode 100644 index 0000000..a6db85e --- /dev/null +++ b/SCHALE.GameServer/Utils/CollectionEntryExt.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace SCHALE.GameServer.Utils; + +public static class CollectionEntryExt +{ + public static void Reload(this CollectionEntry source) + { + if (source.CurrentValue != null) + { + foreach (var item in source.CurrentValue) + { + source.EntityEntry.Context.Entry(item).State = Microsoft.EntityFrameworkCore.EntityState.Detached; + } + source.CurrentValue = null; + source.IsLoaded = false; + source.Load(); + } + } +} \ No newline at end of file diff --git a/SCHALE.GameServer/Utils/InventoryUtils.cs b/SCHALE.GameServer/Utils/InventoryUtils.cs index 83c6ef4..065dca4 100644 --- a/SCHALE.GameServer/Utils/InventoryUtils.cs +++ b/SCHALE.GameServer/Utils/InventoryUtils.cs @@ -17,10 +17,15 @@ namespace SCHALE.Common.Utils var context = connection.Context; var characterExcel = connection.ExcelTableService.GetTable().UnPack().DataList; - var allCharacters = characterExcel.Where(x => x.IsPlayable && x.IsPlayableCharacter && x.CollectionVisible && !account.Characters.Any(c => c.UniqueId == x.Id)).Select(x => - { - return CreateMaxCharacterFromId(x.Id); - }).ToList(); + var allCharacters = characterExcel.Where(x => + x is + { + IsPlayable: true, + IsPlayableCharacter: true, + IsNPC: false, + ProductionStep: ProductionStep.Release, + } + ).Select(x => CreateMaxCharacterFromId(x.Id)).ToList(); account.AddCharacters(context, [.. allCharacters]); context.SaveChanges(); diff --git a/Scripts/redirect_server_frida/README.md b/Scripts/redirect_server_frida/README.md new file mode 100644 index 0000000..967944f --- /dev/null +++ b/Scripts/redirect_server_frida/README.md @@ -0,0 +1,38 @@ +# Redirect server via Frida + +## Install Frida's CLI tools + +Make sure [Python](https://python.org/) is installed before you start. + +Install Frida's CLI tools via Pypi. + +``` +pip install frida-tools +``` + +## Run Frida server on device/emulator + +Download Frida server [here](https://github.com/frida/frida/releases/). + +Make sure adb is enabled and Android is rooted. + +Run: + +``` +adb push frida-server /data/local/tmp +adb shell +# in adb shell +su +chmod 755 /data/local/tmp/frida-server +/data/local/tmp/frida-server +``` + +## Hook client with frida + +Set your server address in `ba.js`. + +Launch the client, then immediately run the following command on host: + +``` +frida -U "ブルアカ" -l ba.js --realm=emulated +``` diff --git a/ba.js b/Scripts/redirect_server_frida/ba.js similarity index 100% rename from ba.js rename to Scripts/redirect_server_frida/ba.js diff --git a/Scripts/redirect_server_mitmproxy/README.md b/Scripts/redirect_server_mitmproxy/README.md new file mode 100644 index 0000000..37035e8 --- /dev/null +++ b/Scripts/redirect_server_mitmproxy/README.md @@ -0,0 +1,21 @@ +# Redirect server via mitmproxy + +## Install mitmproxy + +Download the installer from [mitmproxy.org](https://mitmproxy.org/) + +## Install CA certificate + +Follow the instructions from [System CA on Android Emulator](https://docs.mitmproxy.org/stable/howto-install-system-trusted-ca-android/) + +## Hook client with mitmproxy + +Set your server address and port in `redirect_server.py` + +Install [WireGuard](https://wireguard.com/install/#android-play-store-f-droid) on client, then run mitmproxy: + +``` +mitmweb -m wireguard --no-http2 -s redirect_server.py --set termlog_verbosity=warn +``` + +It also works as a packet dumper. You can save the flow file for further works. diff --git a/Scripts/redirect_server_mitmproxy/packet_analyzer.py b/Scripts/redirect_server_mitmproxy/packet_analyzer.py new file mode 100644 index 0000000..32c0286 --- /dev/null +++ b/Scripts/redirect_server_mitmproxy/packet_analyzer.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +import argparse +import gzip +import json +import os + +from mitmproxy import io +from mitmproxy.http import HTTPFlow + +if __name__ == "__main__": + parser = argparse.ArgumentParser('Flow dumper') + parser.add_argument('file', type=str, help='mitmproxy flow file') + args = parser.parse_args() + + os.makedirs(f'{args.file}.dumps', exist_ok=True) + + f = open(args.file, 'rb') + r = io.FlowReader(f) + + i = 0 + for flow in r.stream(): + if not isinstance(flow, HTTPFlow): + continue + if not flow.request.url.endswith('/api/gateway'): + continue + + req = flow.request.raw_content + res = json.loads(flow.response.text) + protocol = res['protocol'] + + mx_end = req.rfind(b'\r\n', 0, len(req) - 1) + mx_start = req.rfind(b'\r\n\r\n') + req_mx = req[mx_start + 4:mx_end] + req_bytes = req_mx[12:] + req_bytes = bytearray([x ^ 0xD9 for x in req_bytes]) + req_bytes = gzip.decompress(req_bytes) + + packet = json.loads(req_bytes) + with open(f'{args.file}.dumps/{i}_req_{protocol}.json', 'w') as f_req: + json.dump(packet, f_req, indent=2, ensure_ascii=False) + + packet = json.loads(res['packet']) + with open(f'{args.file}.dumps/{i}_resp_{protocol}.json', 'w', encoding='utf8') as f_res: + json.dump(packet, f_res, indent=2, ensure_ascii=False) + i += 1 + + f.close() diff --git a/Scripts/redirect_server_mitmproxy/redirect_server.py b/Scripts/redirect_server_mitmproxy/redirect_server.py new file mode 100644 index 0000000..d49feef --- /dev/null +++ b/Scripts/redirect_server_mitmproxy/redirect_server.py @@ -0,0 +1,45 @@ +import gzip +import json +from mitmproxy import http + +SERVER_HOST = 'YOUR_SERVER_HERE' +SERVER_PORT = 80 + +REWRITE_HOST_LIST = [ + 'ba-jp-sdk.bluearchive.jp', + 'prod-gateway.bluearchiveyostar.com', + 'prod-game.bluearchiveyostar.com', + # 'prod-notice.bluearchiveyostar.com', + # 'prod-logcollector.bluearchiveyostar.com', +] + +def request(flow: http.HTTPFlow) -> None: + if flow.request.pretty_host.endswith('log.aliyuncs.com'): + flow.kill() + return + if flow.request.pretty_host in REWRITE_HOST_LIST: + flow.request.scheme = 'http' + flow.request.host = SERVER_HOST + flow.request.port = SERVER_PORT + return + +def response(flow: http.HTTPFlow) -> None: + if flow.request.url.endswith('/api/gateway'): + try: + req = flow.request.raw_content + res = json.loads(flow.response.text) + protocol = res['protocol'] + + mx_end = req.rfind(b'\r\n', 0, len(req) - 1) + mx_start = req.rfind(b'\r\n\r\n') + req_mx = req[mx_start + 4:mx_end] + req_bytes = req_mx[12:] + req_bytes = bytearray([x ^ 0xD9 for x in req_bytes]) + req_bytes = gzip.decompress(req_bytes) + print(f'Protocol: {protocol}') + print(f'[OUT]->{json.loads(req_bytes)}') + print(f'[IN]<--{json.loads(res["packet"])}') + print('') + except Exception as e: + print('Failed to dump packet', e) + return