fkg-api-proxy/source/app.d

245 lines
7.9 KiB
D

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);
}