From fa22bc80194ee85c5e312a98a6a72e800a788f11 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 9 Jan 2025 23:11:57 +0000 Subject: [PATCH] partial request working --- Core/DI/ISerializer.cs | 7 + Core/DI/Router.cs | 107 ++++++++++ Core/Helpers/HttpServerHelper.cs | 2 +- Core/Models/Eft/Common/EmptyRequestData.cs | 8 +- Core/Models/Enums/BackendErrorCodes.cs | 90 +++++++++ Core/Models/Utils/IRequestData.cs | 5 + Core/Routers/Dynamic/HttpDynamicRouter.cs | 23 +++ Core/Routers/HttpRouter.cs | 94 +++++++++ Core/Routers/ImageRouter.cs | 15 +- Core/Routers/Serializers/ImageSerializer.cs | 25 +++ Core/Routers/Static/LauncherStaticRouter.cs | 60 ++++++ Core/Servers/Http/IHttpListener.cs | 6 +- Core/Servers/Http/SptHttpListener.cs | 188 ++++++++++++++++++ Core/Servers/HttpServer.cs | 10 +- Core/Services/Mod/Image/ImageRouterService.cs | 4 +- Core/Utils/DatabaseImporter.cs | 28 +-- Core/Utils/FileUtil.cs | 4 +- Core/Utils/HttpFileUtil.cs | 6 +- Core/Utils/HttpResponseUtil.cs | 120 +++++++++++ 19 files changed, 766 insertions(+), 36 deletions(-) create mode 100644 Core/DI/ISerializer.cs create mode 100644 Core/DI/Router.cs create mode 100644 Core/Models/Enums/BackendErrorCodes.cs create mode 100644 Core/Models/Utils/IRequestData.cs create mode 100644 Core/Routers/Dynamic/HttpDynamicRouter.cs create mode 100644 Core/Routers/HttpRouter.cs create mode 100644 Core/Routers/Serializers/ImageSerializer.cs create mode 100644 Core/Routers/Static/LauncherStaticRouter.cs create mode 100644 Core/Servers/Http/SptHttpListener.cs create mode 100644 Core/Utils/HttpResponseUtil.cs diff --git a/Core/DI/ISerializer.cs b/Core/DI/ISerializer.cs new file mode 100644 index 00000000..578fe902 --- /dev/null +++ b/Core/DI/ISerializer.cs @@ -0,0 +1,7 @@ +namespace Core.DI; + +public interface ISerializer +{ + public void Serialize(string sessionID, HttpRequest req, HttpResponse resp, object? body); + public bool CanHandle(string something); +} diff --git a/Core/DI/Router.cs b/Core/DI/Router.cs new file mode 100644 index 00000000..68756a31 --- /dev/null +++ b/Core/DI/Router.cs @@ -0,0 +1,107 @@ +using Core.Models.Eft.Common; +using Core.Models.Eft.ItemEvent; +using Core.Models.Eft.Profile; +using Core.Models.Utils; + +namespace Core.DI; + +public abstract class Router +{ + protected List handledRoutes = []; + + public string GetTopLevelRoute() + { + return "spt"; + } + + protected abstract List GetHandledRoutes(); + + protected List GetInternalHandledRoutes() + { + if (handledRoutes.Count == 0) + { + handledRoutes = GetHandledRoutes(); + } + + return handledRoutes; + } + + public bool CanHandle(string url, bool partialMatch = false) + { + if (partialMatch) + { + return GetInternalHandledRoutes() + .Where((r) => r.dynamic) + .Any((r) => url.Contains(r.route)); + } + + return GetInternalHandledRoutes() + .Where((r) => !r.dynamic) + .Any((r) => r.route == url); + } + + public abstract Type? GetBodyDeserializationType(); +} + +public abstract class StaticRouter : Router +{ + private List> actions; + + public StaticRouter(List> routes) : base() + { + actions = routes; + } + + public object HandleStatic(string url, IRequestData? info, string sessionID, string output) + { + return actions.Single(route => route.url == url).action(url, info, sessionID, output); + } + + protected override List GetHandledRoutes() + { + return actions.Select((route) => new HandledRoute(route.url, false)).ToList(); + } +} + +public abstract class DynamicRouter : Router +{ + private List> actions; + + public DynamicRouter(List> routes) : base() + { + actions = routes; + } + + public object HandleDynamic(string url, IRequestData? info, string sessionID, string output) + { + return actions.First(r => url.Contains(r.url)).action(url, info, sessionID, output); + } + + protected override List GetHandledRoutes() + { + return actions.Select((route) => new HandledRoute(route.url, true)).ToList(); + } +} + +// The name of this class should be ItemEventRouter, but that name is taken, +// So instead I added the definition +public abstract class ItemEventRouterDefinition : Router +{ + public abstract object HandleItemEvent( + string url, + PmcData pmcData, + object body, + string sessionID, + ItemEventRouterResponse output + ); +} + +public abstract class SaveLoadRouter : Router +{ + public abstract SptProfile HandleLoad(SptProfile profile); +} + +public record HandledRoute(string route, bool dynamic); + +public record RouteAction(string url, Func action) where T : IRequestData; +//public action: (url: string, info: any, sessionID: string, output: string) => Promise, diff --git a/Core/Helpers/HttpServerHelper.cs b/Core/Helpers/HttpServerHelper.cs index 41009714..55e573a1 100644 --- a/Core/Helpers/HttpServerHelper.cs +++ b/Core/Helpers/HttpServerHelper.cs @@ -60,7 +60,7 @@ public class HttpServerHelper public void SendTextJson(HttpResponse resp, object output) { - resp.Headers.Add("Content-Type", mime["json"]); + resp.Headers.Append("Content-Type", mime["json"]); resp.StatusCode = 200; /* TODO: figure this one out resp.writeHead(200, "OK", { diff --git a/Core/Models/Eft/Common/EmptyRequestData.cs b/Core/Models/Eft/Common/EmptyRequestData.cs index 69c6616a..17da8767 100644 --- a/Core/Models/Eft/Common/EmptyRequestData.cs +++ b/Core/Models/Eft/Common/EmptyRequestData.cs @@ -1,5 +1,7 @@ -namespace Core.Models.Eft.Common; +using Core.Models.Utils; -public class EmptyRequestData +namespace Core.Models.Eft.Common; + +public class EmptyRequestData : IRequestData { -} \ No newline at end of file +} diff --git a/Core/Models/Enums/BackendErrorCodes.cs b/Core/Models/Enums/BackendErrorCodes.cs new file mode 100644 index 00000000..9799f808 --- /dev/null +++ b/Core/Models/Enums/BackendErrorCodes.cs @@ -0,0 +1,90 @@ +namespace Core.Models.Enums; + +public enum BackendErrorCodes +{ + NONE = 0, + UNKNOWN_ERROR = 200, + NOT_AUTHORIZED = 201, + NEED_AUTHORIZATION_CODE = 209, + WRONG_AUTHORIZATION_CODE = 211, + NEED_CAPTCHA = 214, + NO_NEED_CAPTCHA = 215, + CAPTCHA_INVALID_ANSWER = 216, + CAPTCHA_FAILED = 218, + CAPTCHA_BRUTE_FORCED = 219, + NO_ROOM_IN_STASH = 223, + NICKNAME_NOT_UNIQUE = 225, + NICKNAME_NOT_VALID = 226, + UNSUPPORTED_CLIENT_VERSION = 232, + REPORT_NOT_ALLOWED = 238, + NICKNAME_IS_ABUSIVE = 241, + NICKNAME_CHANGE_TIMEOUT = 242, + NOT_ENOUGH_SPACE_TO_UNPACK = 257, + NOT_MODIFIED = 304, + HTTP_BAD_REQUEST = 400, + HTTP_NOT_AUTHORIZED = 401, + HTTP_FORBIDDEN = 403, + HTTP_NOT_FOUND = 404, + HTTP_METHOD_NOT_ALLOWED = 405, + UNKNOWN_TRADING_ERROR = 500, + HTTPNOTIMPLEMENTED = 501, + HTTPBADGATEWAY = 502, + HTTPSERVICEUNAVAILABLE = 503, + HTTPGATEWAYTIMEOUT = 504, + TRADEROUTOFMONEY = 505, + HTTPVARIANTALSONEGOTIATES = 506, + PRICECHANGED = 509, + TRADERDISABLED = 512, + ITEMHASBEENSOLD = 513, + NOTENOUGHSPACEFORMONEY = 518, + HTTPINVALIDSSLCERTIFICATE = 526, + UNKNOWNRAGFAIRERROR = 550, + UNKNOWNRAGFAIRERROR2 = 551, + UNKNOWNMATCHMAKERERROR = 600, + SESSIONPARAMETERSERROR = 601, + SESSIONLOST = 602, + SERVERNOTREGISTERED = 604, + UNKNOWNQUESTERROR = 700, + QUESTBADPARAM = 702, + QUESTNOTFOUND = 703, + QUESTISUNAVAILABLE = 704, + NOFREESPACEFORREWARDS = 705, + WRONGQUESTSTATUS = 706, + CANTCOMPLETEQUEST = 707, + UNKNOWNMAILERROR = 900, + TOOMANYFRIENDREQUESTS = 925, + UNKNOWNSCRIPTEXECUTIONERROR = 1000, + UNKNOWNREPAIRINGERROR = 1200, + UNKNOWNINSURANCEERROR = 1300, + UNKNOWNCURRENCYEXCHANGEERROR = 1400, + OFFERNOTFOUND = 1503, + NOTENOUGHSPACE = 1505, + OFFEROUTOFSTOCK = 1506, + OFFERSOLD = 1507, + RAGFAIRUNAVAILABLE = 1511, + BANNEDERRORCODE = 1513, + INSUFFICIENTNUMBERINSTOCK = 1516, + TOOMANYITEMSTOSELL = 1517, + INCORRECTCLIENTPRICE = 1519, + EXAMINATIONFAILED = 22001, + ITEMALREADYEXAMINED = 22002, + UNKNOWNNGINXERROR = 9000, + PARSERESPONSEERROR = 9001, + UNKNOWNMATCHMAKERERROR2 = 503000, // They have two of these...why :/ + UNKNOWNGROUPERROR = 502000, + GROUPREQUESTNOTFOUND = 502002, + GROUPFULL = 502004, + PLAYERALREADYINGROUP = 502005, + PLAYERNOTINGROUP = 502006, + PLAYERNOTLEADER = 502007, + CANTCHANGEREADYSTATE = 502010, + PLAYERFORBIDDENGROUPINVITES = 502011, + LEADERALREADYREADY = 502012, + GROUPSENDINVITEERROR = 502013, + PLAYERISOFFLINE = 502014, + PLAYERISNOTSEARCHINGFORGROUP = 502018, + PLAYERALREADYLOOKINGFORGAME = 503001, + PLAYERINRAID = 503002, + LIMITFORPRESETSREACHED = 504001, + PLAYERPROFILENOTFOUND = 505001, +} diff --git a/Core/Models/Utils/IRequestData.cs b/Core/Models/Utils/IRequestData.cs new file mode 100644 index 00000000..88164697 --- /dev/null +++ b/Core/Models/Utils/IRequestData.cs @@ -0,0 +1,5 @@ +namespace Core.Models.Utils; + +public interface IRequestData +{ +} diff --git a/Core/Routers/Dynamic/HttpDynamicRouter.cs b/Core/Routers/Dynamic/HttpDynamicRouter.cs new file mode 100644 index 00000000..a97bc98e --- /dev/null +++ b/Core/Routers/Dynamic/HttpDynamicRouter.cs @@ -0,0 +1,23 @@ +using Core.Annotations; +using Core.DI; + +namespace Core.Routers.Dynamic; + +[Injectable(InjectableTypeOverride = typeof(DynamicRouter))] +public class HttpDynamicRouter : DynamicRouter +{ + public HttpDynamicRouter(ImageRouter imageRouter) : base( + [ + new(".jpg", (_, _, _, _) => imageRouter.GetImage()), + new(".png", (_, _, _, _) => imageRouter.GetImage()), + new(".ico", (_, _, _, _) => imageRouter.GetImage()) + ] + ) + { + } + + public override Type? GetBodyDeserializationType() + { + return null; + } +} diff --git a/Core/Routers/HttpRouter.cs b/Core/Routers/HttpRouter.cs new file mode 100644 index 00000000..b66d8103 --- /dev/null +++ b/Core/Routers/HttpRouter.cs @@ -0,0 +1,94 @@ +using System.Text.Json; +using Core.Annotations; +using Core.DI; + +namespace Core.Routers; + +[Injectable] +public class HttpRouter +{ + private readonly IEnumerable _staticRouters; + private readonly IEnumerable _dynamicRoutes; + + public HttpRouter( + IEnumerable staticRouters, + IEnumerable dynamicRoutes + ) + { + _staticRouters = staticRouters; + _dynamicRoutes = dynamicRoutes; + } + + /* + protected groupBy(list: T[], keyGetter: (t: T) => string): Map { + const map: Map = new Map(); + for (const item of list) { + const key = keyGetter(item); + const collection = map.get(key); + if (!collection) { + map.set(key, [item]); + } else { + collection.push(item); + } + } + return map; + } + */ + + public string? GetResponse(HttpRequest req, string sessionID, string? body, out object deserializedObject) + { + var wrapper = new ResponseWrapper(""); + + var handled = HandleRoute(req, sessionID, wrapper, _staticRouters, false, body, out deserializedObject); + if (!handled) { + HandleRoute(req, sessionID, wrapper, _dynamicRoutes, true, body, out deserializedObject); + } + + // TODO: Temporary hack to change ItemEventRouter response sessionID binding to what client expects + if (wrapper.Output?.Contains("\"profileChanges\":{") ?? false) { + wrapper.Output = wrapper.Output.Replace(sessionID, sessionID); + } + + return wrapper.Output; + } + + protected bool HandleRoute( + HttpRequest request, + string sessionID, + ResponseWrapper wrapper, + IEnumerable routers, + bool dynamic, + string? body, + out object deserializedObject + ) + { + var url = request.Path.Value; + deserializedObject = null; + + // remove retry from url + if (url?.Contains("?retry=") ?? false) { + url = url.Split("?retry=")[0]; + } + var matched = false; + foreach (var route in routers) + { + if (route.CanHandle(url, dynamic)) { + var type = route.GetBodyDeserializationType(); + if (type != null && !string.IsNullOrEmpty(body)) + deserializedObject = JsonSerializer.Deserialize(body, type); + if (dynamic) { + wrapper.Output = (route as DynamicRouter).HandleDynamic(url, deserializedObject, sessionID, wrapper.Output) as string; + } else { + wrapper.Output = (route as StaticRouter).HandleStatic(url, deserializedObject, sessionID, wrapper.Output) as string; + } + matched = true; + } + } + return matched; + } + protected class ResponseWrapper(string? output) + { + public string? Output { get; set; } = output; + } +} + diff --git a/Core/Routers/ImageRouter.cs b/Core/Routers/ImageRouter.cs index c3e3156c..32801f4f 100644 --- a/Core/Routers/ImageRouter.cs +++ b/Core/Routers/ImageRouter.cs @@ -25,22 +25,23 @@ public class ImageRouter public void AddRoute(string key, string valueToAdd) { - _imageRouterService.addRoute(key, valueToAdd); + _imageRouterService.AddRoute(key, valueToAdd); } - public Task SendImage(string sessionID, HttpRequest req, HttpResponse resp, object body) + public void SendImage(string sessionID, HttpRequest req, HttpResponse resp, object body) { // remove file extension - var url = _fileUtil.StripExtension(req.Path); + var url = _fileUtil.StripExtension(req.Path, true); // send image - if (_imageRouterService.ExistsByKey(url)) { - return _httpFileUtil.SendFileAsync(resp, _imageRouterService.getByKey(url)); + if (_imageRouterService.ExistsByKey(url)) + { + _httpFileUtil.SendFile(resp, _imageRouterService.GetByKey(url)); } - return Task.CompletedTask; } - public string GetImage() { + public string GetImage() + { return "IMAGE"; } } diff --git a/Core/Routers/Serializers/ImageSerializer.cs b/Core/Routers/Serializers/ImageSerializer.cs new file mode 100644 index 00000000..ff644829 --- /dev/null +++ b/Core/Routers/Serializers/ImageSerializer.cs @@ -0,0 +1,25 @@ +using Core.Annotations; +using Core.DI; + +namespace Core.Routers.Serializers; + +[Injectable] +public class ImageSerializer : ISerializer +{ + protected ImageRouter _imageRouter; + + public ImageSerializer(ImageRouter imageRouter) + { + _imageRouter = imageRouter; + } + + public void Serialize(string sessionID, HttpRequest req, HttpResponse resp, object? body) + { + _imageRouter.SendImage(sessionID, req, resp, body); + } + + public bool CanHandle(string route) + { + return route == "IMAGE"; + } +} diff --git a/Core/Routers/Static/LauncherStaticRouter.cs b/Core/Routers/Static/LauncherStaticRouter.cs new file mode 100644 index 00000000..97d1528f --- /dev/null +++ b/Core/Routers/Static/LauncherStaticRouter.cs @@ -0,0 +1,60 @@ +using Core.Annotations; +using Core.Callbacks; +using Core.DI; +using Core.Models.Eft.Common; + +namespace Core.Routers.Static; + +[Injectable(InjectableTypeOverride = typeof(StaticRouter))] +public class LauncherStaticRouter : StaticRouter { + + public LauncherStaticRouter(LauncherCallbacks launcherCallbacks) : base([ + new RouteAction( + "/launcher/ping", + (url, _, sessionID, _) => launcherCallbacks.Ping(url, null, sessionID)), + new RouteAction( + "/launcher/server/connect", + (_, _, _, _) => launcherCallbacks.Connect()), + new RouteAction( + "/launcher/profile/login", + (url, info, sessionID, _) => launcherCallbacks.Login(url, info, sessionID)), + new RouteAction( + "/launcher/profile/register", + (url, info, sessionID, _) => launcherCallbacks.Register(url, info, sessionID)), + new RouteAction( + "/launcher/profile/get", + (url, info, sessionID, _) => launcherCallbacks.Get(url, info, sessionID)), + new RouteAction( + "/launcher/profile/change/username", + (url, info, sessionID, _) => launcherCallbacks.ChangeUsername(url, info, sessionID)), + new RouteAction( + "/launcher/profile/change/password", + (url, info, sessionID, _) => launcherCallbacks.ChangePassword(url, info, sessionID)), + new RouteAction( + "/launcher/profile/change/wipe", + (url, info, sessionID, _) => launcherCallbacks.Wipe(url, info, sessionID)), + new RouteAction( + "/launcher/profile/remove", + (url, info, sessionID, _) => launcherCallbacks.RemoveProfile(url, info, sessionID)), + new RouteAction( + "/launcher/profile/compatibleTarkovVersion", + (_, _, _, _) => launcherCallbacks.GetCompatibleTarkovVersion()), + new RouteAction( + "/launcher/server/version", + (_, _, _, _) => launcherCallbacks.GetServerVersion()), + new RouteAction( + "/launcher/server/loadedServerMods", + (_, _, _, _) => launcherCallbacks.GetLoadedServerMods()), + new RouteAction( + "/launcher/server/serverModsUsedByProfile", + (url, info, sessionID, _) => launcherCallbacks.GetServerModsProfileUsed(url, info, sessionID)), + ]) + { + } + + public override Type? GetBodyDeserializationType() + { + throw new NotImplementedException(); + } +} +} diff --git a/Core/Servers/Http/IHttpListener.cs b/Core/Servers/Http/IHttpListener.cs index b3a61543..e392f74b 100644 --- a/Core/Servers/Http/IHttpListener.cs +++ b/Core/Servers/Http/IHttpListener.cs @@ -2,6 +2,6 @@ public interface IHttpListener { - bool CanHandle(string sessionId, object req); - Task Handle(string sessionId, object req, object resp); -} \ No newline at end of file + bool CanHandle(string sessionId, HttpRequest req); + void Handle(string sessionId, HttpRequest req, HttpResponse resp); +} diff --git a/Core/Servers/Http/SptHttpListener.cs b/Core/Servers/Http/SptHttpListener.cs new file mode 100644 index 00000000..b54f144c --- /dev/null +++ b/Core/Servers/Http/SptHttpListener.cs @@ -0,0 +1,188 @@ +using System.Collections.Immutable; +using System.IO.Compression; +using System.Text; +using System.Text.Json; +using Core.Annotations; +using Core.DI; +using Core.Routers; +using Core.Services; +using Core.Utils; +using ILogger = Core.Models.Utils.ILogger; + +namespace Core.Servers.Http; + +[Injectable] +public class SptHttpListener : IHttpListener +{ + protected readonly HttpRouter _router; + protected readonly IEnumerable _serializers; + protected readonly ILogger _logger; + protected readonly HttpResponseUtil _httpResponseUtil; + protected readonly LocalisationService _localisationService; + public SptHttpListener( + HttpRouter httpRouter, // TODO: delay required + IEnumerable serializers, + ILogger logger, + // TODO: requestsLogger: ILogger, + // TODO: JsonUtil jsonUtil, + HttpResponseUtil httpHttpResponseUtil, + LocalisationService localisationService + ) + { + _router = httpRouter; + _serializers = serializers; + _logger = logger; + _httpResponseUtil = httpHttpResponseUtil; + _localisationService = localisationService; + } + + private static readonly ImmutableHashSet SupportedMethods = ["GET", "PUT", "POST"]; + public bool CanHandle(string _, HttpRequest req) + { + return SupportedMethods.Contains(req.Method); + } + + public void Handle(string sessionId, HttpRequest req, HttpResponse resp) + { + switch (req.Method) { + case "GET": { + var response = GetResponse(sessionId, req, null); + SendResponse(sessionId, req, resp, null, response); + break; + } + // these are handled almost identically. + case "POST": + case "PUT": { + + // Contrary to reasonable expectations, the content-encoding is _not_ actually used to + // determine if the payload is compressed. All PUT requests are, and POST requests without + // debug = 1 are as well. This should be fixed. + // let compressed = req.headers["content-encoding"] === "deflate"; + var requestIsCompressed = req.Headers.TryGetValue("requestcompressed", out var compressHeader) && + compressHeader != "0"; + var requestCompressed = req.Method == "PUT" || requestIsCompressed; + + var fullTextBody = new StreamReader(req.Body).ReadToEnd(); + ; + var value = requestCompressed ? new StreamReader(new ZLibStream(req.Body, CompressionLevel.SmallestSize, false)).ReadToEnd() : fullTextBody; + if (!requestIsCompressed) { + _logger.Debug(value, true); + } + + var response = GetResponse(sessionId, req, value); + SendResponse(sessionId, req, resp, value, response); + break; + } + + default: { + _logger.Warning($"{_localisationService.GetText("unknown_request")}: {req.Method}"); + break; + } + } + } + + /** + * Send HTTP response back to sender + * @param sessionID Player id making request + * @param req Incoming request + * @param resp Outgoing response + * @param body Buffer + * @param output Server generated response data + */ + public void SendResponse( + string sessionID, + HttpRequest req, + HttpResponse resp, + object? body, + string output + ) + { + if (body == null) + body = "{}"; + var bodyInfo = JsonSerializer.Serialize(body); + + if (IsDebugRequest(req)) { + // Send only raw response without transformation + SendJson(resp, output, sessionID); + // TODO: this.logRequest(req, output); + return; + } + + // Not debug, minority of requests need a serializer to do the job (IMAGE/BUNDLE/NOTIFY) + var serialiser = _serializers.FirstOrDefault((x) => x.CanHandle(output)); + if (serialiser != null) { + serialiser.Serialize(sessionID, req, resp, bodyInfo); + } else { + // No serializer can handle the request (majority of requests dont), zlib the output and send response back + SendZlibJson(resp, output, sessionID); + } + + // TODO: this.LogRequest(req, output); + } + + /** + * Is request flagged as debug enabled + * @param req Incoming request + * @returns True if request is flagged as debug + */ + protected bool IsDebugRequest(HttpRequest req) { + return req.Headers.TryGetValue("responsecompressed", out var value) && value == "0"; + } + + /** + * Log request if enabled + * @param req Incoming message request + * @param output Output string + */ + /* TODO: log requests + protected logRequest(req: IncomingMessage, output: string): void { + // + if (ProgramStatics.ENTRY_TYPE !== EntryType.RELEASE) { + const log = new Response(req.method, output); + this.requestsLogger.info(`RESPONSE=${this.jsonUtil.serialize(log)}`); + } + } + */ + + public string GetResponse(string sessionID, HttpRequest req, string? body) + { + /* TODO: REQUEST LOGGER + if (ProgramStatics.ENTRY_TYPE !== EntryType.RELEASE) { + // Parse quest info into object + const data = typeof info === "object" ? info : this.jsonUtil.deserialize(info); + + const log = new Request(req.method, new RequestData(req.url, req.headers, data)); + this.requestsLogger.info(`REQUEST=${this.jsonUtil.serialize(log)}`); + } + */ + + var output = _router.GetResponse(req, sessionID, body, out var deserializedObject); + /* route doesn't exist or response is not properly set up */ + if (string.IsNullOrEmpty(output)) { + _logger.Error(_localisationService.GetText("unhandled_response", req.Path)); + _logger.Info(JsonSerializer.Serialize(deserializedObject)); + output = _httpResponseUtil.GetBody(null, 404, $"UNHANDLED RESPONSE: {req.Path}"); + } + return output; + } + + public void SendJson(HttpResponse resp, string? output, string sessionID) + { + if (!string.IsNullOrEmpty(output)) + resp.Body = new MemoryStream(Encoding.UTF8.GetBytes(output)); + resp.StatusCode = 200; + resp.ContentType = "application/json"; + resp.Headers.Append("Set-Cookie", $"PHPSESSID={sessionID}"); + resp.StartAsync().Wait(); + resp.CompleteAsync().Wait(); + } + + public void SendZlibJson(HttpResponse resp, string? output, string sessionID) + { + if (!string.IsNullOrEmpty(output)) + new ZLibStream(resp.Body, CompressionLevel.SmallestSize, false).WriteAsync(Encoding.UTF8.GetBytes(output)).AsTask().Wait(); + resp.StartAsync().Wait(); + resp.CompleteAsync().Wait(); + } + +} diff --git a/Core/Servers/HttpServer.cs b/Core/Servers/HttpServer.cs index 1ceb73db..ed6b9342 100644 --- a/Core/Servers/HttpServer.cs +++ b/Core/Servers/HttpServer.cs @@ -53,7 +53,11 @@ public class HttpServer app.UseWebSockets(); app.UseRouting(); - app.UseEndpoints(endpointBuilder => { endpointBuilder.MapFallback(HandleFallback); }); + app.Use((HttpContext req, RequestDelegate _) => + { + return Task.Factory.StartNew(() => HandleFallback(req)); + }); + // app.UseEndpoints(endpointBuilder => { endpointBuilder.MapFallback(HandleFallback); }); started = true; app.Run($"http://{httpConfig.Ip}:{httpConfig.Port}"); } @@ -101,7 +105,7 @@ public class HttpServer } - //_httpListeners.Single() + _httpListeners.SingleOrDefault(l => l.CanHandle(sessionId, context.Request))?.Handle(sessionId, context.Request, context.Response); // This http request would be passed through the SPT Router and handled by an ICallback } @@ -158,4 +162,4 @@ public class HttpServer { return started; } -} \ No newline at end of file +} diff --git a/Core/Services/Mod/Image/ImageRouterService.cs b/Core/Services/Mod/Image/ImageRouterService.cs index 0a5090aa..e7c678c1 100644 --- a/Core/Services/Mod/Image/ImageRouterService.cs +++ b/Core/Services/Mod/Image/ImageRouterService.cs @@ -7,12 +7,12 @@ public class ImageRouterService { protected Dictionary routes = new(); - public void addRoute(string urlKey, string route) + public void AddRoute(string urlKey, string route) { routes[urlKey] = route; } - public string getByKey(string urlKey) + public string GetByKey(string urlKey) { return routes[urlKey]; } diff --git a/Core/Utils/DatabaseImporter.cs b/Core/Utils/DatabaseImporter.cs index 8d639619..9e4cd55b 100644 --- a/Core/Utils/DatabaseImporter.cs +++ b/Core/Utils/DatabaseImporter.cs @@ -51,6 +51,7 @@ public class DatabaseImporter : OnLoad _importerUtil = importerUtil; _configServer = configServer; _fileUtil = fileUtil; + _imageRouter = imageRouter; httpConfig = _configServer.GetConfig(ConfigTypes.HTTP); } @@ -91,7 +92,7 @@ public class DatabaseImporter : OnLoad await HydrateDatabase(filepath); - var imageFilePath = $"${filepath}images/"; + var imageFilePath = $"{filepath}images/"; //var directories = this.vfs.getDirs(imageFilePath); LoadImages(imageFilePath, _fileUtil.GetDirectories(imageFilePath), [ "/files/achievement/", @@ -175,25 +176,26 @@ public class DatabaseImporter : OnLoad { // Get all files in directory var filesInDirectory = _fileUtil.GetFiles(directories[i]); - foreach (var file in filesInDirectory) { + foreach (var file in filesInDirectory) + { + var imagePath = file; // Register each file in image router var filename = _fileUtil.StripExtension(file); var routeKey = $"{routes[i]}{filename}"; - var imagePath = $"{filepath}{directories[i]}/{file}"; -/* - const pathOverride = this.getImagePathOverride(imagePath); - if (pathOverride) { - this.logger.debug(`overrode route: ${routeKey} endpoint: ${imagePath} with ${pathOverride}`); + //var imagePath = $"{filepath}{directories[i]}/{file}"; + + var pathOverride = GetImagePathOverride(imagePath); + if (!string.IsNullOrEmpty(pathOverride)) { + _logger.Debug($"overrode route: {routeKey} endpoint: {imagePath} with {pathOverride}"); imagePath = pathOverride; } - this.imageRouter.addRoute(routeKey, imagePath); - */ + _imageRouter.AddRoute(routeKey, imagePath); } } // Map icon file separately - //this.imageRouter.addRoute("/favicon.ico", `${filepath}icon.ico`); + _imageRouter.AddRoute("/favicon.ico", $"{filepath}icon.ico"); } /** @@ -201,9 +203,11 @@ public class DatabaseImporter : OnLoad * @param imagePath Key * @returns override for key */ - protected string GetImagePathOverride(string imagePath) + protected string? GetImagePathOverride(string imagePath) { - return httpConfig.ServerImagePathOverride[imagePath]; + if (httpConfig.ServerImagePathOverride.TryGetValue(imagePath, out var value)) + return value; + return null; } } diff --git a/Core/Utils/FileUtil.cs b/Core/Utils/FileUtil.cs index cde04efe..79ffca10 100644 --- a/Core/Utils/FileUtil.cs +++ b/Core/Utils/FileUtil.cs @@ -25,8 +25,8 @@ public class FileUtil return Path.GetExtension(path).Replace(".", ""); } - public string StripExtension(string path) + public string StripExtension(string path, bool keepPath = false) { - return Path.GetFileNameWithoutExtension(path); + return keepPath ? path.Split('.').First() : Path.GetFileNameWithoutExtension(path); } } diff --git a/Core/Utils/HttpFileUtil.cs b/Core/Utils/HttpFileUtil.cs index 027f6d30..5e7539c3 100644 --- a/Core/Utils/HttpFileUtil.cs +++ b/Core/Utils/HttpFileUtil.cs @@ -13,12 +13,12 @@ public class HttpFileUtil _httpServerHelper = httpServerHelper; } - public Task SendFileAsync(HttpResponse resp,string filePath) { + public void SendFile(HttpResponse resp,string filePath) { var pathSlice = filePath.Split("/"); var mimePath = _httpServerHelper.GetMimeText(pathSlice[^1].Split(".")[^1]); var type = string.IsNullOrWhiteSpace(mimePath) ? _httpServerHelper.GetMimeText("txt") : mimePath; - resp.Headers.Add("Content-Type", type); - return resp.SendFileAsync(filePath, CancellationToken.None); + resp.Headers.Append("Content-Type", type); + resp.SendFileAsync(filePath, CancellationToken.None).Wait(); // maybe the above is correct? // await pipeline(fs.createReadStream(filePath), resp); } diff --git a/Core/Utils/HttpResponseUtil.cs b/Core/Utils/HttpResponseUtil.cs new file mode 100644 index 00000000..3473263b --- /dev/null +++ b/Core/Utils/HttpResponseUtil.cs @@ -0,0 +1,120 @@ +using System.Collections.Immutable; +using System.Text.Json; +using System.Text.RegularExpressions; +using Core.Annotations; +using Core.Models.Eft.HttpResponse; +using Core.Models.Eft.ItemEvent; +using Core.Models.Enums; +using Core.Services; + +namespace Core.Utils; + +[Injectable] +public class HttpResponseUtil +{ + protected readonly LocalisationService _localisationService; + + public HttpResponseUtil( + // JsonUtil jsonUtil, + LocalisationService localisationService + ) + { + _localisationService = localisationService; + } + + private readonly ImmutableList _cleanupRegexList = + [ + new("[\\b]"), + new("[\\f]"), + new("[\\n]"), + new("[\\r]"), + new("[\\t]") + ]; + + protected string ClearString(string s) + { + var value = s; + foreach (var regex in _cleanupRegexList) + { + value = regex.Replace(value, string.Empty); + } + + return value; + } + + /** + * Return passed in data as JSON string + * @param data + * @returns + */ + public string NoBody(T data) + { + return ClearString(JsonSerializer.Serialize(data)); + } + + /** + * Game client needs server responses in a particular format + * @param data + * @param err + * @param errmsg + * @returns + */ + public string GetBody(T data, int err = 0, string? errmsg = null, bool sanitize = true) + { + return sanitize + ? ClearString(GetUnclearedBody(data, err, errmsg)) + : GetUnclearedBody(data, err, errmsg); + } + + public string GetUnclearedBody(T? data, int err = 0, string? errmsg = null) + { + return JsonSerializer.Serialize(new GetBodyResponseData { Err = err, ErrMsg = errmsg, Data = data }); + } + + public string EmptyResponse() + { + return GetBody("", 0, ""); + } + + public string NullResponse() + { + return ClearString(GetUnclearedBody(null, 0, null)); + } + + public string EmptyArrayResponse() + { + return GetBody(new List()); + } + + /** + * Add an error into the 'warnings' array of the client response message + * @param output IItemEventRouterResponse + * @param message Error message + * @param errorCode Error code + * @returns IItemEventRouterResponse + */ + public ItemEventRouterResponse AppendErrorToOutput( + ItemEventRouterResponse output, + string? message = null, + BackendErrorCodes errorCode = BackendErrorCodes.NONE + ) + { + if (string.IsNullOrEmpty(message)) + message = _localisationService.GetText("http-unknown_error"); + if (output.Warnings?.Count > 0) + { + output.Warnings.Add(new Warning + { + Index = output.Warnings?.Count - 1, + ErrorMessage = message, + Code = errorCode.ToString() + }); + } + else + { + output.Warnings = [new Warning { Index = 0, ErrorMessage = message, Code = errorCode.ToString() }]; + } + + return output; + } +}