From 0b656d2380dd7de9d43c8280d18bc4dfdc2a9e50 Mon Sep 17 00:00:00 2001 From: ac-stef Date: Fri, 26 Aug 2016 16:28:08 +0200 Subject: [PATCH] add server-side player vita database The server keeps track of every player ID that ever connected successfully. This database stores the last names and IPs used by the player and the last countries the connections came from. Some stats are counted, like "time connected" and "total tks". Most importantly, the database stores rights (like admin) and restrictions (like ban). All rights and restrictions are kept as dates (or timecodes), so, for example, if your ban date is in the future, you are banned - and if it's in the past, you can play. The collected stats are meant to help server owners at managing players - especially, if they want to give someone admin rights. The database is periodically saved to a file and read from that file on startup. The default file is config/servervita.cfg and one backup is kept in config/servervita_bak.cfg. During server start, if the regular file can't be loaded, the backup is tried instead. While the server is running, it checks periodically for a file named config/servervita_update.cfg, and if the file exists, it is renamed to a backup filename including a timecode and read and merged into the database. The automatic rename makes sure, that a file is only read once. Using such updates, a server owner can easily give several people admin rights or for example, add clan tags for a whole list of IDs. (Yes, the database also stores a clan tag for every ID...) The database also stores two short comments for every ID. One can only be read by server admins, but the other one is public. The public reason, for example, can state a reason for a ban. One database record takes up 264 bytes of RAM. During autosave, accounts, that have not been used for a specified time (default 12 months), will not be written. After a server restart, those old records will be gone. Currently, there is no process in place, to also clean up old records from RAM. Maybe, that'll be added later... --- .gitignore | 1 + config/servercmdline.txt | 1 + config/serverparameters.cfg | 8 ++ source/src/clients2c.cpp | 20 ++++- source/src/command.cpp | 26 ------ source/src/crypto.cpp | 4 +- source/src/protocol.cpp | 32 +++++++ source/src/protocol.h | 2 + source/src/protos.h | 10 ++- source/src/server.cpp | 71 +++++++++++++-- source/src/server.h | 25 +++++- source/src/serverfiles.h | 175 ++++++++++++++++++++++++++++++++++++ source/src/tools.h | 14 +-- 13 files changed, 344 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index bcd9169f..aca7538a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ config/servers.cfg config/configtemplates.zip config/mapmodelattributes.cfg config/entropy.dat +config/servervita* packages/maps/*.cfg !packages/maps/new_content.cfg packages/maps/*.cgz diff --git a/config/servercmdline.txt b/config/servercmdline.txt index 9af8915a..6e1b57ec 100644 --- a/config/servercmdline.txt +++ b/config/servercmdline.txt @@ -40,6 +40,7 @@ // -H // server logfile path, default logs/ // -r // server maprot file, default config/maprot.cfg // -A // server parameter file, default config/serverparameters.cfg +// -U // vitapath, path & filename prefix to store the server vita database at, default: config/servervita // -X // server passwords file, default config/serverpwd.cfg // -B // server IP blacklist file, default config/serverblacklist.cfg // -G // server geoip file, default config/geoip.cfg diff --git a/config/serverparameters.cfg b/config/serverparameters.cfg index 402e5f6b..c15305a9 100644 --- a/config/serverparameters.cfg +++ b/config/serverparameters.cfg @@ -19,6 +19,14 @@ auth_verify_ip:0 // integer [0..1], default 1, 0:disabled, 1:enabled mandatory_auth:1 +// vitaautosave Vita file autosave interval in minutes (0: only when server is empty) +// integer [0..1440], default 10 +vitaautosave:10 + +// vitamaxage Omit vitas from autosave, if the last login has been more than the specified number of months ago +// integer [3..120], default 12 +vitamaxage:12 + // **** Maprot settings **** diff --git a/source/src/clients2c.cpp b/source/src/clients2c.cpp index 8a955400..3c7781f2 100644 --- a/source/src/clients2c.cpp +++ b/source/src/clients2c.cpp @@ -321,7 +321,13 @@ VARP(voicecomsounds, 0, 1, 2); bool medals_arrived=0; medalsst a_medals[END_MDS]; -struct session_s { enet_uint32 serverip, clientip, clientipcensored, curpeerip; int curpeerport, serverclock, clientclock, cn, clientsalt; uchar serverpubkey[32], clientsignature[64]; } session; +struct session_s +{ + enet_uint32 serverip, clientip, clientipcensored, curpeerip; + int curpeerport, serverclock, clientclock, cn, clientsalt, datecodes; + uchar serverpubkey[32], clientsignature[64]; + string clan, publiccomment; +} session; void parsemessages(int cn, playerent *d, ucharbuf &p, bool demo = false) { @@ -443,6 +449,18 @@ void parsemessages(int cn, playerent *d, ucharbuf &p, bool demo = false) formatstring(servauth)("SERVINFOSIGNED<%s>", bin2hex(tmp, (uchar*)s->clientsignature, 64)); memmove(servauth + 64, servauth, (auth = (int)strlen(servauth)) + 1); p.get((uchar*)servauth, 64); + s->datecodes = getint(p); + getstring(text, p); + filtertext(s->clan, text, FTXT__VITACLAN); + getstring(text, p); + filtertext(s->publiccomment, text, FTXT__VITACOMMENT); +#ifdef _DEBUG + formatstring(text)("server-side%s", s->datecodes ? " timed flags:" : ""); + loopi(VS_NUMCOUNTERS) if(s->datecodes & (1 << i)) concatformatstring(text, " %s", vskeywords[i]); + if(s->clan[0]) concatformatstring(text, " clan tag: \"%s\"", s->clan); + if(s->publiccomment[0]) concatformatstring(text, " comment: \"%s\"", s->publiccomment); + if(strlen(text) > 12) conoutf("%s", text); +#endif } if(curpeer && sk && !auth) conoutf("\f3server refuses to authenticate"); if(sk && auth) diff --git a/source/src/command.cpp b/source/src/command.cpp index fffb24e6..a79ef267 100644 --- a/source/src/command.cpp +++ b/source/src/command.cpp @@ -1492,32 +1492,6 @@ COMMANDF(rnd, "i", (int *a) { intret(*a>0 ? rnd(*a) : 0); }); #ifndef STANDALONE -const char *escapestring(const char *s, bool force, bool noquotes) -{ - static vector strbuf[3]; - static int stridx = 0; - if(noquotes) force = false; - if(!s) return force ? "\"\"" : ""; - if(!force && !*(s + strcspn(s, "\"/\\;()[] \f\t\n\r$"))) return s; - stridx = (stridx + 1) % 3; - vector &buf = strbuf[stridx]; - buf.setsize(0); - if(!noquotes) buf.add('"'); - for(; *s; s++) switch(*s) - { - case '\n': buf.put("\\n", 2); break; - case '\r': buf.put("\\n", 2); break; - case '\t': buf.put("\\t", 2); break; - case '\a': buf.put("\\a", 2); break; - case '\f': buf.put("\\f", 2); break; - case '"': buf.put("\\\"", 2); break; - case '\\': buf.put("\\\\", 2); break; - default: buf.add(*s); break; - } - if(!noquotes) buf.add('"'); - buf.add(0); - return buf.getbuf(); -} COMMANDF(escape, "s", (const char *s) { result(escapestring(s));}); int sortident(ident **a, ident **b) { return strcasecmp((*a)->name, (*b)->name); } diff --git a/source/src/crypto.cpp b/source/src/crypto.cpp index ef1c4df9..58587c51 100644 --- a/source/src/crypto.cpp +++ b/source/src/crypto.cpp @@ -1092,7 +1092,7 @@ static int certtypemaxrank[] = { 0, 1, 2, 3, 3, 3, 0 }; // max allowed rank of static int certtypeexpiration[] = { 0, 0, 0, 2 * CERTTIMEMONTH, CERTTIMEMONTH, 3 * CERTTIMEMONTH, CERTTIMEMONTH }; // time after which certs are usually re-signed, after 3x the time they expire vector certs; -hashtable certblacklist; +hashtable certblacklist; static inline int aktcerttime() { return time(NULL) / (time_t) 60; } // use minutes instead of seconds as timebase, so int will be more than enough @@ -1345,7 +1345,7 @@ void rebuildcerttree() // determines the rank of all certs; ages them; rem if(c->days2renew < 0) c->needsrenewal = true; if(c->days2expire < 0) { c->expired = true; c->isvalid = false; } } - if(c->pubkey && certblacklist.access(c->pubkey)) + if(c->pubkey && certblacklist.access(*((uchar32*)c->pubkey))) { c->blacklisted = true; c->isvalid = false; diff --git a/source/src/protocol.cpp b/source/src/protocol.cpp index 0887c75a..49e71ffa 100644 --- a/source/src/protocol.cpp +++ b/source/src/protocol.cpp @@ -391,6 +391,7 @@ void filterrichtext(char *dst, const char *src, int len) case 'a': c = '\a'; break; case 't': c = '\t'; break; case 'n': c = '\n'; break; + case '_': c = ' '; break; case 'x': b = 16; c = *++src; @@ -410,6 +411,37 @@ void filterrichtext(char *dst, const char *src, int len) *dst = '\0'; } +// counterpart of filterrichtext + +const char *escapestring(const char *s, bool force, bool noquotes, vector *buf) +{ + static vector strbuf[3]; + static int stridx = 0; + extern bool isdedicated; + if(noquotes) force = false; + if(!s) return force ? "\"\"" : ""; + if(!force && !*(s + strcspn(s, "\"/\\;()[] \f\t\a\n\r$"))) return s; + if(!buf) buf = &strbuf[stridx++]; + stridx %= 3; + buf->setsize(0); + if(!noquotes) buf->add('"'); + for(; *s; s++) switch(*s) + { + case '\n': buf->put("\\n", 2); break; + case '\r': buf->put("\\n", 2); break; + case '\t': buf->put("\\t", 2); break; + case '\a': buf->put("\\a", 2); break; + case '\f': buf->put("\\f", 2); break; + case '"': buf->put("\\\"", 2); break; + case '\\': buf->put("\\\\", 2); break; + case ' ': if(isdedicated) { buf->put("\\_", 2); break; } + default: buf->add(*s); break; + } + if(!noquotes) buf->add('"'); + buf->add(0); + return buf->getbuf(); +} + // ensures, that d is either two lowercase chars or "" void filterlang(char *d, const char *s) diff --git a/source/src/protocol.h b/source/src/protocol.h index 225c5c19..43e29ddc 100644 --- a/source/src/protocol.h +++ b/source/src/protocol.h @@ -232,3 +232,5 @@ enum #define FTXT__ZIPDESC (FTXT_NOWHITE | FTXT_ALLOWBLANKS | FTXT_TABTOBLANK | FTXT_NOCOLOR | FTXT_SAFECS) #define FTXT__CERTFILENAME (FTXT_MAPNAME | FTXT_TOLOWER) #define FTXT__CONSOLEPASTE (FTXT_TABTOBLANK) +#define FTXT__VITACOMMENT (FTXT_NOWHITE | FTXT_ALLOWBLANKS | FTXT_NOCOLOR | FTXT_TABTOBLANK) +#define FTXT__VITACLAN (FTXT_NOWHITE | FTXT_NOCOLOR) diff --git a/source/src/protos.h b/source/src/protos.h index 0a20c48a..5e86b3af 100644 --- a/source/src/protos.h +++ b/source/src/protos.h @@ -1019,7 +1019,6 @@ extern void writecfg(); extern void deletecfg(); extern void identnames(vector &names, bool builtinonly); extern void explodelist(const char *s, vector &elems); -extern const char *escapestring(const char *s, bool force = true, bool noquotes = false); extern int execcontext; extern void pushscontext(int newcontext); extern int popscontext(); @@ -1030,6 +1029,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 }; +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); extern void localservertoclient(int chan, uchar *buf, int len, bool demo = false); @@ -1073,6 +1075,7 @@ extern void freegzbuf(ucharbuf *p); // free a ucharbuf created by getgzbuf() extern bool validmapname(const char *s); // checks for length, allowed chars and special DOS filenames extern char *filtertext(char *dst, const char *src, int flags, int len = sizeof(string)-1); extern void filterrichtext(char *dst, const char *src, int len = sizeof(string)-1); +extern const char *escapestring(const char *s, bool force = true, bool noquotes = false, vector *buf = NULL); extern void filterlang(char *d, const char *s); extern void filtercountrycode(char *d, const char *s); // returns exactly two uppercase chars or "--" extern void trimtrailingwhitespace(char *s); @@ -1155,7 +1158,7 @@ struct serverconfigfile struct servercommandline { int uprate, serverport, syslogfacility, filethres, syslogthres, maxdemos, maxclients, kickthreshold, banthreshold, verbose, incoming_limit, afk_limit, ban_time, demotimelocal; - const char *ip, *master, *logident, *serverpassword, *adminpasswd, *demopath, *maprotfile, *pwdfile, *blfile, *geoipfile, *nbfile, *infopath, *motdpath, *forbidden, *demofilenameformat, *demotimestampformat, *service, *logfilepath, *parfilepath, *ssk; + const char *ip, *master, *logident, *serverpassword, *adminpasswd, *demopath, *maprotfile, *pwdfile, *blfile, *geoipfile, *nbfile, *infopath, *motdpath, *forbidden, *demofilenameformat, *demotimestampformat, *service, *logfilepath, *parfilepath, *ssk, *vitabasename; bool logtimestamp, demo_interm, loggamestatus; string motd, servdesc_full, servdesc_pre, servdesc_suf, voteperm, mapperm; int clfilenesting; @@ -1165,7 +1168,7 @@ struct servercommandline maxclients(DEFAULTCLIENTS), kickthreshold(-5), banthreshold(-6), verbose(0), incoming_limit(10), afk_limit(45000), ban_time(20*60*1000), demotimelocal(0), ip(""), master(NULL), logident(""), serverpassword(""), adminpasswd(""), demopath(""), maprotfile("config/maprot.cfg"), pwdfile("config/serverpwd.cfg"), blfile("config/serverblacklist.cfg"), geoipfile("config/geoip.cfg"), nbfile("config/nicknameblacklist.cfg"), - infopath("config/serverinfo"), motdpath("config/motd"), forbidden("config/forbidden.cfg"), service(NULL), logfilepath("logs/"), parfilepath("config/serverparameters.cfg"), ssk(NULL), + infopath("config/serverinfo"), motdpath("config/motd"), forbidden("config/forbidden.cfg"), service(NULL), logfilepath("logs/"), parfilepath("config/serverparameters.cfg"), ssk(NULL), vitabasename("config/servervita"), logtimestamp(false), demo_interm(false), loggamestatus(true), clfilenesting(0) { @@ -1222,6 +1225,7 @@ struct servercommandline break; case 'H': logfilepath = a; break; case 'A': parfilepath = a; break; + case 'U': vitabasename = a; break; case 'c': if(ai > 0) maxclients = min(ai, MAXCLIENTS); break; case 'k': { diff --git a/source/src/server.cpp b/source/src/server.cpp index 45734ea8..8d87490c 100644 --- a/source/src/server.cpp +++ b/source/src/server.cpp @@ -112,12 +112,14 @@ int cn2boot; const char *endis[] = { "disabled", "enabled", "" }; SERVPARLIST(dumpmaprot, 0, 1, 0, endis, "ddump maprot parameters for all maps (once, to logs/debug/maprot_debug_verbose.txt)"); SERVPARLIST(dumpparameters, 0, 0, 0, endis, "ddump server parameters when updated"); +SERVPAR(vitaautosave, 0, 10, 24 * 60, "sVita file autosave interval in minutes (0: only when server is empty)"); +SERVPAR(vitamaxage, 3, 12, 120, "sOmit vitas from autosave, if the last login has been more than the specified number of months ago"); void poll_serverthreads() // called once per mainloop-timeslice { static vector servermapstodelete; - static int stage = 0, lastworkerthreadstart = 0, nextserverconfig = 0; - static bool debugmaprot = true; + static int stage = 0, lastworkerthreadstart = 0, nextserverconfig = 0, lastvitasave = 0; + static bool debugmaprot = true, serverwasnotempty = false; switch(stage) // cycle through some tasks once per minute (mostly file polling) { @@ -188,7 +190,15 @@ void poll_serverthreads() // called once per mainloop-timeslice serverconfigs[nextserverconfig]->process(); // processing of config files is done in mainloop, one at a time nextserverconfig++; } - else stage++; + else + { + if(vitaupdatebuf) + { + mlog(ACLOG_INFO, "read/updated %d vitas from %s", parsevitas(vitaupdatebuf, vitaupdatebuflen), vitafilename_update_backup);; + DELETEA(vitaupdatebuf); + } + stage++; + } } break; } @@ -215,6 +225,25 @@ void poll_serverthreads() // called once per mainloop-timeslice delete f; } } + else if(!vitastosave && serverwasnotempty && (numclients() == 0 || (vitaautosave && (servmillis - lastvitasave) > 1000 * 60 * vitaautosave))) + { + serverwasnotempty = false; + lastvitasave = servmillis; + vitastosave = new vector; + vitakey_s vk; + int too_old = 0; + enumeratekt(vitas, uchar32, k, vita_s, v, + { + if((servclock - v.vs[VS_LASTLOGIN]) / (60 * 24 * 31) <= vitamaxage) + { + vk.k = &k; + vk.v = &v; + vitastosave->add(vk); + } + else too_old++; + }); + if(too_old) mlog(ACLOG_VERBOSE, "omitted %d vitas from autosave (exceeding maxage of %d months)", too_old, vitamaxage); + } else stage++; } case 5: // pause worker threads for a while (restart once a minute) @@ -228,6 +257,7 @@ void poll_serverthreads() // called once per mainloop-timeslice delete servermapstodelete.remove(i); // delete outdated servermaps } } + else serverwasnotempty = true; break; } } @@ -2994,11 +3024,24 @@ void polldeferredprocessing() client *cl = clients[sa->cn]; if(sa->wantauth) { - memcpy(cl->pubkey, sa->clientpubkey, 32); + memcpy(cl->pubkey.u, sa->clientpubkey, 32); memcpy(cl->servinforesponse, sa->clientmsg, sa->clientmsglen + 64); - bin2hex(cl->pubkeyhex, cl->pubkey, 32); + bin2hex(cl->pubkeyhex, cl->pubkey.u, 32); + cl->vita = vitas.access(cl->pubkey); + if(!cl->vita) + { + vita_s v; + memset(&v, 0, sizeof(v)); + cl->vita = &vitas.access(cl->pubkey, v); + cl->vita->vs[VS_FIRSTLOGIN] = servclock; + } + if(cl->country[0] != '-') cl->vita->addcc(cl->country); + cl->vita->addip(cl->ip); + cl->vita->vs[VS_LASTLOGIN] = servclock; cl->needsauth = true; // may have been voluntary, but it's done - sendf(sa->cn, 1, "riim", SV_SERVINFO_CONTD, 1, 64, sa->servermsg); + int datecodes = 0, *vs = cl->vita->vs; + loopi(VS_NUMCOUNTERS) datecodes |= vs[i] == 1 || (servclock - vs[i]) < 0 ? (1 << i) : 0; + sendf(sa->cn, 1, "riimiss", SV_SERVINFO_CONTD, 1, 64, sa->servermsg, datecodes, cl->vita->clan, cl->vita->publiccomment); } else sendf(sa->cn, 1, "rii", SV_SERVINFO_CONTD, 0); } @@ -3152,6 +3195,7 @@ void process(ENetPacket *packet, int sender, int chan) } } + if(cl->vita) cl->vita->addname(cl->name); sendwelcome(cl); if(restorescore(*cl)) { sendresume(*cl, true); senddisconnectedscores(-1); } else if(cl->type==ST_TCPIP) senddisconnectedscores(sender); @@ -3449,6 +3493,7 @@ void process(ENetPacket *packet, int sender, int chan) break; } } + if(cl->vita) cl->vita->addname(cl->name); } break; } @@ -4356,6 +4401,11 @@ void serverslice(uint timeout) // main server update, called from cube main lo linequalitystats(0); } 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]++; + } } ENetEvent event; @@ -4650,6 +4700,15 @@ void initserver(bool dedicated) loopi(scl.maxclients) serverhost->peers[i].data = (void *)-1; if(scl.ssk) mlog(ACLOG_INFO,"server public key: %s", servpubkey); + formatstring(vitafilename)("%s.cfg", scl.vitabasename); + formatstring(vitafilename_backup)("%s_bak.cfg", scl.vitabasename); + formatstring(vitafilename_update)("%s_update.cfg", scl.vitabasename); + formatstring(vitafilename_update_backup_base)("%s_update_", scl.vitabasename); + path(vitafilename); path(vitafilename_backup); path(vitafilename_update); path(vitafilename_update_backup_base); + char *vn; + int gotvitas = readvitas((vn = vitafilename)); + if(gotvitas < 0) gotvitas = readvitas((vn = vitafilename_backup)); + if(gotvitas >= 0) mlog(ACLOG_INFO, "read %d player vitas from %s", gotvitas, vn); maprot.init(scl.maprotfile); passwords.init(scl.pwdfile, scl.adminpasswd); ipblacklist.init(scl.blfile); diff --git a/source/src/server.h b/source/src/server.h index e6093c98..f3b9b59d 100644 --- a/source/src/server.h +++ b/source/src/server.h @@ -223,6 +223,27 @@ struct medals } }; +#define VITANAMEHISTLEN 4 +#define VITAIPHISTLEN 4 +#define VITACCHISTLEN 4 +#define VITACOMMENTLEN 31 +#define VITACLANLEN 7 + +struct vita_s +{ + char namehist[VITANAMEHISTLEN][MAXNAMELEN + 1]; + enet_uint32 iphist[VITAIPHISTLEN]; + char privatecomment[VITACOMMENTLEN + 1], publiccomment[VITACOMMENTLEN + 1]; // private comments can be seen by admins and owners only, public comments can be seen by everyone + char clan[VITACLANLEN + 1]; + char cchist[VITACCHISTLEN * 2]; + int vs[VS_NUM]; + void addname(const char *name); + void addip(enet_uint32 ip); + void addcc(const char *cc); +}; + +struct vitakey_s { const vita_s *v; const uchar32 *k; }; + struct client // server side version of "dynent" type { int type; @@ -232,8 +253,9 @@ struct client // server side version of "dynent" type enet_uint32 ip, ip_censored; int connectclock; char country[4]; - uchar pubkey[32]; + uchar32 pubkey; char pubkeyhex[68]; + vita_s *vita; string servinforesponse; string name; int team; @@ -334,6 +356,7 @@ struct client // server side version of "dynent" type name[0] = pwd[0] = demoflags = 0; ip = ip_censored = 0; pubkeyhex[0] = '\0'; + vita = NULL; bottomRTT = ping = 9999; team = TEAM_SPECT; state.state = CS_SPECTATE; diff --git a/source/src/serverfiles.h b/source/src/serverfiles.h index e77a64d6..e2324fd9 100644 --- a/source/src/serverfiles.h +++ b/source/src/serverfiles.h @@ -715,6 +715,11 @@ void serverconfigfile::process() // in main thread: refill data structures with } } +string vitafilename, vitafilename_backup, vitafilename_update, vitafilename_update_backup_base, vitafilename_update_backup; +char *vitaupdatebuf = NULL; +int vitaupdatebuflen = 0; +vector *vitastosave = NULL; + int readserverconfigsthread(void *data) { while(1) @@ -723,6 +728,27 @@ int readserverconfigsthread(void *data) loopv(serverconfigs) serverconfigs[i]->trypreload(); + // special treatment for vita updates: read once and move out of the way + if(!vitaupdatebuf && getfilesize(vitafilename_update) > 0) + { + formatstring(vitafilename_update_backup)("%s%d.cfg", vitafilename_update_backup_base, (int)(time(NULL) / 60)); + backup(vitafilename_update, vitafilename_update_backup); + serverconfigfile vsf; + vsf.init(vitafilename_update_backup, false); + vsf.load(); + vitaupdatebuf = vsf.buf; + vitaupdatebuflen = vsf.filelen; + vsf.buf = NULL; + } + if(vitastosave) + { // mainloop prepared a selection of vitas to save + backup(vitafilename, vitafilename_backup); + stream *f = openfile(vitafilename, "w"); + extern void writevitas(stream *f, vector &vs); + if(f) writevitas(f, *vitastosave); + DELETEP(f); DELETEP(vitastosave); + } + entropy_save(); recheckallserverconfigs = false; } @@ -1804,4 +1830,153 @@ void serverparameters_dumpdocu(char *fname) } #endif +// 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", "" }; +hashtable vitas; + +void vita_s::addname(const char *name) +{ + int rem = 0; + for(; rem < VITANAMEHISTLEN - 1; rem++) if(!strcasecmp(name, namehist[rem])) break; + if(rem) memmove(namehist[1], namehist[0], rem * (MAXNAMELEN + 1)); + copystring(namehist[0], name, MAXNAMELEN + 1); +} + +void vita_s::addip(enet_uint32 ip) +{ + int rem = 0; + for(; rem < VITAIPHISTLEN - 1; rem++) if(iphist[rem] == ip) break; + if(rem) memmove(iphist + 1, iphist, rem * sizeof(ip)); + iphist[0] = ip; +} + +void vita_s::addcc(const char *cc) +{ + int rem = 0; + for(; rem < VITACCHISTLEN - 1; rem++) if(cchist[rem * 2] == cc[0] && cchist[rem * 2 + 1] == cc[1]) break; + if(rem) memmove(cchist + 2, iphist, rem * 2); + loopi(2) cchist[i] = cc[i]; +} + +int parsevitas(char *buf, int filelen) +{ + vita_s v, *vp = &v; + uchar32 pubkey; + string tmp; + enet_uint32 ip; + int res = 0; + char *l, *r, *p = buf, *pe = p + filelen, *b, *o; + while(p < pe) + { + l = p; p += strlen(p) + 1; + l = strtok_r(l, " \t", &b); + while(l && (r = strtok_r(NULL, " \t", &b))) + { + filterrichtext(r, r); + if(!strcmp(l, "PUBKEY")) + { + if(hex2bin(pubkey.u, r, 32) == 32) + { + if(!(vp = vitas.access(pubkey))) + { + memset(&v, 0, sizeof(v)); + vp = &vitas.access(pubkey, v); + } + res++; + } + else vp = &v; // bad pubkey: point to dummy + break; + } + else if(!strcmp(l, "NAME")) + { + filtertext(tmp, r, FTXT__PLAYERNAME, MAXNAMELEN); + if(*tmp) vp->addname(tmp); + } + else if(!strcmp(l, "PUBCOM") || !strcmp(l, "PRIVCOM")) + { + filtertext(tmp, r, FTXT__VITACOMMENT, VITACOMMENTLEN); + copystring(l[1] == 'U' ? vp->publiccomment : vp->privatecomment, tmp, VITACOMMENTLEN + 1); + break; + } + else if(!strcmp(l, "IP")) + { + if(atoip(r, &ip) && ip) vp->addip(ip); + } + else if(!strcmp(l, "CC")) + { + filtercountrycode(r, r); + if(*r != '-') vp->addcc(r); + } + else if(!strcmp(l, "CLAN")) + { + filtertext(tmp, r, FTXT__VITACLAN, VITACLANLEN); + copystring(vp->clan, tmp, VITACLANLEN + 1); + break; + } + else if(!strcmp(l, "STAT")) + { + if((o = strchr(r, ':'))) // key:value + { + *o++ = '\0'; // r is key, o is value + loopk(VS_NUM) if(!strcmp(vskeywords[k], r)) + { + vp->vs[k] = atoi(o); + break; + } + } + } + } + } + return res; +} + +bool readvitas(const char *fname) +{ + serverconfigfile vsf; + vsf.init(fname, false); + vsf.load(); + return vsf.buf ? parsevitas(vsf.buf, vsf.filelen) : -1; +} + +void writevitas(stream *f, vector &vs) +{ + string tmp; + vector ctmp; + f->printf("// AC server vita database\n// autosaved %s\n// (don't edit this file, while the server is running)\n\n", timestring(true, "%c", tmp)); + loopvj(vs) + { + const vita_s &v = *vs[j].v; + f->printf("PUBKEY %s", bin2hex(tmp, vs[j].k->u, 32)); + if(v.namehist[0][0]) + { + f->printf("\n\tNAME"); + loopirev(VITANAMEHISTLEN) if(v.namehist[i][0]) f->printf(" %s", escapestring(v.namehist[i], true, true, &ctmp)); + } + if(v.iphist[0]) + { + f->printf("\n\tIP"); + loopirev(VITAIPHISTLEN) if(v.iphist[i]) f->printf(" %s", iptoa(v.iphist[i], tmp)); + } + if(v.privatecomment[0]) f->printf("\n\tPRIVCOM %s", escapestring(v.privatecomment, true, true, &ctmp)); + if(v.publiccomment[0]) f->printf("\n\tPUBCOM %s", escapestring(v.publiccomment, true, true, &ctmp)); + if(v.clan[0]) f->printf("\n\tCLAN %s", escapestring(v.clan, true, true, &ctmp)); + if(v.cchist[0]) + { + f->printf("\n\tCC"); + loopirev(VITACCHISTLEN) if(v.cchist[i * 2]) f->printf(" %c%c", v.cchist[i * 2], v.cchist[i * 2 + 1]); + } + f->printf("\n\tSTAT"); + loopi(VS_NUM) if(v.vs[i]) f->printf(" %s:%d", vskeywords[i], v.vs[i]); + f->printf("\n\n"); + } +} + + + + + + diff --git a/source/src/tools.h b/source/src/tools.h index f984a88f..3b6be9d2 100644 --- a/source/src/tools.h +++ b/source/src/tools.h @@ -506,14 +506,16 @@ static inline bool htcmp(const char *x, const char *y) return !strcmp(x, y); } -static inline uint hthash(const uchar *key) +struct uchar32 { uchar u[32]; }; + +static inline uint hthash(const uchar32 &key) { - return *((uint *)key); + return *((uint *)&key); } -static inline bool htcmp(const uchar *x, const uchar *y) // assume "uchar *" points to 32byte public keys +static inline bool htcmp(const uchar32 &x, const uchar32 &y) { - return !memcmp(x, y, 32); + return !memcmp(&x, &y, 32); } static inline uint hthash(int key) @@ -590,10 +592,10 @@ template struct hashtable } chain *c = unused; unused = unused->next; - c->key = key; c->next = table[h]; - table[h] = c; + c->key = key; numelems++; + table[h] = c; return c; }