diff --git a/GenOnlineService/Constants.cs b/GenOnlineService/Constants.cs index 06f02cc..780baee 100644 --- a/GenOnlineService/Constants.cs +++ b/GenOnlineService/Constants.cs @@ -48,16 +48,18 @@ public static class Constants } public class RoomMember { - public RoomMember(Int64 a_UserID, string strName, bool admin) + public RoomMember(Int64 a_UserID, string strName, bool admin, string platform) { UserID = a_UserID; Name = strName; IsAdmin = admin; + Platform = platform; } public Int64 UserID { get; set; } = -1; public String Name { get; set; } = String.Empty; public bool IsAdmin { get; set; } = false; + public String Platform { get; set; } = Platforms.Unknown; } public enum EPendingLoginState @@ -188,13 +190,45 @@ public enum EKnownClients }; } + // The platform is self-reported by the client and echoed back to every other + // member of a lobby, so it is clamped to a known set rather than passed through. + public static class Platforms + { + public const string Unknown = "unknown"; + public const string Windows = "windows"; + public const string MacOS = "macos"; + + private static readonly HashSet Known = new(StringComparer.OrdinalIgnoreCase) + { + Windows, + MacOS + }; + + public static string Normalize(string? platform) + { + if (String.IsNullOrWhiteSpace(platform)) + { + return Unknown; + } + + string trimmed = platform.Trim(); + + if (!Known.Contains(trimmed)) + { + return Unknown; + } + + return trimmed.ToLowerInvariant(); + } + } + // TODO static class WebSocketManager { public static int g_PeakConnectionCount = 0; - public static async Task CreateSession(AppDbContext _db, EUserSessionType sessionType, bool bIsReconnect, Int64 ownerID, KnownClients.EKnownClients client_id, string ipAddr, string strContinent, string strCountry, double dLatitude, double dLongitude, bool bIsAdmin) + public static async Task CreateSession(AppDbContext _db, EUserSessionType sessionType, bool bIsReconnect, Int64 ownerID, KnownClients.EKnownClients client_id, string ipAddr, string strContinent, string strCountry, double dLatitude, double dLongitude, bool bIsAdmin, string platform) { string strDisplayName = await Database.Users.GetDisplayName(_db, ownerID); @@ -266,7 +300,7 @@ public static async Task CreateSession(AppDbContext _db, // get stats PlayerStats GameStats = await Database.UserStats.GetPlayerStats(_db, ownerID); - userCacheData = new UserSession(ownerID, sessionType, client_id, strContinent, strCountry, dLatitude, dLongitude); + userCacheData = new UserSession(ownerID, sessionType, client_id, strContinent, strCountry, dLatitude, dLongitude, platform); m_dictUserSessions[sessionType][ownerID] = userCacheData; // TODO_SOCIAL: Move this to a class @@ -781,7 +815,7 @@ public static async Task TickRoomMemberList() } - memberListUpdate.members.Add(new RoomMember(sess.m_UserID, strDisplayName, sharedUserData.IsAdmin())); + memberListUpdate.members.Add(new RoomMember(sess.m_UserID, strDisplayName, sharedUserData.IsAdmin(), sess.Platform)); // also add to list of users who need this update, since they were in there lstUsersToSend.Add(sess.m_UserID); @@ -899,6 +933,7 @@ public class UserSession private string m_strMiddlewareUserID = String.Empty; public KnownClients.EKnownClients m_client_id = KnownClients.EKnownClients.unknown; + public string Platform { get; private set; } = Platforms.Unknown; DateTime m_CreateTime = DateTime.Now; public DateTime GetCreationTime() { @@ -936,7 +971,7 @@ public TimeSpan GetDuration() return DateTime.Now - m_CreateTime; } - public UserSession(Int64 ownerID, EUserSessionType sessionType, KnownClients.EKnownClients client_id, string strContinent, string strCountry, double dLatitude, double dLongitude) + public UserSession(Int64 ownerID, EUserSessionType sessionType, KnownClients.EKnownClients client_id, string strContinent, string strCountry, double dLatitude, double dLongitude, string platform) { m_sessionType = sessionType; m_client_id = client_id; @@ -944,6 +979,7 @@ public UserSession(Int64 ownerID, EUserSessionType sessionType, KnownClients.EKn m_strCountry = strCountry; m_dLatitude = dLatitude; m_dLongitude = dLongitude; + Platform = platform; m_UserID = ownerID; diff --git a/GenOnlineService/Controllers/CheckLogin/CheckLoginController.cs b/GenOnlineService/Controllers/CheckLogin/CheckLoginController.cs index f4768aa..c69f902 100644 --- a/GenOnlineService/Controllers/CheckLogin/CheckLoginController.cs +++ b/GenOnlineService/Controllers/CheckLogin/CheckLoginController.cs @@ -207,8 +207,10 @@ public async Task Post_InternalHandler(string jsonData, string ipAddr string exe_crc = data.ContainsKey("exe_crc") ? data["exe_crc"].ToString() : "NONE"; Helpers.RegisterInitialPlayerExeCRC(user_id, exe_crc); - var sessiontoken = Program.g_tokenGenerator.GenerateToken(strDisplayName, user_id, ipAddr, Program.JwtTokenGenerator.ETokenType.Session, knownClientID, sessionType, bIsAdmin); - var refreshtoken = Program.g_tokenGenerator.GenerateToken(strDisplayName, user_id, ipAddr, Program.JwtTokenGenerator.ETokenType.Refresh, knownClientID, sessionType, false, out string refreshJti); + string platform = data.ContainsKey("platform") ? data["platform"].ToString() : Platforms.Unknown; + + var sessiontoken = Program.g_tokenGenerator.GenerateToken(strDisplayName, user_id, ipAddr, Program.JwtTokenGenerator.ETokenType.Session, knownClientID, sessionType, bIsAdmin, platform); + var refreshtoken = Program.g_tokenGenerator.GenerateToken(strDisplayName, user_id, ipAddr, Program.JwtTokenGenerator.ETokenType.Refresh, knownClientID, sessionType, false, platform, out string refreshJti); // rotation: only this refresh token is accepted from now on await TokenRevocationManager.OnTokensIssued(user_id, sessionType, refreshJti); diff --git a/GenOnlineService/Controllers/LoginWithToken/LoginWithTokenController.cs b/GenOnlineService/Controllers/LoginWithToken/LoginWithTokenController.cs index e20d2c1..ceded43 100644 --- a/GenOnlineService/Controllers/LoginWithToken/LoginWithTokenController.cs +++ b/GenOnlineService/Controllers/LoginWithToken/LoginWithTokenController.cs @@ -143,6 +143,8 @@ public async Task Post_InternalHandler(string jsonData, string ipAddr string exe_crc = data.ContainsKey("exe_crc") ? data["exe_crc"].ToString() : "NONE"; Helpers.RegisterInitialPlayerExeCRC(user_id, exe_crc); + string platform = data.ContainsKey("platform") ? data["platform"].ToString() : Platforms.Unknown; + string strDisplayName = await Database.Users.GetDisplayName(db, user_id); await SessionHelpers.SetUsedLoggedIn(user_id, clientID, sessionType); @@ -152,8 +154,8 @@ public async Task Post_InternalHandler(string jsonData, string ipAddr // extend token // TODO_TODAY_JWT: just get clientID from token - var sessiontoken = Program.g_tokenGenerator.GenerateToken(strDisplayName, user_id, ipAddr, Program.JwtTokenGenerator.ETokenType.Session, clientID, sessionType, bIsAdmin); - var refreshtoken = Program.g_tokenGenerator.GenerateToken(strDisplayName, user_id, ipAddr, Program.JwtTokenGenerator.ETokenType.Refresh, clientID, sessionType, false, out string refreshJti); + var sessiontoken = Program.g_tokenGenerator.GenerateToken(strDisplayName, user_id, ipAddr, Program.JwtTokenGenerator.ETokenType.Session, clientID, sessionType, bIsAdmin, platform); + var refreshtoken = Program.g_tokenGenerator.GenerateToken(strDisplayName, user_id, ipAddr, Program.JwtTokenGenerator.ETokenType.Refresh, clientID, sessionType, false, platform, out string refreshJti); // rotation: only this refresh token is accepted from now on await TokenRevocationManager.OnTokensIssued(user_id, sessionType, refreshJti); diff --git a/GenOnlineService/Controllers/RefreshToken/RefreshTokenController.cs b/GenOnlineService/Controllers/RefreshToken/RefreshTokenController.cs index 8ce557f..9530224 100644 --- a/GenOnlineService/Controllers/RefreshToken/RefreshTokenController.cs +++ b/GenOnlineService/Controllers/RefreshToken/RefreshTokenController.cs @@ -111,8 +111,11 @@ public async Task Post_InternalHandler(string ipAddr) string strDisplayName = await Database.Users.GetDisplayName(db, user_id); bool bIsAdmin = await Database.Users.IsUserAdmin(db, user_id); - var sessiontoken = Program.g_tokenGenerator.GenerateToken(strDisplayName, user_id, ipAddr, Program.JwtTokenGenerator.ETokenType.Session, clientID, sessionType, bIsAdmin); - var refreshtoken = Program.g_tokenGenerator.GenerateToken(strDisplayName, user_id, ipAddr, Program.JwtTokenGenerator.ETokenType.Refresh, clientID, sessionType, false, out string refreshJti); + // carried over from the token being rotated, so the platform survives a refresh + string platform = TokenHelper.GetPlatform(this); + + var sessiontoken = Program.g_tokenGenerator.GenerateToken(strDisplayName, user_id, ipAddr, Program.JwtTokenGenerator.ETokenType.Session, clientID, sessionType, bIsAdmin, platform); + var refreshtoken = Program.g_tokenGenerator.GenerateToken(strDisplayName, user_id, ipAddr, Program.JwtTokenGenerator.ETokenType.Refresh, clientID, sessionType, false, platform, out string refreshJti); // rotation: only this refresh token is accepted from now on await TokenRevocationManager.OnTokensIssued(user_id, sessionType, refreshJti); diff --git a/GenOnlineService/Controllers/WebSocket/WebSocketController.cs b/GenOnlineService/Controllers/WebSocket/WebSocketController.cs index 21ab9e0..f1d56e8 100644 --- a/GenOnlineService/Controllers/WebSocket/WebSocketController.cs +++ b/GenOnlineService/Controllers/WebSocket/WebSocketController.cs @@ -144,6 +144,7 @@ public async Task Get([FromHeader(Name = "is-reconnect")] bool bIsReconnect) } EUserSessionType sessType = TokenHelper.GetSessionType(this); + string platform = TokenHelper.GetPlatform(this); await using var db = await _dbFactory.CreateDbContextAsync(); UserWebSocketInstance wsSess = await WebSocketManager.CreateSession( @@ -157,7 +158,8 @@ public async Task Get([FromHeader(Name = "is-reconnect")] bool bIsReconnect) ipCountry, dLatitude, dLongitude, - bIsAdmin); + bIsAdmin, + platform); // if null, it was probably a reconnect and they need to fully reconnect, so return an error instead if (wsSess == null) diff --git a/GenOnlineService/LobbyManager.cs b/GenOnlineService/LobbyManager.cs index f32b4e5..fc8acdd 100644 --- a/GenOnlineService/LobbyManager.cs +++ b/GenOnlineService/LobbyManager.cs @@ -1298,6 +1298,7 @@ public void UpdateSlotIndex(UInt16 index) public EPlayerType SlotState { get; private set; } = 0; public UInt16 SlotIndex { get; private set; } = 0; public string Region { get; private set; } = "Unknown"; + public string Platform { get; private set; } = Platforms.Unknown; public string MiddlewareUserID { get; private set; } = String.Empty; [JsonIgnore] // cant serialize refs @@ -1339,6 +1340,7 @@ public LobbyMember(Lobby owningLobby, UserSession? owningSession, Int64 UserID_i IsReady = false; Region = owningSession == null ? "Unknown" : owningSession.GetFullContinentName(); + Platform = owningSession == null ? Platforms.Unknown : owningSession.Platform; } public bool IsHuman() { return SlotState == EPlayerType.SLOT_PLAYER; } diff --git a/GenOnlineService/Program.cs b/GenOnlineService/Program.cs index e2d54e4..659d593 100644 --- a/GenOnlineService/Program.cs +++ b/GenOnlineService/Program.cs @@ -392,6 +392,16 @@ public static EUserSessionType GetSessionType(ControllerBase controller) return EUserSessionType.None; } + public static string GetPlatform(ControllerBase controller) + { + var first = controller.User.FindFirst(Program.JwtTokenGenerator.PlatformClaim); + + if (first == null) + return Platforms.Unknown; + + return Platforms.Normalize(first.Value); + } + public static string GetDisplayName(ControllerBase controller) { // TODO: Handle not finding claims, it is a critical error @@ -710,13 +720,14 @@ public enum ETokenType } public const string TokenGenerationClaim = "tgen"; + public const string PlatformClaim = "platform"; - public string GenerateToken(string displayname, Int64 userID, string ipAddr, ETokenType tokenType, KnownClients.EKnownClients knownClientID, EUserSessionType sessionType, bool bIsAdmin) + public string GenerateToken(string displayname, Int64 userID, string ipAddr, ETokenType tokenType, KnownClients.EKnownClients knownClientID, EUserSessionType sessionType, bool bIsAdmin, string platform) { - return GenerateToken(displayname, userID, ipAddr, tokenType, knownClientID, sessionType, bIsAdmin, out _); + return GenerateToken(displayname, userID, ipAddr, tokenType, knownClientID, sessionType, bIsAdmin, platform, out _); } - public string GenerateToken(string displayname, Int64 userID, string ipAddr, ETokenType tokenType, KnownClients.EKnownClients knownClientID, EUserSessionType sessionType, bool bIsAdmin, out string jti) + public string GenerateToken(string displayname, Int64 userID, string ipAddr, ETokenType tokenType, KnownClients.EKnownClients knownClientID, EUserSessionType sessionType, bool bIsAdmin, string platform, out string jti) { var jwtSettings = _configuration.GetSection("JwtSettings"); @@ -745,6 +756,7 @@ public string GenerateToken(string displayname, Int64 userID, string ipAddr, ETo new Claim(JwtRegisteredClaimNames.Typ, ((int)tokenType).ToString()), new Claim("client_id", ((int)knownClientID).ToString()), new Claim("session_type", ((int)sessionType).ToString()), + new Claim(PlatformClaim, Platforms.Normalize(platform)), // Token generation, checked against the revocation manager on every request so // that bans/logouts can invalidate tokens before they naturally expire.