partial request working
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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<HandledRoute> handledRoutes = [];
|
||||
|
||||
public string GetTopLevelRoute()
|
||||
{
|
||||
return "spt";
|
||||
}
|
||||
|
||||
protected abstract List<HandledRoute> GetHandledRoutes();
|
||||
|
||||
protected List<HandledRoute> 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<RouteAction<IRequestData>> actions;
|
||||
|
||||
public StaticRouter(List<RouteAction<IRequestData>> 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<HandledRoute> GetHandledRoutes()
|
||||
{
|
||||
return actions.Select((route) => new HandledRoute(route.url, false)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class DynamicRouter : Router
|
||||
{
|
||||
private List<RouteAction<IRequestData>> actions;
|
||||
|
||||
public DynamicRouter(List<RouteAction<IRequestData>> 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<HandledRoute> 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<T>(string url, Func<string, T?, string?, string?, object> action) where T : IRequestData;
|
||||
//public action: (url: string, info: any, sessionID: string, output: string) => Promise<any>,
|
||||
@@ -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", {
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Core.Models.Utils;
|
||||
|
||||
public interface IRequestData
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System.Text.Json;
|
||||
using Core.Annotations;
|
||||
using Core.DI;
|
||||
|
||||
namespace Core.Routers;
|
||||
|
||||
[Injectable]
|
||||
public class HttpRouter
|
||||
{
|
||||
private readonly IEnumerable<StaticRouter> _staticRouters;
|
||||
private readonly IEnumerable<DynamicRouter> _dynamicRoutes;
|
||||
|
||||
public HttpRouter(
|
||||
IEnumerable<StaticRouter> staticRouters,
|
||||
IEnumerable<DynamicRouter> dynamicRoutes
|
||||
)
|
||||
{
|
||||
_staticRouters = staticRouters;
|
||||
_dynamicRoutes = dynamicRoutes;
|
||||
}
|
||||
|
||||
/*
|
||||
protected groupBy<T>(list: T[], keyGetter: (t: T) => string): Map<string, T[]> {
|
||||
const map: Map<string, T[]> = 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<Router> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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<EmptyRequestData?>(
|
||||
"/launcher/ping",
|
||||
(url, _, sessionID, _) => launcherCallbacks.Ping(url, null, sessionID)),
|
||||
new RouteAction<EmptyRequestData?>(
|
||||
"/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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
public interface IHttpListener
|
||||
{
|
||||
bool CanHandle(string sessionId, object req);
|
||||
Task Handle(string sessionId, object req, object resp);
|
||||
}
|
||||
bool CanHandle(string sessionId, HttpRequest req);
|
||||
void Handle(string sessionId, HttpRequest req, HttpResponse resp);
|
||||
}
|
||||
|
||||
@@ -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<ISerializer> _serializers;
|
||||
protected readonly ILogger _logger;
|
||||
protected readonly HttpResponseUtil _httpResponseUtil;
|
||||
protected readonly LocalisationService _localisationService;
|
||||
public SptHttpListener(
|
||||
HttpRouter httpRouter, // TODO: delay required
|
||||
IEnumerable<ISerializer> 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<string> 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<object?>(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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ public class ImageRouterService
|
||||
{
|
||||
protected Dictionary<string, string> 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];
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ public class DatabaseImporter : OnLoad
|
||||
_importerUtil = importerUtil;
|
||||
_configServer = configServer;
|
||||
_fileUtil = fileUtil;
|
||||
_imageRouter = imageRouter;
|
||||
httpConfig = _configServer.GetConfig<HttpConfig>(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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<Regex> _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>(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>(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>(T? data, int err = 0, string? errmsg = null)
|
||||
{
|
||||
return JsonSerializer.Serialize(new GetBodyResponseData<T> { Err = err, ErrMsg = errmsg, Data = data });
|
||||
}
|
||||
|
||||
public string EmptyResponse()
|
||||
{
|
||||
return GetBody("", 0, "");
|
||||
}
|
||||
|
||||
public string NullResponse()
|
||||
{
|
||||
return ClearString(GetUnclearedBody<object>(null, 0, null));
|
||||
}
|
||||
|
||||
public string EmptyArrayResponse()
|
||||
{
|
||||
return GetBody(new List<object>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user