/* Copyright (C) 1997-2001 Id Software, Inc. This program 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 2 of the License, or (at your option) any later version. This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "server.h" netadr_t master_adr[MAX_MASTERS]; // address of group servers mempool_t *sv_mempool; client_t *sv_client; // current client cvar_t *sv_enforcetime; cvar_t *sv_timeout; // seconds without any message cvar_t *sv_zombietime; // seconds to sink messages after disconnect cvar_t *rcon_password; // password for remote server commands cvar_t *sv_uploads; cvar_t *sv_uploads_from_server; cvar_t *sv_uploads_baseurl; cvar_t *sv_noreload; // don't reload level state when reentering cvar_t *sv_maxclients; cvar_t *sv_showclamp; cvar_t *sv_hostname; cvar_t *sv_public; // should heartbeats be sent cvar_t *sv_reconnectlimit; // minimum seconds between connect messages // wsw : jal cvar_t *sv_maxrate; cvar_t *sv_masterservers; cvar_t *sv_skilllevel; // wsw : debug netcode cvar_t *sv_debug_serverCmd; #ifdef BATTLEYE cvar_t *sv_battleye; #endif //============================================================================ /* ===================== 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. ===================== */ void SV_DropClient( client_t *drop, int type, char *fmt, ... ) { va_list argptr; char string[1024]; va_start( argptr, fmt ); vsnprintf( string, sizeof(string), fmt, argptr ); va_end( argptr ); if( dedicated->integer ) Com_Printf( "SV_DropClient: \"%s\"\n", string ); // add the disconnect if( drop->edict && (drop->edict->r.svflags & SVF_FAKECLIENT) ) { //if( drop->state == CS_SPAWNED ) ge->ClientDisconnect( drop->edict ); // reset the reliable commands chain drop->clientCommandExecuted = 0; drop->reliableAcknowledge = 0; drop->reliableSequence = 0; drop->reliableSent = 0; MSG_Init( &drop->soundsmsg, drop->soundsmsgData, sizeof(drop->soundsmsgData) ); drop->soundsmsg.allowoverflow = qtrue; drop->lastPacketReceivedTime = svs.realtime; drop->lastconnect = svs.realtime; #ifdef BATTLEYE memset( &drop->BE, 0, sizeof(drop->BE) ); #endif } else { MSG_Clear( &tmpMessage ); SV_SendServerCommand( drop, "disconnect %i \"%s\"", type, string ); SV_AddReliableCommandsToMessage( drop, &tmpMessage ); SV_SendMessageToClient( drop, &tmpMessage ); Netchan_PushAllFragments( &drop->netchan ); if( drop->state >= CS_CONNECTED ) { // call the prog function for removing a client // this will remove the body, among other things ge->ClientDisconnect( drop->edict ); #ifdef BATTLEYE if (sv_battleye->integer && drop->battleye) { // inform BE Master about removed player qbyte removeplayer_packet[] = { 1 /*packet type*/, drop - svs.clients /*player id*/ }; SV_BE_SendToMaster(removeplayer_packet, sizeof(removeplayer_packet)); } #endif } } if( drop->download ) { FS_FreeFile( drop->download ); drop->download = NULL; drop->downloadname[0] = 0; drop->downloadsize = 0; } drop->state = CS_ZOMBIE; // become free in a few seconds drop->name[0] = 0; } /* ============================================================================== CONNECTIONLESS COMMANDS ============================================================================== */ /* =============== SV_StatusString Builds the string that is sent as heartbeats and status replies =============== */ char *SV_StatusString( void ) { char tempstr[1024]; static char status[MAX_MSGLEN - 16]; int i, bots, count; client_t *cl; size_t statusLength; size_t tempstrLength; Q_strncpyz( status, Cvar_Serverinfo(), sizeof(status) ); statusLength = strlen(status); bots = 0; count = 0; for( i = 0; i < sv_maxclients->integer; i++ ) { cl = &svs.clients[i]; if( cl->state >= CS_CONNECTED ) { if( cl->edict->r.svflags & SVF_FAKECLIENT ) bots++; count++; } } if( bots ) Q_snprintfz( tempstr, sizeof(tempstr), "\\bots\\%i", bots ); Q_snprintfz( tempstr, sizeof(tempstr), "\\clients\\%i\n", count ); tempstrLength = strlen(tempstr); if( statusLength + tempstrLength >= sizeof(status) ) return status; // can't hold any more Q_strncpyz( status + statusLength, tempstr, sizeof(status) - statusLength ); statusLength += tempstrLength; for( i = 0; i < sv_maxclients->integer; i++ ) { cl = &svs.clients[i]; if( cl->state == CS_CONNECTED || cl->state == CS_SPAWNED ) { Q_snprintfz( tempstr, sizeof(tempstr), "%i %i \"%s\" %i\n", cl->edict->r.client->r.frags, cl->ping, cl->name, cl->edict->s.team ); tempstrLength = strlen(tempstr); if( statusLength + tempstrLength >= sizeof(status) ) break; // can't hold any more Q_strncpyz( status + statusLength, tempstr, sizeof(status) - statusLength ); statusLength += tempstrLength; } } return status; } /* ================ SVC_Ack ================ */ void SVC_Ack( void ) { Com_Printf( "Ping acknowledge from %s\n", NET_AdrToString(&net_from) ); } /* ================ SVC_Info Responds with short info for broadcast scans The second parameter should be the current protocol version number. ================ */ #define MAX_STRING_SVCINFOSTRING 160 #define MAX_SVCINFOSTRING_LEN (MAX_STRING_SVCINFOSTRING - 4) void SVC_Info( void ) { char string[MAX_STRING_SVCINFOSTRING]; char hostname[64]; char entry[16]; size_t len; int i, count, bots; int version; qboolean allow_empty=qfalse, allow_full=qfalse; if( sv_maxclients->integer == 1 ) return; // ignore in single player version = atoi (Cmd_Argv(1)); if( version != PROTOCOL_VERSION ) { Q_snprintfz( string, sizeof(string), "%s: wrong version\n", sv_hostname->string, sizeof(string) ); return; } bots = 0; count = 0; for( i = 0; i < sv_maxclients->integer; i++ ) { if( svs.clients[i].state >= CS_CONNECTED ) { if( svs.clients[i].edict->r.svflags & SVF_FAKECLIENT ) bots++; count++; } } for( i = 0; i < Cmd_Argc(); i++ ) { if( !Q_stricmp( Cmd_Argv(i), "full" ) ) allow_full = qtrue; if( !Q_stricmp( Cmd_Argv(i), "empty" ) ) allow_empty = qtrue; } if( (count == sv_maxclients->integer) && !allow_full ) { return; } if( (count == 0) && !allow_empty ) { return; } //format: //" \377\377\377\377info\\n\\server_name\\m\\map name\\u\\clients/maxclients\\g\\gametype\\s\\skill\\EOT " if( sv_skilllevel->integer > 2 ) Cvar_ForceSet( "sv_skilllevel", "2" ); if( sv_skilllevel->integer < 0 ) Cvar_ForceSet( "sv_skilllevel", "0" ); Q_strncpyz( hostname, sv_hostname->string, sizeof(hostname) ); Q_snprintfz( string, sizeof(string), "\\\\n\\\\%s\\\\m\\\\%8s\\\\u\\\\%2i/%2i\\\\", hostname, sv.name, count > 99 ? 99 : count, sv_maxclients->integer > 99 ? 99 : sv_maxclients->integer ); len = strlen(string); *entry = 0; Q_snprintfz( entry, sizeof(entry), "g\\\\%5s\\\\", Cvar_VariableString( "g_gametype" ) ); if( MAX_SVCINFOSTRING_LEN - len > strlen(entry) ) { Q_strncatz( string, entry, sizeof(string) ); len = strlen(string); } *entry = 0; Q_snprintfz( entry, sizeof(entry), "s\\\\%1d\\\\", sv_skilllevel->integer ); if( MAX_SVCINFOSTRING_LEN - len > strlen(entry) ) { Q_strncatz( string, entry, sizeof(string) ); len = strlen(string); } if( (strlen(Cvar_VariableString( "password" )) > 0) ) { *entry = 0; Q_snprintfz( entry, sizeof(entry), "p\\\\%i\\\\", (strlen(Cvar_VariableString( "password" )) > 0) ); if( MAX_SVCINFOSTRING_LEN - len > strlen(entry) ) { Q_strncatz( string, entry, sizeof(string) ); len = strlen(string); } } if( bots ) { *entry = 0; Q_snprintfz( entry, sizeof(entry), "b\\\\%2i\\\\", bots > 99 ? 99 : bots ); if( MAX_SVCINFOSTRING_LEN - len > strlen(entry) ) { Q_strncatz( string, entry, sizeof(string) ); len = strlen(string); } } #ifdef BATTLEYE if( sv_battleye->integer ) { *entry = 0; Q_snprintfz( entry, sizeof(entry), "be\\\\%i\\\\", sv_battleye->integer ); if( MAX_SVCINFOSTRING_LEN - len > strlen(entry) ) { Q_strncatz( string, entry, sizeof(string) ); len = strlen(string); } } #endif // finish it Q_strncatz( string, "EOT", sizeof(string) ); Netchan_OutOfBandPrint( NS_SERVER, net_from, "info\n%s", string ); } /* ================ SVC_Ping Just responds with an acknowledgement ================ */ void SVC_Ping( void ) { Netchan_OutOfBandPrint( NS_SERVER, net_from, "ack" ); } /* ================= SVC_GetChallenge Returns a challenge number that can be used in a subsequent client_connect 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. ================= */ void SVC_GetChallenge( void ) { int i; int oldest; int oldestTime; oldest = 0; oldestTime = 0x7fffffff; // see if we already have a challenge for this ip for( i = 0 ; i < MAX_CHALLENGES ; i++ ) { if( NET_CompareBaseAdr (&net_from, &svs.challenges[i].adr) ) break; if( svs.challenges[i].time < oldestTime ) { oldestTime = svs.challenges[i].time; oldest = i; } } if( i == MAX_CHALLENGES ) { // overwrite the oldest svs.challenges[oldest].challenge = rand() & 0x7fff; svs.challenges[oldest].adr = net_from; svs.challenges[oldest].time = curtime; i = oldest; } // send it back Netchan_OutOfBandPrint( NS_SERVER, net_from, "challenge %i", svs.challenges[i].challenge ); } /* ================== SVC_ClientConnect ================== */ qboolean SVC_ClientConnect( client_t *cl, char *userinfo, int challenge, qboolean fakeClient ) { edict_t *ent; int edictnum; // build a new connection // accept the new client // this is the only place a client_t is ever initialized memset( cl, 0, sizeof( *cl ) ); sv_client = cl; edictnum = (cl - svs.clients) + 1; ent = EDICT_NUM(edictnum); cl->edict = ent; cl->challenge = challenge; // save challenge for checksumming if( !ge->ClientConnect( ent, userinfo, fakeClient ) ) return qfalse; #ifdef BATTLEYE if( sv_battleye->integer ) { cl->battleye = (atoi(Info_ValueForKey( userinfo, "cl_battleye" )) != 0); if( sv_battleye->integer == 2 && !cl->battleye ) { Info_SetValueForKey( userinfo, "rejtype", va("%i", DROP_TYPE_GENERAL) ); Info_SetValueForKey( userinfo, "rejflag", va("%i", 0) ); Info_SetValueForKey( userinfo, "rejmsg", "BattlEye is required in this server" ); Com_DPrintf( "Rejected a non-BattlEye client.\n" ); return qfalse; } memset( &cl->BE, 0, sizeof(cl->BE) ); if( cl->battleye ) { // inform BE Master about new player qbyte addplayer_packet[] = { 1 /*packet type*/, cl - svs.clients /*player id*/ }; SV_BE_SendToMaster(addplayer_packet, sizeof(addplayer_packet)); } } #endif // get the game a chance to reject this connection or modify the userinfo return qtrue; } /* ================== SVC_DirectConnect A connection request that did not come from the master ================== */ void SVC_DirectConnect( void ) { int i; char userinfo[MAX_INFO_STRING]; netadr_t adr; client_t *cl, *newcl; int version; int qport; int challenge; adr = net_from; Com_DPrintf( "SVC_DirectConnect ()\n" ); version = atoi( Cmd_Argv(1) ); if( version != PROTOCOL_VERSION ) { if( version <= 6 ) { // before reject packet was added Netchan_OutOfBandPrint( NS_SERVER, adr, "print\nServer is version %4.2f. Protocol %3i\n", VERSION, PROTOCOL_VERSION ); } else { Netchan_OutOfBandPrint( NS_SERVER, adr, "reject\n%i\n%i\nServer and client don't have the same version\n", DROP_TYPE_GENERAL, 0 ); } Com_DPrintf( " rejected connect from protocol %i\n", version ); return; } qport = atoi( Cmd_Argv(2) ); challenge = atoi( Cmd_Argv(3) ); // wsw : jal : check size of userinfo + ip before adding it if( strlen(Cmd_Argv(4)) + strlen( NET_AdrToString(&net_from) ) >= MAX_INFO_STRING ) { Netchan_OutOfBandPrint( NS_SERVER, adr, "reject\n%i\n%i\nUserinfo string too large\n", DROP_TYPE_GENERAL ); Com_Printf( "ClientConnect: userinfo string exceeded max valid size. Connection refused.\n" ); return; } Q_strncpyz( userinfo, Cmd_Argv(4), sizeof(userinfo) ); // wsw : jal : check for empty userinfo if( !(*userinfo) ) { Netchan_OutOfBandPrint( NS_SERVER, adr, "reject\n%i\n%i\nInvalid userinfo string\n", DROP_TYPE_GENERAL, 0 ); Com_Printf( "ClientConnect: Empty userinfo string. Connection refused.\n" ); return; } // force the IP key/value pair so the game can filter based on ip Info_SetValueForKey( userinfo, "ip", NET_AdrToString(&net_from) ); // see if the challenge is valid // wsw: mdr: removing this fixed the loopback stuff // probably because first client_connect is accepted by server, but client still sends another // and that's seen as a reconnect by server //if( !NET_IsLocalAddress(&adr) ) //{ for( i = 0; i < MAX_CHALLENGES; i++ ) { if( NET_CompareBaseAdr( &net_from, &svs.challenges[i].adr ) ) { if( challenge == svs.challenges[i].challenge ) { svs.challenges[i].challenge = 0; // wsw : r1q2 : reset challenge break; // good } Netchan_OutOfBandPrint( NS_SERVER, adr, "reject\n%i\n%i\nBad challenge\n", DROP_TYPE_GENERAL, DROP_FLAG_AUTORECONNECT ); return; } } if( i == MAX_CHALLENGES ) { Netchan_OutOfBandPrint( NS_SERVER, adr, "reject\n%i\n%i\nNo challenge for address\n", DROP_TYPE_GENERAL, DROP_FLAG_AUTORECONNECT ); return; } //} // 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(&adr, &cl->netchan.remoteAddress) && ( cl->netchan.qport == qport || adr.port == cl->netchan.remoteAddress.port ) ) { if( !NET_IsLocalAddress(&adr) && (svs.realtime - cl->lastconnect) < (unsigned)(sv_reconnectlimit->integer * 1000) ) { Com_DPrintf( "%s:reconnect rejected : too soon\n", NET_AdrToString(&adr) ); return; } Com_Printf( "%s:reconnect\n", NET_AdrToString(&adr) ); newcl = cl; goto gotnewcl; } } // find a client slot newcl = NULL; for( i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++ ) { if( cl->state == CS_FREE ) { newcl = cl; break; } } if( !newcl ) { Netchan_OutOfBandPrint( NS_SERVER, adr, "reject\n%i\n%i\nServer is full\n", DROP_TYPE_GENERAL, DROP_FLAG_AUTORECONNECT ); Com_DPrintf( "Server is full. Rejected a connection.\n" ); return; } gotnewcl: // wsw : r1q2[start] : check for end-of-message-in-string exploit if( strchr(userinfo, '\xFF') ) { Com_Printf( "wsw : r1q2[start] : check for end-of-message-in-string exploit. Connection refused.\n" ); Netchan_OutOfBandPrint( NS_SERVER, adr, "reject\n%i\n%i\nInvalid userinfo string\n", DROP_TYPE_GENERAL, 0 ); return; } // wsw : r1q2[end] // get the game a chance to reject this connection or modify the userinfo if( !SVC_ClientConnect(newcl, userinfo, challenge, qfalse) ) { char *rejtypeflag, *rejmsg; // hax because Info_ValueForKey can only be called twice in a row rejtypeflag = va("%s\n%s", Info_ValueForKey(userinfo, "rejtype"), Info_ValueForKey(userinfo, "rejflag")); rejmsg = Info_ValueForKey(userinfo, "rejmsg"); Netchan_OutOfBandPrint( NS_SERVER, adr, "reject\n%s\n%s\n", rejtypeflag, rejmsg ); Com_DPrintf( "Game rejected a connection.\n"); return; } // parse some info from the info strings Q_strncpyz( newcl->userinfo, userinfo, sizeof(newcl->userinfo) ); SV_UserinfoChanged( newcl ); // send the connect packet to the client Netchan_OutOfBandPrint( NS_SERVER, adr, "client_connect" ); Netchan_Setup( NS_SERVER, &newcl->netchan, adr, qport ); newcl->state = CS_CONNECTED; newcl->clientCommandExecuted = 0; newcl->reliableAcknowledge = 0; newcl->reliableSequence = 0; newcl->reliableSent = 0; memset( &newcl->reliableCommands, 0, sizeof(newcl->reliableCommands) ); #ifdef BATTLEYE memset( &newcl->BE, 0, sizeof(newcl->BE) ); #endif MSG_Init( &newcl->soundsmsg, newcl->soundsmsgData, sizeof(newcl->soundsmsgData) ); newcl->soundsmsg.allowoverflow = qtrue; newcl->lastPacketReceivedTime = svs.realtime; // don't timeout newcl->lastconnect = svs.realtime; } /* ================== SVC_FakeConnect A connection request that came from the game module ================== */ void SVC_FakeConnect( char *fakeUserinfo, char *fakeIP ) { int i; char userinfo[MAX_INFO_STRING]; client_t *cl, *newcl; Com_DPrintf( "SVC_FakeConnect ()\n" ); if( !fakeUserinfo ) fakeUserinfo = ""; if( !fakeIP ) fakeIP = "127.0.0.1"; Q_strncpyz( userinfo, fakeUserinfo, sizeof(userinfo) ); // force the IP key/value pair so the game can filter based on ip Info_SetValueForKey( userinfo, "ip", fakeIP ); // find a client slot newcl = NULL; for( i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++ ) { if( cl->state == CS_FREE ) { newcl = cl; break; } } if( !newcl ) { Com_DPrintf( "Rejected a connection.\n" ); return; } // get the game a chance to reject this connection or modify the userinfo if( !SVC_ClientConnect(newcl, userinfo, -1, qtrue) ) { Com_DPrintf( "Game rejected a connection.\n" ); return; } // parse some info from the info strings Q_strncpyz( newcl->userinfo, userinfo, sizeof(newcl->userinfo) ); SV_UserinfoChanged( newcl ); newcl->state = CS_SPAWNED; newcl->lastPacketReceivedTime = svs.realtime; // don't timeout newcl->lastconnect = svs.realtime; // wsw : jal : fakeclients can't transmit newcl->netchan.remoteAddress.type = NA_NOTRANSMIT; // call the game begin function ge->ClientBegin( newcl->edict ); } /* =============== Rcon_Validate =============== */ int Rcon_Validate( void ) { if( !strlen(rcon_password->string) ) return 0; if( strcmp( Cmd_Argv(1), rcon_password->string ) ) return 0; return 1; } /* =============== SVC_RemoteCommand A client issued an rcon command. Shift down the remaining args Redirect all printfs =============== */ void SVC_RemoteCommand( msg_t *msg ) { int i; char remaining[1024]; i = Rcon_Validate(); if( !msg || !msg->data || msg->cursize < 5 ) { Com_Printf( "Bad rcon from %s:\n", NET_AdrToString (&net_from) ); } if( i == 0 ) Com_Printf( "Bad rcon from %s:\n%s\n", NET_AdrToString (&net_from), msg->data+4 ); else Com_Printf( "Rcon from %s:\n%s\n", NET_AdrToString (&net_from), msg->data+4 ); Com_BeginRedirect( RD_PACKET, sv_outputbuf, SV_OUTPUTBUF_LENGTH, SV_FlushRedirect ); if( !Rcon_Validate() ) { Com_Printf( "Bad rcon_password.\n" ); } else { remaining[0] = 0; for( i = 2; i < Cmd_Argc(); i++ ) { Q_strncatz( remaining, Cmd_Argv(i), sizeof(remaining) ); Q_strncatz( remaining, " ", sizeof(remaining) ); } Cmd_ExecuteString( remaining ); } Com_EndRedirect(); } /* ================= SV_ConnectionlessPacket A connectionless packet has four leading 0xff characters to distinguish it from a game channel. Clients that are in the game can still send connectionless packets. ================= */ void SV_ConnectionlessPacket( msg_t *msg ) { char *s; char *c; MSG_BeginReading( msg ); MSG_ReadLong( msg ); // skip the -1 marker s = MSG_ReadStringLine( msg ); Cmd_TokenizeString( s, qfalse ); c = Cmd_Argv(0); Com_DPrintf( "Packet %s : %s\n", NET_AdrToString(&net_from), c ); if( !strcmp(c, "ping") ) SVC_Ping(); else if( !strcmp(c, "ack") ) SVC_Ack(); else if( !strcmp(c, "info") ) SVC_Info(); else if( !strcmp(c, "getinfo") ) SVC_MasterInfoResponse(); else if( !strcmp(c, "getchallenge") ) SVC_GetChallenge(); else if( !strcmp(c, "connect") ) SVC_DirectConnect(); else if( !strcmp(c, "rcon") ) SVC_RemoteCommand( msg ); else Com_Printf( "bad connectionless packet from %s:\n%s\n" , NET_AdrToString(&net_from), s ); } //============================================================================ /* =================== SV_CalcPings Updates the cl->ping variables =================== */ void SV_CalcPings( void ) { int i; client_t *cl; int total, count, j; for( i = 0 ; i < sv_maxclients->integer; i++ ) { cl = &svs.clients[i]; if( cl->state != CS_SPAWNED ) continue; total = 0; count = 0; for( j = 0; jframe_latency[j] > 0 ) { count++; total += cl->frame_latency[j]; } } if( !count ) cl->ping = 0; else #if 0 cl->ping = total*100/count - 100; #else cl->ping = total / count; #endif // let the game dll know about the ping cl->edict->r.client->r.ping = cl->ping; } } /* =================== SV_GiveMsec Every few frames, gives all clients an allotment of milliseconds for their command moves. If they exceed it, assume cheating. =================== */ void SV_GiveMsec( void ) { int i; client_t *cl; if( sv.framenum & 15 ) return; for( i = 0 ; i < sv_maxclients->integer; i++ ) { cl = &svs.clients[i]; if( cl->state == CS_FREE ) continue; cl->commandMsec = 1800; // 1600 + some slop } } //================= // SV_ProcessPacket //================= qboolean SV_ProcessPacket( netchan_t *netchan, msg_t *msg ) { if( !Netchan_Process( netchan, msg ) ) return qfalse; // wasn't accepted for some reason { int sequence, sequence_ack; int qport = -1; int zerror; // now if compressed, expand it MSG_BeginReading( msg ); sequence = MSG_ReadLong( msg ); sequence_ack = MSG_ReadLong( msg ); qport = MSG_ReadShort( msg ); //if( sequence_ack & FRAGMENT_BIT ) // it is compressed if( msg->compressed ) { zerror = Netchan_DecompressMessage( msg ); if( zerror < 0 ) { // compression error. Drop the packet Com_DPrintf( "SV_ProcessPacket: Compression error %i. Dropping packet\n", zerror ); return qfalse; } } } return qtrue; } /* ================= SV_ReadPackets ================= */ void SV_ReadPackets( void ) { int i; client_t *cl; int qport; static msg_t msg; static qbyte msgData[MAX_MSGLEN]; MSG_Init( &msg, msgData, sizeof(msgData) ); MSG_Clear( &msg ); while( NET_GetPacket( NS_SERVER, &net_from, &msg ) ) { // check for connectionless packet (0xffffffff) first if( *(int *)msg.data == -1 ) { SV_ConnectionlessPacket( &msg ); continue; } // read the qport out of the message so we can fix up // stupid address translating routers MSG_BeginReading( &msg ); MSG_ReadLong( &msg ); // sequence number MSG_ReadLong( &msg ); // sequence number qport = MSG_ReadShort( &msg ) & 0xffff; // data follows // check for packets from connected clients for( i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++ ) { if( cl->state == CS_FREE ) continue; if( cl->edict && (cl->edict->r.svflags & SVF_FAKECLIENT) ) continue; if( !NET_CompareBaseAdr(&net_from, &cl->netchan.remoteAddress) ) continue; if( cl->netchan.qport != qport ) continue; if( cl->netchan.remoteAddress.port != net_from.port ) { Com_Printf( "SV_ReadPackets: fixing up a translated port\n" ); cl->netchan.remoteAddress.port = net_from.port; } if( SV_ProcessPacket( &cl->netchan, &msg ) ) { // this is a valid, sequenced packet, so process it if( cl->state != CS_ZOMBIE ) { cl->lastPacketReceivedTime = svs.realtime; // don't timeout SV_ExecuteClientMessage( cl, &msg ); } } break; } if( i != sv_maxclients->integer ) continue; } } /* ================== SV_CheckTimeouts If a packet has not been received from a client for timeout->value seconds, drop the conneciton. Server frames are used instead of realtime to avoid dropping the local client while debugging. When a client is normally dropped, the client_t goes into a zombie state for a few seconds to make sure any final reliable message gets resent if necessary ================== */ void SV_CheckTimeouts( void ) { int i; client_t *cl; for( i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++ ) { // fake clients do not timeout if( cl->edict && (cl->edict->r.svflags & SVF_FAKECLIENT) ) cl->lastPacketReceivedTime = svs.realtime; // message times may be wrong across a changelevel else if( cl->lastPacketReceivedTime > svs.realtime ) cl->lastPacketReceivedTime = svs.realtime; if( cl->state == CS_ZOMBIE && cl->lastPacketReceivedTime + 1000*sv_zombietime->value < svs.realtime ) { cl->state = CS_FREE; // can now be reused continue; } if( (cl->state != CS_FREE && cl->state != CS_ZOMBIE) && ( cl->lastPacketReceivedTime + 1000*sv_timeout->value < svs.realtime ) ) { SV_DropClient( cl, DROP_TYPE_GENERAL, "Connection timed out" ); cl->state = CS_FREE; // don't bother with zombie state SV_ClientPrintf( NULL, "%s timed out\n", cl->name ); } // timeout downloads left open if( (cl->state != CS_FREE && cl->state != CS_ZOMBIE) && (cl->download && cl->downloadtimeout < svs.realtime) ) { Com_Printf( "Download of %s to %s timed out\n", cl->downloadname, cl->name ); FS_FreeFile( cl->download ); cl->download = NULL; cl->downloadtimeout = 0; cl->downloadname[0] = 0; cl->downloadsize = 0; } } } /* ================ SV_PrepWorldFrame This has to be done before the world logic, because player processing happens outside RunWorldFrame ================ */ void SV_PrepWorldFrame( void ) { edict_t *ent; int i; for( i = 0; i < sv.num_edicts; i++, ent++ ) { ent = EDICT_NUM(i); // events only last for a single message ent->s.events[0] = ent->s.events[1] = 0; ent->s.eventParms[0] = ent->s.eventParms[1] = 0; } } /* ================= SV_RunGameFrame ================= */ qboolean SV_RunGameFrame( void ) { // move autonomous things around if enough time has passed if( svs.realtime < sv.time ) { // never let the time get too far off if( sv.time - svs.realtime > svc.frametime ) { if( sv_showclamp->integer ) Com_Printf( "sv lowclamp\n" ); sv.time = svs.realtime + svc.frametime; } if( !SV_SendClientsFragments() ) NET_Sleep( sv.time - svs.realtime ); return qfalse; } // update ping based on the last known frame from all clients SV_CalcPings(); if( host_speeds->integer ) time_before_game = Sys_Milliseconds(); // we always need to bump framenum, even if we // don't run the world, otherwise the delta // compression can get confused when a client // has the "current" frame sv.framenum++; sv.time = svs.realtime + svc.frametime; ge->RunFrame( svc.frametime ); if( host_speeds->integer ) time_after_game = Sys_Milliseconds(); return qtrue; } /* ================== SV_Frame ================== */ void SV_Frame( int msec ) { time_before_game = time_after_game = 0; // if server is not active, do nothing if( !svs.initialized ) return; svs.realtime += msec; // check timeouts SV_CheckTimeouts(); // get packets from clients SV_ReadPackets(); #ifdef BATTLEYE // check for connection to and incoming packets from BattlEye Master Server if (sv_battleye->integer) SV_BattlEyeFrame(msec); #endif // let everything in the world think and move if( SV_RunGameFrame() ) { // give the clients some timeslices SV_GiveMsec(); #ifdef COLLISION4D // backup current frame data for 4Dcollision SV_BackUpCollisionFrame(); #endif // send messages back to the clients that had packets read this frame SV_SendClientMessages(); #ifdef SERVERSIDE_DEMOS // save the entire world state if recording a serverdemo SV_RecordDemoMessage(); #endif // send a heartbeat to the master if needed SV_MasterHeartbeat(); // clear teleport flags, etc for next frame SV_PrepWorldFrame(); } } #ifdef BATTLEYE /* ================== SV_BattlEyeFrame ================== */ void SV_BattlEyeFrame( int msec ) { // connectstate: -1 = disconnected, 0 = connecting, 1 = connected static int connectstate = -1; static int nextconnect_countdown = 0; int i; client_t* cl; if (connectstate == -1) { if ((nextconnect_countdown -= msec) <= 0) { int numplayers = 0; // only use BE if there are (human) players on the server for (i = 0; i < sv_maxclients->integer; i++) { cl = &svs.clients[i]; if (cl->state >= CS_CONNECTED && !(cl->edict && (cl->edict->r.svflags & SVF_FAKECLIENT)) && cl->battleye) numplayers++; } if (numplayers > 0) connectstate = (SV_BE_ConnectToMaster() ? 0 : -1); } // do not re-set nextconnect_countdown below else return; } else if (connectstate == 0) { connectstate = SV_BE_CheckConnectAttempt(); if (connectstate == 1) { char version_packet[20]; Com_Printf("Connected to BattlEye Master Server\n"); // send game version info version_packet[0] = (char)sprintf(version_packet+1, "%.3f", VERSION) + 1; SV_BE_SendToMaster(version_packet, version_packet[0]+1); // re-inform BE Master about (human) players on the server (after a disconnect) for (i = 0; i < sv_maxclients->integer; i++) { cl = &svs.clients[i]; if (cl->state >= CS_CONNECTED && !(cl->edict && (cl->edict->r.svflags & SVF_FAKECLIENT)) && cl->battleye) { qbyte addplayer_packet[] = { 1 /*packet type*/, i /*player id*/ }; SV_BE_SendToMaster(addplayer_packet, sizeof(addplayer_packet)); } } } else if (connectstate == -1) Com_Printf("Couldn't connect to BattlEye Master Server: %s\n", NET_ErrorString()); } else { qbyte packetid; int result; qboolean firstreceived = qfalse; // result: -1 = failed or not enough data (!= param len), 0 = no data, 1 = succeeded if ((result = SV_BE_ReceiveFromMaster(&packetid, sizeof(packetid))) == 1) { firstreceived = qtrue; // distribute packet to all connected (human) players if (packetid == 0) { short packetlen; static qbyte packet[BE_MAX_PACKET_SIZE]; // receive actual packet if ((result = SV_BE_ReceiveFromMaster(&packetlen, sizeof(packetlen))) != 1) goto disconnect; if (packetlen <= 0 || packetlen > BE_MAX_PACKET_SIZE) { result = -1; goto disconnect; } if ((result = SV_BE_ReceiveFromMaster(packet, packetlen)) != 1) goto disconnect; for (i = 0; i < sv_maxclients->integer; i++) { cl = &svs.clients[i]; if (cl->state >= CS_CONNECTED && !(cl->edict && (cl->edict->r.svflags & SVF_FAKECLIENT)) && cl->battleye) { // send packet to client memcpy(cl->BE.packets[cl->BE.headPacket & BE_UPDATE_MASK], packet, packetlen); cl->BE.packetLens[cl->BE.headPacket & BE_UPDATE_MASK] = packetlen; cl->BE.headPacket++; } } } // violation: display a server message and kick specified player (with that msg) else if (packetid == 1) { qbyte playerid; qbyte messagelen; static char message[255]; if ((result = SV_BE_ReceiveFromMaster(&playerid, sizeof(playerid))) != 1) goto disconnect; if (playerid >= sv_maxclients->integer) { result = -1; goto disconnect; } if ((result = SV_BE_ReceiveFromMaster(&messagelen, sizeof(messagelen))) != 1) goto disconnect; if ((result = SV_BE_ReceiveFromMaster(message, messagelen)) != 1) goto disconnect; cl = &svs.clients[playerid]; if( cl->state >= CS_CONNECTED ) { SV_ClientPrintf(NULL, "%s^7 was kicked by BattlEye: ^3%s\n", cl->name, message); SV_DropClient(cl, DROP_TYPE_NORECONNECT, "BattlEye: %s", message); } } } disconnect: // disconnect if failed, not enough or no at all (after first recv call) returned data if (result == -1 || (firstreceived && result == 0)) { SV_BE_DisconnectFromMaster(); Com_Printf("Disconnected from BattlEye Master Server\n"); connectstate = -1; } } // when disconnected, try to reconnect every 60 secs if (connectstate == -1) nextconnect_countdown = 60000; } #endif //============================================================================ /* ==================== SV_AddMaster_f Add a master server to the list ==================== */ void SV_AddMaster_f( char *master ) { int i; if( !master || !master[0] ) return; if( !sv_public || !sv_public->integer ) { Com_Printf( "'SV_AddMaster_f' Only public servers use masters.\n" ); return; } //never go public when not acting as a game server if( sv.state > ss_game ) return; for( i=0; i ss_game ) return; if( !sv_public || !sv_public->integer ) return; mlist = sv_masterservers->string; if( *mlist ) { while( mlist ) { master = COM_Parse( &mlist ); if( !master || !master[0] ) break; SV_AddMaster_f( master ); } } svc.last_heartbeat = HEARTBEAT_SECONDS * 1000; // wait a while before sending first heartbeat } /* ================ SV_MasterHeartbeat Send a message to the master every few minutes to let it know we are alive, and log information ================ */ void SV_MasterHeartbeat( void ) { int i; svc.last_heartbeat -= svc.frametime; if( svc.last_heartbeat > 0 ) return; svc.last_heartbeat = HEARTBEAT_SECONDS * 1000; if( !sv_public || !sv_public->integer ) return; // never go public when not acting as a game server if( sv.state > ss_game ) return; // send to group master for( i = 0; i < MAX_MASTERS; i++ ) { if( master_adr[i].port ) { if( dedicated && dedicated->integer ) Com_Printf( "Sending heartbeat to %s\n", NET_AdrToString(&master_adr[i]) ); Netchan_OutOfBandPrint( NS_SERVER, master_adr[i], "heartbeat %s\n", APPLICATION ); } } } /* ================ SVC_MasterInfoResponse ================ */ void SVC_MasterInfoResponse( void ) { char *string; if( !sv_public || !sv_public->integer ) return; // a private dedicated game // never go public when not acting as a game server if( sv.state > ss_game ) return; // send the same string that we would give for a status OOB command string = SV_StatusString(); Netchan_OutOfBandPrint( NS_SERVER, net_from, "infoResponse\n%s\\challenge\\%s", string, Cmd_Argv(1) ); } //============================================================================ /* ================= 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; char *s; int i; // wsw : r1q2 : check userinfo string for end-of-message-in-string exploit if( strchr (cl->userinfo, '\xFF') ) { SV_DropClient( cl, DROP_TYPE_GENERAL, "Tried to use end-of-message-in-string exploit" ); return; } // wsw : jal : validate model and skin names ( can't contain "/" ) // skin val = Info_ValueForKey( cl->userinfo, "skin" ); if( strchr(val, '/') ) { Com_Printf( "SV_UserinfoChanged: Fixing invalid skin name %s\n", val ); s = strchr(val, '/'); *s = 0; } if( !strlen(val) ) val = DEFAULT_PLAYERSKIN; Info_SetValueForKey( cl->userinfo, "skin", val ); // model val = Info_ValueForKey( cl->userinfo, "model" ); if( strchr(val, '/') ) { Com_Printf( "SV_UserinfoChanged: Fixing invalid model name %s\n", val ); s = strchr(val, '/'); *s = 0; } if( !strlen(val) ) val = DEFAULT_PLAYERMODEL; Info_SetValueForKey( cl->userinfo, "model", val ); // call prog code to allow overrides ge->ClientUserinfoChanged( cl->edict, cl->userinfo ); // wsw : r1q2[start] : verify, validate, truncate and print name changes val = Info_ValueForKey( cl->userinfo, "name" ); if( !val || !val[0] ) cl->name[0] = 0; else { //truncate val[sizeof(cl->name)-1] = 0; //val[15] = 0; //print to server console if( dedicated->integer && cl->name[0] && strcmp(cl->name, val) ) { Com_Printf( "%s[%s] changed name to %s.\n", cl->name, NET_AdrToString(&cl->netchan.remoteAddress), val ); } // name for C code Q_strncpyz( cl->name, val, sizeof(cl->name) ); } // wsw : r1q2[end] // mask off high bit for( i = 0; i < sizeof(cl->name); i++ ) cl->name[i] &= 127; // rate command if( Sys_IsLANAddress( cl->netchan.remoteAddress ) && sv_public->integer != 1 ) { #ifndef RATEKILLED cl->rate = 99999; // lans should not rate limit #endif } else { val = Info_ValueForKey( cl->userinfo, "rate" ); if( strlen(val) ) { int newrate; newrate = atoi(val); if( sv_maxrate->integer && newrate > sv_maxrate->integer ) newrate = sv_maxrate->integer; else if( newrate > 90000 ) newrate = 90000; if( newrate < 1000 ) newrate = 1000; #ifndef RATEKILLED if( cl->rate != newrate ) { cl->rate = newrate; Com_Printf( "%s%s has rate %i\n", cl->name, S_COLOR_WHITE, cl->rate ); } #endif } #ifndef RATEKILLED else cl->rate = 5000; #endif } } //============================================================================ /* =============== SV_Init Only called at plat.exe startup, not for each game =============== */ void SV_Init( void ) { cvar_t *sv_pps; SV_InitOperatorCommands(); sv_mempool = Mem_AllocPool( NULL, "Server" ); Cvar_Get( "dmflags", 0, CVAR_SERVERINFO ); Cvar_Get( "sv_cheats", "0", CVAR_SERVERINFO|CVAR_LATCH ); Cvar_Get( "protocol", va("%i", PROTOCOL_VERSION), CVAR_SERVERINFO|CVAR_NOSET ); rcon_password = Cvar_Get( "rcon_password", "", 0 ); sv_hostname = Cvar_Get( "sv_hostname", "warsow server", CVAR_SERVERINFO | CVAR_ARCHIVE ); sv_timeout = Cvar_Get( "sv_timeout", "125", 0 ); sv_zombietime = Cvar_Get( "sv_zombietime", "2", 0 ); sv_showclamp = Cvar_Get( "sv_showclamp", "0", 0 ); sv_enforcetime = Cvar_Get( "sv_enforcetime", "0", 0 ); sv_uploads = Cvar_Get( "sv_uploads", "1", CVAR_ARCHIVE ); sv_uploads_from_server = Cvar_Get( "sv_uploads_from_server", "1", CVAR_ARCHIVE ); sv_uploads_baseurl = Cvar_Get( "sv_uploads_baseurl", "", CVAR_ARCHIVE ); #ifdef BATTLEYE if( dedicated->integer ) sv_battleye = Cvar_Get( "sv_battleye", "1", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_LATCH ); else sv_battleye = Cvar_Get( "sv_battleye", "0", CVAR_SERVERINFO | CVAR_READONLY ); #endif sv_noreload = Cvar_Get( "sv_noreload", "0", 0 ); if( dedicated->integer ) sv_public = Cvar_Get( "sv_public", "1", CVAR_ARCHIVE|CVAR_NOSET ); else sv_public = Cvar_Get( "sv_public", "0", CVAR_ARCHIVE ); sv_reconnectlimit = Cvar_Get( "sv_reconnectlimit", "3", CVAR_ARCHIVE ); sv_maxclients = Cvar_Get( "sv_maxclients", "1", CVAR_ARCHIVE | CVAR_SERVERINFO | CVAR_LATCH ); // fix invalid sv_maxclients values if( sv_maxclients->integer < 1 ) Cvar_FullSet( "sv_maxclients", "1", CVAR_ARCHIVE|CVAR_SERVERINFO|CVAR_LATCH, qtrue ); else if( sv_maxclients->integer > MAX_CLIENTS ) Cvar_FullSet( "sv_maxclients", va("%i", MAX_CLIENTS), CVAR_ARCHIVE|CVAR_SERVERINFO|CVAR_LATCH, qtrue ); // wsw : jal : cap client's exceding server rules sv_maxrate = Cvar_Get( "sv_maxrate", "15000", CVAR_SERVERINFO|CVAR_ARCHIVE ); sv_skilllevel = Cvar_Get( "sv_skilllevel", "1", CVAR_SERVERINFO|CVAR_ARCHIVE ); if( dedicated->integer ) sv_masterservers = Cvar_Get( "masterservers", DEFAULT_MASTER_SERVERS_IPS, CVAR_NOSET ); else sv_masterservers = Cvar_Get( "masterservers", DEFAULT_MASTER_SERVERS_IPS, 0 ); sv_debug_serverCmd = Cvar_Get( "sv_debug_serverCmd", "0", CVAR_ARCHIVE ); // this is a message holder for shared use MSG_Init( &tmpMessage, tmpMessageData, sizeof(tmpMessageData) ); // init server updates ratio if( dedicated->integer ) sv_pps = Cvar_Get( "sv_pps", "20", CVAR_SERVERINFO|CVAR_NOSET ); else sv_pps = Cvar_Get( "sv_pps", "20", CVAR_SERVERINFO ); svc.frametime = (int)( 1000 / sv_pps->value ); if( svc.frametime > 200 ) { // too slow, also, netcode uses a byte Cvar_ForceSet( "sv_pps", "5" ); svc.frametime = 200; } else if( svc.frametime < 10 ) { // abusive Cvar_ForceSet( "sv_pps", "100" ); svc.frametime = 10; } Com_Printf( "Server running at %i pps\n", sv_pps->integer ); // wsw : jal if( dedicated->integer ) { Com_Printf( "sv_maxrate is %i\n", sv_maxrate->integer ); } //init the master servers list SV_InitMaster(); } /* ================== SV_FinalMessage Used by SV_Shutdown to send a final message to all connected clients before the server goes down. The messages are sent immediately, not just stuck on the outgoing message list, because the server is going to totally exit after returning from this function. ================== */ void SV_FinalMessage( char *message, qboolean reconnect ) { int i; client_t *sv_client; int j; for( i = 0, sv_client = svs.clients; i < sv_maxclients->integer; i++, sv_client++ ) { if( sv_client->edict && (sv_client->edict->r.svflags & SVF_FAKECLIENT) ) continue; if( sv_client->state >= CS_CONNECTED ) { SV_ClientPrintf( sv_client, "\"%s\"", message ); if( reconnect ) SV_SendServerCommand( sv_client, "reconnect" ); else SV_SendServerCommand( sv_client, "disconnect" ); MSG_Clear( &tmpMessage ); SV_AddReliableCommandsToMessage( sv_client, &tmpMessage ); for( j = 0; j < 2; j++ ) { // send it twice SV_SendMessageToClient( sv_client, &tmpMessage ); } } } } /* ================ SV_Shutdown Called when each game quits, before Sys_Quit or Sys_Error ================ */ void SV_Shutdown( char *finalmsg, qboolean reconnect ) { if( svs.clients ) SV_FinalMessage( finalmsg, reconnect ); SV_FreeCachedChecksums(); SV_ShutdownGameProgs(); // get any latched variable changes (sv_maxclients, etc) Cvar_GetLatchedVars( CVAR_LATCH ); memset( &sv, 0, sizeof(sv) ); Com_SetServerState( sv.state ); if( sv_mempool ) Mem_EmptyPool( sv_mempool ); memset( &svs, 0, sizeof(svs) ); #ifdef BATTLEYE if( sv_battleye->integer ) SV_BE_DisconnectFromMaster(); #endif #ifdef SERVERSIDE_DEMOS if (svs.demofile) fclose (svs.demofile); #endif }