Skip to content

Commit

Permalink
Add presence server + hook up to APIv3 setAsOverride
Browse files Browse the repository at this point in the history
  • Loading branch information
Beyley committed Sep 3, 2024
1 parent 6173582 commit 8727ffe
Show file tree
Hide file tree
Showing 29 changed files with 967 additions and 7 deletions.
6 changes: 6 additions & 0 deletions Refresh.Common/Constants/EndpointRoutes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Refresh.Common.Constants;

public static class EndpointRoutes
{
public const string PresenceBaseRoute = "/_internal/presence/";
}
110 changes: 110 additions & 0 deletions Refresh.Common/Helpers/ResourceHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Buffers.Binary;
using System.IO.Compression;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using FastAes;
using IronCompress;
Expand Down Expand Up @@ -107,4 +109,112 @@ public static byte[] PspDecrypt(Span<byte> data, ReadOnlySpan<byte> key)
//Return a copy of the decompressed data
return decompressed.AsSpan().ToArray();
}

static int XXTEA_DELTA = Unsafe.BitCast<uint, int>(0x9e3779b9);

/// <summary>
/// In-place encrypts byte data using big endian XXTEA.
///
/// Due to how XXTEA data works, you must pad the data to a multiple of 4 bytes.
/// </summary>
/// <param name="byteData">The data to encrypt</param>
/// <param name="key">The key used to encrypt the data</param>
/// <exception cref="ArgumentException">The input is not a multiple of 4 bytes</exception>
/// <remarks>
/// Referenced from https://github.com/ennuo/toolkit/blob/dc82bee57ab58e9f4bf35993d405529d4cbc7d00/lib/cwlib/src/main/java/cwlib/util/Crypto.java#L97
/// </remarks>
public static void XxteaEncrypt(Span<byte> byteData, Span<int> key)
{
if (byteData.Length % 4 != 0)
throw new ArgumentException("Data must be padded to a multiple of 4 bytes.", nameof(byteData));

// Alias the byte data as integers
Span<int> data = MemoryMarshal.Cast<byte, int>(byteData);

// endian swap from BE so the math happens in LE space
BinaryPrimitives.ReverseEndianness(data, data);

int n = data.Length - 1;
if (n < 1)
{
BinaryPrimitives.ReverseEndianness(data, data);

return;
}

int p, q = 6 + 52 / (n + 1);

int z = data[n], y, sum = 0, e;
while (q-- > 0)
{
sum += XXTEA_DELTA;
e = sum >>> 2 & 3;
for (p = 0; p < n; p++)
{
y = data[p + 1];
z =
data[p] += ((z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (key[p & 3 ^ e] ^ z));
}

y = data[0];
z =
data[n] += ((z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (key[p & 3 ^ e] ^ z));
}

// endian swap so the final data is in LE again
BinaryPrimitives.ReverseEndianness(data, data);
}

/// <summary>
/// In-place decrypts byte data using big endian XXTEA.
///
/// Due to how XXTEA data works, you must pad the data to a multiple of 4 bytes.
/// </summary>
/// <param name="byteData">The data to decrypt</param>
/// <param name="key">The key used to decrypt the data</param>
/// <exception cref="ArgumentException">The input is not a multiple of 4 bytes</exception>
/// <remarks>
/// Referenced from https://github.com/ennuo/toolkit/blob/dc82bee57ab58e9f4bf35993d405529d4cbc7d00/lib/cwlib/src/main/java/cwlib/util/Crypto.java#L97
/// </remarks>
public static void XxteaDecrypt(Span<byte> byteData, Span<int> key)
{
if (byteData.Length % 4 != 0)
throw new ArgumentException("Data must be padded to 4 bytes.", nameof(byteData));

// Alias the byte data as integers
Span<int> data = MemoryMarshal.Cast<byte, int>(byteData);

// endian swap from BE so the math happens in LE space
BinaryPrimitives.ReverseEndianness(data, data);

int n = data.Length - 1;
if (n < 1)
{
BinaryPrimitives.ReverseEndianness(data, data);

return;
}

int p, q = 6 + 52 / (n + 1);

int z, y = data[0], sum = q * XXTEA_DELTA, e;
while (sum != 0)
{
e = sum >>> 2 & 3;
for (p = n; p > 0; p--)
{
z = data[p - 1];
y = data[p] -=
((z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (key[p & 3 ^ e] ^ z));
}

z = data[n];
y =
data[0] -= ((z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (key[p & 3 ^ e] ^ z));
sum -= XXTEA_DELTA;
}

// endian swap so the final data is in LE again
BinaryPrimitives.ReverseEndianness(data, data);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public GameAuthenticationProvider(GameServerConfig? config)

public Token? AuthenticateToken(ListenerContext request, Lazy<IDatabaseContext> db)
{
// Dont attempt to authenticate presence endpoints, as authentication is handled by PresenceAuthenticationMiddleware
if (request.Uri.AbsolutePath.StartsWith(PresenceEndpointAttribute.BaseRoute))
return null;

// First try to grab game token data from MM_AUTH
string? tokenData = request.Cookies["MM_AUTH"];
TokenType tokenType = TokenType.Game;
Expand Down
12 changes: 11 additions & 1 deletion Refresh.GameServer/Configuration/IntegrationConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Refresh.GameServer.Configuration;
/// </summary>
public class IntegrationConfig : Config
{
public override int CurrentConfigVersion => 5;
public override int CurrentConfigVersion => 6;
public override int Version { get; set; }
protected override void Migrate(int oldVer, dynamic oldConfig)
{
Expand Down Expand Up @@ -56,6 +56,16 @@ protected override void Migrate(int oldVer, dynamic oldConfig)

public bool AipiRestrictAccountOnDetection { get; set; } = false;

#endregion

#region Presence

public bool PresenceEnabled { get; set; } = false;

public string PresenceBaseUrl { get; set; } = "http://localhost:10073";

public string PresenceSharedSecret { get; set; } = "SHARED_SECRET";

#endregion

public string? GrafanaDashboardUrl { get; set; }
Expand Down
8 changes: 8 additions & 0 deletions Refresh.GameServer/Database/GameDatabaseContext.Users.cs
Original file line number Diff line number Diff line change
Expand Up @@ -455,4 +455,12 @@ public void SetUserRootPlaylist(GameUser user, GamePlaylist playlist)
user.RootPlaylist = playlist;
});
}

public void SetUserPresenceAuthToken(GameUser user, string? token)
{
this.Write(() =>
{
user.PresenceServerAuthToken = token;
});
}
}
2 changes: 1 addition & 1 deletion Refresh.GameServer/Database/GameDatabaseProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ protected GameDatabaseProvider(IDateTimeProvider time)
this._time = time;
}

protected override ulong SchemaVersion => 156;
protected override ulong SchemaVersion => 159;

protected override string Filename => "refreshGameServer.realm";

Expand Down
13 changes: 10 additions & 3 deletions Refresh.GameServer/Endpoints/ApiV3/LevelApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,20 @@ public ApiOkResponse DeleteLevelById(RequestContext context, GameDatabaseContext
[ApiV3Endpoint("levels/id/{id}/setAsOverride", HttpMethods.Post)]
[DocSummary("Marks the level to show in the next slot list gotten from the game")]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)]
public ApiOkResponse SetLevelAsOverrideById(RequestContext context, GameDatabaseContext database, GameUser user, LevelListOverrideService service,
public ApiOkResponse SetLevelAsOverrideById(RequestContext context,
GameDatabaseContext database,
GameUser user,
LevelListOverrideService overrideService,
PresenceService presenceService,
[DocSummary("The ID of the level")] int id)
{
GameLevel? level = database.GetLevelById(id);
if (level == null) return ApiNotFoundError.LevelMissingError;

service.AddIdOverridesForUser(user, level);

// If the user isn't on the presence server, or it's unavailable, fallback to a slot override
// TODO: return whether or not the presence server was used
if (!presenceService.PlayLevel(user, id))
overrideService.AddIdOverridesForUser(user, level);

return new ApiOkResponse();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,12 @@ public class AuthenticationEndpoints : EndpointGroup

Token token = database.GenerateTokenForUser(user, TokenType.Game, game.Value, platform.Value, context.RemoteIp(), GameDatabaseContext.GameTokenExpirySeconds); // 4 hours

//Clear the user's force match
// Clear the user's force match
database.ClearForceMatch(user);

// Mark the user as disconnected from the presence server
database.SetUserPresenceAuthToken(user, null);

context.Logger.LogInfo(BunkumCategory.Authentication, $"{user} successfully logged in on {game} via {platform}");

if (game == TokenGame.LittleBigPlanetPSP)
Expand Down Expand Up @@ -278,6 +281,8 @@ public Response RevokeThisToken(RequestContext context, GameDatabaseContext data

// Revoke the token
database.RevokeToken(token);
// Mark them as disconnected from the presence server
database.SetUserPresenceAuthToken(user, null);

context.Logger.LogInfo(BunkumCategory.Authentication, $"{user} logged out");
return OK;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Bunkum.Core;
using Bunkum.Core.Endpoints;
using Bunkum.Core.Responses;
using Bunkum.Protocols.Http;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
using Refresh.GameServer.Types.UserData;

namespace Refresh.GameServer.Endpoints.Internal.Presence;

public class PresenceEndpoints : EndpointGroup
{
// Test endpoint to allow the presence server to make sure presence support is enabled and configured correctly
[PresenceEndpoint("test", HttpMethods.Post), Authentication(false)]
public Response TestSecret(RequestContext context)
{
return OK;
}

[PresenceEndpoint("informConnection", HttpMethods.Post), Authentication(false)]
public Response InformConnection(RequestContext context, GameDatabaseContext database, string body)
{
GameUser? user = database.GetUserFromTokenData(body, TokenType.Game);

if (user == null)
return NotFound;

database.SetUserPresenceAuthToken(user, body);

context.Logger.LogInfo(RefreshContext.Presence, $"User {user} connected to the presence server");

return OK;
}

[PresenceEndpoint("informDisconnection", HttpMethods.Post), Authentication(false)]
public Response InformDisconnection(RequestContext context, GameDatabaseContext database, string body)
{
GameUser? user = database.GetUserFromTokenData(body, TokenType.Game);

if (user == null)
return NotFound;

database.SetUserPresenceAuthToken(user, null);

context.Logger.LogInfo(RefreshContext.Presence, $"User {user} disconnected from the presence server");

return OK;
}
}
19 changes: 19 additions & 0 deletions Refresh.GameServer/Endpoints/PresenceEndpointAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Bunkum.Protocols.Http;
using JetBrains.Annotations;
using Refresh.Common.Constants;

namespace Refresh.GameServer.Endpoints;

[MeansImplicitUse]
public class PresenceEndpointAttribute : HttpEndpointAttribute
{
public const string BaseRoute = EndpointRoutes.PresenceBaseRoute;

public PresenceEndpointAttribute(string route, HttpMethods method = HttpMethods.Get, string contentType = Bunkum.Listener.Protocol.ContentType.Plaintext)
: base(BaseRoute + route, method, contentType)
{}

public PresenceEndpointAttribute(string route, string contentType, HttpMethods method = HttpMethods.Get)
: base(BaseRoute + route, method, contentType)
{}
}
39 changes: 39 additions & 0 deletions Refresh.GameServer/Middlewares/PresenceAuthenticationMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Bunkum.Core.Database;
using Bunkum.Core.Endpoints.Middlewares;
using Bunkum.Listener.Request;
using Refresh.GameServer.Configuration;
using Refresh.GameServer.Endpoints;

namespace Refresh.GameServer.Middlewares;

public class PresenceAuthenticationMiddleware : IMiddleware
{
private IntegrationConfig _config;

public PresenceAuthenticationMiddleware(IntegrationConfig config)
{
this._config = config;
}

public void HandleRequest(ListenerContext context, Lazy<IDatabaseContext> database, Action next)
{
if (context.Uri.AbsolutePath.StartsWith(PresenceEndpointAttribute.BaseRoute))
{
// Block presence requests if not enabled
if (!this._config.PresenceEnabled)
{
context.ResponseCode = NotImplemented;
return;
}

// Block presence requests with a bad auth token
if (context.RequestHeaders["Authorization"] != this._config.PresenceSharedSecret)
{
context.ResponseCode = Unauthorized;
return;
}
}

next();
}
}
7 changes: 6 additions & 1 deletion Refresh.GameServer/Middlewares/WebsiteMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ private static bool HandleWebsiteRequest(ListenerContext context)

string uri = context.Uri.AbsolutePath;

if (uri.StartsWith(GameEndpointAttribute.BaseRoute) || uri.StartsWith("/api") || uri == "/autodiscover" || uri == "/_health" || uri.StartsWith("/gameAssets")) return false;
if (uri.StartsWith(GameEndpointAttribute.BaseRoute) ||
uri.StartsWith(PresenceEndpointAttribute.BaseRoute) ||
uri.StartsWith("/api") ||
uri == "/autodiscover" ||
uri == "/_health" ||
uri.StartsWith("/gameAssets")) return false;

if (uri == "/" || (context.RequestHeaders["Accept"] ?? "").Contains("text/html"))
uri = "/index.html";
Expand Down
1 change: 1 addition & 0 deletions Refresh.GameServer/RefreshContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ public enum RefreshContext
CoolLevels,
Publishing,
Aipi,
Presence,
}
4 changes: 4 additions & 0 deletions Refresh.GameServer/RefreshGameServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Refresh.Common;
using Refresh.Common.Verification;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Authentication.Presence;
using Refresh.GameServer.Configuration;
using Refresh.GameServer.Database;
using Refresh.GameServer.Documentation;
Expand Down Expand Up @@ -99,6 +100,7 @@ protected override void SetupMiddlewares()
this.Server.AddMiddleware<CrossOriginMiddleware>();
this.Server.AddMiddleware<PspVersionMiddleware>();
this.Server.AddMiddleware<LegacyAdapterMiddleware>();
this.Server.AddMiddleware(new PresenceAuthenticationMiddleware(this._integrationConfig!));
}

protected override void SetupConfiguration()
Expand Down Expand Up @@ -142,6 +144,8 @@ protected override void SetupServices()
this.Server.AddService<CommandService>();
this.Server.AddService<DiscordStaffService>();

this.Server.AddService<PresenceService>();

if(this._integrationConfig!.AipiEnabled)
this.Server.AddService<AipiService>();

Expand Down
Loading

0 comments on commit 8727ffe

Please sign in to comment.