summaryrefslogtreecommitdiff
path: root/src/server/sv_client.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/sv_client.cpp')
-rw-r--r--src/server/sv_client.cpp1949
1 files changed, 1949 insertions, 0 deletions
diff --git a/src/server/sv_client.cpp b/src/server/sv_client.cpp
new file mode 100644
index 0000000..0a54a32
--- /dev/null
+++ b/src/server/sv_client.cpp
@@ -0,0 +1,1949 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2013 Darklegion Development
+Copyright (C) 2012-2018 ET:Legacy team <mail@etlegacy.com>
+Copyright (C) 2015-2019 GrangerHub
+
+This file is part of Tremulous.
+
+Tremulous is free software; you can redistribute it
+and/or modify it under the terms of the GNU General Public License as
+published by the Free Software Foundation; either version 3 of the License,
+or (at your option) any later version.
+
+Tremulous is distributed in the hope that it will be
+useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Tremulous; if not, see <https://www.gnu.org/licenses/>
+
+===========================================================================
+*/
+// sv_client.c -- server code for dealing with clients
+
+#include "server.h"
+
+static void SV_CloseDownload( client_t *cl );
+
+/*
+=================
+SV_RSA_VerifySignature
+
+Verifies the signature of data and on success it returns the sha256
+fingerprint of the public key. pubkey, signature, and fingerprint
+are all base 16 encoded strings.
+
+Returns true on success
+=================
+*/
+static bool SV_RSA_VerifySignature( const char *pubkey, const char *signature, const char *data, char *fingerprint )
+{
+ struct rsa_public_key public_key;
+ struct sha256_ctx sha256_hash;
+ uint8_t buf[RSA_STRING_LENGTH];
+ int err;
+ mpz_t n;
+
+ if ( !*pubkey || !*signature )
+ return false;
+
+ // load public key
+ rsa_public_key_init( &public_key );
+ mpz_set_ui( public_key.e, RSA_PUBLIC_EXPONENT );
+ err = mpz_set_str( public_key.n, pubkey, 16 );
+ if ( err ) {
+ rsa_public_key_clear( &public_key );
+ return false;
+ }
+
+ err = !rsa_public_key_prepare( &public_key );
+ if ( err ) {
+ rsa_public_key_clear( &public_key );
+ return false;
+ }
+
+ // load signature
+ mpz_init( n );
+ err = mpz_set_str( n, signature, 16 );
+ if ( err ) {
+ mpz_clear( n );
+ rsa_public_key_clear( &public_key );
+ return false;
+ }
+
+ // hash data
+ sha256_init( &sha256_hash );
+ sha256_update( &sha256_hash, strlen(data), (uint8_t *) data);
+
+ if ( !rsa_sha256_verify( &public_key, &sha256_hash, n ) ) {
+ mpz_clear( n );
+ rsa_public_key_clear( &public_key );
+ return false;
+ }
+
+ // VERIFIED, save the sha256 fingerprint of the key
+ nettle_mpz_get_str_256( sizeof(buf), buf, public_key.n );
+
+ sha256_update( &sha256_hash, sizeof(buf), buf );
+ sha256_digest( &sha256_hash, SHA256_DIGEST_SIZE, buf );
+
+ nettle_mpz_set_str_256_u( n, SHA256_DIGEST_SIZE, buf );
+ mpz_get_str( fingerprint, 16, n );
+
+ mpz_clear( n );
+ rsa_public_key_clear( &public_key );
+ return true;
+}
+
+/*
+=================
+SV_GetChallenge
+
+A "getchallenge" OOB command has been received
+Returns a challenge number that can be used
+in a subsequent connectResponse command.
+We do this to prevent denial of service attacks that
+flood the server with invalid connection IPs. With a
+challenge, they must give a valid IP address.
+
+If we are authorizing, a challenge request will cause a packet
+to be sent to the authorize server.
+
+When an authorizeip is returned, a challenge response will be
+sent to that ip.
+
+ioquake3: we added a possibility for clients to add a challenge
+to their packets, to make it more difficult for malicious servers
+to hi-jack client connections.
+Also, the auth stuff is completely disabled for com_standalone games
+as well as IPv6 connections, since there is no way to use the
+v4-only auth server for these new types of connections.
+=================
+*/
+void SV_GetChallenge(netadr_t from)
+{
+ int i;
+ int oldest;
+ int oldestTime;
+ int oldestClientTime;
+ int clientChallenge;
+ challenge_t *challenge;
+ bool wasfound = false;
+ byte buf[16];
+ mpz_t n;
+
+ if (sv_protect->integer & SVP_IOQ3)
+ if ( SVC_RateLimitAddress( from, 10, 1000 ) ) {
+ {
+ Com_DPrintf( "SV_GetChallenge: rate limit from %s exceeded, dropping request\n",
+ // Prevent using getchallenge as an amplifier
+ NET_AdrToString( from ) );
+ if (SVC_RateLimitAddress(from, 10, 1000))
+ return;
+ {
+ }
+ SV_WriteAttackLog(va("SV_GetChallenge: rate limit from %s exceeded, dropping request\n",
+ NET_AdrToString(from)));
+ return;
+ }
+
+
+ // Allow getchallenge to be DoSed relatively easily, but prevent
+ // Allow getchallenge to be DoSed relatively easily, but prevent
+ // excess outbound bandwidth usage when being flooded inbound
+ // excess outbound bandwidth usage when being flooded inbound
+ if ( SVC_RateLimit( &outboundLeakyBucket, 10, 100 ) ) {
+ if (SVC_RateLimit(&outboundLeakyBucket, 10, 100))
+ Com_DPrintf( "SV_GetChallenge: rate limit exceeded, dropping request\n" );
+ {
+ return;
+ SV_WriteAttackLog("SV_GetChallenge: rate limit exceeded, dropping request\n");
+ return;
+ }
+ }
+ }
+
+ oldest = 0;
+ oldestClientTime = oldestTime = 0x7fffffff;
+
+ // see if we already have a challenge for this ip
+ challenge = &svs.challenges[0];
+ clientChallenge = atoi(Cmd_Argv(1));
+
+ for(i = 0 ; i < MAX_CHALLENGES ; i++, challenge++)
+ {
+ if(!challenge->connected && NET_CompareAdr(from, challenge->adr))
+ {
+ wasfound = true;
+
+ if(challenge->time < oldestClientTime)
+ oldestClientTime = challenge->time;
+ }
+
+ if(wasfound && i >= MAX_CHALLENGES_MULTI)
+ {
+ i = MAX_CHALLENGES;
+ break;
+ }
+
+ if(challenge->time < oldestTime)
+ {
+ oldestTime = challenge->time;
+ oldest = i;
+ }
+ }
+
+ if (i == MAX_CHALLENGES)
+ {
+ // this is the first time this client has asked for a challenge
+ challenge = &svs.challenges[oldest];
+ challenge->clientChallenge = clientChallenge;
+ challenge->adr = from;
+ challenge->firstTime = svs.time;
+ challenge->connected = false;
+
+ if ( sv_rsaAuth->integer ) {
+ Sys_CryptoRandomBytes( buf, sizeof(buf) );
+ nettle_mpz_init_set_str_256_u( n, sizeof(buf), buf );
+ mpz_get_str( challenge->challenge2, 16, n );
+ mpz_clear( n );
+ }
+ }
+
+ // always generate a new challenge number, so the client cannot circumvent sv_maxping
+ challenge->challenge = ( (rand() << 16) ^ rand() ) ^ svs.time;
+ challenge->wasrefused = false;
+ challenge->time = svs.time;
+ challenge->pingTime = svs.time;
+
+ if ( sv_rsaAuth->integer ) {
+ NET_OutOfBandPrint( NS_SERVER, challenge->adr, "challengeResponse %d %d %d %s",
+ challenge->challenge, clientChallenge, PROTOCOL_VERSION, challenge->challenge2 );
+ }
+ else {
+ NET_OutOfBandPrint( NS_SERVER, challenge->adr, "challengeResponse %d %d %d",
+ challenge->challenge, clientChallenge, PROTOCOL_VERSION );
+ }
+}
+
+/*
+==================
+SV_DirectConnect
+
+A "connect" OOB command has been received
+==================
+*/
+void SV_DirectConnect( netadr_t from ) {
+ char userinfo[MAX_INFO_STRING];
+ int i;
+ client_t *cl, *newcl;
+ client_t temp;
+ sharedEntity_t *ent;
+ int clientNum;
+ int version;
+ int qport;
+ int challenge;
+ char *password;
+ int startIndex;
+ intptr_t denied;
+ int count;
+ const char *ip;
+ char *challenge2;
+ bool challenge2Verified = false;
+
+ Com_DPrintf ("SVC_DirectConnect ()\n");
+
+ // Prevent using connect as an amplifier
+ if (sv_protect->integer & SVP_IOQ3)
+ {
+ if(SVC_RateLimitAddress(from, 10, 1000))
+ {
+ SV_WriteAttackLog(va("Bad direct connect - rate limit from %s exceeded, dropping request\n",
+ NET_AdrToString(from)));
+ return;
+ }
+ }
+
+ Q_strncpyz( userinfo, Cmd_Argv(1), sizeof(userinfo) );
+
+ version = atoi( Info_ValueForKey( userinfo, "protocol" ) );
+ if ( version != PROTOCOL_VERSION && version != 70 && version != 69 ) {
+ NET_OutOfBandPrint(NS_SERVER, from, "print\nServer uses either protocol version %i, 70 or 69 "
+ "(yours is %i).\n", PROTOCOL_VERSION, version);
+ Com_DPrintf(" rejected connect from version %i\n", version);
+ return;
+ }
+
+ challenge = atoi( Info_ValueForKey( userinfo, "challenge" ) );
+ qport = atoi( Info_ValueForKey( userinfo, "qport" ) );
+
+ // quick reject
+ for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) {
+ if ( cl->state == CS_FREE ) {
+ continue;
+ }
+ if ( NET_CompareBaseAdr( from, cl->netchan.remoteAddress )
+ && ( cl->netchan.qport == qport
+ || from.port == cl->netchan.remoteAddress.port ) ) {
+ if (( svs.time - cl->lastConnectTime)
+ < (sv_reconnectlimit->integer * 1000)) {
+ Com_DPrintf ("%s:reconnect rejected : too soon\n", NET_AdrToString (from));
+ return;
+ }
+ break;
+ }
+ }
+
+ // don't let "ip" overflow userinfo string
+ if ( NET_IsLocalAddress (from) )
+ ip = "localhost";
+ else
+ ip = (char *)NET_AdrToString( from );
+ if( ( strlen( ip ) + strlen( userinfo ) + 4 ) >= MAX_INFO_STRING ) {
+ NET_OutOfBandPrint( NS_SERVER, from,
+ "print\nUserinfo string length exceeded. "
+ "Try removing setu cvars from your config.\n" );
+ return;
+ }
+ Info_SetValueForKey( userinfo, "ip", ip );
+
+ // see if the challenge is valid (LAN clients don't need to challenge)
+ if (!NET_IsLocalAddress(from))
+ {
+ int ping;
+ challenge_t *challengeptr;
+
+ for (i=0; i<MAX_CHALLENGES; i++)
+ {
+ if (NET_CompareAdr(from, svs.challenges[i].adr))
+ {
+ if(challenge == svs.challenges[i].challenge)
+ break;
+ }
+ }
+ if (i == MAX_CHALLENGES)
+ {
+ NET_OutOfBandPrint( NS_SERVER, from, "print\nNo or bad challenge for your address.\n" );
+ return;
+ }
+
+ if ( sv_rsaAuth->integer )
+ {
+ challenge2 = Info_ValueForKey( userinfo, "challenge2" );
+ if ( !Q_stricmp( challenge2, svs.challenges[i].challenge2 ) )
+ {
+ challenge2Verified = true;
+ }
+ }
+
+ challengeptr = &svs.challenges[i];
+
+ if(challengeptr->wasrefused)
+ {
+ // Return silently, so that error messages written by the server keep being displayed.
+ return;
+ }
+
+ ping = svs.time - challengeptr->pingTime;
+
+ // never reject a LAN client based on ping
+ if ( !Sys_IsLANAddress( from ) )
+ {
+ if ( sv_minPing->value && ping < sv_minPing->value ) {
+ NET_OutOfBandPrint( NS_SERVER, from, "print\nServer is for high pings only\n" );
+ Com_DPrintf ("Client %i rejected on a too low ping\n", i);
+ challengeptr->wasrefused = true;
+ return;
+ }
+ if ( sv_maxPing->value && ping > sv_maxPing->value ) {
+ NET_OutOfBandPrint( NS_SERVER, from, "print\nServer is for low pings only\n" );
+ Com_DPrintf ("Client %i rejected on a too high ping\n", i);
+ challengeptr->wasrefused = true;
+ return;
+ }
+ }
+
+ Com_Printf("Client %i connecting with %i challenge ping\n", i, ping);
+ challengeptr->connected = true;
+ }
+
+ // ignore any fingerprint set by the client
+ char fingerprint[SHA256_DIGEST_SIZE * 2 + 1];
+ Info_RemoveKey(userinfo, "fingerprint");
+ fingerprint[0] = '\0';
+
+ if ( sv_rsaAuth->integer && (NET_IsLocalAddress(from) || challenge2Verified) )
+ {
+ if ( SV_RSA_VerifySignature(Cmd_Argv(2), Cmd_Argv(3), Cmd_Argv(1), fingerprint) )
+ {
+ if( strlen(fingerprint) + strlen(userinfo) + 13 >= MAX_INFO_STRING )
+ {
+ NET_OutOfBandPrint( NS_SERVER, from, "print\nUserinfo string length exceeded.\n" );
+ return;
+ }
+ Info_SetValueForKey( userinfo, "fingerprint", fingerprint );
+ Com_DPrintf( "Public key fingerprint: %s\n", fingerprint );
+ }
+ }
+
+ newcl = &temp;
+ ::memset(newcl, 0, sizeof(client_t));
+
+ // if there is already a slot for this ip, reuse it
+ for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++)
+ {
+ if ( cl->state == CS_FREE )
+ continue;
+
+ if ( NET_CompareBaseAdr(from, cl->netchan.remoteAddress)
+ && (cl->netchan.qport == qport || from.port == cl->netchan.remoteAddress.port) )
+ {
+ Com_Printf ("%s:reconnect\n", NET_AdrToString (from));
+ newcl = cl;
+
+ // this doesn't work because it nukes the players userinfo
+ // disconnect the client from the game first so any flags the
+ // player might have are dropped
+ // VM_Call( sv.gvm, GAME_CLIENT_DISCONNECT, newcl - svs.clients );
+ goto gotnewcl;
+ }
+ }
+
+ // find a client slot
+ // if "sv_privateClients" is set > 0, then that number
+ // of client slots will be reserved for connections that
+ // have "password" set to the value of "sv_privatePassword"
+ // Info requests will report the maxclients as if the private
+ // slots didn't exist, to prevent people from trying to connect
+ // to a full server.
+ // This is to allow us to reserve a couple slots here on our
+ // servers so we can play without having to kick people.
+
+ // check for privateClient password
+ password = Info_ValueForKey( userinfo, "password" );
+ if ( *password && !strcmp( password, sv_privatePassword->string ) ) {
+ startIndex = 0;
+ } else {
+ // skip past the reserved slots
+ startIndex = sv_privateClients->integer;
+ }
+
+ newcl = NULL;
+ for ( i = startIndex; i < sv_maxclients->integer ; i++ ) {
+ cl = &svs.clients[i];
+ if (cl->state == CS_FREE) {
+ newcl = cl;
+ break;
+ }
+ }
+
+ if ( !newcl ) {
+ if ( NET_IsLocalAddress( from ) ) {
+ Com_Error( ERR_FATAL, "server is full on local connect" );
+ return;
+ }
+ else {
+ NET_OutOfBandPrint( NS_SERVER, from, "print\nServer is full\n" );
+ Com_DPrintf ("Rejected a connection.\n");
+ return;
+ }
+ }
+
+ // we got a newcl, so reset the reliableSequence and reliableAcknowledge
+ cl->reliableAcknowledge = 0;
+ cl->reliableSequence = 0;
+
+gotnewcl:
+ // build a new connection
+ // accept the new client
+ // this is the only place a client_t is ever initialized
+ *newcl = temp;
+ clientNum = newcl - svs.clients;
+ ent = SV_GentityNum( clientNum );
+ newcl->gentity = ent;
+
+ Cvar_Set( va( "sv_clAltProto%i", clientNum ), ( version == 69 ? "2" : version == 70 ? "1" : "0" ) );
+
+ // save the challenge
+ newcl->challenge = challenge;
+
+ // save the address
+ Netchan_Setup((version == 69 ? 2 : version == 70 ? 1 : 0), NS_SERVER, &newcl->netchan, from, qport, challenge);
+ // init the netchan queue
+ newcl->netchan_end_queue = &newcl->netchan_start_queue;
+
+ // save the fingerprint
+ Q_strncpyz( newcl->fingerprint, fingerprint, sizeof(newcl->fingerprint) );
+
+ // save the userinfo
+ Q_strncpyz( newcl->userinfo, userinfo, sizeof(newcl->userinfo) );
+
+ // get the game a chance to reject this connection or modify the userinfo
+ denied = VM_Call( sv.gvm, GAME_CLIENT_CONNECT, clientNum, true ); // firstTime = true
+ if ( denied ) {
+ // we can't just use VM_ArgPtr, because that is only valid inside a VM_Call
+ char *str = (char*)VM_ExplicitArgPtr( sv.gvm, denied );
+
+ NET_OutOfBandPrint( NS_SERVER, from, "print\n%s\n", str );
+ Com_DPrintf ("Game rejected a connection: %s.\n", str);
+ return;
+ }
+
+ SV_UserinfoChanged( newcl );
+
+ // send the connect packet to the client
+ NET_OutOfBandPrint(NS_SERVER, from, "connectResponse %d", challenge);
+
+ Com_DPrintf( "Going from CS_FREE to CS_CONNECTED for %s\n", newcl->name );
+
+ newcl->state = CS_CONNECTED;
+ newcl->lastSnapshotTime = 0;
+ newcl->lastPacketTime = svs.time;
+ newcl->lastConnectTime = svs.time;
+
+ // when we receive the first packet from the client, we will
+ // notice that it is from a different serverid and that the
+ // gamestate message was not just sent, forcing a retransmit
+ newcl->gamestateMessageNum = -1;
+
+ // if this was the first client on the server, or the last client
+ // the server can hold, send a heartbeat to the master.
+ count = 0;
+ for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) {
+ if ( svs.clients[i].state >= CS_CONNECTED )
+ count++;
+ }
+ if ( count == 1 || count == sv_maxclients->integer ) {
+ SV_Heartbeat_f();
+ }
+}
+
+/*
+=====================
+SV_FreeClient
+
+Destructor for data allocated in a client structure
+=====================
+*/
+void SV_FreeClient(client_t *client)
+{
+#ifdef USE_VOIP
+ int index;
+
+ for(index = client->queuedVoipIndex; index < client->queuedVoipPackets; index++)
+ {
+ index %= ARRAY_LEN(client->voipPacket);
+
+ Z_Free(client->voipPacket[index]);
+ }
+
+ client->queuedVoipPackets = 0;
+#endif
+
+ SV_Netchan_FreeQueue(client);
+ SV_CloseDownload(client);
+}
+
+/*
+=====================
+SV_DropClient
+
+Called when the player is totally leaving the server, either willingly
+or unwillingly. This is NOT called if the entire server is quiting
+or crashing -- SV_FinalMessage() will handle that
+=====================
+*/
+void SV_DropClient( client_t *drop, const char *reason ) {
+ int i;
+ challenge_t *challenge;
+
+ if ( drop->state == CS_ZOMBIE ) {
+ return; // already dropped
+ }
+
+ // see if we already have a challenge for this ip
+ challenge = &svs.challenges[0];
+
+ for (i = 0 ; i < MAX_CHALLENGES ; i++, challenge++) {
+ if ( NET_CompareAdr( drop->netchan.remoteAddress, challenge->adr ) ) {
+ ::memset(challenge, 0, sizeof(*challenge));
+ break;
+ }
+ }
+
+ // Free all allocated data on the client structure
+ SV_FreeClient(drop);
+
+ // tell everyone why they got dropped
+ SV_SendServerCommand( NULL, "print \"%s" S_COLOR_WHITE " %s\n\"", drop->name, reason );
+
+ // call the prog function for removing a client
+ // this will remove the body, among other things
+ VM_Call( sv.gvm, GAME_CLIENT_DISCONNECT, drop - svs.clients );
+
+ // add the disconnect command
+ SV_SendServerCommand( drop, "disconnect \"%s\"", reason);
+
+ // nuke user info
+ SV_SetUserinfo( drop - svs.clients, "" );
+
+ Com_DPrintf( "Going to CS_ZOMBIE for %s\n", drop->name );
+ drop->state = CS_ZOMBIE; // become free in a few seconds
+
+ // if this was the last client on the server, send a heartbeat
+ // to the master so it is known the server is empty
+ // send a heartbeat now so the master will get up to date info
+ // if there is already a slot for this ip, reuse it
+ for (i=0 ; i < sv_maxclients->integer ; i++ ) {
+ if ( svs.clients[i].state >= CS_CONNECTED ) {
+ break;
+ }
+ }
+ if ( i == sv_maxclients->integer ) {
+ SV_Heartbeat_f();
+ }
+}
+
+extern char alternateInfos[2][2][BIG_INFO_STRING];
+
+/*
+================
+SV_SendClientGameState
+
+Sends the first message from the server to a connected client.
+This will be sent on the initial connection and upon each new map load.
+
+It will be resent if the client acknowledges a later message but has
+the wrong gamestate.
+================
+*/
+static void SV_SendClientGameState( client_t *client ) {
+ int start;
+ entityState_t *base, nullstate;
+ msg_t msg;
+ byte msgBuffer[MAX_MSGLEN];
+ const char *configstring;
+
+ Com_DPrintf ("SV_SendClientGameState() for %s\n", client->name);
+ Com_DPrintf( "Going from CS_CONNECTED to CS_PRIMED for %s\n", client->name );
+ client->state = CS_PRIMED;
+ client->pureAuthentic = 0;
+ client->gotCP = false;
+
+ // when we receive the first packet from the client, we will
+ // notice that it is from a different serverid and that the
+ // gamestate message was not just sent, forcing a retransmit
+ client->gamestateMessageNum = client->netchan.outgoingSequence;
+
+ MSG_Init( &msg, msgBuffer, sizeof( msgBuffer ) );
+
+ // NOTE, MRE: all server->client messages now acknowledge
+ // let the client know which reliable clientCommands we have received
+ MSG_WriteLong( &msg, client->lastClientCommand );
+
+ // send any server commands waiting to be sent first.
+ // we have to do this cause we send the client->reliableSequence
+ // with a gamestate and it sets the clc.serverCommandSequence at
+ // the client side
+ SV_UpdateServerCommandsToClient( client, &msg );
+
+ // send the gamestate
+ MSG_WriteByte( &msg, svc_gamestate );
+ MSG_WriteLong( &msg, client->reliableSequence );
+
+ // write the configstrings
+ for ( start = 0 ; start < MAX_CONFIGSTRINGS ; start++ ) {
+ if ( start <= CS_SYSTEMINFO && client->netchan.alternateProtocol != 0 ) {
+ configstring = alternateInfos[start][ client->netchan.alternateProtocol - 1 ];
+ } else {
+ configstring = sv.configstrings[start].s;
+ }
+
+ if (configstring[0]) {
+ MSG_WriteByte( &msg, svc_configstring );
+ MSG_WriteShort( &msg, start );
+ MSG_WriteBigString( &msg, configstring );
+ }
+ }
+
+ // write the baselines
+ ::memset( &nullstate, 0, sizeof( nullstate ) );
+ for ( start = 0 ; start < MAX_GENTITIES; start++ ) {
+ base = &sv.svEntities[start].baseline;
+ if ( !base->number ) {
+ continue;
+ }
+ MSG_WriteByte( &msg, svc_baseline );
+ MSG_WriteDeltaEntity( client->netchan.alternateProtocol, &msg, &nullstate, base, true );
+ }
+
+ MSG_WriteByte( &msg, svc_EOF );
+
+ MSG_WriteLong( &msg, client - svs.clients);
+
+ // write the checksum feed
+ MSG_WriteLong( &msg, sv.checksumFeed);
+
+ // deliver this to the client
+ SV_SendMessageToClient( &msg, client );
+}
+
+
+/*
+==================
+SV_ClientEnterWorld
+==================
+*/
+void SV_ClientEnterWorld( client_t *client, usercmd_t *cmd ) {
+ int clientNum;
+ sharedEntity_t *ent;
+
+ Com_DPrintf( "Going from CS_PRIMED to CS_ACTIVE for %s\n", client->name );
+ client->state = CS_ACTIVE;
+
+ // resend all configstrings using the cs commands since these are
+ // no longer sent when the client is CS_PRIMED
+ SV_UpdateConfigstrings( client );
+
+ // set up the entity for the client
+ clientNum = client - svs.clients;
+ ent = SV_GentityNum( clientNum );
+ ent->s.number = clientNum;
+ client->gentity = ent;
+
+ client->deltaMessage = -1;
+ client->lastSnapshotTime = 0; // generate a snapshot immediately
+
+ if(cmd)
+ memcpy(&client->lastUsercmd, cmd, sizeof(client->lastUsercmd));
+ else
+ memset(&client->lastUsercmd, '\0', sizeof(client->lastUsercmd));
+
+ // call the game begin function
+ VM_Call( sv.gvm, GAME_CLIENT_BEGIN, client - svs.clients );
+}
+
+/*
+============================================================
+
+CLIENT COMMAND EXECUTION
+
+============================================================
+*/
+
+/*
+==================
+SV_CloseDownload
+
+clear/free any download vars
+==================
+*/
+static void SV_CloseDownload( client_t *cl ) {
+ int i;
+
+ // EOF
+ if (cl->download) {
+ FS_FCloseFile( cl->download );
+ }
+ cl->download = 0;
+ *cl->downloadName = 0;
+
+ // Free the temporary buffer space
+ for (i = 0; i < MAX_DOWNLOAD_WINDOW; i++) {
+ if (cl->downloadBlocks[i]) {
+ Z_Free(cl->downloadBlocks[i]);
+ cl->downloadBlocks[i] = NULL;
+ }
+ }
+
+}
+
+/*
+==================
+SV_StopDownload_f
+
+Abort a download if in progress
+==================
+*/
+static void SV_StopDownload_f( client_t *cl ) {
+ if (*cl->downloadName)
+ Com_DPrintf( "clientDownload: %d : file \"%s\" aborted\n", (int) (cl - svs.clients), cl->downloadName );
+
+ SV_CloseDownload( cl );
+}
+
+/*
+==================
+SV_DoneDownload_f
+
+Downloads are finished
+==================
+*/
+static void SV_DoneDownload_f( client_t *cl ) {
+ if ( cl->state == CS_ACTIVE )
+ return;
+
+ Com_DPrintf( "clientDownload: %s Done\n", cl->name);
+ // resend the game state to update any clients that entered during the download
+ SV_SendClientGameState(cl);
+}
+
+/*
+==================
+SV_NextDownload_f
+
+The argument will be the last acknowledged block from the client, it should be
+the same as cl->downloadClientBlock
+==================
+*/
+static void SV_NextDownload_f( client_t *cl )
+{
+ int block = atoi( Cmd_Argv(1) );
+
+ if (block == cl->downloadClientBlock) {
+ Com_DPrintf( "clientDownload: %d : client acknowledge of block %d\n", (int) (cl - svs.clients), block );
+
+ // Find out if we are done. A zero-length block indicates EOF
+ if (cl->downloadBlockSize[cl->downloadClientBlock % MAX_DOWNLOAD_WINDOW] == 0) {
+ Com_Printf( "clientDownload: %d : file \"%s\" completed\n", (int) (cl - svs.clients), cl->downloadName );
+ SV_CloseDownload( cl );
+ return;
+ }
+
+ cl->downloadSendTime = svs.time;
+ cl->downloadClientBlock++;
+ return;
+ }
+ // We aren't getting an acknowledge for the correct block, drop the client
+ // FIXME: this is bad... the client will never parse the disconnect message
+ // because the cgame isn't loaded yet
+ SV_DropClient( cl, "broken download" );
+}
+
+/*
+==================
+SV_BeginDownload_f
+==================
+*/
+static void SV_BeginDownload_f( client_t *cl ) {
+
+ // Kill any existing download
+ SV_CloseDownload( cl );
+
+ // cl->downloadName is non-zero now, SV_WriteDownloadToClient will see this and open
+ // the file itself
+ Q_strncpyz( cl->downloadName, Cmd_Argv(1), sizeof(cl->downloadName) );
+}
+
+/*
+==================
+SV_WriteDownloadToClient
+
+Check to see if the client wants a file, open it if needed and start pumping the client
+Fill up msg with data, return number of download blocks added
+==================
+*/
+int SV_WriteDownloadToClient(client_t *cl, msg_t *msg)
+{
+ int curindex;
+ int unreferenced = 1;
+ char errorMessage[1024];
+ char pakbuf[MAX_QPATH], *pakptr;
+ int numRefPaks;
+
+ if (!*cl->downloadName)
+ return 0; // Nothing being downloaded
+
+ if(!cl->download)
+ {
+ // Chop off filename extension.
+ Com_sprintf(pakbuf, sizeof(pakbuf), "%s", cl->downloadName);
+ pakptr = strrchr(pakbuf, '.');
+
+ if(pakptr)
+ {
+ *pakptr = '\0';
+
+ // Check for pk3 filename extension
+ if(!Q_stricmp(pakptr + 1, "pk3"))
+ {
+ const char *referencedPaks = FS_ReferencedPakNames( cl->netchan.alternateProtocol == 2 );
+
+ // Check whether the file appears in the list of referenced
+ // paks to prevent downloading of arbitrary files.
+ Cmd_TokenizeStringIgnoreQuotes(referencedPaks);
+ numRefPaks = Cmd_Argc();
+
+ for(curindex = 0; curindex < numRefPaks; curindex++)
+ {
+ if(!FS_FilenameCompare(Cmd_Argv(curindex), pakbuf))
+ {
+ unreferenced = 0;
+ break;
+ }
+ }
+ }
+ }
+
+ cl->download = 0;
+
+ // We open the file here
+ if ( !(sv_allowDownload->integer & DLF_ENABLE) ||
+ (sv_allowDownload->integer & DLF_NO_UDP) ||
+ unreferenced ||
+ ( cl->downloadSize = FS_SV_FOpenFileRead( cl->downloadName, &cl->download ) ) < 0 ) {
+ // cannot auto-download file
+ if(unreferenced)
+ {
+ Com_Printf("clientDownload: %d : \"%s\" is not referenced and cannot be downloaded.\n", (int) (cl - svs.clients), cl->downloadName);
+ Com_sprintf(errorMessage, sizeof(errorMessage), "File \"%s\" is not referenced and cannot be downloaded.", cl->downloadName);
+ }
+ else if ( !(sv_allowDownload->integer & DLF_ENABLE) ||
+ (sv_allowDownload->integer & DLF_NO_UDP) ) {
+
+ Com_Printf("clientDownload: %d : \"%s\" download disabled\n", (int) (cl - svs.clients), cl->downloadName);
+ if (sv_pure->integer) {
+ Com_sprintf(errorMessage, sizeof(errorMessage), "Could not download \"%s\" because autodownloading is disabled on the server.\n\n"
+ "You will need to get this file elsewhere before you "
+ "can connect to this pure server.\n", cl->downloadName);
+ } else {
+ Com_sprintf(errorMessage, sizeof(errorMessage), "Could not download \"%s\" because autodownloading is disabled on the server.\n\n"
+ "The server you are connecting to is not a pure server, "
+ "set autodownload to No in your settings and you might be "
+ "able to join the game anyway.\n", cl->downloadName);
+ }
+ } else {
+ // NOTE TTimo this is NOT supposed to happen unless bug in our filesystem scheme?
+ // if the pk3 is referenced, it must have been found somewhere in the filesystem
+ Com_Printf("clientDownload: %d : \"%s\" file not found on server\n", (int) (cl - svs.clients), cl->downloadName);
+ Com_sprintf(errorMessage, sizeof(errorMessage), "File \"%s\" not found on server for autodownloading.\n", cl->downloadName);
+ }
+ MSG_WriteByte( msg, svc_download );
+ MSG_WriteShort( msg, 0 ); // client is expecting block zero
+ MSG_WriteLong( msg, -1 ); // illegal file size
+ MSG_WriteString( msg, errorMessage );
+
+ *cl->downloadName = 0;
+
+ if(cl->download)
+ FS_FCloseFile(cl->download);
+
+ return 1;
+ }
+
+ Com_Printf( "clientDownload: %d : beginning \"%s\"\n", (int) (cl - svs.clients), cl->downloadName );
+
+ // Init
+ cl->downloadCurrentBlock = cl->downloadClientBlock = cl->downloadXmitBlock = 0;
+ cl->downloadCount = 0;
+ cl->downloadEOF = false;
+ }
+
+ // Perform any reads that we need to
+ while (cl->downloadCurrentBlock - cl->downloadClientBlock < MAX_DOWNLOAD_WINDOW &&
+ cl->downloadSize != cl->downloadCount) {
+
+ curindex = (cl->downloadCurrentBlock % MAX_DOWNLOAD_WINDOW);
+
+ if (!cl->downloadBlocks[curindex])
+ cl->downloadBlocks[curindex] = (unsigned char*)Z_Malloc(MAX_DOWNLOAD_BLKSIZE);
+
+ cl->downloadBlockSize[curindex] = FS_Read( cl->downloadBlocks[curindex], MAX_DOWNLOAD_BLKSIZE, cl->download );
+
+ if (cl->downloadBlockSize[curindex] < 0) {
+ // EOF right now
+ cl->downloadCount = cl->downloadSize;
+ break;
+ }
+
+ cl->downloadCount += cl->downloadBlockSize[curindex];
+
+ // Load in next block
+ cl->downloadCurrentBlock++;
+ }
+
+ // Check to see if we have eof condition and add the EOF block
+ if (cl->downloadCount == cl->downloadSize
+ && !cl->downloadEOF
+ && cl->downloadCurrentBlock - cl->downloadClientBlock < MAX_DOWNLOAD_WINDOW)
+ {
+ cl->downloadBlockSize[cl->downloadCurrentBlock % MAX_DOWNLOAD_WINDOW] = 0;
+ cl->downloadCurrentBlock++;
+
+ cl->downloadEOF = true; // We have added the EOF block
+ }
+
+ if (cl->downloadClientBlock == cl->downloadCurrentBlock)
+ return 0; // Nothing to transmit
+
+ // Write out the next section of the file, if we have already reached our window,
+ // automatically start retransmitting
+ if (cl->downloadXmitBlock == cl->downloadCurrentBlock)
+ {
+ // We have transmitted the complete window, should we start resending?
+ if (svs.time - cl->downloadSendTime > 1000)
+ cl->downloadXmitBlock = cl->downloadClientBlock;
+ else
+ return 0;
+ }
+
+ // Send current block
+ curindex = (cl->downloadXmitBlock % MAX_DOWNLOAD_WINDOW);
+
+ MSG_WriteByte( msg, svc_download );
+ MSG_WriteShort( msg, cl->downloadXmitBlock );
+
+ // block zero is special, contains file size
+ if ( cl->downloadXmitBlock == 0 )
+ MSG_WriteLong( msg, cl->downloadSize );
+
+ MSG_WriteShort( msg, cl->downloadBlockSize[curindex] );
+
+ // Write the block
+ if(cl->downloadBlockSize[curindex])
+ MSG_WriteData(msg, cl->downloadBlocks[curindex], cl->downloadBlockSize[curindex]);
+
+ Com_DPrintf( "clientDownload: %d : writing block %d\n", (int) (cl - svs.clients), cl->downloadXmitBlock );
+
+ // Move on to the next block
+ // It will get sent with next snap shot. The rate will keep us in line.
+ cl->downloadXmitBlock++;
+ cl->downloadSendTime = svs.time;
+
+ return 1;
+}
+
+/*
+==================
+SV_SendQueuedMessages
+
+Send one round of fragments, or queued messages to all clients that have data pending.
+Return the shortest time interval for sending next packet to client
+==================
+*/
+
+int SV_SendQueuedMessages(void)
+{
+ int i, retval = -1, nextFragT;
+ client_t *cl;
+
+ for(i=0; i < sv_maxclients->integer; i++)
+ {
+ cl = &svs.clients[i];
+
+ if(cl->state)
+ {
+ nextFragT = SV_RateMsec(cl);
+
+ if(!nextFragT)
+ nextFragT = SV_Netchan_TransmitNextFragment(cl);
+
+ if(nextFragT >= 0 && (retval == -1 || retval > nextFragT))
+ retval = nextFragT;
+ }
+ }
+
+ return retval;
+}
+
+
+/*
+==================
+SV_SendDownloadMessages
+
+Send one round of download messages to all clients
+==================
+*/
+
+int SV_SendDownloadMessages(void)
+{
+ int i, numDLs = 0, retval;
+ client_t *cl;
+ msg_t msg;
+ byte msgBuffer[MAX_MSGLEN];
+
+ for(i=0; i < sv_maxclients->integer; i++)
+ {
+ cl = &svs.clients[i];
+
+ if(cl->state && *cl->downloadName)
+ {
+ MSG_Init(&msg, msgBuffer, sizeof(msgBuffer));
+ MSG_WriteLong(&msg, cl->lastClientCommand);
+
+ retval = SV_WriteDownloadToClient(cl, &msg);
+
+ if(retval)
+ {
+ MSG_WriteByte(&msg, svc_EOF);
+ SV_Netchan_Transmit(cl, &msg);
+ numDLs += retval;
+ }
+ }
+ }
+
+ return numDLs;
+}
+
+/*
+=================
+SV_Disconnect_f
+
+The client is going to disconnect, so remove the connection immediately FIXME: move to game?
+=================
+*/
+static void SV_Disconnect_f( client_t *cl ) {
+ SV_DropClient( cl, "disconnected" );
+}
+
+/*
+=================
+SV_VerifyPaks_f
+
+If we are pure, disconnect the client if they do no meet the following conditions:
+
+1. the first two checksums match our view of cgame and ui
+2. there are no any additional checksums that we do not have
+
+This routine would be a bit simpler with a goto but i abstained
+
+=================
+*/
+static void SV_VerifyPaks_f( client_t *cl ) {
+ int nChkSum1, nChkSum2, nClientPaks, nServerPaks, i, j, nCurArg;
+ int nClientChkSum[1024];
+ int nServerChkSum[1024];
+ const char *pPaks, *pArg;
+ bool bGood = true;
+
+ // if we are pure, we "expect" the client to load certain things from
+ // certain pk3 files, namely we want the client to have loaded the
+ // ui and cgame that we think should be loaded based on the pure setting
+ //
+ if ( sv_pure->integer != 0 ) {
+
+ nChkSum1 = nChkSum2 = 0;
+ // we run the game, so determine which cgame and ui the client "should" be running
+ bGood = (FS_FileIsInPAK_A((cl->netchan.alternateProtocol == 2), "vm/cgame.qvm", &nChkSum1) == 1);
+ if (bGood)
+ bGood = (FS_FileIsInPAK_A((cl->netchan.alternateProtocol == 2), "vm/ui.qvm", &nChkSum2) == 1);
+
+ nClientPaks = Cmd_Argc();
+
+ // start at arg 2 ( skip serverId cl_paks )
+ nCurArg = 1;
+
+ pArg = Cmd_Argv(nCurArg++);
+ if(!pArg) {
+ bGood = false;
+ }
+ else
+ {
+ // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=475
+ // we may get incoming cp sequences from a previous checksumFeed, which we need to ignore
+ // since serverId is a frame count, it always goes up
+ if (atoi(pArg) < sv.checksumFeedServerId)
+ {
+ Com_DPrintf("ignoring outdated cp command from client %s\n", cl->name);
+ return;
+ }
+ }
+
+ // we basically use this while loop to avoid using 'goto' :)
+ while (bGood) {
+
+ // must be at least 6: "cl_paks cgame ui @ firstref ... numChecksums"
+ // numChecksums is encoded
+ if (nClientPaks < 6) {
+ bGood = false;
+ break;
+ }
+ // verify first to be the cgame checksum
+ pArg = Cmd_Argv(nCurArg++);
+ if (!pArg || *pArg == '@' || atoi(pArg) != nChkSum1 ) {
+ bGood = false;
+ break;
+ }
+ // verify the second to be the ui checksum
+ pArg = Cmd_Argv(nCurArg++);
+ if (!pArg || *pArg == '@' || atoi(pArg) != nChkSum2 ) {
+ bGood = false;
+ break;
+ }
+ // should be sitting at the delimeter now
+ pArg = Cmd_Argv(nCurArg++);
+ if (*pArg != '@') {
+ bGood = false;
+ break;
+ }
+ // store checksums since tokenization is not re-entrant
+ for (i = 0; nCurArg < nClientPaks; i++) {
+ nClientChkSum[i] = atoi(Cmd_Argv(nCurArg++));
+ }
+
+ // store number to compare against (minus one cause the last is the number of checksums)
+ nClientPaks = i - 1;
+
+ // make sure none of the client check sums are the same
+ // so the client can't send 5 the same checksums
+ for (i = 0; i < nClientPaks; i++) {
+ for (j = 0; j < nClientPaks; j++) {
+ if (i == j)
+ continue;
+ if (nClientChkSum[i] == nClientChkSum[j]) {
+ bGood = false;
+ break;
+ }
+ }
+ if (bGood == false)
+ break;
+ }
+ if (bGood == false)
+ break;
+
+ // get the pure checksums of the pk3 files loaded by the server
+ pPaks = FS_LoadedPakPureChecksums(cl->netchan.alternateProtocol == 2);
+ Cmd_TokenizeString( pPaks );
+ nServerPaks = Cmd_Argc();
+ if (nServerPaks > 1024)
+ nServerPaks = 1024;
+
+ for (i = 0; i < nServerPaks; i++) {
+ nServerChkSum[i] = atoi(Cmd_Argv(i));
+ }
+
+ // check if the client has provided any pure checksums of pk3 files not loaded by the server
+ for (i = 0; i < nClientPaks; i++) {
+ for (j = 0; j < nServerPaks; j++) {
+ if (nClientChkSum[i] == nServerChkSum[j]) {
+ break;
+ }
+ }
+ if (j >= nServerPaks) {
+ bGood = false;
+ break;
+ }
+ }
+ if ( bGood == false ) {
+ break;
+ }
+
+ // check if the number of checksums was correct
+ nChkSum1 = sv.checksumFeed;
+ for (i = 0; i < nClientPaks; i++) {
+ nChkSum1 ^= nClientChkSum[i];
+ }
+ nChkSum1 ^= nClientPaks;
+ if (nChkSum1 != nClientChkSum[nClientPaks]) {
+ bGood = false;
+ break;
+ }
+
+ // break out
+ break;
+ }
+
+ cl->gotCP = true;
+
+ if (bGood) {
+ cl->pureAuthentic = 1;
+ }
+ else {
+ cl->pureAuthentic = 0;
+ cl->lastSnapshotTime = 0;
+ cl->state = CS_ACTIVE;
+ SV_SendClientSnapshot( cl );
+ SV_SendServerCommand( cl, "disconnect \"Unpure Client. "
+ "You may need to enable in-game downloads "
+ "to connect to this server (set "
+ "cl_allowDownload 1)\"" );
+ SV_DropClient( cl, "Unpure client detected. Invalid .PK3 files referenced!" );
+ }
+ }
+}
+
+/*
+=================
+SV_ResetPureClient_f
+=================
+*/
+static void SV_ResetPureClient_f( client_t *cl ) {
+ cl->pureAuthentic = 0;
+ cl->gotCP = false;
+}
+
+/*
+=================
+SV_UserinfoChanged
+
+Pull specific info from a newly changed userinfo string
+into a more C friendly form.
+=================
+*/
+void SV_UserinfoChanged( client_t *cl ) {
+ char *val;
+ const char *ip;
+ int i;
+ int len;
+
+ // name for C code
+ Q_strncpyz( cl->name, Info_ValueForKey (cl->userinfo, "name"), sizeof(cl->name) );
+
+ // rate command
+
+ // if the client is on the same subnet as the server and we aren't running an
+ // internet public server, assume they don't need a rate choke
+ if ( Sys_IsLANAddress( cl->netchan.remoteAddress ) && com_dedicated->integer != 2 && sv_lanForceRate->integer == 1) {
+ cl->rate = 99999; // lans should not rate limit
+ } else {
+ val = Info_ValueForKey (cl->userinfo, "rate");
+ if (strlen(val)) {
+ i = atoi(val);
+ cl->rate = i;
+ if (cl->rate < 1000) {
+ cl->rate = 1000;
+ } else if (cl->rate > 90000) {
+ cl->rate = 90000;
+ }
+ } else {
+ cl->rate = 3000;
+ }
+ }
+ val = Info_ValueForKey (cl->userinfo, "handicap");
+ if (strlen(val)) {
+ i = atoi(val);
+ if (i<=0 || i>100 || strlen(val) > 4) {
+ Info_SetValueForKey( cl->userinfo, "handicap", "100" );
+ }
+ }
+
+ // snaps command
+ val = Info_ValueForKey (cl->userinfo, "snaps");
+
+ if(strlen(val))
+ {
+ i = atoi(val);
+
+ if(i < 1)
+ i = 1;
+ else if(i > sv_fps->integer)
+ i = sv_fps->integer;
+
+ i = 1000 / i;
+ }
+ else
+ i = 50;
+
+ if(i != cl->snapshotMsec)
+ {
+ // Reset last sent snapshot so we avoid desync between server frame time and snapshot send time
+ cl->lastSnapshotTime = 0;
+ cl->snapshotMsec = i;
+ }
+
+#ifdef USE_VOIP
+ val = Info_ValueForKey(cl->userinfo, "cl_voipProtocol");
+ cl->hasVoip = !Q_stricmp( val, "opus" );
+#endif
+
+ // TTimo
+ // maintain the IP information
+ // the banning code relies on this being consistently present
+ if( NET_IsLocalAddress(cl->netchan.remoteAddress) )
+ ip = "localhost";
+ else
+ ip = (char*)NET_AdrToString( cl->netchan.remoteAddress );
+
+ val = Info_ValueForKey( cl->userinfo, "ip" );
+ if( val[0] )
+ len = strlen( ip ) - strlen( val ) + strlen( cl->userinfo );
+ else
+ len = strlen( ip ) + 4 + strlen( cl->userinfo );
+
+ if( len >= MAX_INFO_STRING )
+ SV_DropClient( cl, "userinfo string length exceeded" );
+ else
+ Info_SetValueForKey( cl->userinfo, "ip", ip );
+
+ val = Info_ValueForKey( cl->userinfo, "fingerprint" );
+ if( val[0] )
+ len = strlen(cl->fingerprint) - strlen(val) + strlen(cl->userinfo);
+ else
+ len = strlen(cl->fingerprint) + 13 + strlen(cl->userinfo);
+
+ if( len >= MAX_INFO_STRING )
+ SV_DropClient( cl, "userinfo string length exceeded" );
+ else
+ Info_SetValueForKey( cl->userinfo, "fingerprint", cl->fingerprint );
+}
+
+
+/*
+==================
+SV_UpdateUserinfo_f
+==================
+*/
+static void SV_UpdateUserinfo_f( client_t *cl ) {
+ Q_strncpyz( cl->userinfo, Cmd_Argv(1), sizeof(cl->userinfo) );
+
+ SV_UserinfoChanged( cl );
+ // call prog code to allow overrides
+ VM_Call( sv.gvm, GAME_CLIENT_USERINFO_CHANGED, cl - svs.clients );
+}
+
+
+#ifdef USE_VOIP
+static void SV_UpdateVoipIgnore(client_t *cl, const char *idstr, bool ignore)
+{
+ if ((*idstr >= '0') && (*idstr <= '9')) {
+ const int id = atoi(idstr);
+ if ((id >= 0) && (id < MAX_CLIENTS)) {
+ cl->ignoreVoipFromClient[id] = ignore;
+ }
+ }
+}
+
+/*
+==================
+SV_Voip_f
+==================
+*/
+static void SV_Voip_f( client_t *cl )
+{
+ const char *cmd = Cmd_Argv(1);
+ if (strcmp(cmd, "ignore") == 0) {
+ SV_UpdateVoipIgnore(cl, Cmd_Argv(2), true);
+ } else if (strcmp(cmd, "unignore") == 0) {
+ SV_UpdateVoipIgnore(cl, Cmd_Argv(2), false);
+ } else if (strcmp(cmd, "muteall") == 0) {
+ cl->muteAllVoip = true;
+ } else if (strcmp(cmd, "unmuteall") == 0) {
+ cl->muteAllVoip = false;
+ }
+}
+#endif
+
+
+typedef struct {
+ const char *name;
+ void (*func)( client_t *cl );
+} ucmd_t;
+
+static ucmd_t ucmds[] = {
+ {"userinfo", SV_UpdateUserinfo_f},
+ {"disconnect", SV_Disconnect_f},
+ {"cp", SV_VerifyPaks_f},
+ {"vdr", SV_ResetPureClient_f},
+ {"download", SV_BeginDownload_f},
+ {"nextdl", SV_NextDownload_f},
+ {"stopdl", SV_StopDownload_f},
+ {"donedl", SV_DoneDownload_f},
+
+#ifdef USE_VOIP
+ {"voip", SV_Voip_f},
+#endif
+
+ {NULL, NULL}
+};
+
+/*
+==================
+SV_ExecuteClientCommand
+
+Also called by bot code
+==================
+*/
+void SV_ExecuteClientCommand( client_t *cl, const char *s, bool clientOK ) {
+ ucmd_t *u;
+ bool bProcessed = false;
+
+ Cmd_TokenizeString( s );
+
+ // see if it is a server level command
+ for (u=ucmds ; u->name ; u++) {
+ if (!strcmp (Cmd_Argv(0), u->name) ) {
+ u->func( cl );
+ bProcessed = true;
+ break;
+ }
+ }
+
+ if (clientOK) {
+ // pass unknown strings to the game
+ if (!u->name && sv.state == SS_GAME && (cl->state == CS_ACTIVE || cl->state == CS_PRIMED)) {
+ VM_Call( sv.gvm, GAME_CLIENT_COMMAND, cl - svs.clients );
+ }
+ }
+ else if (!bProcessed)
+ Com_DPrintf( "client text ignored for %s: %s\n", cl->name, Cmd_Argv(0) );
+}
+
+/*
+===============
+SV_ClientCommand
+===============
+*/
+static bool SV_ClientCommand( client_t *cl, msg_t *msg ) {
+ int seq;
+ const char *s;
+ bool clientOk = true;
+
+ seq = MSG_ReadLong( msg );
+ s = MSG_ReadString( msg );
+
+ // see if we have already executed it
+ if ( cl->lastClientCommand >= seq ) {
+ return true;
+ }
+
+ Com_DPrintf( "clientCommand: %s : %i : %s\n", cl->name, seq, s );
+
+ // drop the connection if we have somehow lost commands
+ if ( seq > cl->lastClientCommand + 1 ) {
+ Com_Printf( "Client %s lost %i clientCommands\n", cl->name,
+ seq - cl->lastClientCommand + 1 );
+ SV_DropClient( cl, "Lost reliable commands" );
+ return false;
+ }
+
+ // malicious users may try using too many string commands
+ // to lag other players. If we decide that we want to stall
+ // the command, we will stop processing the rest of the packet,
+ // including the usercmd. This causes flooders to lag themselves
+ // but not other people
+ // We don't do this when the client hasn't been active yet since it's
+ // normal to spam a lot of commands when downloading
+#if 0 // flood protection in game for trem
+ if ( !com_cl_running->integer &&
+ cl->state >= CS_ACTIVE &&
+ sv_floodProtect->integer &&
+ svs.time < cl->nextReliableTime ) {
+ // ignore any other text messages from this client but let them keep playing
+ // TTimo - moved the ignored verbose to the actual processing in SV_ExecuteClientCommand, only printing if the core doesn't intercept
+ clientOk = false;
+ }
+#endif
+
+ // don't allow another command for one second
+ cl->nextReliableTime = svs.time + 1000;
+
+ SV_ExecuteClientCommand( cl, s, clientOk );
+
+ cl->lastClientCommand = seq;
+ Com_sprintf(cl->lastClientCommandString, sizeof(cl->lastClientCommandString), "%s", s);
+
+ return true; // continue procesing
+}
+
+
+//==================================================================================
+
+
+/*
+==================
+SV_ClientThink
+
+Also called by bot code
+==================
+*/
+void SV_ClientThink (client_t *cl, usercmd_t *cmd) {
+ cl->lastUsercmd = *cmd;
+
+ if ( cl->state != CS_ACTIVE ) {
+ return; // may have been kicked during the last usercmd
+ }
+
+ VM_Call( sv.gvm, GAME_CLIENT_THINK, cl - svs.clients );
+}
+
+/*
+==================
+SV_UserMove
+
+The message usually contains all the movement commands
+that were in the last three packets, so that the information
+in dropped packets can be recovered.
+
+On very fast clients, there may be multiple usercmd packed into
+each of the backup packets.
+==================
+*/
+static void SV_UserMove( client_t *cl, msg_t *msg, bool delta ) {
+ int i, key;
+ int cmdCount;
+ usercmd_t nullcmd;
+ usercmd_t cmds[MAX_PACKET_USERCMDS];
+ usercmd_t *cmd, *oldcmd;
+
+ if ( delta ) {
+ cl->deltaMessage = cl->messageAcknowledge;
+ } else {
+ cl->deltaMessage = -1;
+ }
+
+ cmdCount = MSG_ReadByte( msg );
+
+ if ( cmdCount < 1 ) {
+ Com_Printf( "cmdCount < 1\n" );
+ return;
+ }
+
+ if ( cmdCount > MAX_PACKET_USERCMDS ) {
+ Com_Printf( "cmdCount > MAX_PACKET_USERCMDS\n" );
+ return;
+ }
+
+ // use the checksum feed in the key
+ key = sv.checksumFeed;
+ // also use the message acknowledge
+ key ^= cl->messageAcknowledge;
+ // also use the last acknowledged server command in the key
+ key ^= MSG_HashKey(cl->netchan.alternateProtocol, cl->reliableCommands[ cl->reliableAcknowledge & (MAX_RELIABLE_COMMANDS-1) ], 32);
+
+ ::memset( &nullcmd, 0, sizeof(nullcmd) );
+ oldcmd = &nullcmd;
+ for ( i = 0 ; i < cmdCount ; i++ ) {
+ cmd = &cmds[i];
+ MSG_ReadDeltaUsercmdKey( msg, key, oldcmd, cmd );
+ oldcmd = cmd;
+ }
+
+ // save time for ping calculation
+ cl->frames[ cl->messageAcknowledge & PACKET_MASK ].messageAcked = svs.time;
+
+ // TTimo
+ // catch the no-cp-yet situation before SV_ClientEnterWorld
+ // if CS_ACTIVE, then it's time to trigger a new gamestate emission
+ // if not, then we are getting remaining parasite usermove commands, which we should ignore
+ if (sv_pure->integer != 0 && cl->pureAuthentic == 0 && !cl->gotCP) {
+ if (cl->state == CS_ACTIVE)
+ {
+ // we didn't get a cp yet, don't assume anything and just send the gamestate all over again
+ Com_DPrintf( "%s: didn't get cp command, resending gamestate\n", cl->name);
+ SV_SendClientGameState( cl );
+ }
+ return;
+ }
+
+ // if this is the first usercmd we have received
+ // this gamestate, put the client into the world
+ if ( cl->state == CS_PRIMED ) {
+ SV_ClientEnterWorld( cl, &cmds[0] );
+ // the moves can be processed normaly
+ }
+
+ // a bad cp command was sent, drop the client
+ if (sv_pure->integer != 0 && cl->pureAuthentic == 0) {
+ SV_DropClient( cl, "Cannot validate pure client!");
+ return;
+ }
+
+ if ( cl->state != CS_ACTIVE ) {
+ cl->deltaMessage = -1;
+ return;
+ }
+
+ // usually, the first couple commands will be duplicates
+ // of ones we have previously received, but the servertimes
+ // in the commands will cause them to be immediately discarded
+ for ( i = 0 ; i < cmdCount ; i++ ) {
+ // if this is a cmd from before a map_restart ignore it
+ if ( cmds[i].serverTime > cmds[cmdCount-1].serverTime ) {
+ continue;
+ }
+ // extremely lagged or cmd from before a map_restart
+ //if ( cmds[i].serverTime > svs.time + 3000 ) {
+ // continue;
+ //}
+ // don't execute if this is an old cmd which is already executed
+ // these old cmds are included when cl_packetdup > 0
+ if ( cmds[i].serverTime <= cl->lastUsercmd.serverTime ) {
+ continue;
+ }
+ SV_ClientThink (cl, &cmds[ i ]);
+ }
+}
+
+
+#ifdef USE_VOIP
+/*
+==================
+SV_ShouldIgnoreVoipSender
+
+Blocking of voip packets based on source client
+==================
+*/
+
+static bool SV_ShouldIgnoreVoipSender(const client_t *cl)
+{
+ if (!sv_voip->integer)
+ return true; // VoIP disabled on this server.
+ else if (!cl->hasVoip) // client doesn't have VoIP support?!
+ return true;
+
+ // !!! FIXME: implement player blacklist.
+
+ return false; // don't ignore.
+}
+
+static void SV_UserVoip(client_t *cl, msg_t *msg, bool ignoreData)
+{
+ int sender, generation, sequence, frames;
+ uint8_t recips[(MAX_CLIENTS + 7) / 8];
+ int recip1 = 0, recip2 = 0, recip3 = 0; // silence warning
+ int flags = 0;
+ byte encoded[sizeof(cl->voipPacket[0]->data)];
+ client_t *client = NULL;
+ voipServerPacket_t *packet = NULL;
+ int i;
+
+ sender = cl - svs.clients;
+ generation = MSG_ReadByte(msg);
+ sequence = MSG_ReadLong(msg);
+ frames = MSG_ReadByte(msg);
+ if (cl->netchan.alternateProtocol == 0) {
+ MSG_ReadData(msg, recips, sizeof(recips));
+ flags = MSG_ReadByte(msg);
+ } else {
+ recip1 = MSG_ReadLong(msg);
+ recip2 = MSG_ReadLong(msg);
+ recip3 = MSG_ReadLong(msg);
+ }
+ size_t packetsize = MSG_ReadShort(msg);
+
+ if (msg->readcount > msg->cursize)
+ return; // short/invalid packet, bail.
+
+ if (packetsize > sizeof(encoded)) { // overlarge packet?
+ size_t bytesleft = packetsize;
+ while (bytesleft) {
+ size_t br = bytesleft;
+ if (br > sizeof(encoded))
+ br = sizeof(encoded);
+ MSG_ReadData(msg, encoded, br);
+ bytesleft -= br;
+ }
+ return; // overlarge packet, bail.
+ }
+
+ MSG_ReadData(msg, encoded, packetsize);
+
+ if (ignoreData || SV_ShouldIgnoreVoipSender(cl))
+ return; // Blacklisted, disabled, etc.
+
+ // !!! FIXME: see if we read past end of msg...
+
+ // !!! FIXME: reject if not opus data.
+ // !!! FIXME: decide if this is bogus data?
+
+ // decide who needs this VoIP packet sent to them...
+ for (i = 0, client = svs.clients; i < sv_maxclients->integer ; i++, client++) {
+ if (client->state != CS_ACTIVE)
+ continue; // not in the game yet, don't send to this guy.
+ else if (i == sender)
+ continue; // don't send voice packet back to original author.
+ else if (!client->hasVoip)
+ continue; // no VoIP support, or unsupported protocol
+ else if (client->muteAllVoip)
+ continue; // client is ignoring everyone.
+ else if (client->ignoreVoipFromClient[sender])
+ continue; // client is ignoring this talker.
+ else if (*cl->downloadName) // !!! FIXME: possible to DoS?
+ continue; // no VoIP allowed if downloading, to save bandwidth.
+
+ if (cl->netchan.alternateProtocol == 0) {
+ if(Com_IsVoipTarget(recips, sizeof(recips), i))
+ flags |= VOIP_DIRECT;
+ else
+ flags &= ~VOIP_DIRECT;
+ } else {
+ if (i < 31 && (recip1 & (1 << (i - 0))) == 0)
+ continue; // not addressed to this player.
+ else if (i >= 31 && i < 62 && (recip2 & (1 << (i - 31))) == 0)
+ continue; // not addressed to this player.
+ else if (i >= 62 && (recip3 & (1 << (i - 62))) == 0)
+ continue; // not addressed to this player.
+
+ flags |= VOIP_DIRECT;
+ }
+
+ if (!(flags & (VOIP_SPATIAL | VOIP_DIRECT)))
+ continue; // not addressed to this player.
+
+ // Transmit this packet to the client.
+ if (client->queuedVoipPackets >= ARRAY_LEN(client->voipPacket)) {
+ Com_Printf("Too many VoIP packets queued for client #%d\n", i);
+ continue; // no room for another packet right now.
+ }
+
+ packet = (voipServerPacket_t*)Z_Malloc(sizeof(*packet));
+ packet->sender = sender;
+ packet->frames = frames;
+ packet->len = packetsize;
+ packet->generation = generation;
+ packet->sequence = sequence;
+ packet->flags = flags;
+ memcpy(packet->data, encoded, packetsize);
+
+ client->voipPacket[(client->queuedVoipIndex + client->queuedVoipPackets) % ARRAY_LEN(client->voipPacket)] = packet;
+ client->queuedVoipPackets++;
+ }
+}
+#endif
+
+
+
+/*
+===========================================================================
+
+USER CMD EXECUTION
+
+===========================================================================
+*/
+
+/*
+===================
+SV_ExecuteClientMessage
+
+Parse a client packet
+===================
+*/
+void SV_ExecuteClientMessage( client_t *cl, msg_t *msg ) {
+ int c;
+ int serverId;
+
+ MSG_Bitstream(msg);
+
+ serverId = MSG_ReadLong( msg );
+ cl->messageAcknowledge = MSG_ReadLong( msg );
+
+ if (cl->messageAcknowledge < 0) {
+ // usually only hackers create messages like this
+ // it is more annoying for them to let them hanging
+#ifndef NDEBUG
+ SV_DropClient( cl, "DEBUG: illegible client message" );
+#endif
+ return;
+ }
+
+ cl->reliableAcknowledge = MSG_ReadLong( msg );
+
+ // NOTE: when the client message is fux0red the acknowledgement numbers
+ // can be out of range, this could cause the server to send thousands of server
+ // commands which the server thinks are not yet acknowledged in SV_UpdateServerCommandsToClient
+ if (cl->reliableAcknowledge < cl->reliableSequence - MAX_RELIABLE_COMMANDS) {
+ // usually only hackers create messages like this
+ // it is more annoying for them to let them hanging
+#ifndef NDEBUG
+ SV_DropClient( cl, "DEBUG: illegible client message" );
+#endif
+ cl->reliableAcknowledge = cl->reliableSequence;
+ return;
+ }
+ // if this is a usercmd from a previous gamestate,
+ // ignore it or retransmit the current gamestate
+ //
+ // if the client was downloading, let it stay at whatever serverId and
+ // gamestate it was at. This allows it to keep downloading even when
+ // the gamestate changes. After the download is finished, we'll
+ // notice and send it a new game state
+ //
+ // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=536
+ // don't drop as long as previous command was a nextdl, after a dl is done, downloadName is set back to ""
+ // but we still need to read the next message to move to next download or send gamestate
+ // I don't like this hack though, it must have been working fine at some point, suspecting the fix is somewhere else
+ if ( serverId != sv.serverId && !*cl->downloadName && !strstr(cl->lastClientCommandString, "nextdl") ) {
+ if ( serverId >= sv.restartedServerId && serverId < sv.serverId ) { // TTimo - use a comparison here to catch multiple map_restart
+ // they just haven't caught the map_restart yet
+ Com_DPrintf("%s : ignoring pre map_restart / outdated client message\n", cl->name);
+ return;
+ }
+ // if we can tell that the client has dropped the last
+ // gamestate we sent them, resend it
+ if ( cl->messageAcknowledge > cl->gamestateMessageNum ) {
+ Com_DPrintf( "%s : dropped gamestate, resending\n", cl->name );
+ SV_SendClientGameState( cl );
+ }
+ return;
+ }
+
+ // this client has acknowledged the new gamestate so it's
+ // safe to start sending it the real time again
+ if( cl->oldServerTime && serverId == sv.serverId ){
+ Com_DPrintf( "%s acknowledged gamestate\n", cl->name );
+ cl->oldServerTime = 0;
+ }
+
+ // read optional clientCommand strings
+ do {
+ c = MSG_ReadByte( msg );
+
+ if ( cl->netchan.alternateProtocol != 0 ) {
+ // See if this is an extension command after the EOF, which means we
+ // got data that a legacy server should ignore.
+ if ( c == clc_EOF && MSG_LookaheadByte( msg ) == clc_voipSpeex ) {
+ MSG_ReadByte( msg ); // throw the clc_extension byte away.
+ c = MSG_ReadByte( msg ); // something legacy servers can't do!
+ if ( c == clc_voipSpeex + 1 ) {
+ c = clc_voipSpeex;
+ }
+ // sometimes you get a clc_extension at end of stream...dangling
+ // bits in the huffman decoder giving a bogus value?
+ if ( c == -1 ) {
+ c = clc_EOF;
+ }
+ }
+
+ if ( c == svc_voipSpeex ) {
+ c = svc_voipSpeex + 1;
+ } else if ( c == svc_voipSpeex + 1 ) {
+ c = svc_voipSpeex;
+ }
+ }
+
+ if ( c == clc_EOF ) {
+ break;
+ }
+
+ if ( c != clc_clientCommand ) {
+ break;
+ }
+ if ( !SV_ClientCommand( cl, msg ) ) {
+ return; // we couldn't execute it because of the flood protection
+ }
+ if (cl->state == CS_ZOMBIE) {
+ return; // disconnect command
+ }
+ } while ( 1 );
+
+ // skip legacy speex voip data
+ if ( c == clc_voipSpeex ) {
+#ifdef USE_VOIP
+ SV_UserVoip( cl, msg, true );
+ c = MSG_ReadByte( msg );
+#endif
+ }
+
+ // read optional voip data
+ if ( c == clc_voipOpus ) {
+#ifdef USE_VOIP
+ SV_UserVoip( cl, msg, false );
+ c = MSG_ReadByte( msg );
+#endif
+ }
+
+ // read the usercmd_t
+ if ( c == clc_move ) {
+ SV_UserMove( cl, msg, true );
+ } else if ( c == clc_moveNoDelta ) {
+ SV_UserMove( cl, msg, false );
+ } else if ( c != clc_EOF ) {
+ Com_Printf( "WARNING: bad command byte for client %i\n", (int) (cl - svs.clients) );
+ }
+// if ( msg->readcount != msg->cursize ) {
+// Com_Printf( "WARNING: Junk at end of packet for client %i\n", cl - svs.clients );
+// }
+}