import vibe.vibe; import std.base64 : Base64; import std.conv : to; import botan.filters.pipe : Pipe; import botan.filters.b64_filt : Base64Encoder, Base64Decoder; import botan.filters.transform_filter : TransformationFilter; import botan.compression.zlib: ZlibDecompression; import botan.libstate.lookup : getCipher; import botan.algo_base.symkey : SymmetricKey, InitializationVector; import botan.utils.types; import config; void main() { auto router = new URLRouter; router.get("*", reverseProxyRequest(serverHost, serverPort)); router.post("/api/v1/*", &onAPIRequest); auto settings = new HTTPServerSettings; settings.port = proxyPortNumber; settings.options = HTTPServerOption.parseURL | HTTPServerOption.errorStackTraces; listenHTTP(settings, router); runApplication(); } void onAPIRequest(HTTPServerRequest request, HTTPServerResponse response) { auto apiEndpoint = request.path.split("/api/v1")[1]; auto nonce = request.headers["auth_nonce"]; auto requestBody = request.bodyReader.readAll(); auto requestJson = getRequestJson(requestBody, apiEndpoint, nonce); handleRequest(apiEndpoint, requestJson); //TODO: Allow request editing (requires recalculation of signature) //NOTE: This would not be a very useful feature, so this might be removed later. // One thing it would be useful for is creating a bot for keeping track of API changes. // This would also require quite a bit of refactoring however. auto apiResponse = sendRequest(apiEndpoint, request.headers.dup, requestBody); auto responseBody = apiResponse.bodyReader.readAll(); auto responseJson = getResponseJson(responseBody, apiEndpoint, nonce); auto finalResponseJson = handleResponse(apiEndpoint, responseJson); auto finalResponseBody = constructResponseBody(apiEndpoint, finalResponseJson, nonce); sendResponse(response, apiResponse, finalResponseBody); import std.stdio : writeln; writeln(apiEndpoint); import std.file : append; struct LogEntry { string endpoint; Json request; Json response; } append("api.log", LogEntry(apiEndpoint, requestJson, finalResponseJson).serializeToJson.toPrettyString ~ "\n\n"); } Json getRequestJson(ubyte[] input, string endpoint, string nonce) { auto originalRequestPipe = Pipe ( new Base64Decoder, getCipher ( cipherName, SymmetricKey((cast(ubyte[]) keyString).ptr, keyString.length), InitializationVector((cast(ubyte[]) nonce).ptr, nonce.length), DECRYPTION ) ); originalRequestPipe.processMsg(input.ptr, input.length); auto requestJsonString = originalRequestPipe.toString; return requestJsonString.parseJson; } Json getResponseJson(ubyte[] input, string endpoint, string nonce) { import std.zlib : uncompress; switch(endpoint) { case "/config/getGameConfig": return getRequestJson(input, endpoint, nonce); case "/master/getMaster": auto decompressedInput = cast(ubyte[])uncompress(input); //TODO: Use pipe instead (how?) auto responseJsonString = cast(string) decompressedInput; return responseJsonString.parseJson; default: auto decompressedInput = cast(ubyte[])uncompress(input); //TODO: Use pipe instead (how?) auto responsePipe = Pipe ( new Base64Decoder ); responsePipe.processMsg(decompressedInput.ptr, decompressedInput.length); auto responseJsonString = responsePipe.toString; return responseJsonString.parseJson; } } void handleRequest(string endpoint, Json json) { } Json handleResponse(string endpoint, Json json) { if(endpoint == "/master/getMaster") { import models.master; auto getMasterResponse = json.deserializeJson!GetMasterResponse; import dataMixins : applyMasterTranslation; mixin(applyMasterTranslation!("Character", 0, 5, "characterNames")); mixin(applyMasterTranslation!("CharacterSkill", 0, 1, "skillNames", 6, "skillDescriptions")); mixin(applyMasterTranslation!("Stage", 0, 1, "stageNames")); //TODO: Figure out a way to find the wrong ones mixin(applyMasterTranslation!("Mission", 0, 1, "missionNames", 2, "missionDescriptions", 3, "missionRewards")); mixin(applyMasterTranslation!("Dungeon", 10, 12, "dungeonNames")); mixin(applyMasterTranslation!("Guest", 0, 1, "guestPartyNames")); mixin(applyMasterTranslation!("Item", 0, 1, "itemNames")); mixin(applyMasterTranslation!("WorldMap", 0, 3, "worldMapLocationNames", 4, "worldMapLocationCaptions", 5, "worldMapLocationDescriptions")); mixin(applyMasterTranslation!("CharacterTextResource", 0, 3, "characterTextResource")); mixin(applyMasterTranslation!("CharacterLeaderSkill", 0, 1, "characterLeaderSkillNames", 14, "characterLeaderSkillDescriptions")); mixin(applyMasterTranslation!("CharacterBook", 0, 5, "characterBookFlowerLanguage")); mixin(applyMasterTranslation!("CharacterCategory", 0, 1, "characterCategories")); return getMasterResponse.serializeToJson; } return json; } ubyte[] constructResponseBody(string endpoint, Json json, string nonce) { import std.zlib : compress; auto jsonStringBytes = cast(ubyte[]) json.toString; switch(endpoint) { case "/config/getGameConfig": auto responsePipe = Pipe ( getCipher ( cipherName, SymmetricKey((cast(ubyte[]) keyString).ptr, keyString.length), InitializationVector((cast(ubyte[]) nonce).ptr, nonce.length), ENCRYPTION ), new Base64Encoder ); //TODO: Refactor this if(jsonStringBytes.length % 16 != 0) { jsonStringBytes.length += 16 - jsonStringBytes.length % 16; } responsePipe.processMsg(jsonStringBytes.ptr, jsonStringBytes.length); return cast(ubyte[]) responsePipe.toString; case "/master/getMaster": return compress(jsonStringBytes); //TODO: Use pipe instead (how?) default: auto responsePipe = Pipe ( new Base64Encoder ); responsePipe.processMsg(jsonStringBytes.ptr, jsonStringBytes.length); return compress(responsePipe.toString); } } HTTPClientResponse sendRequest(string endpoint, InetHeaderMap headers, ubyte[] requestBody) { return requestHTTP("http://" ~ serverHost ~ "/api/v1" ~ endpoint, (scope HTTPClientRequest r) { r.method = HTTPMethod.POST; r.headers = headers.dup; r.writeBody(requestBody, r.headers["Content-Type"]); } ); } void sendResponse(HTTPServerResponse response, HTTPClientResponse apiResponse, ubyte[] responseBody) { //HACK:This is necessary because of something, but I don't remember what if("Transfer-Encoding" in apiResponse.headers) { apiResponse.headers.remove("Transfer-Encoding"); } response.statusCode = apiResponse.statusCode; response.statusPhrase = apiResponse.statusPhrase; response.headers = apiResponse.headers.dup; response.writeBody(responseBody); }