Properly re-assemble all data in http requests before handling it (!162)

Remove unneeded HttpBufferHandler

-----------

The old code processed each chunk of data as an entire request, which is not correct. It was observed split data after ~14600 bytes (on a 1 gig lan connection). I think it was worse on remote connections.

This was the cause of the "unknown compression method" and invalid json parse errors when saving the profile.

Co-authored-by: Decoy <redacted@example.com>
Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/162
Reviewed-by: Terkoiz <terkoiz@noreply.dev.sp-tarkov.com>
Co-authored-by: ree <ree@noreply.dev.sp-tarkov.com>
Co-committed-by: ree <ree@noreply.dev.sp-tarkov.com>
This commit is contained in:
ree 2023-10-30 09:23:30 +00:00 committed by chomp
parent fe703b34ec
commit 9fa0bcc705
2 changed files with 35 additions and 77 deletions

View File

@ -5,7 +5,6 @@ import { inject, injectAll, injectable } from "tsyringe";
import { Serializer } from "@spt-aki/di/Serializer";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { HttpRouter } from "@spt-aki/routers/HttpRouter";
import { HttpBufferHandler } from "@spt-aki/servers/http/HttpBufferHandler";
import { IHttpListener } from "@spt-aki/servers/http/IHttpListener";
import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil";
@ -22,8 +21,7 @@ export class AkiHttpListener implements IHttpListener
@inject("RequestsLogger") protected requestsLogger: ILogger,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("HttpBufferHandler") protected httpBufferHandler: HttpBufferHandler
@inject("LocalisationService") protected localisationService: LocalisationService
)
{
}
@ -35,7 +33,6 @@ export class AkiHttpListener implements IHttpListener
public handle(sessionId: string, req: IncomingMessage, resp: ServerResponse): void
{
// TODO: cleanup into interface IVerbHandler
switch (req.method)
{
case "GET":
@ -44,51 +41,48 @@ export class AkiHttpListener implements IHttpListener
this.sendResponse(sessionId, req, resp, null, response);
break;
}
// these are handled almost identically.
case "POST":
{
req.on("data", (data: any) =>
{
const value = (req.headers["debug"] === "1") ? data.toString() : zlib.inflateSync(data);
const response = this.getResponse(sessionId, req, value);
this.sendResponse(sessionId, req, resp, value, response);
});
break;
}
case "PUT":
{
req.on("data", (data) =>
{
// receive data
if ("expect" in req.headers)
{
const requestLength = parseInt(req.headers["content-length"]);
// Data can come in chunks. Notably, if someone saves their profile (which can be
// kinda big), on a slow connection. We need to re-assemble the entire http payload
// before processing it.
if (!this.httpBufferHandler.putInBuffer(req.headers.sessionid, data, requestLength))
{
resp.writeContinue();
}
}
const requestLength = parseInt(req.headers["content-length"]);
const buffer = Buffer.alloc(requestLength);
let written = 0;
req.on("data", (data: any) => {
data.copy(buffer, written, 0);
written += data.length;
});
req.on("end", async () =>
req.on("end", () =>
{
const data = this.httpBufferHandler.getFromBuffer(sessionId);
this.httpBufferHandler.resetBuffer(sessionId);
// 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";
let compressed = req.method === "PUT" || req.headers["debug"] !== "1";
let value = zlib.inflateSync(data);
if (!value)
const value = compressed ? zlib.inflateSync(buffer) : buffer;
if (req.headers["debug"] === "1")
{
value = data;
console.log(value.toString());
}
const response = this.getResponse(sessionId, req, value);
this.sendResponse(sessionId, req, resp, value, response);
});
break;
}
default:
{
this.logger.warning(this.localisationService.getText("unknown_request"));
this.logger.warning(this.localisationService.getText("unknown_request") + ": " + req.method);
break;
}
}

View File

@ -1,36 +0,0 @@
import { injectable } from "tsyringe";
@injectable()
export class HttpBufferHandler
{
protected buffers = {};
public resetBuffer(sessionID: string): void
{
this.buffers[sessionID] = undefined;
}
public putInBuffer(sessionID: any, data: any, bufLength: number): boolean
{
if (this.buffers[sessionID] === undefined || this.buffers[sessionID].allocated !== bufLength)
{
this.buffers[sessionID] = {
written: 0,
allocated: bufLength,
buffer: Buffer.alloc(bufLength)
};
}
const buf = this.buffers[sessionID];
data.copy(buf.buffer, buf.written, 0);
buf.written += data.length;
return buf.written === buf.allocated;
}
public getFromBuffer(sessionID: string): any
{
return this.buffers[sessionID].buffer;
}
}