//============================================================================= // AccessControl. // // AccessControl is a helper class for GameInfo. // The AccessControl class determines whether or not the player is allowed to // login in the PreLogin() function, controls whether or not a player can enter // as a spectator or a game administrator, and handles authentication of // clients with the online subsystem (including the listen server host). // // Copyright 1998-2013 Epic Games, Inc. All Rights Reserved. //============================================================================= class AccessControl extends Info dependson(OnlineAuthInterface) config(Game); /** Contains policies for allowing/denying IP addresses */ var globalconfig array IPPolicies; /** Contains the list of banned UIDs */ var globalconfig array BannedIDs; /** Various localized strings */ var localized string IPBanned; var localized string WrongPassword; var localized string NeedPassword; var localized string SessionBanned; var localized string KickedMsg; var localized string DefaultKickReason; var localized string IdleKickReason; var class AdminClass; /** Password required for admin privileges */ var private globalconfig string AdminPassword; /** Password required to enter the game */ var private globalconfig string GamePassword; var localized string ACDisplayText[3]; var localized string ACDescText[3]; var bool bDontAddDefaultAdmin; /** Whether or not to authenticate clients (specifically their UID's) when they join; client UID's can't be trusted until they are authenticated */ var globalconfig bool bAuthenticateClients; /** Whether or not to authenticate the game server with clients; a client must be fully authenticated before it can authenticate the server */ var globalconfig bool bAuthenticateServer; /** Whether or not to authenticate the listen host, on lists servers */ var globalconfig bool bAuthenticateListenHost; /** The maximum number of times to retry authentication */ var globalconfig int MaxAuthRetryCount; /** The delay between authentication attempts */ var globalconfig int AuthRetryDelay; /** Caches a local reference to the online subsystem */ var OnlineSubsystem OnlineSub; /** Caches a local reference to the online subsystems auth interface, if it has one set */ var OnlineAuthInterface CachedAuthInt; /** Whether or not this classes auth delegates have been registered with the auth interface */ var bool bAuthDelegatesRegistered; /** Struct used for tracking clients pending authentication */ struct PendingClientAuth { var Player ClientConnection; // The NetConnection of the client pending auth var UniqueNetId ClientUID; // The UID of the client var float AuthTimestamp; // The timestamp for when authentication was started var int AuthRetryCount; // The number of times authentication has been retried for this client }; /** Tracks clients who are currently pending authentication */ var array ClientsPendingAuth; /** Struct used for tracking server auth retry counts */ struct ServerAuthRetry { var UniqueNetId ClientUID; // The UID of the client requesting retries var int AuthRetryCount; // The number of times server authentication has been retried for this client }; /** Tracks server auth retry requests for clients */ var array ServerAuthRetries; /** Whether or not the listen host is pending authentication */ var bool bPendingListenAuth; /** Stores the UID of the listen server auth ticket */ var int ListenAuthTicketUID; /** The number of times listen host auth has been retried */ var int ListenAuthRetryCount; function PostBeginPlay() { OnlineSub = Class'GameEngine'.static.GetOnlineSubsystem(); InitAuthHooks(); } function Destroyed() { Cleanup(); } /** * Checks whether or not the specified PlayerController is an admin * * @param P The PlayerController to check * @return TRUE if the specified player has admin privileges. */ function bool IsAdmin(PlayerController P) { if ( P != None ) { if ( Admin(P) != None ) { return true; } if ( P.PlayerReplicationInfo != None && P.PlayerReplicationInfo.bAdmin ) { return true; } } return false; } function bool SetAdminPassword(string P) { AdminPassword = P; return true; } function SetGamePassword(string P) { GamePassword = P; WorldInfo.Game.UpdateGameSettings(); } function bool RequiresPassword() { return GamePassword != "" || GameEngine(class'Engine'.static.GetEngine()).bPrivateServer; } /** * Takes a string and tries to find the matching controller associated with it. First it searches as if the string is the * player's name. If it doesn't find a match, it attempts to resolve itself using the target as the player id. * * @Params Target The search key * * @returns the controller associated with the key. NONE is a valid return and means not found. */ function Controller GetControllerFromString(string Target) { local Controller C,FinalC; local int i; FinalC = none; foreach WorldInfo.AllControllers(class'Controller', C) { if (C.PlayerReplicationInfo != None && (C.PlayerReplicationInfo.PlayerName ~= Target || C.PlayerReplicationInfo.PlayerName ~= Target)) { FinalC = C; break; } } // if we didn't find it by name, attempt to convert the target to a player index and look him up if possible. if ( C == none && WorldInfo != none && WorldInfo.GRI != none ) { for (i=0;i"; `else OutError = PathName(WorldInfo.Game.GameMessageClass)$".MaxedOutMessage"; `endif } // BWJ - 8-11-16 - Require bJoinViaInvite in join URL for private servers else if ( (GamePassword != "" && !(InPassword == GamePassword) && (AdminPassword == "" || !(InPassword == AdminPassword))) || ( Engine.bPrivateServer && !bHasPrivateServerOption ) ) { `if(`__TW_NETWORKING_) OutError = "" : "Engine.AccessControl.WrongPassword>"; `else OutError = (InPassword == "") ? "Engine.AccessControl.NeedPassword" : "Engine.AccessControl.WrongPassword"; `endif } // Check server IP bans (UID bans are checked in GameInfo::PreLogin) if (!CheckIPPolicy(Address)) { `if(`__TW_) OutError = ""; `else OutError = "Engine.AccessControl.IPBanned"; `endif } // If the client was not already rejected, handle authentication of the clients UID if (bAuthenticateClients && OutError == "" && CachedAuthInt != None && bAuthDelegatesRegistered) { if (OnlineSub != None && OnlineSub.GameInterface != None) { GameSettings = OnlineSub.GameInterface.GetGameSettings(WorldInfo.Game.PlayerReplicationInfoClass.default.SessionName); `if(`__TW_NETWORKING_) //If we're connecting locally, don't even bother with the authentication stuff in the rest of the function if (Address == "127.0.0.1") { `log("Skipping online subsystem authentication because it's a local connection from address:" $ Address); return; } `endif // If 'bIsLanMatch' is set, do not enable any authentication if ((WorldInfo.NetMode == NM_DedicatedServer || WorldInfo.NetMode == NM_ListenServer) && GameSettings != None && !GameSettings.bIsLanMatch) { // If the client does not support authentication, reject him immediately if (!bSupportsAuth) { if (OnlineSub.Class.Name == 'OnlineSubsystemSteamworks') { `if(`__TW_) OutError = ""; `else OutError = "Engine.Errors.SteamClientRequired"; `endif } else { `if(`__TW_) OutError = ""; `else OutError = "Server requires authentication"; `endif } } // Pause the login process for the client if (OutError == "") { ClientConn = WorldInfo.Game.PauseLogin(); } if (ClientConn != none) { // If there are any other client connections from the same UID and IP, kick them (fixes an auth issue, // preventing players from rejoining if they were disconnected, and the old connection still lingers) // First find the joining clients IP foreach WorldInfo.AllClientConnections(CurConn, CurIP, CurPort) { if (CurConn == ClientConn) { ClientIP = CurIP; break; } } // See if there is an active auth session matching the same IP and UID LingeringPort = 0; foreach CachedAuthInt.AllClientAuthSessions(CurClientSession) { if (CurClientSession.EndPointIP == ClientIP && CurClientSession.EndPointUID == UniqueId) { LingeringPort = CurClientSession.EndPointPort; break; } } // If there was an existing active auth session, match it up to the lingering connection and disconnect it if (LingeringPort != 0) { foreach WorldInfo.AllClientConnections(CurConn, CurIP, CurPort) { if (CurConn != ClientConn && CurIP == ClientIP && CurPort == LingeringPort) { `log("Closing old connection with duplicate IP ("$Address$") and SteamId ("$ Class'OnlineSubsystem'.static.UniqueNetIdToString(UniqueId)$")",, 'DevNet'); WorldInfo.Game.RejectLogin(CurConn, ""); break; } } } // If there are other client connections from the same UID, but not the same IP, reject the new player // NOTE: The above code shouldn't affect this, as OnClientConnectionClose (which cleans up lists) // is called during RejectLogin for (i=0; i= AuthRetryDelay) { if (CachedAuthInt.FindClientAuthSession(ClientsPendingAuth[i].ClientConnection, CurClientSession)) { if (ClientsPendingAuth[i].AuthRetryCount < MaxAuthRetryCount) { // End the auth session first before retrying CachedAuthInt.EndRemoteClientAuthSession(CurClientSession.EndPointUID, CurClientSession.EndPointIP); // Get the client to end it on his end too (this should execute on client before the new auth request below) CachedAuthInt.SendClientAuthEndSessionRequest(ClientsPendingAuth[i].ClientConnection); // Start the new auth session if (CachedAuthInt.SendClientAuthRequest(ClientsPendingAuth[i].ClientConnection, CurClientSession.EndPointUID)) { ClientsPendingAuth[i].AuthTimestamp = WorldInfo.RealTimeSeconds; ClientsPendingAuth[i].AuthRetryCount++; } else { bFailed = True; } } else { bFailed = True; } if (bFailed) { `log("Client authentication timed out after"@MaxAuthRetryCount@"tries",, 'DevOnline'); OldLength = ClientsPendingAuth.Length; WorldInfo.Game.RejectLogin(ClientsPendingAuth[i].ClientConnection, "Authentication failed"); // If OnClientConnectionClose did not alter ClientsPendingAuth, remove the entry now if (OldLength == ClientsPendingAuth.Length) { ClientsPendingAuth.Remove(i, 1); } i--; } } } } if (ClientsPendingAuth.Length == 0) { ClearTimer('PendingAuthTimer'); } } /** * Called when the auth interface is ready to perform authentication (may not be called, if the auth interface was already ready) * NOTE: Listen host authentication may be kicked off here */ function OnAuthReady() { local int i, OldLength; if (bAuthDelegatesRegistered) { // If there are any pending client auth's queued, kickoff authentication for (i=0; i 0) { `log("OnAuthReady: Kicking off delayed auth for clients"); SetTimer(3.0, True, nameof(PendingAuthTimer)); } if (bAuthenticateListenHost && WorldInfo.NetMode == NM_ListenServer && bPendingListenAuth) { BeginListenHostAuth(); } } } /** * Called when the server receives auth data from a client, needed for authentication * * @param ClientUID The UID of the client * @param ClientIP The IP of the client * @param AuthTicketUID The UID used to reference the auth data */ //@HSL_BEGIN_XBOX function ProcessClientAuthResponse(UniqueNetId ClientUID, IpAddr ClientIP, int AuthTicketUID) { //@HSL_END_XBOX local bool bSuccess; local int i, PendingIdx, OldLength; // Check that we are expecting auth data from this client PendingIdx = INDEX_None; for (i=0; inot< pause at login, the UID needs to be stored in the PRI from here P.PlayerReplicationInfo.SetUniqueId(ClientUID); `log("Client '"$PRI.PlayerName$"'passed authentication, UID:"@ Class'OnlineSubsystem'.static.UniqueNetIdToString(ClientUID)); } else { `log("Client passed authentication, UID:"@Class'OnlineSubsystem'.static.UniqueNetIdToString(ClientUID)); } bResumeLogin = True; // Kick off server auth if (bAuthenticateServer) { if (CachedAuthInt.FindClientAuthSession(ClientConnection, CurClientSession)) { ProcessServerAuthRequest(ClientConnection, ClientUID, CurClientSession.EndPointIP, CurClientSession.EndPointPort); } else { `log("Failed to kickoff server auth; could not find matching client session"); } } } else { `log("Client failed authentication (unauthenticated UID:"@ Class'OnlineSubsystem'.static.UniqueNetIdToString(ClientUID)$"), kicking"); // Kick the client WorldInfo.Game.RejectLogin(ClientConnection, "Authentication failed"); } } // Remove the tracking entry, if it was not removed above if (ClientsPendingAuth.Length == PendingLen) { ClientsPendingAuth.Remove(PendingIdx, 1); } } else { `log("AccessControl::OnClientAuthComplete: Received unexpected auth result for client",, 'DevOnline'); } if (bResumeLogin) { WorldInfo.Game.ResumeLogin(ClientConnection); } } /** * Server authentication */ /** * Called when the server receives a message from a client, requesting a server auth session * * @param ClientConnection The NetConnection of the client the request came from * @param ClientUID The UID of the client making the request * @param ClientIP The IP of the client making the request * @param ClientPort The port the client is on */ //@HSL_BEGIN_XBOX function ProcessServerAuthRequest(Player ClientConnection, UniqueNetId ClientUID, IpAddr ClientIP, int ClientPort) { //@HSL_END_XBOX local int AuthTicketUID; local LocalAuthSession CurServerSession; local bool bFound; // NOTE: Native code handles checking of whether or not client is authenticated if (bAuthenticateServer) { // Make sure there is not already a server auth session for this client foreach CachedAuthInt.AllLocalServerAuthSessions(CurServerSession) { if (CurServerSession.EndPointUID == ClientUID && CurServerSession.EndPointIP == ClientIP) { bFound = true; } } if (!bFound) { // Kickoff server auth if (CachedAuthInt.CreateServerAuthSession(ClientUID, ClientIP, ClientPort, AuthTicketUID)) { if (!CachedAuthInt.SendServerAuthResponse(ClientConnection, AuthTicketUID)) { `log("WARNING!!! Failed to send auth ticket to client"); } } else { `log("Failed to kickoff server auth",, 'DevOnline'); } } } } /** * Called when the server receives a server auth retry request from a client * * @param ClientConnection The client NetConnection */ function ProcessServerAuthRetryRequest(Player ClientConnection) { local bool bFoundAndAuthenticated; //@HSL_BEGIN_XBOX local IpAddr ClientIP; //@HSL_END_XBOX local int ClientPort, i, CurRetryIdx; local UniqueNetId ClientUID; local AuthSession CurClientSession; local LocalAuthSession CurServerSession; if (bAuthenticateServer && ClientConnection != none) { bFoundAndAuthenticated = CachedAuthInt.FindClientAuthSession(ClientConnection, CurClientSession) && CurClientSession.AuthStatus == AUS_Authenticated; // Only execute a server auth retry, if the client is fully authenticated if (bFoundAndAuthenticated) { ClientUID = CurClientSession.EndPointUID; ClientIP = CurClientSession.EndPointIP; ClientPort = CurClientSession.EndPointPort; CurRetryIdx = INDEX_None; for (i=0; i Max(30, MaxAuthRetryCount + 20)) { WorldInfo.Game.RejectLogin(ClientConnection, "Spamming server auth"); } else { // Update the retry count ServerAuthRetries[CurRetryIdx].AuthRetryCount++; } } } } /** * Listen host authentication */ /** * Kicks off authentication of the listen host */ function BeginListenHostAuth(optional bool bRetry) { local UniqueNetId ServerUID, HostUID; //@HSL_BEGIN_XBOX local IpAddr ServerIP; local int ServerPort; //@HSL_END_XBOX local OnlineGameSettings GameSettings; local bool bGotHostInfo, bFound, bSecure; local AuthSession CurClientSession, ListenSession; bPendingListenAuth = false; if (CachedAuthInt.IsReady()) { bGotHostInfo = CachedAuthInt.GetServerUniqueId(ServerUID) && CachedAuthInt.GetServerAddr(ServerIP, ServerPort) && OnlineSub.PlayerInterface.GetUniquePlayerId(0, HostUID); } if (bGotHostInfo) { // Search for an existing listen host auth session first foreach CachedAuthInt.AllClientAuthSessions(CurClientSession) { if (CurClientSession.EndPointUID == HostUID && CurClientSession.EndPointIP == ServerIP) { ListenSession = CurClientSession; bFound = true; break; } } // If there is not an existing session, kick one off if (!bFound || bRetry) { `log("Kicking off listen auth session"); if (OnlineSub.GameInterface != none) { GameSettings = OnlineSub.GameInterface.GetGameSettings(WorldInfo.Game.PlayerReplicationInfoClass.default.SessionName); } if (GameSettings != none) { bSecure = GameSettings.bAntiCheatProtected; } // Kickoff authentication if (CachedAuthInt.CreateClientAuthSession(ServerUID, ServerIP, ServerPort, bSecure, ListenAuthTicketUID)) { // Give the auth interface a moment to setup the auth session, before verifying SetTimer(1.0, false, nameof(ContinueListenHostAuth)); } SetTimer(AuthRetryDelay, false, nameof(ListenHostAuthTimeout)); } // If there is an existing session, do nothing if already authenticated, or enable timeout if not else if (ListenSession.AuthStatus != AUS_Authenticated && !IsTimerActive('ListenHostAuthTimeout')) { `log("BeginListenHostAuth was called when there is already a listen auth session, but the timeout is not active"); SetTimer(AuthRetryDelay, false, nameof(ListenHostAuthTimeout)); } } else { `log("Failed to kickoff listen host authentication"); // Go straight to failed auth OnlineSub.PlayerInterface.GetUniquePlayerId(0, HostUID); OnClientAuthComplete(false, HostUID, None, "Failed to kickoff listen host authentication"); } } /** * After listen host authentication kicks off, this is called after a short delay, to continue authentication */ function ContinueListenHostAuth() { local bool bGotHostInfo; local UniqueNetId HostUID; //@HSL_BEGIN_XBOX local IpAddr ServerIP; local int ServerPort; //@HSL_END_XBOX if (OnlineSub.PlayerInterface != none) { bGotHostInfo = OnlineSub.PlayerInterface.GetUniquePlayerId(0, HostUID) && CachedAuthInt.GetServerAddr(ServerIP, ServerPort); } if (!bGotHostInfo || !CachedAuthInt.VerifyClientAuthSession(HostUID, ServerIP, ServerPort, ListenAuthTicketUID)) { `log("VerifyClientAuthSession failed for listen host"); OnlineSub.PlayerInterface.GetUniquePlayerId(0, HostUID); OnClientAuthComplete(false, HostUID, None, "VerifyClientAuthSession failed for listen host"); } } /** * Ends any active listen host auth sessions */ function EndListenHostAuth() { local bool bGotHostInfo; local UniqueNetId ServerUID, HostUID; //@HSL_BEGIN_XBOX local IpAddr ServerIP; local int ServerPort; //@HSL_END_XBOX if (OnlineSub.PlayerInterface != none) { bGotHostInfo = CachedAuthInt.GetServerUniqueId(ServerUID) && CachedAuthInt.GetServerAddr(ServerIP, ServerPort) && OnlineSub.PlayerInterface.GetUniquePlayerId(0, HostUID); } if (bGotHostInfo) { CachedAuthInt.EndLocalClientAuthSession(ServerUID, ServerIP, ServerPort); CachedAuthInt.EndRemoteClientAuthSession(HostUID, ServerIP); } else { `log("Failed to end listen host auth session"); } } /** * Triggered upon listen host authentication failure, or timeout */ function ListenHostAuthTimeout() { local UniqueNetId HostUID; ClearTimer('ListenHostAuthTimeout'); ClearTimer('ContinueListenHostAuth'); if (ListenAuthRetryCount < MaxAuthRetryCount) { ListenAuthRetryCount++; // Retry auth again BeginListenHostAuth(true); } else { `log("Listen host authentication failed after"@MaxAuthRetryCount@"attempts"); OnlineSub.PlayerInterface.GetUniquePlayerId(0, HostUID); OnClientAuthComplete(false, HostUID, None, "VerifyClientAuthSession failed for listen host"); EndListenHostAuth(); } } /** * Client disconnect cleanup */ /** * Called on the server when a clients net connection is closing (so auth sessions can be ended) * * @param ClientConnection The client NetConnection that is closing */ function OnClientConnectionClose(Player ClientConnection) { local int i; if (ClientConnection != none) { // End the auth session for the exiting client (done in the static function to keep it in one place) StaticOnClientConnectionClose(ClientConnection); // Remove from tracking for (i=0; i