Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add presence server #652

Merged
Merged
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/";
}
jvyden marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 3 additions & 0 deletions Refresh.Common/Constants/SystemUsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ public static class SystemUsers

public const string UnknownUserName = "!Unknown";
public const string UnknownUserDescription = "I'm a fake user that represents a non existent publisher for re-published levels.";

public const string HashedUserName = "!Hashed";
public const string HashedUserDescription = "I'm a fake user that represents an unknown publisher for hashed levels.";
}
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ public class ApiExtendedGameUserResponse : IApiResponse, IDataConvertableFrom<Ap

public required ApiGameUserStatisticsResponse Statistics { get; set; }
public required ApiGameRoomResponse? ActiveRoom { get; set; }
public required bool ConnectedToPresenceServer { get; set; }

[ContractAnnotation("null => null; notnull => notnull")]
[ContractAnnotation("user:null => null; user:notnull => notnull")]
public static ApiExtendedGameUserResponse? FromOld(GameUser? user, DataContext dataContext)
{
if (user == null) return null;
Expand Down Expand Up @@ -77,6 +78,7 @@ public class ApiExtendedGameUserResponse : IApiResponse, IDataConvertableFrom<Ap
LevelVisibility = user.LevelVisibility,
ProfileVisibility = user.ProfileVisibility,
ShowModdedContent = user.ShowModdedContent,
ConnectedToPresenceServer = user.PresenceServerAuthToken != null,
};
}

Expand Down
18 changes: 12 additions & 6 deletions Refresh.GameServer/Endpoints/ApiV3/LevelApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Refresh.GameServer.Endpoints.ApiV3.ApiTypes.Errors;
using Refresh.GameServer.Endpoints.ApiV3.DataTypes.Request;
using Refresh.GameServer.Endpoints.ApiV3.DataTypes.Response.Levels;
using Refresh.GameServer.Endpoints.Game.DataTypes.Response;
using Refresh.GameServer.Endpoints.Game.Levels.FilterSettings;
using Refresh.GameServer.Extensions;
using Refresh.GameServer.Services;
Expand Down Expand Up @@ -144,13 +145,17 @@ 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,
PlayNowService overrideService,
[DocSummary("The ID of the level")] int id)
{
GameLevel? level = database.GetLevelById(id);
if (level == null) return ApiNotFoundError.LevelMissingError;

service.AddIdOverridesForUser(user, level);

// TODO: return whether or not the presence server was used
overrideService.PlayNowLevel(user, level);

return new ApiOkResponse();
}
Expand All @@ -159,12 +164,13 @@ public ApiOkResponse SetLevelAsOverrideById(RequestContext context, GameDatabase
[DocSummary("Marks the level hash to show in the next slot list gotten from the game")]
[DocError(typeof(ApiValidationError), ApiValidationError.HashInvalidErrorWhen)]
public ApiOkResponse SetLevelAsOverrideByHash(RequestContext context, GameDatabaseContext database, GameUser user,
LevelListOverrideService service, [DocSummary("The hash of level root resource")] string hash)
PlayNowService service, PresenceService presenceService, [DocSummary("The hash of level root resource")] string hash)
{
if (!CommonPatterns.Sha1Regex().IsMatch(hash))
return ApiValidationError.HashInvalidError;

service.AddHashOverrideForUser(user, hash);

// TODO: return whether presence/hash play now was used
service.PlayNowHash(user, hash);

return new ApiOkResponse();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public static GameLevelResponse FromHash(string hash, DataContext dataContext)
Location = new GameLocation(),
Handle = new SerializedUserHandle
{
Username = $"!Hashed",
Username = SystemUsers.HashedUserName,
IconHash = "0",
},
Type = GameSlotType.User.ToGameType(),
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);
jvyden marked this conversation as resolved.
Show resolved Hide resolved

context.Logger.LogInfo(BunkumCategory.Authentication, $"{user} logged out");
return OK;
Expand Down
14 changes: 7 additions & 7 deletions Refresh.GameServer/Endpoints/Game/Levels/LevelEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Bunkum.Core;
using Bunkum.Core.Endpoints;
using Bunkum.Core.Endpoints.Debugging;
using Bunkum.Core.Storage;
using Bunkum.Listener.Protocol;
using Refresh.Common.Constants;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
using Refresh.GameServer.Endpoints.Game.DataTypes.Response;
Expand All @@ -26,7 +26,7 @@ public class LevelEndpoints : EndpointGroup
public SerializedMinimalLevelList? GetLevels(RequestContext context,
GameDatabaseContext database,
CategoryService categoryService,
LevelListOverrideService overrideService,
PlayNowService overrideService,
GameUser user,
Token token,
DataContext dataContext,
Expand All @@ -47,7 +47,7 @@ public class LevelEndpoints : EndpointGroup

// If we are getting the levels by a user, and that user is "!Hashed", then we pull that user's overrides
if (route == "by"
&& (context.QueryString.Get("u") == "!Hashed" || user.Username == "!Hashed")
&& (context.QueryString.Get("u") == SystemUsers.HashedUserName || user.Username == SystemUsers.HashedUserName)
&& overrideService.GetLastHashOverrideForUser(token, out string hash))
{
return new SerializedMinimalLevelList
Expand Down Expand Up @@ -101,7 +101,7 @@ public class LevelEndpoints : EndpointGroup
public SerializedMinimalLevelList? GetLevelsWithPlayer(RequestContext context,
GameDatabaseContext database,
CategoryService categories,
LevelListOverrideService overrideService,
PlayNowService overrideService,
Token token,
DataContext dataContext,
string route,
Expand All @@ -118,7 +118,7 @@ public class LevelEndpoints : EndpointGroup
[MinimumRole(GameUserRole.Restricted)]
public GameLevelResponse? LevelById(RequestContext context, GameDatabaseContext database, Token token,
string slotType, int id,
LevelListOverrideService overrideService, DataContext dataContext)
PlayNowService overrideService, DataContext dataContext)
{
// If the user has had a hash override in the past, and the level id they requested matches the level ID associated with that hash
if (overrideService.GetLastHashOverrideForUser(token, out string hash) && GameLevelResponse.LevelIdFromHash(hash) == id)
Expand Down Expand Up @@ -204,7 +204,7 @@ public SerializedMinimalLevelResultsList GetLevelsFromCategory(RequestContext co
GameDatabaseContext database,
CategoryService categories,
MatchService matchService,
LevelListOverrideService overrideService,
PlayNowService overrideService,
GameUser user,
IDataStore dataStore,
Token token,
Expand All @@ -218,7 +218,7 @@ public SerializedMinimalLevelResultsList GetLevelsFromCategory(RequestContext co
GameDatabaseContext database,
CategoryService categories,
MatchService matchService,
LevelListOverrideService overrideService,
PlayNowService overrideService,
Token token,
IDataStore dataStore,
DataContext dataContext,
Expand Down
Loading
Loading