1688 lines
47 KiB
Ucode
1688 lines
47 KiB
Ucode
//=============================================================================
|
|
// 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<string> IPPolicies;
|
|
|
|
/** Contains the list of banned UIDs */
|
|
var globalconfig array<UniqueNetID> 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<Admin> 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<PendingClientAuth> 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<ServerAuthRetry> 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<WorldInfo.GRI.PRIArray.Length;i++)
|
|
{
|
|
if ( String(WorldInfo.GRI.PRIArray[i].PlayerID) == Target )
|
|
{
|
|
FinalC = Controller(WorldInfo.GRI.PRIArray[i].Owner);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return FinalC;
|
|
}
|
|
|
|
function Kick( string Target )
|
|
{
|
|
local Controller C;
|
|
|
|
C = GetControllerFromString(Target);
|
|
if ( C != none && C.PlayerReplicationInfo != None )
|
|
{
|
|
if (PlayerController(C) != None)
|
|
{
|
|
KickPlayer(PlayerController(C), DefaultKickReason);
|
|
}
|
|
else if (C.PlayerReplicationInfo != None)
|
|
{
|
|
if (C.Pawn != None)
|
|
{
|
|
C.Pawn.Destroy();
|
|
}
|
|
if (C != None)
|
|
{
|
|
C.Destroy();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function KickBan( string Target )
|
|
{
|
|
local PlayerController P;
|
|
local string IP;
|
|
|
|
P = PlayerController( GetControllerFromString(Target) );
|
|
if ( NetConnection(P.Player) != None )
|
|
{
|
|
if (!WorldInfo.IsConsoleBuild())
|
|
{
|
|
IP = P.GetPlayerNetworkAddress();
|
|
if( CheckIPPolicy(IP) )
|
|
{
|
|
IP = Left(IP, InStr(IP, ":"));
|
|
`Log("Adding IP Ban for: "$IP);
|
|
IPPolicies[IPPolicies.length] = "DENY," $ IP;
|
|
SaveConfig();
|
|
}
|
|
}
|
|
|
|
if ( P.PlayerReplicationInfo.UniqueId != P.PlayerReplicationInfo.default.UniqueId &&
|
|
!IsIDBanned(P.PlayerReplicationInfo.UniqueID) )
|
|
{
|
|
BannedIDs.AddItem(P.PlayerReplicationInfo.UniqueId);
|
|
SaveConfig();
|
|
}
|
|
KickPlayer(P, DefaultKickReason);
|
|
return;
|
|
}
|
|
}
|
|
|
|
function bool ForceKickPlayer(PlayerController C, string KickReason)
|
|
{
|
|
if (C != None && NetConnection(C.Player)!=None )
|
|
{
|
|
if (C.Pawn != None)
|
|
{
|
|
C.Pawn.Suicide();
|
|
}
|
|
C.ClientWasKicked();
|
|
if (C != None)
|
|
{
|
|
C.Destroy();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function bool KickPlayer(PlayerController C, string KickReason)
|
|
{
|
|
// Do not kick logged admins
|
|
if (C != None && !IsAdmin(C) && NetConnection(C.Player)!=None )
|
|
{
|
|
return ForceKickPlayer(C, KickReason);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function bool AdminLogin( PlayerController P, string Password )
|
|
{
|
|
if (AdminPassword == "")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Password == AdminPassword)
|
|
{
|
|
P.PlayerReplicationInfo.bAdmin = true;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function bool AdminLogout(PlayerController P)
|
|
{
|
|
if (P.PlayerReplicationInfo.bAdmin)
|
|
{
|
|
P.PlayerReplicationInfo.bAdmin = false;
|
|
P.bGodMode = false;
|
|
P.Suicide();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function AdminEntered( PlayerController P )
|
|
{
|
|
local string LoginString;
|
|
|
|
LoginString = P.PlayerReplicationInfo.PlayerName@"logged in as a server administrator.";
|
|
|
|
`log(LoginString);
|
|
WorldInfo.Game.Broadcast( P, LoginString );
|
|
}
|
|
function AdminExited( PlayerController P )
|
|
{
|
|
local string LogoutString;
|
|
|
|
LogoutString = P.PlayerReplicationInfo.PlayerName$"is no longer logged in as a server administrator.";
|
|
|
|
`log(LogoutString);
|
|
WorldInfo.Game.Broadcast( P, LogoutString );
|
|
}
|
|
|
|
/**
|
|
* Parses the specified string for admin auto-login options
|
|
*
|
|
* @param Options a string containing key/pair options from the URL (?key=value,?key=value)
|
|
*
|
|
* @return TRUE if the options contained name and password which were valid for admin login.
|
|
*/
|
|
function bool ParseAdminOptions( string Options )
|
|
{
|
|
local string InAdminName, InPassword;
|
|
|
|
InPassword = class'GameInfo'.static.ParseOption( Options, "Password" );
|
|
InAdminName= class'GameInfo'.static.ParseOption( Options, "AdminName" );
|
|
|
|
return ValidLogin(InAdminName, InPassword);
|
|
}
|
|
|
|
/**
|
|
* @return TRUE if the specified username + password match the admin username/password
|
|
*/
|
|
function bool ValidLogin(string UserName, string Password)
|
|
{
|
|
return (AdminPassword != "" && Password==AdminPassword);
|
|
}
|
|
|
|
function bool CheckIPPolicy(string Address)
|
|
{
|
|
local int i, j;
|
|
`if(`notdefined(FINAL_RELEASE))
|
|
local int LastMatchingPolicy;
|
|
`endif
|
|
local string Policy, Mask;
|
|
local bool bAcceptAddress, bAcceptPolicy;
|
|
|
|
// strip port number
|
|
j = InStr(Address, ":");
|
|
if(j != -1)
|
|
Address = Left(Address, j);
|
|
|
|
bAcceptAddress = True;
|
|
for(i=0; i<IPPolicies.Length; i++)
|
|
{
|
|
j = InStr(IPPolicies[i], ",");
|
|
if(j==-1)
|
|
continue;
|
|
Policy = Left(IPPolicies[i], j);
|
|
Mask = Mid(IPPolicies[i], j+1);
|
|
if(Policy ~= "ACCEPT")
|
|
bAcceptPolicy = True;
|
|
else if(Policy ~= "DENY")
|
|
bAcceptPolicy = False;
|
|
else
|
|
continue;
|
|
|
|
j = InStr(Mask, "*");
|
|
if(j != -1)
|
|
{
|
|
if(Left(Mask, j) == Left(Address, j))
|
|
{
|
|
bAcceptAddress = bAcceptPolicy;
|
|
`if(`notdefined(FINAL_RELEASE))
|
|
LastMatchingPolicy = i;
|
|
`endif
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(Mask == Address)
|
|
{
|
|
bAcceptAddress = bAcceptPolicy;
|
|
`if(`notdefined(FINAL_RELEASE))
|
|
LastMatchingPolicy = i;
|
|
`endif
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!bAcceptAddress)
|
|
{
|
|
`Log("Denied connection for "$Address$" with IP policy "$IPPolicies[LastMatchingPolicy]);
|
|
}
|
|
|
|
return bAcceptAddress;
|
|
}
|
|
|
|
function bool IsIDBanned(const out UniqueNetID NetID)
|
|
{
|
|
local int i;
|
|
|
|
for (i = 0; i < BannedIDs.length; i++)
|
|
{
|
|
if (BannedIDs[i] == NetID)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Client authentication (and PreLogin handling)
|
|
*/
|
|
|
|
/**
|
|
* Initialized auth interface hooks
|
|
*/
|
|
function InitAuthHooks()
|
|
{
|
|
local OnlineGameSettings GameSettings;
|
|
local bool bIsLanMatch;
|
|
|
|
if (OnlineSub != None)
|
|
{
|
|
CachedAuthInt = OnlineSub.AuthInterface;
|
|
if (OnlineSub.GameInterface != none)
|
|
{
|
|
GameSettings = OnlineSub.GameInterface.GetGameSettings(WorldInfo.Game.PlayerReplicationInfoClass.default.SessionName);
|
|
}
|
|
|
|
// If 'bIsLanMatch' is set, do not enable any authentication
|
|
if ((WorldInfo.NetMode == NM_DedicatedServer || WorldInfo.NetMode == NM_ListenServer) && GameSettings != none &&
|
|
GameSettings.bIsLanMatch)
|
|
{
|
|
if (bAuthenticateClients)
|
|
{
|
|
`log("Disabling all authentication, due to bIsLanMatch being set to true");
|
|
}
|
|
|
|
bIsLanMatch = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bIsLanMatch = true;
|
|
}
|
|
|
|
if (!bIsLanMatch && bAuthenticateClients)
|
|
{
|
|
RegisterAuthDelegates();
|
|
}
|
|
}
|
|
|
|
function RegisterAuthDelegates()
|
|
{
|
|
if (CachedAuthInt != None)
|
|
{
|
|
CachedAuthInt.AddAuthReadyDelegate(OnAuthReady);
|
|
CachedAuthInt.AddServerAuthRequestDelegate(ProcessServerAuthRequest);
|
|
CachedAuthInt.AddClientAuthResponseDelegate(ProcessClientAuthResponse);
|
|
CachedAuthInt.AddClientAuthCompleteDelegate(OnClientAuthComplete);
|
|
CachedAuthInt.AddClientConnectionCloseDelegate(OnClientConnectionClose);
|
|
CachedAuthInt.AddServerAuthRetryRequestDelegate(ProcessServerAuthRetryRequest);
|
|
|
|
CachedAuthInt.ClearClientConnectionCloseDelegate(Class'AccessControl'.static.StaticOnClientConnectionClose);
|
|
|
|
if (OnlineSub.GameInterface != None)
|
|
{
|
|
OnlineSub.GameInterface.AddDestroyOnlineGameCompleteDelegate(OnDestroyOnlineGameComplete);
|
|
}
|
|
|
|
bAuthDelegatesRegistered = true;
|
|
}
|
|
else
|
|
{
|
|
// Don't display this message for OnlineSubsystemSteamworks
|
|
if (OnlineSub.Class.Name != 'OnlineSubsystemSteamworks')
|
|
{
|
|
`log("AccessControl: Trying to register authentication delegates with an online subsystem that does not support authentication");
|
|
}
|
|
|
|
bAuthDelegatesRegistered = false;
|
|
}
|
|
}
|
|
|
|
function ClearAuthDelegates(bool bExiting)
|
|
{
|
|
if (CachedAuthInt != None)
|
|
{
|
|
CachedAuthInt.ClearAuthReadyDelegate(OnAuthReady);
|
|
CachedAuthInt.ClearServerAuthRequestDelegate(ProcessServerAuthRequest);
|
|
CachedAuthInt.ClearClientAuthResponseDelegate(ProcessClientAuthResponse);
|
|
CachedAuthInt.ClearClientAuthCompleteDelegate(OnClientAuthComplete);
|
|
CachedAuthInt.ClearClientConnectionCloseDelegate(OnClientConnectionClose);
|
|
CachedAuthInt.ClearServerAuthRetryRequestDelegate(ProcessServerAuthRetryRequest);
|
|
|
|
// OnClientConnectionClose must still be handled, even if the AccessControl does not exist during non-seamless travel
|
|
if (!bExiting)
|
|
{
|
|
CachedAuthInt.AddClientConnectionCloseDelegate(class'AccessControl'.static.StaticOnClientConnectionClose);
|
|
}
|
|
`if(`__TW_NETWORKING_)
|
|
// fix for crash when exiting the game on a listen server [aladenberger 9/6/2013]
|
|
// https://udn.unrealengine.com/questions/173743/assert-on-staticonclientconnectionclose.html
|
|
else
|
|
{
|
|
CachedAuthInt.ClearClientConnectionCloseDelegate(class'AccessControl'.static.StaticOnClientConnectionClose);
|
|
}
|
|
`endif
|
|
|
|
if (OnlineSub != None && OnlineSub.GameInterface != None)
|
|
{
|
|
OnlineSub.GameInterface.ClearDestroyOnlineGameCompleteDelegate(OnDestroyOnlineGameComplete);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
`log("AccessControl: Trying to clear authentication delegates with an online subsystem that does not support authentication");
|
|
}
|
|
|
|
bAuthDelegatesRegistered = false;
|
|
}
|
|
|
|
/**
|
|
* Accept or reject a joining player on the server; fails login if OutError is set to a non-empty string
|
|
* NOTE: UniqueId requires authentication before it can be trusted
|
|
*
|
|
* @param Options URL options the player used when connecting
|
|
* @param Address The IP address of the player
|
|
* @param UniqueId The UID of the player (requires authentication before it can be trusted)
|
|
* @param bSupportsAuth whether or not the client supports authentication (i.e. has an AuthInterface set)
|
|
* @param OutError If the player fails any checks in this function, set this to a non-empty value to reject the player
|
|
* @param bSpectator whether or not the player is trying to join as a spectator
|
|
*/
|
|
event PreLogin(string Options, string Address, const UniqueNetId UniqueId, bool bSupportsAuth, out string OutError, bool bSpectator)
|
|
{
|
|
local string InPassword;
|
|
local int i, CurPort, LingeringPort;
|
|
//@HSL_BEGIN_XBOX
|
|
local IpAddr CurIP, ClientIP;
|
|
//@HSL_END_XBOX
|
|
local bool bFound, bSuccess, bHasPrivateServerOption;
|
|
local UniqueNetId NullId, HostUID;
|
|
local Player ClientConn, CurConn;
|
|
local AuthSession CurClientSession;
|
|
local OnlineGameSettings GameSettings;
|
|
local GameEngine Engine;
|
|
|
|
OutError="";
|
|
InPassword = WorldInfo.Game.ParseOption(Options, "Password");
|
|
|
|
Engine = GameEngine(class'Engine'.static.GetEngine());
|
|
`if (`__TW_)
|
|
// BWJ - This option exists only for dedicated server on consoles
|
|
if (WorldInfo.IsConsoleDedicatedServer() )
|
|
{
|
|
bHasPrivateServerOption = WorldInfo.Game.HasOption( Options, "bJoinViaInvite" );
|
|
}
|
|
else
|
|
{
|
|
bHasPrivateServerOption = WorldInfo.Game.HasOption(Options, "friend") || InPassword != "";
|
|
}
|
|
`endif
|
|
|
|
// Check server capacity and passwords
|
|
`if (`__TW_NETWORKING_)
|
|
if (WorldInfo.NetMode != NM_Standalone && WorldInfo.Game.AtCapacity(bSpectator, UniqueId))
|
|
`else
|
|
if (WorldInfo.NetMode != NM_Standalone && WorldInfo.Game.AtCapacity(bSpectator))
|
|
`endif
|
|
{
|
|
`if(`__TW_NETWORKING_)
|
|
OutError = "<Strings:"$PathName(WorldInfo.Game.GameMessageClass)$".MaxedOutMessage>";
|
|
`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 = "<Strings:"$(InPassword == "") ? "Engine.AccessControl.NeedPassword>" : "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 = "<Strings:Engine.AccessControl.IPBanned>";
|
|
`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 = "<Strings:Engine.Errors.SteamClientRequired>";
|
|
`else
|
|
OutError = "Engine.Errors.SteamClientRequired";
|
|
`endif
|
|
}
|
|
else
|
|
{
|
|
`if(`__TW_)
|
|
OutError = "<Strings:KFEngine.Errors.RequiresAuthentication>";
|
|
`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<ClientsPendingAuth.Length; i++)
|
|
{
|
|
if (ClientsPendingAuth[i].ClientUID == UniqueId)
|
|
{
|
|
bFound = True;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bFound)
|
|
{
|
|
foreach CachedAuthInt.AllClientAuthSessions(CurClientSession)
|
|
{
|
|
if (CurClientSession.EndPointUID == UniqueId && CurClientSession.EndPointIP != ClientIP)
|
|
{
|
|
bFound = True;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Make sure the player is not trying to join with a listen hosts UID
|
|
if (WorldInfo.NetMode == NM_ListenServer && OnlineSub.PlayerInterface != none &&
|
|
OnlineSub.PlayerInterface.GetUniquePlayerId(0, HostUID) && UniqueId == HostUID)
|
|
{
|
|
bFound = True;
|
|
}
|
|
|
|
|
|
// If the UID is not already present on server, and is not otherwise invalid, begin authentication
|
|
if (!bFound && UniqueId != NullId)
|
|
{
|
|
// Begin authentication, and if it kicks off successfully, start tracking the auth progress
|
|
if (CachedAuthInt.IsReady())
|
|
{
|
|
bSuccess = CachedAuthInt.SendClientAuthRequest(ClientConn, UniqueId);
|
|
|
|
if (bSuccess && !IsTimerActive('PendingAuthTimer'))
|
|
{
|
|
SetTimer(3.0, True, nameof(PendingAuthTimer));
|
|
}
|
|
}
|
|
// If the auth interface is not ready, add an entry anyway, and kick off auth later when it is ready
|
|
else
|
|
{
|
|
bSuccess = True;
|
|
}
|
|
|
|
if (bSuccess)
|
|
{
|
|
i = ClientsPendingAuth.Length;
|
|
ClientsPendingAuth.Length = i+1;
|
|
|
|
ClientsPendingAuth[i].ClientConnection = ClientConn;
|
|
ClientsPendingAuth[i].ClientUID = UniqueId;
|
|
ClientsPendingAuth[i].AuthTimestamp = WorldInfo.RealTimeSeconds;
|
|
}
|
|
else
|
|
{
|
|
`if(`__TW_)
|
|
OutError = "<Strings:KFEngine.Errors.FailedKickOff>";
|
|
`else
|
|
OutError = "Failed to kickoff authentication";
|
|
`endif
|
|
}
|
|
}
|
|
// Reject the client if the current UID is already being authenticated
|
|
else if (bFound)
|
|
{
|
|
`if(`__TW_)
|
|
OutError = "<Strings:KFEngine.Errors.DuplicateUID>";
|
|
`else
|
|
OutError = "Duplicate UID";
|
|
`endif
|
|
}
|
|
|
|
// Reject the client straight away if their UID is null
|
|
else
|
|
{
|
|
`if(`__TW_)
|
|
OutError = "<Strings:KFEngine.Errors.InvalidUID>";
|
|
`else
|
|
OutError = "Invalid UID";
|
|
`endif
|
|
}
|
|
}
|
|
else if (OutError == "")
|
|
{
|
|
`if(`__TW_)
|
|
OutError = "<Strings:KFEngine.Errors.FailedKickOff>";
|
|
`else
|
|
OutError = "Failed to kickoff authentication";
|
|
`endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Triggered after a player has successfully joined the game (post-auth for remote clients, pre-auth for listen host);
|
|
* used to kickoff authentication of listen hosts
|
|
*
|
|
* @param NewPlayer The newly logged in player
|
|
*/
|
|
function PostLogin(PlayerController NewPlayer)
|
|
{
|
|
if (LocalPlayer(NewPlayer.Player) != none && bAuthDelegatesRegistered && bAuthenticateListenHost &&
|
|
WorldInfo.NetMode == NM_ListenServer && CachedAuthInt != none)
|
|
{
|
|
if (CachedAuthInt.IsReady())
|
|
{
|
|
BeginListenHostAuth();
|
|
}
|
|
else
|
|
{
|
|
bPendingListenAuth = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called once every 3 seconds, to see if any auth attempts have timed out
|
|
*/
|
|
function PendingAuthTimer()
|
|
{
|
|
local int i, OldLength;
|
|
local bool bFailed;
|
|
local AuthSession CurClientSession;
|
|
|
|
for (i=0; i<ClientsPendingAuth.Length; ++i)
|
|
{
|
|
// Remove any connections that have become invalid
|
|
if (ClientsPendingAuth[i].ClientConnection == none)
|
|
{
|
|
ClientsPendingAuth.Remove(i, 1);
|
|
i--;
|
|
}
|
|
// Need to detect level change messing up timestamps (and reset it)
|
|
else if (WorldInfo.RealTimeSeconds < ClientsPendingAuth[i].AuthTimestamp)
|
|
{
|
|
ClientsPendingAuth[i].AuthTimestamp = WorldInfo.RealTimeSeconds;
|
|
}
|
|
// Handle timeouts and retries
|
|
else if (WorldInfo.RealTimeSeconds - ClientsPendingAuth[i].AuthTimestamp >= 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<ClientsPendingAuth.Length; ++i)
|
|
{
|
|
// Remove invalid entries
|
|
if (ClientsPendingAuth[i].ClientConnection == none)
|
|
{
|
|
ClientsPendingAuth.Remove(i, 1);
|
|
i--;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (CachedAuthInt.SendClientAuthRequest(ClientsPendingAuth[i].ClientConnection, ClientsPendingAuth[i].ClientUID))
|
|
{
|
|
ClientsPendingAuth[i].AuthTimestamp = WorldInfo.RealTimeSeconds;
|
|
}
|
|
else
|
|
{
|
|
OldLength = ClientsPendingAuth.Length;
|
|
|
|
// Kick the client
|
|
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--;
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// If any clients are now pending auth, activate the pending auth timer
|
|
if (ClientsPendingAuth.Length > 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; i<ClientsPendingAuth.Length; i++)
|
|
{
|
|
if (ClientsPendingAuth[i].ClientUID == ClientUID)
|
|
{
|
|
PendingIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (PendingIdx != INDEX_None)
|
|
{
|
|
// Now that the client has sent auth data required for verification, finish verifying the client
|
|
bSuccess = CachedAuthInt.VerifyClientAuthSession(ClientUID, ClientIP, 0, AuthTicketUID);
|
|
|
|
// If auth verification failed to kickoff successfully, kick the client
|
|
if (!bSuccess)
|
|
{
|
|
OldLength = ClientsPendingAuth.Length;
|
|
|
|
// Kick the client
|
|
WorldInfo.Game.RejectLogin(ClientsPendingAuth[i].ClientConnection, "Authentication failed");
|
|
|
|
// If OnClientConnectionClose did not alter ClientsPendingAuth, remove the tracking entry here
|
|
if (OldLength == ClientsPendingAuth.Length)
|
|
{
|
|
ClientsPendingAuth.Remove(PendingIdx, 1);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
`log("AccessControl::ProcessClientAuthResponse: Received unexpected auth ticket from client",, 'DevOnline');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called on the server, when the authentication result for a client auth session has returned
|
|
* NOTE: This is the first place where a clients UID is verified as valid
|
|
*
|
|
* @param bSuccess whether or not authentication was successful
|
|
* @param ClientUID The UID of the client
|
|
* @param ClientConnection The connection associated with the client (for retrieving auth session data)
|
|
* @param ExtraInfo Extra information about authentication, e.g. failure reasons
|
|
*/
|
|
function OnClientAuthComplete(bool bSuccess, UniqueNetId ClientUID, Player ClientConnection, string ExtraInfo)
|
|
{
|
|
local UniqueNetId HostUID;
|
|
local int i, PendingLen, PendingIdx;
|
|
local PlayerController P;
|
|
local PlayerReplicationInfo PRI;
|
|
local bool bResumeLogin;
|
|
local AuthSession CurClientSession;
|
|
|
|
// Check if the auth result was for the listen host
|
|
if (WorldInfo.NetMode == NM_ListenServer && OnlineSub.PlayerInterface != none &&
|
|
OnlineSub.PlayerInterface.GetUniquePlayerId(0, HostUID) && HostUID == ClientUID)
|
|
{
|
|
if (bSuccess)
|
|
{
|
|
`log("Listen host successfully authenticated");
|
|
|
|
ClearTimer('ListenHostAuthTimeout');
|
|
ClearTimer('ContinueListenHostAuth');
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Check that we are expecting an auth result for this client
|
|
PendingLen = ClientsPendingAuth.Length;
|
|
PendingIdx = INDEX_None;
|
|
|
|
for (i=0; i<PendingLen; i++)
|
|
{
|
|
if ((ClientConnection != none && ClientsPendingAuth[i].ClientConnection == ClientConnection) ||
|
|
(ClientConnection == none && ClientsPendingAuth[i].ClientUID == ClientUID))
|
|
{
|
|
PendingIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (PendingIdx != INDEX_None)
|
|
{
|
|
if (ClientConnection != none)
|
|
{
|
|
if (bSuccess)
|
|
{
|
|
foreach WorldInfo.AllControllers(Class'PlayerController', P)
|
|
{
|
|
if (P.Player == ClientConnection)
|
|
{
|
|
PRI = P.PlayerReplicationInfo;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (PRI != none)
|
|
{
|
|
// If the code is setup to >not< 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<ServerAuthRetries.Length; ++i)
|
|
{
|
|
if (ServerAuthRetries[i].ClientUID == ClientUID)
|
|
{
|
|
CurRetryIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (CurRetryIdx == INDEX_None)
|
|
{
|
|
CurRetryIdx = ServerAuthRetries.Length;
|
|
ServerAuthRetries.Length = CurRetryIdx + 1;
|
|
|
|
ServerAuthRetries[CurRetryIdx].ClientUId = ClientUID;
|
|
}
|
|
|
|
|
|
// Only attempt server auth retry, if the retry count has not been exceeded
|
|
if (ServerAuthRetries[CurRetryIdx].AuthRetryCount < MaxAuthRetryCount)
|
|
{
|
|
// End the current server auth session
|
|
foreach CachedAuthInt.AllLocalServerAuthSessions(CurServerSession)
|
|
{
|
|
if (CurServerSession.EndPointUID == ClientUID)
|
|
{
|
|
CachedAuthInt.EndLocalServerAuthSession(ClientUID, ClientIP);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Kick off a new server auth session
|
|
ProcessServerAuthRequest(ClientConnection, ClientUID, ClientIP, ClientPort);
|
|
|
|
// Update the retry count
|
|
ServerAuthRetries[CurRetryIdx].AuthRetryCount++;
|
|
}
|
|
// Kick the client, if they spam retry requests
|
|
else if (ServerAuthRetries[CurRetryIdx].AuthRetryCount > 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<ClientsPendingAuth.Length; ++i)
|
|
{
|
|
if (ClientConnection != none && ClientsPendingAuth[i].ClientConnection == ClientConnection)
|
|
{
|
|
ClientsPendingAuth.Remove(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* It is extremely important that client disconnects are detected, even when an AccessControl does not exist;
|
|
* otherwise, clients may be kept in an active auth session, even though they should not be (Steam in particular, is picky about this).
|
|
*
|
|
* When the AccessControl is cleaning up before server travel, it adds this static function as a delegate, until after server travel;
|
|
* ensuring disconnects are always handled
|
|
*
|
|
* @param ClientConnection The client NetConnection that is closing
|
|
*/
|
|
static final function StaticOnClientConnectionClose(Player ClientConnection)
|
|
{
|
|
local OnlineSubsystem CurOnlineSub;
|
|
local OnlineAuthInterface CurAuthInt;
|
|
local int i;
|
|
local WorldInfo WI;
|
|
local AuthSession CurClientSession;
|
|
local LocalAuthSession CurServerSession;
|
|
|
|
CurOnlineSub = Class'GameEngine'.static.GetOnlineSubsystem();
|
|
|
|
if (CurOnlineSub != none)
|
|
{
|
|
CurAuthInt = CurOnlineSub.AuthInterface;
|
|
}
|
|
|
|
if (CurAuthInt != none && ClientConnection != none)
|
|
{
|
|
// If the client is authenticated, end the client auth session
|
|
if (CurAuthInt.FindClientAuthSession(ClientConnection, CurClientSession) && CurClientSession.AuthStatus == AUS_Authenticated)
|
|
{
|
|
CurAuthInt.EndRemoteClientAuthSession(CurClientSession.EndPointUID, CurClientSession.EndPointIP);
|
|
}
|
|
|
|
// End any local server session
|
|
if (CurAuthInt.FindLocalServerAuthSession(ClientConnection, CurServerSession))
|
|
{
|
|
CurAuthInt.EndLocalServerAuthSession(CurServerSession.EndPointUID, CurServerSession.EndPointIP);
|
|
|
|
// Remove any 'ServerAuthRetries' entry
|
|
WI = Class'WorldInfo'.static.GetWorldInfo();
|
|
|
|
if (WI != none && WI.Game != none && WI.Game.AccessControl != none)
|
|
{
|
|
for (i=0; i<WI.Game.AccessControl.ServerAuthRetries.Length; ++i)
|
|
{
|
|
if (WI.Game.AccessControl.ServerAuthRetries[i].ClientUID == CurServerSession.EndPointUID)
|
|
{
|
|
WI.Game.AccessControl.ServerAuthRetries.Remove(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Exit/mapchange cleanup
|
|
*/
|
|
|
|
/**
|
|
* Triggered when the current online game has ended; used to end auth sessions
|
|
* NOTE: Delegate cleanup does not happen here
|
|
*/
|
|
function OnDestroyOnlineGameComplete(name SessionName, bool bWasSuccessful)
|
|
{
|
|
//@HSL_BEGIN_XBOX
|
|
local IpAddr CurIP;
|
|
local int CurPort;
|
|
//@HSL_END_XBOX
|
|
local Player ClientConn, CurConn;
|
|
local AuthSession CurClientSession;
|
|
|
|
// End listen host auth
|
|
if (WorldInfo.NetMode == NM_ListenServer)
|
|
{
|
|
EndListenHostAuth();
|
|
}
|
|
|
|
|
|
// End auth for all connected clients
|
|
foreach CachedAuthInt.AllClientAuthSessions(CurClientSession)
|
|
{
|
|
if (CurClientSession.AuthStatus == AUS_Authenticated)
|
|
{
|
|
// End the client auth session
|
|
CachedAuthInt.EndRemoteClientAuthSession(CurClientSession.EndPointUID, CurClientSession.EndPointIP);
|
|
|
|
// Tell the client to end the auth session their end
|
|
foreach WorldInfo.AllClientConnections(CurConn, CurIP, CurPort)
|
|
{
|
|
if (CurIP == CurClientSession.EndPointIP && CurPort == CurClientSession.EndPointPort)
|
|
{
|
|
ClientConn = CurConn;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ClientConn != none)
|
|
{
|
|
if (!CachedAuthInt.SendClientAuthEndSessionRequest(ClientConn))
|
|
{
|
|
`log("Failed to send client kill auth request");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
`log("WARNING!!! Came across client auth session with no matching NetConnection");
|
|
}
|
|
}
|
|
}
|
|
|
|
// End all local server auth sessions
|
|
CachedAuthInt.EndAllLocalServerAuthSessions();
|
|
|
|
// Clear the 'ServerAuthRetries' lists
|
|
ServerAuthRetries.Length = 0;
|
|
}
|
|
|
|
/**
|
|
* Called by GameInfo when servertravel begins, to allow for online subsystem cleanup
|
|
* NOTE: Worth keeping, in addition to NotifyGameEnding, as it is triggered earlier and can check for seamless travel
|
|
*
|
|
* @param bSeamless whether or not travel is seamless
|
|
*/
|
|
function NotifyServerTravel(bool bSeamless)
|
|
{
|
|
if (!bSeamless)
|
|
{
|
|
Cleanup();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called by GameInfo when the game is ending (either through exit or non-seamless travel), to allow for online subsystem cleanup
|
|
*/
|
|
function NotifyGameEnding()
|
|
{
|
|
local GameEngine Engine;
|
|
|
|
Engine = GameEngine(Class'Engine'.static.GetEngine());
|
|
|
|
// If the server is just switching level, do a normal cleanup
|
|
// @todo JohnB: This way of distinguishing travel from exit is quite hacky (but works and is necessary); implement a better solution
|
|
if (WorldInfo.NextURL != "" || Engine.TravelURL != "")
|
|
{
|
|
Cleanup();
|
|
}
|
|
// Otherwise, the game is exiting and NotifyExit may need to do special handling
|
|
else
|
|
{
|
|
NotifyExit();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called by GameInfo when the game is exiting (PreExit), to allow for online subsystem cleanup
|
|
*/
|
|
function NotifyExit()
|
|
{
|
|
Cleanup(true);
|
|
}
|
|
|
|
/**
|
|
* Cleanup any online subsystem references
|
|
*/
|
|
function Cleanup(optional bool bExit)
|
|
{
|
|
if (CachedAuthInt != none)
|
|
{
|
|
ClearAuthDelegates(bExit);
|
|
|
|
// If the game is exiting, end all auth sessions
|
|
if (bExit)
|
|
{
|
|
// End all remote client auth sessions
|
|
CachedAuthInt.EndAllRemoteClientAuthSessions();
|
|
|
|
// End all local server auth sessions
|
|
CachedAuthInt.EndAllLocalServerAuthSessions();
|
|
|
|
// Clear the 'ServerAuthRetries' list
|
|
ServerAuthRetries.Length = 0;
|
|
}
|
|
}
|
|
|
|
CachedAuthInt = None;
|
|
OnlineSub = None;
|
|
}
|
|
|
|
|
|
/**
|
|
* Helper functions
|
|
*/
|
|
|
|
/**
|
|
* Whether or not the specified player UID is awaiting authentication
|
|
*
|
|
* @param PlayerUID The UID of the player
|
|
* @return Returns True if the UID is awaiting authentication, False otherwise
|
|
*/
|
|
function bool IsPendingAuth(UniqueNetId PlayerUID)
|
|
{
|
|
local int i;
|
|
|
|
for (i=0; i<ClientsPendingAuth.Length; ++i)
|
|
{
|
|
if (ClientsPendingAuth[i].ClientUID == PlayerUID)
|
|
{
|
|
return True;
|
|
}
|
|
}
|
|
|
|
return False;
|
|
}
|
|
|
|
|
|
defaultproperties
|
|
{
|
|
AdminClass=class'Engine.Admin'
|
|
bAlwaysTick=True
|
|
}
|