diff --git a/Refresh.Common/Refresh.Common.csproj b/Refresh.Common/Refresh.Common.csproj
index be271166..d86ddc9a 100644
--- a/Refresh.Common/Refresh.Common.csproj
+++ b/Refresh.Common/Refresh.Common.csproj
@@ -7,8 +7,8 @@
-
-
+
+
diff --git a/Refresh.GameServer/Configuration/GameServerConfig.cs b/Refresh.GameServer/Configuration/GameServerConfig.cs
index d2d4bd21..9213021f 100644
--- a/Refresh.GameServer/Configuration/GameServerConfig.cs
+++ b/Refresh.GameServer/Configuration/GameServerConfig.cs
@@ -9,7 +9,7 @@ namespace Refresh.GameServer.Configuration;
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
public class GameServerConfig : Config
{
- public override int CurrentConfigVersion => 13;
+ public override int CurrentConfigVersion => 14;
public override int Version { get; set; } = 0;
protected override void Migrate(int oldVer, dynamic oldConfig) {}
@@ -27,6 +27,11 @@ protected override void Migrate(int oldVer, dynamic oldConfig) {}
public bool MaintenanceMode { get; set; } = false;
public bool RequireGameLoginToRegister { get; set; } = false;
public bool TrackRequestStatistics { get; set; } = false;
+ ///
+ /// Whether to use deflate compression for responses.
+ /// If this is disabled, large enough responses will cause LBP to overflow its read buffer and eventually corrupt its own memory to the point of crashing.
+ ///
+ public bool UseDeflateCompression { get; set; } = true;
public string WebExternalUrl { get; set; } = "https://refresh.example.com";
///
/// The base URL that LBP3 uses to grab config files like `network_settings.nws`.
diff --git a/Refresh.GameServer/Middlewares/DeflateMiddleware.cs b/Refresh.GameServer/Middlewares/DeflateMiddleware.cs
new file mode 100644
index 00000000..50656802
--- /dev/null
+++ b/Refresh.GameServer/Middlewares/DeflateMiddleware.cs
@@ -0,0 +1,61 @@
+using Bunkum.Core.Database;
+using Bunkum.Core.Endpoints.Middlewares;
+using Bunkum.Listener.Request;
+using Org.BouncyCastle.Utilities.Zlib;
+using Refresh.GameServer.Configuration;
+using Refresh.GameServer.Endpoints;
+
+namespace Refresh.GameServer.Middlewares;
+
+///
+/// Adds deflate encoding to LBP game server responses if they reach a certain size.
+/// The game will eventually corrupt its own memory if we do not do this,
+/// since non-deflate requests only get a very small request buffer, and if you send too big of a request,
+/// it overruns that buffer and starts corrupting random heap memory.
+/// Requests encoded with deflate get a much larger read buffer, so they avoid this problem until much larger buffer sizes.
+///
+public class DeflateMiddleware(GameServerConfig config) : IMiddleware
+{
+ ///
+ /// After this many bytes, start deflating request bodies
+ ///
+ private const int DeflateCutoff = 1024;
+
+ public void HandleRequest(ListenerContext context, Lazy database, Action next)
+ {
+ next();
+
+ if (!config.UseDeflateCompression)
+ return;
+
+ // If this isn't a game request or it's a resource request, don't deflate, since resource requests will be corrupted if deflated
+ if (!context.Uri.AbsolutePath.StartsWith(GameEndpointAttribute.BaseRoute) || context.Uri.AbsolutePath.StartsWith($"{GameEndpointAttribute.BaseRoute}r/"))
+ return;
+
+ string? encodings = context.RequestHeaders.Get("Accept-Encoding");
+ // If the accepted encodings aren't specified, or they don't contain deflate, or we don't need to use deflate on the data, do nothing.
+ if (encodings == null || !encodings.Contains("deflate") || context.ResponseStream.Length <= DeflateCutoff)
+ return;
+
+ // Update the headers marking that we are sending encoded data
+ context.ResponseHeaders["X-Original-Content-Length"] = context.ResponseStream.Length.ToString();
+ context.ResponseHeaders["Vary"] = "Accept-Encoding";
+ context.ResponseHeaders["Content-Encoding"] = "deflate";
+
+ // Create a copy of our uncompressed data
+ byte[] uncompressed = context.ResponseStream.ToArray();
+
+ // Reset the response stream position and length to 0, so we can start writing to it again
+ context.ResponseStream.Position = 0;
+ context.ResponseStream.SetLength(0);
+
+ // Compress our source data into the response stream
+ using ZOutputStreamLeaveOpen zlibStream = new(context.ResponseStream, 6);
+ zlibStream.Write(uncompressed);
+ zlibStream.Finish();
+ zlibStream.Close();
+
+ // Seek back to the start
+ context.ResponseStream.Position = 0;
+ }
+}
\ No newline at end of file
diff --git a/Refresh.GameServer/Middlewares/DigestMiddleware.cs b/Refresh.GameServer/Middlewares/DigestMiddleware.cs
index 32bb4f55..40a5af97 100644
--- a/Refresh.GameServer/Middlewares/DigestMiddleware.cs
+++ b/Refresh.GameServer/Middlewares/DigestMiddleware.cs
@@ -84,7 +84,7 @@ private void SetDigestResponse(ListenerContext context)
public void HandleRequest(ListenerContext context, Lazy database, Action next)
{
- //If this isnt an LBP endpoint, dont do digest
+ //If this isn't an LBP endpoint, dont do digest
if (!context.Uri.AbsolutePath.StartsWith(GameEndpointAttribute.BaseRoute))
{
next();
diff --git a/Refresh.GameServer/Refresh.GameServer.csproj b/Refresh.GameServer/Refresh.GameServer.csproj
index ae711ea8..8784cd97 100644
--- a/Refresh.GameServer/Refresh.GameServer.csproj
+++ b/Refresh.GameServer/Refresh.GameServer.csproj
@@ -52,12 +52,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/Refresh.GameServer/RefreshGameServer.cs b/Refresh.GameServer/RefreshGameServer.cs
index 47ae1199..d3c68f7b 100644
--- a/Refresh.GameServer/RefreshGameServer.cs
+++ b/Refresh.GameServer/RefreshGameServer.cs
@@ -87,6 +87,7 @@ protected override void SetupMiddlewares()
{
this.Server.AddMiddleware();
this.Server.AddMiddleware();
+ this.Server.AddMiddleware(new DeflateMiddleware(this._config!));
this.Server.AddMiddleware();
this.Server.AddMiddleware();
this.Server.AddMiddleware();