Merge pull request #352 from RonaldReagan/authimprovements_263
Improve current auth system 263
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// try to authenticate game account during startup in silent mode so that users with saved passwords are authenticated asap
|
||||
sleep 1000 [ checkaccount 1 ]
|
||||
sleep 100 [ checkaccount 1 ]
|
||||
|
||||
sleep 1000 [
|
||||
echo "\fs\f3Beware: This is an ALPHA version of AssaultCube v1.3"
|
||||
|
||||
+2
-2
@@ -7388,9 +7388,9 @@ docremark ["authsetup genpriv" - generates private key from prepriv key.];
|
||||
|
||||
docremark ["authsetup genpub" - generates public key from private key.];
|
||||
|
||||
docremark ["authsetup newppass preprivpass [preprivfilename]" - encrypts prepriv key with new password and saves the result to file. The default filename is config/authpreprivate.cfg and an existing file gets overwritten - but the former file content is kept and also written, each line commented out.];
|
||||
docremark ["authsetup newppass preprivpass [preprivfilename]" - encrypts prepriv key with new password and saves the result to file. The default filename is private/authpreprivate.cfg and an existing file gets overwritten - but the former file content is kept and also written, each line commented out.];
|
||||
|
||||
docremark ["authsetup newpass privpass [privatefilename]" - encrypts private key with new password and saves the encrypted private key and the public key to file. The default filename is config/authprivate.cfg and an existing file gets overwritten - but the former file content is kept and also written, each line commented out.];
|
||||
docremark ["authsetup newpass privpass [privatefilename]" - encrypts private key with new password and saves the encrypted private key and the public key to file. The default filename is private/authprivate.cfg and an existing file gets overwritten - but the former file content is kept and also written, each line commented out.];
|
||||
|
||||
docremark ["authsetup unarmed" - sets private and public keys to a fixed value. For testing only. Can be used to "log out".];
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ exec config/menus_bot.cfg
|
||||
exec config/menus_edit.cfg
|
||||
exec config/menus_licenses.cfg
|
||||
exec config/menus_settings.cfg
|
||||
exec config/menus_auth.cfg
|
||||
exec config/menus_multiplayer.cfg
|
||||
exec config/menus_voicecom.cfg
|
||||
exec config/favourites.cfg
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
|
||||
newmenu [auth setup]
|
||||
menuinit [ tempalias ___ppass "" ; tempalias ___pass ""; tempalias ___rememberpass 0; ]
|
||||
menuitem "To play online you need to create a game account which will be used" -1
|
||||
menuitem "identify you to game servers. This identity is stored as a public/private" -1
|
||||
menuitem "keypair and only the public key is ever transmitted to the server. You are" -1
|
||||
menuitem "able to change the password at a future point in time." -1
|
||||
menuitem "" -1
|
||||
menuitem "There exists a mechanism to recover your identity if lost that uses a" -1
|
||||
menuitem "seperate password." -1
|
||||
menuitem "" -1
|
||||
menuitem "To learn more about this functionality you can go to this URL in your browser" -1
|
||||
menuitem "\f1https://assault.cubers.net/docs/auth.html" -1
|
||||
menuitem "" -1
|
||||
menuitem "\f0Set passwords \f4(recommended)"
|
||||
menuitemtextinput "Password for your game account\t" "" [___pass = $arg1] [] 64
|
||||
menuitemtextinput "Password for account restoration\t" "" [___ppass = $arg1] [] 64
|
||||
menuitem "" -1
|
||||
menuitemradio "Remember game account password: " 0 1 [ ] [ "No \f4(recommended)" "Yes"] [ ___rememberpass = (= $arg1 1)]
|
||||
menuitem "\f2\t\t\t [ OK ]" [
|
||||
authsetup genpre
|
||||
authsetup genpriv
|
||||
authsetup genpub
|
||||
if (= $___rememberpass 1) [ alias savedpass $___pass ] [ if (checkalias savedpass) [ delalias savedpass ] ]
|
||||
|
||||
closecurmenu
|
||||
showmenu "auth setup progress"
|
||||
]
|
||||
|
||||
newmenu "auth setup progress"
|
||||
menuinit [ sleep 100 [
|
||||
if (strcmp $___ppass "") [ authsetup savepre ] [ authsetup newppass $___ppass; authsetup savepre ]
|
||||
if (strcmp $___pass "") [ authsetup savepriv ] [ authsetup newpass $___pass ]
|
||||
___ppass = ""
|
||||
___pass = ""
|
||||
if (authsetup) [ echo "\f0created game account" ]
|
||||
closecurmenu
|
||||
checkaccount
|
||||
] ]
|
||||
menuitem "Please wait while we create your account..." -1
|
||||
|
||||
|
||||
newmenu [auth password change]
|
||||
menuinit [ tempalias ___pass ""; tempalias ___rememberpass 0; ]
|
||||
menuitem "Please choose a new password for your private key" -1
|
||||
menuitem "" -1
|
||||
menuitemtextinput "Password for your game account\t" "" [___pass = $arg1] [] 64
|
||||
menuitemradio "Remember game account password: " 0 1 [ ] [ "No \f4(recommended)" "Yes"] [ ___rememberpass = (= $arg1 1)]
|
||||
menuitem "\f2[ Set Password ]" [
|
||||
if (= $___rememberpass 1) [ alias savedpass $___pass ] [ if (checkalias savedpass) [ delalias savedpass ] ]
|
||||
|
||||
closecurmenu
|
||||
showmenu "auth password change progress"
|
||||
]
|
||||
menuitem "\f7[ Cancel ]" closecurmenu
|
||||
|
||||
newmenu "auth password change progress"
|
||||
menuinit [ sleep 100 [
|
||||
if (strcmp $___pass "") [ ] [ authsetup newpass $___pass ]
|
||||
___pass = ""
|
||||
if (authsetup) [ echo "\f0updated game account" ]
|
||||
closecurmenu
|
||||
checkaccount
|
||||
] ]
|
||||
menuitem "Please wait while we update your account..." -1
|
||||
|
||||
|
||||
newmenu [auth]
|
||||
menuinit [
|
||||
resetcurmenu
|
||||
|
||||
tempalias ___pass ""; tempalias ___rememberpass 0;
|
||||
|
||||
if (authsetup) [
|
||||
menuitem "\f0Pub key:" -1
|
||||
menuitem (format "\fY%1" (mypubkey)) -1
|
||||
menuitem "\f2[ Copy to clipboard ]" [setclipboardtext (mypubkey)]
|
||||
menuitem "" -1
|
||||
menuitem "\f0Change Password" -1
|
||||
menuitemtextinput "New password" "" [___pass = $arg1] [] 64
|
||||
menuitemradio "Remember game account password: " 0 1 [ ] [ "No \f4(recommended)" "Yes"] [ ___rememberpass = (= $arg1 1)]
|
||||
menuitem "\f2[ Update Password ]" [
|
||||
if (= $___rememberpass 1) [ alias savedpass $___pass ] [ if (checkalias savedpass) [ delalias savedpass ] ]
|
||||
|
||||
closecurmenu
|
||||
showmenu "auth password change progress"
|
||||
]
|
||||
menuitem "" -1
|
||||
menuitem "\f3[ Reset auth ]" [ showmenu [auth setup]]
|
||||
] [
|
||||
if (authsetup needpass) [
|
||||
menuitem "\f3Password required to authenticate \f2[ Enter password ]" [ showmenu [enter password]]
|
||||
menuitem "" -1
|
||||
menuitem "\f3[ Reset Auth ]" [ showmenu [auth setup] ]
|
||||
] [
|
||||
menuitem "\f3Auth not setup" -1
|
||||
menuitem "\f2[ Setup Auth ]" [ showmenu [auth setup] ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
newmenu "game account"
|
||||
menuitem "To play online you need to create a game account first." -1
|
||||
menuitem "" -1
|
||||
menuitem "\t\t\f0[Create game account]" [ closecurmenu; showmenu "auth setup"; ]
|
||||
menuitem "\t\t[Not now...]" closecurmenu
|
||||
|
||||
|
||||
newmenu "enter password"
|
||||
menuitemtextinput "please enter the password for your game account\t" "" [ __loginpass = $arg1; closecurmenu; showmenu authentication ] [] 64 1
|
||||
|
||||
newmenu "authentication"
|
||||
menuinit [ sleep 100 [ echo "checking..." ; authsetup passd $__loginpass authfinish; __loginpass = "" ] ]
|
||||
menuitem "Please wait while we authenticate your game account..." -1
|
||||
|
||||
const authfinish [
|
||||
closecurmenu
|
||||
if (authsetup) [
|
||||
echo "\f0authentication successful"
|
||||
] [
|
||||
echo "\f3authentication failed, please enter the correct password for your game account"
|
||||
]
|
||||
]
|
||||
|
||||
// try to authenticate the game account or offer to create a new game account if none exists
|
||||
// set $arg1 to 1 to enable silent mode to prevent any user interactions
|
||||
const checkaccount [
|
||||
if (authsetup) [] [
|
||||
if (authsetup needpass) [] [
|
||||
// load the password protected private key
|
||||
exec private/authprivate.cfg
|
||||
]
|
||||
if (authsetup needpass) [
|
||||
if (checkalias savedpass) [
|
||||
// try the saved password if there is one
|
||||
authsetup passd $savedpass
|
||||
] [
|
||||
// saved password does not exist so ask for password
|
||||
if (= $arg1 0) [ showmenu "enter password" ]
|
||||
]
|
||||
] [
|
||||
// game account does not seem to be set up, ask user to create new game account
|
||||
if (= $arg1 0) [ showmenu "game account" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -172,7 +172,7 @@ menuitem [Join a LAN server] [ ifauthsetup joinlanservermenu ]
|
||||
menuitem [Custom connect] [ ifauthsetup customconnectmenu ]
|
||||
menuitem [Connect as Admin] [ ifauthsetup adminconnectmenu ]
|
||||
menuitem [] -1
|
||||
menuitem [Reset game account] [ showmenu [create game account] ]
|
||||
menuitem [Auth settings] [ showmenu [auth] ]
|
||||
menuitem [Force serverlist update] [ updatefrommaster 1; ifauthsetup joinservermenu ]
|
||||
|
||||
//// Main > Multiplayer > Custom connect ////
|
||||
@@ -330,83 +330,4 @@ menuinit [
|
||||
menuitem "\f2 [ Download the servers map version and join the game ]" [ getmap ]
|
||||
menuitem "\f3 [ Keep the local map version and disconnect from the server ]" [ showmenu ""; disconnect ]
|
||||
menuinitselection 4
|
||||
]
|
||||
|
||||
// game account management menus
|
||||
|
||||
newmenu "game account"
|
||||
menuitem "To play online you need to create a game account first." -1
|
||||
menuitem "" -1
|
||||
menuitem "\t\t\f0[Create game account]" [ closecurmenu; showmenu "create game account"; ]
|
||||
menuitem "\t\t[Not now...]" closecurmenu
|
||||
|
||||
newmenu "create game account"
|
||||
menuinit [ tempalias ___ppass "" ; tempalias ___pass ""; tempalias ___rememberpass 0; ]
|
||||
menuitemtextinput "Please enter a password for account restoration\t" "" [___ppass = $arg1] [] 64
|
||||
menuitemtextinput "Please enter a password for your game account\t" "" [___pass = $arg1] [] 64
|
||||
menuitem "" -1
|
||||
menuitemradio "Remember game account password: " 0 1 [ ] [ "No (recommended)" "Yes"] [ ___rememberpass = (= $arg1 1)]
|
||||
menuitem "" -1
|
||||
menuitem "\t\t\f0[Ok]" [
|
||||
if (strcmp $___ppass "") [ menuselection (curmenu) 0; -1 ] [
|
||||
if (strcmp $___pass "") [ menuselection (curmenu) 1; -1 ] [
|
||||
authsetup genpre
|
||||
authsetup genpriv
|
||||
authsetup genpub
|
||||
if (= $___rememberpass 1) [ alias savedpass $___pass ] [ if (checkalias savedpass) [ delalias savedpass ] ]
|
||||
closecurmenu
|
||||
showmenu "progress"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
newmenu "progress"
|
||||
menuinit [ sleep 100 [
|
||||
authsetup newppass $___ppass
|
||||
authsetup newpass $___pass
|
||||
___ppass = ""
|
||||
___pass = ""
|
||||
if (authsetup) [ echo "\f0created game account" ]
|
||||
closecurmenu
|
||||
checkaccount
|
||||
] ]
|
||||
menuitem "Please wait while we create your account..." -1
|
||||
|
||||
newmenu "enter password"
|
||||
menuitemtextinput "please enter the password for your game account\t" "" [ __loginpass = $arg1; closecurmenu; showmenu authentication ] [] 64 1
|
||||
|
||||
newmenu "authentication"
|
||||
menuinit [ sleep 100 [ echo "checking..." ; authsetup passd $__loginpass authfinish; __loginpass = "" ] ]
|
||||
menuitem "Please wait while we authenticate your game account..." -1
|
||||
|
||||
const authfinish [
|
||||
closecurmenu
|
||||
if (authsetup) [
|
||||
echo "\f0authentication successful"
|
||||
] [
|
||||
echo "\f3authentication failed, please enter the correct password for your game account"
|
||||
]
|
||||
]
|
||||
|
||||
// try to authenticate the game account or offer to create a new game account if none exists
|
||||
// set $arg1 to 1 to enable silent mode to prevent any user interactions
|
||||
const checkaccount [
|
||||
if (authsetup) [] [
|
||||
if (authsetup needpass) [] [
|
||||
// load the password protected private key
|
||||
exec config/authprivate.cfg
|
||||
]
|
||||
if (authsetup needpass) [
|
||||
if (checkalias savedpass) [
|
||||
// try the saved password if there is one
|
||||
authsetup passd $savedpass
|
||||
] [
|
||||
// saved password does not exist so ask for password
|
||||
if (= $arg1 0) [ showmenu "enter password" ]
|
||||
]
|
||||
] [
|
||||
// game account does not seem to be set up, ask user to create new game account
|
||||
if (= $arg1 0) [ showmenu "game account" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -720,6 +720,7 @@ menuinit [
|
||||
fullscreen (= $__windowtype 0)
|
||||
fullscreendesktop (= $__windowtype 0)
|
||||
resetgl
|
||||
showmenu [auth setup]
|
||||
]
|
||||
if (checkalias __firstrun) [] [ __firstrun = 1; menuselection (curmenu) 9 ] // preselect nickname field
|
||||
]
|
||||
|
||||
+11
-5
@@ -1728,7 +1728,11 @@ name="AssaultCube Documentation" version="Current" xsi:schemaLocation="xml/cuber
|
||||
<section name="General" sortindex="01">
|
||||
<description>This section describes general identifiers.</description>
|
||||
<identifiers sort="true">
|
||||
|
||||
<command name="setclipboardtext">
|
||||
<arguments>
|
||||
<argument token="T" description="Text to put in the clipboard."/>
|
||||
</arguments>
|
||||
</command>
|
||||
<variable name="autoscreenshot">
|
||||
<description>Toggle for taking an automatic screenshot during intermission.</description>
|
||||
<value token="B" description="0=Off, 1=On" minValue="0" maxValue="1" defaultValue="1"/>
|
||||
@@ -10212,7 +10216,7 @@ name="AssaultCube Documentation" version="Current" xsi:schemaLocation="xml/cuber
|
||||
and to lock and unlock the private key with a password.
|
||||
</remark>
|
||||
<remark>
|
||||
Private and public keys are used to authenticate the player to servers, and are loaded from config/authprivate.cfg.
|
||||
Private and public keys are used to authenticate the player to servers, and are loaded from private/authprivate.cfg.
|
||||
Private keys can be stored in an "encrypted with password" version - in which case the player has to enter
|
||||
the password after game start. To prevent brute-force cracking of the password, the key-derivation function uses
|
||||
a constant time to encode (usually several seconds). Because of that, the decoding at game start can be done
|
||||
@@ -10241,17 +10245,19 @@ name="AssaultCube Documentation" version="Current" xsi:schemaLocation="xml/cuber
|
||||
<remark>"authsetup genpre [prelen]" - generates new prepriv key.</remark>
|
||||
<remark>"authsetup genpriv" - generates private key from prepriv key.</remark>
|
||||
<remark>"authsetup genpub" - generates public key from private key.</remark>
|
||||
<remark>"authsetup savepre [preprivfilename]" - saves the prepriv key. The default filename is private/authpreprivate.cfg</remark>
|
||||
<remark>"authsetup savepre [privatefilename]" - saves the private key. The default filename is private/authprivate.cfg</remark>
|
||||
<remark>
|
||||
"authsetup newppass preprivpass [preprivfilename]" - encrypts prepriv key with new password and saves the result to file.
|
||||
The default filename is config/authpreprivate.cfg and an existing file gets overwritten - but the former file
|
||||
The default filename is private/authpreprivate.cfg and an existing file gets overwritten - but the former file
|
||||
content is kept and also written, each line commented out.
|
||||
</remark>
|
||||
<remark>
|
||||
"authsetup newpass privpass [privatefilename]" - encrypts private key with new password and saves the encrypted
|
||||
private key and the public key to file. The default filename is config/authprivate.cfg and an existing file
|
||||
private key and the public key to file. The default filename is private/authprivate.cfg and an existing file
|
||||
gets overwritten - but the former file content is kept and also written, each line commented out.
|
||||
</remark>
|
||||
<remark>"authsetup unarmed" - sets private and public keys to a fixed value. For testing only. Can be used to "log out".</remark>
|
||||
<remark>"authsetup unarmed" - sets private and public keys to a fixed value. For testing only. Can be used to "log out". Only available in development versions of the game</remark>
|
||||
<remark>----------</remark>
|
||||
<remark>
|
||||
"authmemusage", "authrounds", "authmaxtime" are only used, when new passwords are created (authsetup new[p]pass).
|
||||
|
||||
@@ -115,7 +115,6 @@ COMMANDN(connect, connectserv, "sis");
|
||||
void connectadmin(char *servername, int *serverport, char *password)
|
||||
{
|
||||
modprotocol = false;
|
||||
if(!password[0]) return;
|
||||
connectserv_(servername, *serverport, password, CR_ADMIN);
|
||||
}
|
||||
COMMAND(connectadmin, "sis");
|
||||
@@ -137,7 +136,6 @@ COMMANDN(modconnect, modconnectserv, "sis");
|
||||
void modconnectadmin(char *servername, int *serverport, char *password)
|
||||
{
|
||||
modprotocol = true;
|
||||
if(!password[0]) return;
|
||||
connectserv_(servername, *serverport, password, CR_ADMIN);
|
||||
}
|
||||
COMMAND(modconnectadmin, "sis");
|
||||
@@ -755,3 +753,5 @@ COMMANDF(timestring, "", ()
|
||||
});
|
||||
|
||||
COMMANDF(millis, "", () { intret(totalmillis); });
|
||||
|
||||
COMMANDN(setclipboardtext, SDL_SetClipboardText, "s");
|
||||
|
||||
@@ -1636,6 +1636,13 @@ void whois(int *cn)
|
||||
}
|
||||
COMMAND(whois, "i");
|
||||
|
||||
void getvita(int *cn)
|
||||
{
|
||||
addmsg(SV_GETVITA, "ri", *cn);
|
||||
}
|
||||
COMMAND(getvita, "i");
|
||||
|
||||
|
||||
void findcn(char *name)
|
||||
{
|
||||
loopv(players) if(players[i] && !strcmp(name, players[i]->name))
|
||||
@@ -1659,6 +1666,8 @@ void setadmin(int *claim, char *password)
|
||||
}
|
||||
else if(*claim != 0 && *password)
|
||||
addmsg(SV_SETADMIN, "ris", *claim, genpwdhash(player1->name, password, sessionid));
|
||||
else if(*claim != 0)
|
||||
addmsg(SV_SETADMIN, "ri", *claim);
|
||||
}
|
||||
COMMAND(setadmin, "is");
|
||||
|
||||
|
||||
@@ -1354,6 +1354,39 @@ void parsemessages(int cn, playerent *d, ucharbuf &p, bool demo = false)
|
||||
getip4(p);
|
||||
p.get((uchar*)text, 64);
|
||||
break;
|
||||
|
||||
case SV_VITADATA: { // Server sending vita data of a client. Possibly expose this to cubescript in the future
|
||||
uchar pubkey[32];
|
||||
char pubkeyhex[68];
|
||||
string privatecomment;
|
||||
string publiccomment;
|
||||
int vs[VS_NUM];
|
||||
|
||||
int targetcn = getint(p);
|
||||
playerent *target = getclient(targetcn);
|
||||
|
||||
if (target && p.get(pubkey, 32) == 32) {
|
||||
getstring(text, p);
|
||||
filtertext(privatecomment, text, FTXT__VITACOMMENT);
|
||||
getstring(text, p);
|
||||
filtertext(publiccomment, text, FTXT__VITACOMMENT);
|
||||
|
||||
loopi(VS_NUM) vs[i] = getint(p);
|
||||
|
||||
bin2hex(pubkeyhex, pubkey, 32);
|
||||
|
||||
conoutf("\f0Vita Info for \f5%s \f0(\f5%d\f0) \f2pubkey: \f1%s", target->name, targetcn, pubkeyhex);
|
||||
if(privatecomment[0]) conoutf("\f0Private Comment: \f1%s", privatecomment);
|
||||
if(publiccomment[0]) conoutf("\f0Public Comment: \f1%s", publiccomment);
|
||||
|
||||
string msg = "\f0Flags:";
|
||||
for (int i = 0; i < VS_NUM; i++) {
|
||||
concatformatstring(msg, " \f2%s=\f1%d", vskeywords[i], vs[i]);
|
||||
}
|
||||
conoutf("%s", msg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
neterr("type");
|
||||
|
||||
+77
-30
@@ -377,7 +377,7 @@ void popMT() // undo last seedMT()
|
||||
|
||||
#define ENTCHUNKS 32 // pool size (in 128-bytes chunks)
|
||||
#define ENTPOOLSIZE (ENTCHUNKS * 128)
|
||||
#define ENTROPYSAVEFILE "config" PATHDIVS "entropy.dat"
|
||||
#define ENTROPYSAVEFILE "private" PATHDIVS "entropy.dat"
|
||||
|
||||
static uchar *entpool = NULL;
|
||||
|
||||
@@ -719,8 +719,8 @@ COMMAND(ed25519speedtest, "");
|
||||
// * it is used during playing to authenticate the player to the server
|
||||
// * needs special protection...
|
||||
|
||||
#define AUTHPREPRIVATECFGFILE "config" PATHDIVS "authpreprivate.cfg"
|
||||
#define AUTHPRIVATECFGFILE "config" PATHDIVS "authprivate.cfg"
|
||||
#define AUTHPREPRIVATECFGFILE "private" PATHDIVS "authpreprivate.cfg"
|
||||
#define AUTHPRIVATECFGFILE "private" PATHDIVS "authprivate.cfg"
|
||||
|
||||
VARP(authmemusage, 2, 24, (1<<10) - 1); // megabytes of RAM to use for password hash (when using a new password)
|
||||
VARP(authrounds, 0, 0, INT_MAX - 1); // create new password hashes with a fixed number of rounds (if authrounds > 0)
|
||||
@@ -741,6 +741,63 @@ static int passdeferred(void *pass) // decrypt the private key in the backgrou
|
||||
return 0;
|
||||
}
|
||||
|
||||
void savepreprivkey(const char *filename, uchar *preprivkey, uchar preprivlen, uchar *psalt, uint preprivpwdcfg) {
|
||||
char hextemp[128 * 2 + 1];
|
||||
char *oldfile = loadfile(filename, NULL), *b;
|
||||
stream *f = openfile(filename, "wb");
|
||||
if(f)
|
||||
{
|
||||
if(!oldfile)
|
||||
{
|
||||
f->printf("\n" "// remove this file from your computer immediately!\n"
|
||||
"// either print it out and delete it or move it to a thumbdrive.\n"
|
||||
"// YOU DO NOT NEED THIS FILE TO PLAY AC!\n" "\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
for(char *l = strtok_r(oldfile, "\n\r", &b); l; l = strtok_r(NULL, "\n\r", &b))
|
||||
{
|
||||
if(*l && l[0] != '/')
|
||||
f->printf("// %s\n", l);
|
||||
if(*l && l[0] == '/')
|
||||
f->printf("%s\n", l);
|
||||
}
|
||||
}
|
||||
|
||||
f->printf("authsetup pre %s", bin2hex(hextemp, preprivkey, preprivlen));
|
||||
if(preprivpwdcfg) f->printf(" %s %u", bin2hex(hextemp, psalt, 16), preprivpwdcfg & ~1);
|
||||
f->printf("\n\n");
|
||||
delete f;
|
||||
}
|
||||
DELETEA(oldfile);
|
||||
}
|
||||
|
||||
|
||||
void saveprivkey(const char *filename, uchar *privkey, uchar *pubkey, uchar salt[16], uint privpwdcfg) {
|
||||
char hextemp[65];
|
||||
char *oldfile = loadfile(filename, NULL), *b;
|
||||
stream *f = openfile(filename, "wb");
|
||||
if(f)
|
||||
{
|
||||
if(oldfile)
|
||||
{
|
||||
for(char *l = strtok_r(oldfile, "\n\r", &b); l; l = strtok_r(NULL, "\n\r", &b))
|
||||
{
|
||||
if(*l && l[0] != '/')
|
||||
f->printf("// %s\n", l);
|
||||
if(*l && l[0] == '/')
|
||||
f->printf("%s\n", l);
|
||||
}
|
||||
}
|
||||
|
||||
f->printf("\nauthsetup priv %s", bin2hex(hextemp, privkey, 32));
|
||||
if(privpwdcfg) f->printf(" %s %u", bin2hex(hextemp, salt, 16), privpwdcfg);
|
||||
f->printf("\nauthsetup pub %s\n\n", bin2hex(hextemp, pubkey, 32));
|
||||
delete f;
|
||||
}
|
||||
DELETEA(oldfile);
|
||||
}
|
||||
|
||||
void authsetup(char **args, int numargs) // set up private and public keys
|
||||
{
|
||||
const int preprivminlen = 32, preprivmaxlen = 128;
|
||||
@@ -865,22 +922,10 @@ void authsetup(char **args, int numargs) // set up private and public keys
|
||||
preprivpwdcfg = (iterations << 10) | authmemusage;
|
||||
xor_block(prepriv, keyhash, preprivlen);
|
||||
}
|
||||
const char *fn = numargs > 2 && args[2][0] ? path(args[2]) : AUTHPREPRIVATECFGFILE;
|
||||
char *oldfile = loadfile(fn, NULL), *b;
|
||||
stream *f = openfile(fn, "wb");
|
||||
if(f)
|
||||
{
|
||||
f->printf("\n" "// remove this file from your computer immediately!\n"
|
||||
"// either print it out and delete it or move it to a thumbdrive.\n"
|
||||
"// YOU DO NOT NEED THIS FILE TO PLAY AC!\n" "\n");
|
||||
if(oldfile) for(char *l = strtok_r(oldfile, "\n\r", &b); l; l = strtok_r(NULL, "\n\r", &b)) if(*l) f->printf("// %s\n", l);
|
||||
f->printf("authsetup pre %s", bin2hex(hextemp, prepriv, preprivlen));
|
||||
if(preprivpwdcfg) f->printf(" %s %u", bin2hex(hextemp, psalt, 16), preprivpwdcfg & ~1);
|
||||
f->printf("\n\n");
|
||||
delete f;
|
||||
}
|
||||
DELETEA(oldfile);
|
||||
}
|
||||
|
||||
savepreprivkey(numargs > 2 && args[2][0] ? path(args[2]) : AUTHPREPRIVATECFGFILE,
|
||||
prepriv, preprivlen, psalt, preprivpwdcfg);
|
||||
}
|
||||
else if(!strcasecmp(args[0], "NEWPASS"))
|
||||
{
|
||||
@@ -896,20 +941,21 @@ void authsetup(char **args, int numargs) // set up private and public keys
|
||||
privpwdcfg = (iterations << 10) | authmemusage;
|
||||
xor_block(priv, keyhash, 32);
|
||||
}
|
||||
const char *fn = numargs > 2 && args[2][0] ? path(args[2]) : AUTHPRIVATECFGFILE;
|
||||
char *oldfile = loadfile(fn, NULL), *b;
|
||||
stream *f = openfile(fn, "wb");
|
||||
if(f)
|
||||
{
|
||||
if(oldfile) for(char *l = strtok_r(oldfile, "\n\r", &b); l; l = strtok_r(NULL, "\n\r", &b)) if(*l) f->printf("// %s\n", l);
|
||||
f->printf("\nauthsetup priv %s", bin2hex(hextemp, priv, 32));
|
||||
if(privpwdcfg) f->printf(" %s %u", bin2hex(hextemp, salt, 16), privpwdcfg);
|
||||
f->printf("\nauthsetup pub %s\n\n", bin2hex(hextemp, keyhash + 32, 32));
|
||||
delete f;
|
||||
}
|
||||
DELETEA(oldfile);
|
||||
saveprivkey(numargs > 2 && args[2][0] ? path(args[2]) : AUTHPRIVATECFGFILE,
|
||||
priv, keyhash + 32, salt, privpwdcfg);
|
||||
}
|
||||
}
|
||||
else if(!strcasecmp(args[0], "SAVEPRIV"))
|
||||
{
|
||||
ed25519_pubkey_from_private(keyhash + 32, priv);
|
||||
saveprivkey(numargs > 1 && args[1][0] ? path(args[1]) : AUTHPRIVATECFGFILE,
|
||||
priv, keyhash + 32, salt, privpwdcfg);
|
||||
}
|
||||
else if(!strcasecmp(args[0], "SAVEPRE"))
|
||||
{
|
||||
savepreprivkey(numargs > 1 && args[1][0] ? path(args[1 ]) : AUTHPREPRIVATECFGFILE,
|
||||
prepriv, preprivlen, psalt, preprivpwdcfg);
|
||||
}
|
||||
#ifndef PRODUCTION
|
||||
else if(!strcasecmp(args[0], "UNARMED")) // FIXME: this is for testing purposes only - to test simultaneous logins from the same account - delete before release!
|
||||
{
|
||||
@@ -935,6 +981,7 @@ void authsetup(char **args, int numargs) // set up private and public keys
|
||||
}
|
||||
COMMAND(authsetup, "v");
|
||||
|
||||
|
||||
void mypubkey()
|
||||
{
|
||||
string res = "";
|
||||
|
||||
@@ -605,6 +605,7 @@ static const int msgsizes[] = // size inclusive message token, 0 f
|
||||
SV_EXTENSION, 0,
|
||||
SV_MAPIDENT, 3, SV_DEMOCHECKSUM, 0, SV_DEMOSIGNATURE, 0,
|
||||
SV_PAUSEMODE, 1,
|
||||
SV_GETVITA, 2, SV_VITADATA, 0,
|
||||
-1
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#define CUBE_SERVINFO_PORT(serverport) (serverport+1)
|
||||
#define CUBE_SERVINFO_TO_SERV_PORT(servinfoport) (servinfoport-1)
|
||||
#define CLIENTIPCENSOR 12 // blank out the last 12 bits of client's IPs before telling them to others (compromise between "16" from AC 0.9x and "8" from AC 1.1)
|
||||
#define PROTOCOL_VERSION 1203 // bump when protocol changes (use negative numbers for mods!)
|
||||
#define PROTOCOL_VERSION 1204 // bump when protocol changes (use negative numbers for mods!)
|
||||
#define DEMO_VERSION 2 // bump when demo format changes
|
||||
#define DEMO_MAGIC "ASSAULTCUBE_DEMO"
|
||||
#define DEMO_MINTIME 10000 // don't keep demo recordings with less than 10 seconds
|
||||
@@ -50,6 +50,7 @@ enum
|
||||
SV_EXTENSION,
|
||||
SV_MAPIDENT, SV_DEMOCHECKSUM, SV_DEMOSIGNATURE,
|
||||
SV_PAUSEMODE,
|
||||
SV_GETVITA, SV_VITADATA,
|
||||
SV_NUM
|
||||
};
|
||||
|
||||
@@ -86,7 +87,7 @@ enum { DISC_PROTOCOL = 0, DISC_EOP, DISC_CN, DISC_TAGT, DISC_DUP, DISC_OVERFLOW,
|
||||
DISC_REMOVED = 32, DISC_SOPLOGINFAIL, DISC_BADNICK, DISC_ANNOY, DISC_SPAM, DISC_DOS, DISC_SPEEDING, DISC_FISHSLAP, DISC_AFK, // disconnected
|
||||
DISC_KICK = 48, DISC_BAN, DISC_VOTEKICK, DISC_VOTEBAN, DISC_OPKICK, DISC_OPBAN, DISC_TKKICK, DISC_TKBAN, DISC_FFKICK, DISC_FFBAN, DISC_AUTOKICK, DISC_AUTOBAN,
|
||||
DISC_UNKNOWN = 99, DISC_BECAUSE };
|
||||
enum { BAN_NONE = 0, BAN_VOTE, BAN_AUTO, BAN_BLACKLIST, BAN_MASTER };
|
||||
enum { BAN_NONE = 0, BAN_VOTE, BAN_AUTO, BAN_BLACKLIST, BAN_VITA, BAN_MASTER };
|
||||
|
||||
#define EXT_ACK -1
|
||||
#define EXT_VERSION 104
|
||||
|
||||
+3
-2
@@ -1026,8 +1026,9 @@ extern void dumpexecutionstack(stream *f);
|
||||
extern const char *currentserver(int i);
|
||||
|
||||
// server
|
||||
enum { /* timecodes */ VS_FIRSTLOGIN = 0, VS_LASTLOGIN, VS_MUTE, VS_NOVOTE, VS_BAN, VS_ADMIN, VS_OWNER, VS_MAPUPLOAD,
|
||||
/* stats/counters */ VS_MINUTESCONNECTED, VS_MINUTESACTIVE, VS_FLAGS, VS_FRAGS, VS_DEATHS, VS_TKS, VS_DAMAGE, VS_FF, VS_NUM, VS_NUMCOUNTERS = VS_MINUTESCONNECTED };
|
||||
enum { /* timecodes */ VS_FIRSTLOGIN = 0, VS_LASTLOGIN, VS_BAN, VS_WHITELISTED, VS_ADMIN, VS_OWNER,
|
||||
/* stats/counters */ VS_MINUTESCONNECTED, VS_MINUTESACTIVE, VS_FLAGS, VS_ANTIFLAGS, VS_FRAGS, VS_DEATHS, VS_TKS, VS_SUICIDES, VS_DAMAGE, VS_FRIENDLYDAMAGE,
|
||||
/* meta */ VS_NUM, VS_NUMCOUNTERS = VS_MINUTESCONNECTED };
|
||||
extern const char *vskeywords[VS_NUM + 1], *vsnames[VS_NUM + 1];
|
||||
extern int modeacronyms;
|
||||
extern void servertoclient(int chan, uchar *buf, int len, bool demo = false);
|
||||
|
||||
+102
-10
@@ -241,7 +241,10 @@ void poll_serverthreads() // called once per mainloop-timeslice
|
||||
int too_old = 0;
|
||||
enumeratekt(vitas, uchar32, k, vita_s, v,
|
||||
{
|
||||
if((servclock - v.vs[VS_LASTLOGIN]) / (60 * 24 * 31) <= vitamaxage)
|
||||
if((servclock - v.vs[VS_LASTLOGIN]) / (60 * 24 * 31) <= vitamaxage // Vita is within the max age (months)
|
||||
|| (v.vs[VS_BAN] == 1 || (servclock - v.vs[VS_BAN]) < 0) // Do not remove if vita has the ban, whitelist, or admin flag set
|
||||
|| (v.vs[VS_WHITELISTED] == 1 || (servclock - v.vs[VS_WHITELISTED]) < 0)
|
||||
|| (v.vs[VS_ADMIN] == 1 || (servclock - v.vs[VS_ADMIN]) < 0))
|
||||
{
|
||||
vk.k = &k;
|
||||
vk.v = &v;
|
||||
@@ -1437,6 +1440,7 @@ void flagaction(int flag, int action, int actor)
|
||||
{
|
||||
client *c = clients[actor];
|
||||
c->state.flagscore += score;
|
||||
c->incrementvitacounter(VS_FLAGS, score);
|
||||
sendf(-1, 1, "riii", SV_FLAGCNT, actor, c->state.flagscore);
|
||||
}
|
||||
if(valid_client(actor))
|
||||
@@ -1875,7 +1879,18 @@ void serverdamage(client *target, client *actor, int damage, int gun, bool gib,
|
||||
if(damage < INT_MAX)
|
||||
{
|
||||
actor->state.damage += damage;
|
||||
|
||||
sendf(-1, 1, "ri7", gib ? SV_GIBDAMAGE : SV_DAMAGE, target->clientnum, actor->clientnum, gun, damage, ts.armour, ts.health);
|
||||
|
||||
if (!isteam(target->team, actor->team))
|
||||
{
|
||||
actor->incrementvitacounter(VS_DAMAGE, damage);
|
||||
}
|
||||
else
|
||||
{
|
||||
actor->incrementvitacounter(VS_FRIENDLYDAMAGE, damage);
|
||||
}
|
||||
|
||||
if(target!=actor)
|
||||
{
|
||||
if(!hitpush.iszero())
|
||||
@@ -1892,19 +1907,27 @@ void serverdamage(client *target, client *actor, int damage, int gun, bool gib,
|
||||
int targethasflag = clienthasflag(target->clientnum);
|
||||
bool tk = false, suic = false;
|
||||
target->state.deaths++;
|
||||
target->incrementvitacounter(VS_DEATHS, 1);
|
||||
|
||||
if(target!=actor)
|
||||
{
|
||||
if(!isteam(target->team, actor->team)) actor->state.frags += gib && gun != GUN_GRENADE && gun != GUN_SHOTGUN ? 2 : 1;
|
||||
if(!isteam(target->team, actor->team))
|
||||
{
|
||||
actor->state.frags += gib && gun != GUN_GRENADE && gun != GUN_SHOTGUN ? 2 : 1;
|
||||
actor->incrementvitacounter(VS_FRAGS, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
actor->state.frags--;
|
||||
actor->state.teamkills++;
|
||||
actor->incrementvitacounter(VS_TKS, 1);
|
||||
tk = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{ // suicide
|
||||
actor->state.frags--;
|
||||
actor->incrementvitacounter(VS_TKS, 1);
|
||||
suic = true;
|
||||
mlog(ACLOG_INFO, "[%s] %s suicided", actor->hostname, actor->name);
|
||||
}
|
||||
@@ -1912,6 +1935,7 @@ void serverdamage(client *target, client *actor, int damage, int gun, bool gib,
|
||||
if((suic || tk) && (m_htf || m_ktf) && targethasflag >= 0)
|
||||
{
|
||||
actor->state.flagscore--;
|
||||
actor->incrementvitacounter(VS_ANTIFLAGS, 1);
|
||||
sendf(-1, 1, "riii", SV_FLAGCNT, actor->clientnum, actor->state.flagscore);
|
||||
}
|
||||
target->position.setsize(0);
|
||||
@@ -2404,6 +2428,7 @@ int getbantype(int cn)
|
||||
if(!valid_client(cn)) return BAN_NONE;
|
||||
client &c = *clients[cn];
|
||||
if(c.type==ST_LOCAL) return BAN_NONE;
|
||||
if(c.checkvitadate(VS_BAN)) return BAN_VITA;
|
||||
if(checkgban(c.peer->address.host)) return BAN_MASTER;
|
||||
if(ipblacklist.check(c.peer->address.host)) return BAN_BLACKLIST;
|
||||
loopv(bans)
|
||||
@@ -2623,23 +2648,38 @@ void callvotepacket (int cn, voteinfo *v = curvote)
|
||||
sendpacket(cn, 1, q.finalize());
|
||||
}
|
||||
|
||||
// TODO: use AUTH code
|
||||
void changeclientrole(int client, int role, char *pwd, bool force)
|
||||
{
|
||||
pwddetail pd;
|
||||
bool success = false;
|
||||
if(!isdedicated || !valid_client(client)) return;
|
||||
pd.line = -1;
|
||||
if(force || role == CR_DEFAULT || (role == CR_ADMIN && pwd && pwd[0] && passwords.check(clients[client]->name, pwd, clients[client]->salt, &pd) && !pd.denyadmin))
|
||||
{
|
||||
if(role == clients[client]->role) return;
|
||||
|
||||
if(role == clients[client]->role) return;
|
||||
|
||||
if(force) success = true;
|
||||
|
||||
if(role == CR_ADMIN
|
||||
&& pwd
|
||||
&& pwd[0]
|
||||
&& passwords.check(clients[client]->name, pwd, clients[client]->salt, &pd)
|
||||
&& !pd.denyadmin) {
|
||||
mlog(ACLOG_INFO,"[%s] player %s used admin password in line %d", clients[client]->hostname, clients[client]->name[0] ? clients[client]->name : "[unnamed]", pd.line);
|
||||
success = true;
|
||||
}
|
||||
|
||||
if(clients[client]->checkvitadate(VS_ADMIN)) {
|
||||
mlog(ACLOG_INFO,"[%s] player %s claimed admin through valid vita claim (pubkey: %s)", clients[client]->hostname, clients[client]->name[0] ? clients[client]->name : "[unnamed]", clients[client]->pubkeyhex);
|
||||
success = true;
|
||||
}
|
||||
|
||||
if(success) {
|
||||
if(role > CR_DEFAULT)
|
||||
{
|
||||
loopv(clients) clients[i]->role = CR_DEFAULT;
|
||||
}
|
||||
clients[client]->role = role;
|
||||
sendserveropinfo(-1);
|
||||
if(pd.line > -1)
|
||||
mlog(ACLOG_INFO,"[%s] player %s used admin password in line %d", clients[client]->hostname, clients[client]->name[0] ? clients[client]->name : "[unnamed]", pd.line);
|
||||
mlog(ACLOG_INFO,"[%s] set role of player %s to %s", clients[client]->hostname, clients[client]->name[0] ? clients[client]->name : "[unnamed]", role == CR_ADMIN ? "admin" : "normal player"); // flowtron : connecting players haven't got a name yet (connectadmin)
|
||||
if(role > CR_DEFAULT) sendiplist(client);
|
||||
}
|
||||
@@ -3078,9 +3118,19 @@ void process(ENetPacket *packet, int sender, int chan)
|
||||
bool srvfull = numnonlocalclients() > scl.maxclients;
|
||||
bool srvprivate = sg->mastermode == MM_PRIVATE || sg->mastermode == MM_MATCH;
|
||||
bool matchreconnect = sg->mastermode == MM_MATCH && findscore(*cl, false);
|
||||
if (cl->pubkeyhex[0]) {
|
||||
concatformatstring(tags, ", pubkey: %s", cl->pubkeyhex);
|
||||
}
|
||||
int bl = 0, wl = nickblacklist.checkwhitelist(*cl);
|
||||
if(wl == NWL_PASS) concatstring(tags, ", nickname whitelist match");
|
||||
if(wl == NWL_UNLISTED) bl = nickblacklist.checkblacklist(cl->name);
|
||||
|
||||
bool vitawhitelist = cl->checkvitadate(VS_WHITELISTED);
|
||||
if (vitawhitelist) {
|
||||
concatstring(tags, ", vita whitelist match");
|
||||
banned = false;
|
||||
}
|
||||
|
||||
if(matchreconnect && !banned)
|
||||
{ // former player reconnecting to a server in match mode
|
||||
cl->isauthed = true;
|
||||
@@ -3114,6 +3164,24 @@ void process(ENetPacket *packet, int sender, int chan)
|
||||
}
|
||||
mlog(ACLOG_INFO, "[%s] %s logged in using the admin password in line %d%s", cl->hostname, cl->name, pd.line, tags);
|
||||
}
|
||||
else if(wantrole == CR_ADMIN && cl->checkvitadate(VS_ADMIN) && bantype != BAN_MASTER) // pass admins always through
|
||||
{ // Set as admin in vita and is trying to connect as admin
|
||||
cl->isauthed = true;
|
||||
clientrole = CR_ADMIN;
|
||||
if(bantype == BAN_VOTE)
|
||||
{
|
||||
loopv(bans) if(bans[i].address.host == cl->peer->address.host) { bans.remove(i); concatstring(tags, ", ban removed"); break; } // remove admin bans
|
||||
}
|
||||
if(srvfull)
|
||||
{
|
||||
loopv(clients) if(i != sender && clients[i]->type==ST_TCPIP)
|
||||
{
|
||||
disconnect_client(i, DISC_MAXCLIENTS); // disconnect someone else to fit maxclients again
|
||||
break;
|
||||
}
|
||||
}
|
||||
mlog(ACLOG_INFO, "[%s] %s logged in using the vita admin claim%s", cl->hostname, cl->name, tags);
|
||||
}
|
||||
else if(scl.serverpassword[0] && !(srvprivate || srvfull || banned))
|
||||
{ // server password required
|
||||
if(!strcmp(genpwdhash(cl->name, scl.serverpassword, cl->salt), cl->pwd))
|
||||
@@ -3974,6 +4042,30 @@ void process(ENetPacket *packet, int sender, int chan)
|
||||
|
||||
case SV_PAUSEMODE:
|
||||
break;
|
||||
|
||||
case SV_GETVITA:
|
||||
{
|
||||
int targetcn = getint(p);
|
||||
|
||||
// If the client is admin or could be admin through vita
|
||||
if (!(cl->role >= CR_ADMIN || cl->checkvitadate(VS_ADMIN) || cl->checkvitadate(VS_OWNER))) {
|
||||
sendservmsg("\f3You must be admin to get vita");
|
||||
break;
|
||||
}
|
||||
|
||||
if (valid_client(targetcn)) {
|
||||
client *target = clients[targetcn];
|
||||
|
||||
if (target->vita) {
|
||||
sendf(sender, 1, "riimssv", SV_VITADATA, targetcn, 32, target->pubkey.u,
|
||||
target->vita->privatecomment, target->vita->publiccomment,
|
||||
VS_NUM, target->vita->vs);
|
||||
}
|
||||
} else {
|
||||
sendservmsg("\f3Invalid cn to retrieve vita data");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SV_EXTENSION:
|
||||
{
|
||||
@@ -4425,8 +4517,8 @@ void serverslice(uint timeout) // main server update, called from cube main lo
|
||||
serverhost->totalSentData = serverhost->totalReceivedData = 0;
|
||||
loopv(clients) if(clients[i]->type == ST_TCPIP && clients[i]->isauthed && clients[i]->vita)
|
||||
{
|
||||
clients[i]->vita->vs[VS_MINUTESCONNECTED]++;
|
||||
if(clients[i]->isonrightmap && team_isactive(clients[i]->team)) clients[i]->vita->vs[VS_MINUTESACTIVE]++;
|
||||
clients[i]->incrementvitacounter(VS_MINUTESCONNECTED, 1);
|
||||
if(clients[i]->isonrightmap && team_isactive(clients[i]->team)) clients[i]->incrementvitacounter(VS_MINUTESACTIVE, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -353,6 +353,12 @@ struct client // server side version of "dynent" type
|
||||
extern int servclock;
|
||||
return vita && (vita->vs[n] == 1 || (servclock - vita->vs[n]) < 0);
|
||||
}
|
||||
|
||||
void incrementvitacounter(int vitastat, int amount)
|
||||
{
|
||||
if (!vita) return;
|
||||
vita->vs[vitastat] += amount;
|
||||
}
|
||||
};
|
||||
|
||||
struct ban
|
||||
|
||||
@@ -1983,9 +1983,9 @@ void serverparameters_dumpdocu(char *fname)
|
||||
|
||||
// player ID bookkeeping
|
||||
|
||||
const char *vskeywords[VS_NUM + 1] = { "first", "last", "mute", "novote", "ban", "admin", "owner", "mapupload", "minconn", "minact", "flags", "frags", "deaths", "tks", "damage", "ff", "" };
|
||||
const char *vsnames[VS_NUM + 1] = { "first login", "last login", "muted until", "novote until", "banned until", "admin until", "owner until", "mapupload until",
|
||||
"minutes connected", "minutes active", "flags", "frags", "deaths", "teamkills", "damage dealt", "team damage dealt", "" };
|
||||
const char *vskeywords[VS_NUM + 1] = { "first", "last", "ban", "whitelist", "admin", "owner", "minconn", "minact", "flags", "antiflags", "frags", "deaths", "tks", "suicides", "damage", "ff", "" };
|
||||
const char *vsnames[VS_NUM + 1] = { "first login", "last login", "banned until", "whitelisted untils", "admin until", "owner until",
|
||||
"minutes connected", "minutes active", "flags", "flag score lost", "frags", "deaths", "teamkills", "suicides", "damage dealt", "team damage dealt", "" };
|
||||
hashtable<uchar32, vita_s> vitas;
|
||||
|
||||
void vita_s::addname(const char *name)
|
||||
|
||||
Reference in New Issue
Block a user