2025-09-11 16:48:23 -07:00
// Tribes 2 Unofficial Authentication System
// http://www.tribesnext.com/
2026-03-09 12:38:40 -07:00
// Written by Electricutioner/Thyth
2025-09-11 16:48:23 -07:00
// Copyright 2008 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative
2026-03-09 12:38:40 -07:00
// Version 1.2: 2009-02-16
2025-09-11 16:48:23 -07:00
// Clan/Rename Certificate support is included in this version.
2026-03-09 12:38:40 -07:00
// initialize the SHA1 digester in Ruby
function t2csri_initDigester ( )
{
$ SHA1 : : Initialized = 1 ;
rubyEval ( "$sha1hasher = SHA1Pure.new" ) ;
}
// use Ruby to get the SHA1 hash of the string
function sha1sum ( % string )
{
if ( ! $ SHA1 : : Initialized )
t2csri_initDigester ( ) ;
% string = strReplace ( % string , "'" , "\\'" ) ;
rubyEval ( "$sha1hasher.prepare" ) ;
rubyEval ( "$sha1hasher.append('" @ % string @ "')" ) ;
rubyEval ( "tsEval '$temp=\"' + $sha1hasher.hexdigest + '\";'" ) ;
% temp = $ temp ;
$ temp = "" ;
return % temp ;
}
// verify with the auth server's RSA public key... hard coded in the executable
function t2csri_verify_auth_signature ( % sig )
{
rubyEval ( "tsEval '$temp=\"' + t2csri_verify_auth_signature('" @ % sig @ "').to_s(16) + '\";'" ) ;
while ( strLen ( $ temp ) < 40 )
$ temp = "0" @ $ temp ;
return $ temp ;
}
2025-09-11 16:48:23 -07:00
// server sends the client a certificate in chunks, since they can be rather large
function serverCmdt2csri_sendCertChunk ( % client , % chunk )
{
if ( % client . doneAuthenticating )
return ;
//echo("Client sent certificate chunk.");
% client . t2csri_cert = % client . t2csri_cert @ % chunk ;
if ( strlen ( % client . t2csri_cert ) > 20000 )
{
% client . setDisconnectReason ( "Account certificate too long. Check your account key for corruption." ) ;
2026-03-09 12:38:40 -07:00
% client . delete ( ) ;
2025-09-11 16:48:23 -07:00
}
}
// gets a hex version of the client's IP address
// used to prevent a replay attack as described by Rain
function t2csri_gameClientHexAddress ( % client )
{
% ip = % client . getAddress ( ) ;
% ip = getSubStr ( % ip , strstr ( % ip , ":" ) + 1 , strlen ( % ip ) ) ;
% ip = getSubStr ( % ip , 0 , strstr ( % ip , ":" ) ) ;
% ip = strReplace ( % ip , "." , " " ) ;
for ( % i = 0 ; % i < getWordCount ( % ip ) ; % i + + )
{
% byte = DecToHex ( getWord ( % ip , % i ) ) ;
if ( strLen ( % byte ) < 2 )
% byte = "0" @ % byte ;
% hex = % hex @ % byte ;
}
return % hex ;
}
// client is done sending their cert... verify it, and encrypt a challenge for the client
// challenge sent to client is %clientChallenge @ %serverChallenge.
function serverCmdt2csri_sendChallenge ( % client , % clientChallenge )
{
if ( % client . doneAuthenticating )
return ;
2026-03-09 12:38:40 -07:00
//echo("Client requesting challenge. CC: " @ %clientChallenge);
//echo("Client's certificate: " @ %client.t2csri_cert);
2025-09-11 16:48:23 -07:00
// verify that the certificate the client sent is signed by the authentication server
% user = strReplace ( getField ( % client . t2csri_cert , 0 ) , "\x27" , "\\\x27" ) ;
% guid = getField ( % client . t2csri_cert , 1 ) ;
// sanitize GUID
for ( % i = 0 ; % i < strlen ( % guid ) ; % i + + )
{
2026-03-09 12:38:40 -07:00
% char = strcmp ( getSubStr ( % guid , % i , 1 ) , "" ) ;
2025-09-11 16:48:23 -07:00
if ( % char > 57 | | % char < 48 )
{
% client . setDisconnectReason ( "Invalid characters in client GUID." ) ;
2026-03-09 12:38:40 -07:00
% client . delete ( ) ;
2025-09-11 16:48:23 -07:00
return ;
}
}
% e = getField ( % client . t2csri_cert , 2 ) ;
% n = getField ( % client . t2csri_cert , 3 ) ;
% sig = getField ( % client . t2csri_cert , 4 ) ;
// sanitize e, n, sig... all of which are just hex
% rsa_chunk = strlwr ( % e @ % n @ % sig ) ;
for ( % i = 0 ; % i < strlen ( % rsa_chunk ) ; % i + + )
{
2026-03-09 12:38:40 -07:00
% char = strcmp ( getSubStr ( % rsa_chunk , % i , 1 ) , "" ) ;
if ( ( % char < 48 | | % char > 102 ) | | ( % char > 57 & & % char < 97 ) )
2025-09-11 16:48:23 -07:00
{
% client . setDisconnectReason ( "Invalid characters in certificate RSA fields." ) ;
2026-03-09 12:38:40 -07:00
% client . delete ( ) ;
2025-09-11 16:48:23 -07:00
return ;
}
}
// get a SHA1 sum
% sumStr = % user @ "\t" @ % guid @ "\t" @ % e @ "\t" @ % n ;
% certSum = sha1sum ( % sumStr ) ;
% verifSum = t2csri_verify_auth_signature ( % sig ) ;
while ( strLen ( % verifSum ) < 40 )
% verifSum = "0" @ % verifSum ;
2026-03-09 12:38:40 -07:00
//echo("Calc'd SHA1: " @ %certSum);
//echo("Signed SHA1: " @ %verifSum);
2025-09-11 16:48:23 -07:00
// verify signature
if ( % verifSum ! $ = % certSum )
{
// client supplied a bogus certificate that was never signed by the auth server
// abort their connection
% client . setDisconnectReason ( "Invalid account certificate." ) ;
2026-03-09 12:38:40 -07:00
% client . delete ( ) ;
2025-09-11 16:48:23 -07:00
return ;
}
// process client challenge half
% client . t2csri_clientChallenge = % clientChallenge ;
// sanitize the challenge to make sure it contains nothing but hex characters.
// anything else means that the client is trying to hijack control of the interpreter
% clientChallenge = strlwr ( % clientChallenge ) ;
for ( % i = 0 ; % i < strlen ( % clientChallenge ) ; % i + + )
{
2026-03-09 12:38:40 -07:00
% char = strcmp ( getSubStr ( % clientChallenge , % i , 1 ) , "" ) ;
if ( ( % char < 48 | | % char > 102 ) | | ( % char > 57 & & % char < 97 ) )
2025-09-11 16:48:23 -07:00
{
% client . setDisconnectReason ( "Invalid characters in client challenge." ) ;
2026-03-09 12:38:40 -07:00
% client . delete ( ) ;
2025-09-11 16:48:23 -07:00
return ;
}
}
// verify that the IP address the client thinks it is connecting to is the address this server
// is reasonable... take into account connections from the same private IP subnet (192.168.*.*, 10.*.*.*, etc)
% sanityIP = ipv4_hexBlockToIP ( getSubStr ( % clientChallenge , strLen ( % clientChallenge ) - 8 , 8 ) ) ;
% sourceIP = ipv4_hexBlockToIP ( t2csri_gameClientHexAddress ( % client ) ) ;
if ( ! ipv4_reasonableConnection ( % sourceIP , % sanityIP ) )
{
% client . setDisconnectReason ( "Potential man in the middle attack detected. Your client claims it connected to: " @ % sanityIP @ ", but the server does not consider this reasonable." ) ;
2026-03-09 12:38:40 -07:00
% client . delete ( ) ;
2025-09-11 16:48:23 -07:00
return ;
}
// calculate a random 64-bit server side challenge
2026-03-09 12:38:40 -07:00
rubyEval ( "tsEval '$temp=\"' + rand(18446744073709551615).to_s(16) + '\";'" ) ;
% client . t2csri_serverChallenge = $ temp @ t2csri_gameClientHexAddress ( % client ) ;
2025-09-11 16:48:23 -07:00
% fullChallenge = % client . t2csri_clientChallenge @ % client . t2csri_serverChallenge ;
2026-03-09 12:38:40 -07:00
rubyEval ( "tsEval '$temp=\"' + rsa_mod_exp('" @ % fullChallenge @ "'.to_i(16), '" @ % e @ "'.to_i(16), '" @ % n @ "'.to_i(16)).to_s(16) + '\";'" ) ;
2025-09-11 16:48:23 -07:00
// send the challenge in 200 byte chunks
2026-03-09 12:38:40 -07:00
for ( % i = 0 ; % i < strlen ( $ temp ) ; % i + = 200 )
2025-09-11 16:48:23 -07:00
{
2026-03-09 12:38:40 -07:00
commandToClient ( % client , ' t2csri_getChallengeChunk ' , getSubStr ( $ temp , % i , 200 ) ) ;
2025-09-11 16:48:23 -07:00
}
// tell the client we're done sending
commandToClient ( % client , ' t2csri_decryptChallenge ' ) ;
// set up the "auth" info retrieved by cid.getAuthInfo()
% client . t2csri_authinfo = % user @ "\t\t0\t" @ % guid @ "\n0\n" ;
// clan support: check supplemental time limited certificate, if it was sent
% comCert = % client . t2csri_comCert ;
if ( strLen ( % comCert ) > 0 )
{
// assuming there is a comCert, and we aren't running in bare mode
if ( getField ( % comCert , 3 ) $ = % guid )
{
// GUID in the community cert matches that of the account cert
% client . t2csri_authinfo = % client . t2csri_comInfo ;
}
else
{
// uh oh... someone's being naughty.. valid cert, but for a different player. kill them!
% client . setDisconnectReason ( "Community supplemental certificate doesn't match account certificate." ) ;
2026-03-09 12:38:40 -07:00
% client . delete ( ) ;
2025-09-11 16:48:23 -07:00
return ;
}
}
}
// verify the client's server challenge matches the one stored, if so, continue
// loading sequence
function serverCmdt2csri_challengeResponse ( % client , % serverChallenge )
{
if ( % client . doneAuthenticating )
return ;
if ( % client . t2csri_serverChallenge $ = % serverChallenge )
{
// check to see if the client is GUID banned, now that we verified their certificate
2026-03-09 12:38:40 -07:00
if ( banList_checkGUID ( getField ( % client . t2csri_authInfo , 3 ) ) )
2025-09-11 16:48:23 -07:00
{
% client . setDisconnectReason ( "You are not allowed to play on this server." ) ;
2026-03-09 12:38:40 -07:00
% client . delete ( ) ;
2025-09-11 16:48:23 -07:00
return ;
}
// client checks out... continue loading sequence
% client . onConnect ( % client . tname , % client . trgen , % client . tskin , % client . tvoic , % client . tvopi ) ;
}
else
{
% client . setDisconnectReason ( "Invalid server challenge. Check your account key for corruption." ) ;
2026-03-09 12:38:40 -07:00
% client . delete ( ) ;
2025-09-11 16:48:23 -07:00
}
}
// delete a client if they spend more than 15 seconds authenticating
function t2csri_expireClient ( % client )
{
if ( ! isObject ( % client ) )
return ;
2026-03-09 12:38:40 -07:00
% client . setDisconnectReason ( "This is a TribesNext server. You must install the TribesNext client to play. See www.tribesnext.com for info." ) ;
% client . delete ( ) ;
2025-09-11 16:48:23 -07:00
}
package t2csri_server
{
// packaged to create the "pre-connection" authentication phase
function GameConnection : : onConnect ( % client , % name , % raceGender , % skin , % voice , % voicePitch )
{
2026-03-09 12:38:40 -07:00
if ( % client . t2csri_serverChallenge $ = "" & & ! % client . isAIControlled ( ) & & % client . getAddress ( ) ! $ = "Local" )
2025-09-11 16:48:23 -07:00
{
// check to see if the client is IP banned
2026-03-09 12:38:40 -07:00
if ( banList_checkIP ( % client ) )
2025-09-11 16:48:23 -07:00
{
% client . setDisconnectReason ( "You are not allowed to play on this server." ) ;
2026-03-09 12:38:40 -07:00
% client . delete ( ) ;
2025-09-11 16:48:23 -07:00
return ;
}
2026-03-09 12:38:40 -07:00
//echo("Client connected. Initializing pre-connection authentication phase...");
2025-09-11 16:48:23 -07:00
// save these for later
% client . tname = % name ;
% client . trgen = % raceGender ;
% client . tskin = % skin ;
% client . tvoic = % voice ;
% client . tvopi = % voicePitch ;
// start the 15 second count down
% client . tterm = schedule ( 15000 , 0 , t2csri_expireClient , % client ) ;
2026-03-09 12:38:40 -07:00
commandToClient ( % client , ' t2csri_pokeClient ' , "T2CSRI 1.1 - 03/18/2009" ) ;
2025-09-11 16:48:23 -07:00
return ;
}
2026-03-09 12:38:40 -07:00
//echo("Client completed pre-authentication phase.");
2025-09-11 16:48:23 -07:00
// continue connection process
if ( isEventPending ( % client . tterm ) )
cancel ( % client . tterm ) ;
Parent : : onConnect ( % client , % name , % raceGender , % skin , % voice , % voicePitch ) ;
% client . doneAuthenticating = 1 ;
}
// packaged to prevent game leaving messages for clients that are in the authentication phase
function GameConnection : : onDrop ( % client , % reason )
{
if ( ! isObject ( % client ) | | ! % client . doneAuthenticating )
return ;
Parent : : onDrop ( % client , % reason ) ;
}
// packaged to pull info from the certificate, rather than some internal data structures
// format is kept consistent though:
// >Name ActiveClanTag Prepend(0)/Postpend(1)Tag guid
// >NumberOfClans
// >ClanName TagForClan Prepend(0)/Postpend(1)Tag clanid rank title
// in this version, there is no clan support, so those fields are empty
// clan support will be implemented via delegation to a community server
function GameConnection : : getAuthInfo ( % client )
{
2026-03-09 12:38:40 -07:00
if ( % client . getAddress ( ) $ = "Local" & & % client . t2csri_authInfo $ = "" )
2025-09-11 16:48:23 -07:00
% client . t2csri_authInfo = WONGetAuthInfo ( ) ;
return % client . t2csri_authInfo ;
}
} ;
2026-03-09 12:38:40 -07:00
if ( $ PlayingOnline )
activatePackage ( t2csri_server ) ;