diff options
author | IronClawTrem <louie.nutman@gmail.com> | 2020-02-16 03:40:06 +0000 |
---|---|---|
committer | IronClawTrem <louie.nutman@gmail.com> | 2020-02-16 03:40:06 +0000 |
commit | 425decdf7e9284d15aa726e3ae96b9942fb0e3ea (patch) | |
tree | 6c0dd7edfefff1be7b9e75fe0b3a0a85fe1595f3 /src/server | |
parent | ccb0b2e4d6674a7a00c9bf491f08fc73b6898c54 (diff) |
create tremded branch
Diffstat (limited to 'src/server')
-rw-r--r-- | src/server/CMakeLists.txt | 114 | ||||
-rw-r--r-- | src/server/server.h | 529 | ||||
-rw-r--r-- | src/server/sv_admin.cpp | 0 | ||||
-rw-r--r-- | src/server/sv_admin.h | 61 | ||||
-rw-r--r-- | src/server/sv_ccmds.cpp | 441 | ||||
-rw-r--r-- | src/server/sv_client.cpp | 1949 | ||||
-rw-r--r-- | src/server/sv_game.cpp | 602 | ||||
-rw-r--r-- | src/server/sv_init.cpp | 1004 | ||||
-rw-r--r-- | src/server/sv_main.cpp | 1551 | ||||
-rw-r--r-- | src/server/sv_net_chan.cpp | 259 | ||||
-rw-r--r-- | src/server/sv_snapshot.cpp | 749 | ||||
-rw-r--r-- | src/server/sv_world.cpp | 745 |
12 files changed, 8004 insertions, 0 deletions
diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt new file mode 100644 index 0000000..98f3a23 --- /dev/null +++ b/src/server/CMakeLists.txt @@ -0,0 +1,114 @@ + +# +## .dMMMb dMMMMMP dMMMMb dMP dMP dMMMMMP dMMMMb +## dMP" VP dMP dMP.dMP dMP dMP dMP dMP.dMP +## VMMMb dMMMP dMMMMK" dMP dMP dMMMP dMMMMK" +## dP .dMP dMP dMP"AMF YMvAP" dMP dMP"AMF +## VMMMP" dMMMMMP dMP dMP VP" dMMMMMP dMP dMP +# + +add_definitions( + -DDEDICATED + -DUSE_LOCAL_HEADERS + -DPRODUCT_VERSION="1.2.0 pre-release" + -DUSE_VOIP + -DNDEBUG + ) + +set(EXTERNAL_DIR ${CMAKE_SOURCE_DIR}/external) +set(PARENT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) +if(APPLE) +set(APPLE_SOURCES ${PARENT_DIR}/sys/sys_osx.mm) +endif(APPLE) + +add_executable( + tremded + # + server.h + # + sv_ccmds.cpp + sv_client.cpp + sv_game.cpp + sv_init.cpp + sv_main.cpp + sv_net_chan.cpp + sv_snapshot.cpp + sv_world.cpp + # + ${PARENT_DIR}/qcommon/cm_load.cpp + ${PARENT_DIR}/qcommon/cm_patch.cpp + ${PARENT_DIR}/qcommon/cm_polylib.cpp + ${PARENT_DIR}/qcommon/cm_test.cpp + ${PARENT_DIR}/qcommon/cm_trace.cpp + ${PARENT_DIR}/qcommon/cmd.cpp + ${PARENT_DIR}/qcommon/common.cpp + ${PARENT_DIR}/qcommon/crypto.cpp + ${PARENT_DIR}/qcommon/cvar.cpp + ${PARENT_DIR}/qcommon/files.cpp + ${PARENT_DIR}/qcommon/huffman.cpp + ${PARENT_DIR}/qcommon/huffman.h + ${PARENT_DIR}/qcommon/ioapi.cpp + ${PARENT_DIR}/qcommon/md4.cpp + ${PARENT_DIR}/qcommon/msg.h + ${PARENT_DIR}/qcommon/msg.cpp + ${PARENT_DIR}/qcommon/net.h + ${PARENT_DIR}/qcommon/net_chan.cpp + ${PARENT_DIR}/qcommon/net_ip.cpp + ${PARENT_DIR}/qcommon/parse.cpp + ${PARENT_DIR}/qcommon/q3_lauxlib.cpp + ${PARENT_DIR}/qcommon/q_math.c + ${PARENT_DIR}/qcommon/q_shared.c + ${PARENT_DIR}/qcommon/unzip.cpp + ${PARENT_DIR}/qcommon/vm.cpp + ${PARENT_DIR}/qcommon/vm_interpreted.cpp + ${PARENT_DIR}/qcommon/vm_x86.cpp + # + ${PARENT_DIR}/null/null_client.cpp + ${PARENT_DIR}/null/null_input.cpp + ${PARENT_DIR}/null/null_snddma.cpp + # + ${PARENT_DIR}/asm/snapvector.c + # + ${PARENT_DIR}/sys/con_log.cpp + ${PARENT_DIR}/sys/con_tty.cpp + ${PARENT_DIR}/sys/sys_main.cpp + ${PARENT_DIR}/sys/sys_unix.cpp + ${PARENT_DIR}/sys/sys_shared.h + ${APPLE_SOURCES} + # + ${EXTERNAL_DIR}/zlib/adler32.c + ${EXTERNAL_DIR}/zlib/crc32.c + ${EXTERNAL_DIR}/zlib/inffast.c + ${EXTERNAL_DIR}/zlib/inflate.c + ${EXTERNAL_DIR}/zlib/inftrees.c + ${EXTERNAL_DIR}/zlib/zutil.c + ) + +if(APPLE) + # FIXME Prefixed with "lua" to prevent cmake from doing "-l-framework Cocoa" + set(FRAMEWORKS "-framework Cocoa -framework Security -framework OpenAL -framework IOKit") +else(APPLE) + if(UNIX) + set(SYSLIBS dl rt) + endif(UNIX) +endif(APPLE) + +target_link_libraries( + tremded + # + lua + script_api + nettle + zlib + ${FRAMEWORKS} + ${SYSLIBS} + ) + +include_directories( + ${PARENT_DIR}/script + ${EXTERNAL_DIR}/lua-5.3.3/include + ${EXTERNAL_DIR}/sol + ${EXTERNAL_DIR}/script/rapidjson + ${EXTERNAL_DIR}/nettle-3.3 + ${EXTERNAL_DIR}/zlib + ) diff --git a/src/server/server.h b/src/server/server.h new file mode 100644 index 0000000..a07b66c --- /dev/null +++ b/src/server/server.h @@ -0,0 +1,529 @@ +/* +=========================================================================== +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/> + +=========================================================================== +*/ +// server.h + +#ifndef SERVER_H +#define SERVER_H 1 + +#include "game/g_public.h" +#include "qcommon/cmd.h" +#include "qcommon/crypto.h" +#include "qcommon/cvar.h" +#include "qcommon/files.h" +#include "qcommon/huffman.h" +#include "qcommon/msg.h" +#include "qcommon/net.h" +#include "qcommon/q_shared.h" +#include "qcommon/qcommon.h" +#include "qcommon/vm.h" +#include "sys/sys_shared.h" + +//============================================================================= + +#define PERS_SCORE 0 // !!! MUST NOT CHANGE, SERVER AND GAME BOTH REFERENCE !!! +#define CS_WARMUP 5 // !!! MUST NOT CHANGE, SERVER AND GAME BOTH REFERENCE !!! + +// server attack protection +#define SVP_IOQ3 0x0001 ///< 1 - ioQuake3 way +#define SVP_OWOLF 0x0002 ///< 2 - OpenWolf way +#define SVP_CONSOLE 0x0004 ///< 4 - console print + +#define MAX_ENT_CLUSTERS 16 + +#ifdef USE_VOIP +#define VOIP_QUEUE_LENGTH 64 +struct voipServerPacket_t { + int generation; + int sequence; + int frames; + int len; + int sender; + int flags; + byte data[4000]; +}; +#endif // USE_VOIP + +struct svEntity_t { + struct worldSector_t *worldSector; + svEntity_t *nextEntityInWorldSector; + + entityState_t baseline; // for delta compression of initial sighting + int numClusters; // if -1, use headnode instead + int clusternums[MAX_ENT_CLUSTERS]; + int lastCluster; // if all the clusters don't fit in clusternums + int areanum, areanum2; + int snapshotCounter; // used to prevent double adding from portal views +}; + +enum serverState_t { + SS_DEAD, // no map loaded + SS_LOADING, // spawning level entities + SS_GAME // actively running +}; + +struct configString_t { + char *s; + bool restricted; // if true, don't send to clientList + clientList_t clientList; +}; + +struct server_t { + serverState_t state; + bool restarting; // if true, send configstring changes during SS_LOADING + int serverId; // changes each server start + int restartedServerId; // serverId before a map_restart + int checksumFeed; // the feed key that we use to compute the pure checksum strings + // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=475 + // the serverId associated with the current checksumFeed (always <= serverId) + int checksumFeedServerId; + int snapshotCounter; // incremented for each snapshot built + int timeResidual; // <= 1000 / sv_frame->value + int nextFrameTime; // when time > nextFrameTime, process world + configString_t configstrings[MAX_CONFIGSTRINGS]; + svEntity_t svEntities[MAX_GENTITIES]; + + char *entityParsePoint; // used during game VM init + + // the game virtual machine will update these on init and changes + sharedEntity_t *gentities; + int gentitySize; + int num_entities; // current number, <= MAX_GENTITIES + + playerState_t *gameClients; + int gameClientSize; // will be > sizeof(playerState_t) due to game private data + + int restartTime; + int time; + + vm_t *gvm; // game virtual machine +}; + +struct clientSnapshot_t { + int areabytes; + byte areabits[MAX_MAP_AREA_BYTES]; // portalarea visibility bits + playerState_t ps; + int num_entities; + int first_entity; // into the circular sv_packet_entities[] + // the entities MUST be in increasing state number + // order, otherwise the delta compression will fail + int messageSent; // time the message was transmitted + int messageAcked; // time the message was acked + int messageSize; // used to rate drop packets +}; + +enum clientState_t { + CS_FREE, // can be reused for a new connection + CS_ZOMBIE, // client has been disconnected, but don't reuse + // connection for a couple seconds + CS_CONNECTED, // has been assigned to a client_t, but no gamestate yet + CS_PRIMED, // gamestate has been sent, but client hasn't sent a usercmd + CS_ACTIVE // client is fully in game +}; + +struct netchan_buffer_t { + msg_t msg; + byte msgBuffer[MAX_MSGLEN]; + netchan_buffer_t *next; +}; + +struct client_t { + clientState_t state; + char userinfo[MAX_INFO_STRING]; // name, etc + char userinfobuffer[MAX_INFO_STRING]; ///< used for buffering of user info + + char reliableCommands[MAX_RELIABLE_COMMANDS][MAX_STRING_CHARS]; + int reliableSequence; // last added reliable message, not necesarily sent or acknowledged yet + int reliableAcknowledge; // last acknowledged reliable message + int reliableSent; // last sent reliable message, not necesarily acknowledged yet + int messageAcknowledge; + + int gamestateMessageNum; // netchan->outgoingSequence of gamestate + int challenge; + + usercmd_t lastUsercmd; + int lastMessageNum; // for delta compression + int lastClientCommand; // reliable client message sequence + char lastClientCommandString[MAX_STRING_CHARS]; + sharedEntity_t *gentity; // SV_GentityNum(clientnum) + char name[MAX_NAME_LENGTH]; // extracted from userinfo, high bits masked + + // downloading + char downloadName[MAX_QPATH]; // if not empty string, we are downloading + fileHandle_t download; // file being downloaded + int downloadSize; // total bytes (can't use EOF because of paks) + int downloadCount; // bytes sent + int downloadClientBlock; // last block we sent to the client, awaiting ack + int downloadCurrentBlock; // current block number + int downloadXmitBlock; // last block we xmited + unsigned char *downloadBlocks[MAX_DOWNLOAD_WINDOW]; // the buffers for the download blocks + int downloadBlockSize[MAX_DOWNLOAD_WINDOW]; + bool downloadEOF; // We have sent the EOF block + int downloadSendTime; // time we last got an ack from the client + + int deltaMessage; // frame last client usercmd message + int nextReliableTime; // svs.time when another reliable command will be allowed + int nextReliableUserTime; // svs.time when another userinfo change will be allowed + int lastPacketTime; // svs.time when packet was last received + int lastConnectTime; // svs.time when connection started + int lastSnapshotTime; // svs.time of last sent snapshot + bool rateDelayed; // true if nextSnapshotTime was set based on rate instead of snapshotMsec + int timeoutCount; // must timeout a few frames in a row so debugging doesn't break + clientSnapshot_t frames[PACKET_BACKUP]; // updates can be delta'd from here + int ping; + int rate; // bytes / second + int snapshotMsec; // requests a snapshot every snapshotMsec unless rate choked + int pureAuthentic; + bool gotCP; // TTimo - additional flag to distinguish between a bad pure checksum, and no cp command at all + netchan_t netchan; + // TTimo + // queuing outgoing fragmented messages to send them properly, without udp packet bursts + // in case large fragmented messages are stacking up + // buffer them into this queue, and hand them out to netchan as needed + netchan_buffer_t *netchan_start_queue; + netchan_buffer_t **netchan_end_queue; + + char fingerprint[SHA256_DIGEST_SIZE * 2 + 1]; + +#ifdef USE_VOIP + bool hasVoip; + bool muteAllVoip; + bool ignoreVoipFromClient[MAX_CLIENTS]; + voipServerPacket_t *voipPacket[VOIP_QUEUE_LENGTH]; + size_t queuedVoipPackets; + int queuedVoipIndex; +#endif + + int oldServerTime; + bool csUpdated[MAX_CONFIGSTRINGS]; +}; + +//============================================================================= +#define STATFRAMES 200 ///< 5 seconds - assumed we run 40 fps + +/** + * @struct svstats_t + * @brief + */ +struct svstats_t { + double active; + double idle; + int count; + + double latched_active; + double latched_idle; + + float cpu; + float avg; +}; + +// MAX_CHALLENGES is made large to prevent a denial +// of service attack that could cycle all of them +// out before legitimate users connected +#define MAX_CHALLENGES 2048 +// Allow a certain amount of challenges to have the same IP address +// to make it a bit harder to DOS one single IP address from connecting +// while not allowing a single ip to grab all challenge resources +#define MAX_CHALLENGES_MULTI (MAX_CHALLENGES / 2) + +#define AUTHORIZE_TIMEOUT 5000 + +struct challenge_t { + netadr_t adr; + int challenge; + char challenge2[33]; + int clientChallenge; // challenge number coming from the client + int time; // time the last packet was sent to the autherize server + int pingTime; // time the challenge response was sent to client + int firstTime; // time the adr was first used, for authorize timeout checks + bool wasrefused; + bool connected; +}; + +/** + * @struct receipt_t + * @brief + */ +struct receipt_t { + netadr_t adr; + int time; +}; + +/** + * @def MAX_INFO_RECEIPTS + * @brief the maximum number of getstatus+getinfo responses that we send in + * a two second time period. + */ +#define MAX_INFO_RECEIPTS 48 + +/** + * @struct tempBan_s + * @typedef tempBan_t + * @brief + */ +struct tempBan_t { + netadr_t adr; + int endtime; +}; + +#define MAX_TEMPBAN_ADDRESSES MAX_CLIENTS + +#define SERVER_PERFORMANCECOUNTER_FRAMES 600 +#define SERVER_PERFORMANCECOUNTER_SAMPLES 6 + +// this structure will be cleared only when the game dll changes +struct serverStatic_t { + bool initialized; // sv_init has completed + + int time; // will be strictly increasing across level changes + + int snapFlagServerBit; // ^= SNAPFLAG_SERVERCOUNT every SV_SpawnServer() + + client_t *clients; // [sv_maxclients->integer]; + int numSnapshotEntities; // sv_maxclients->integer*PACKET_BACKUP*MAX_SNAPSHOT_ENTITIES + int nextSnapshotEntities; // next snapshotEntities to use + entityState_t *snapshotEntities; // [numSnapshotEntities] + int nextHeartbeatTime; + challenge_t challenges[MAX_CHALLENGES]; // to prevent invalid IPs from connecting + receipt_t infoReceipts[MAX_INFO_RECEIPTS]; + netadr_t redirectAddress; // for rcon return messages + + netadr_t authorizeAddress; // for rcon return messages + + int sampleTimes[SERVER_PERFORMANCECOUNTER_SAMPLES]; + int currentSampleIndex; + int totalFrameTime; + int currentFrameIndex; + int serverLoad; + svstats_t stats; +}; + +//============================================================================= + +extern serverStatic_t svs; // persistant server info across maps +extern server_t sv; // cleared each map + +extern cvar_t *sv_fps; +extern cvar_t *sv_timeout; +extern cvar_t *sv_zombietime; +extern cvar_t *sv_rconPassword; +extern cvar_t *sv_privatePassword; +extern cvar_t *sv_allowDownload; +extern cvar_t *sv_maxclients; + +extern cvar_t *sv_privateClients; +extern cvar_t *sv_hostname; +extern cvar_t *sv_masters[3][MAX_MASTER_SERVERS]; +extern cvar_t *sv_reconnectlimit; +extern cvar_t *sv_showloss; +extern cvar_t *sv_padPackets; +extern cvar_t *sv_killserver; +extern cvar_t *sv_mapname; +extern cvar_t *sv_mapChecksum; +extern cvar_t *sv_serverid; +extern cvar_t *sv_minRate; +extern cvar_t *sv_maxRate; +extern cvar_t *sv_dlRate; +extern cvar_t *sv_minPing; +extern cvar_t *sv_maxPing; +extern cvar_t *sv_pure; +extern cvar_t *sv_lanForceRate; +extern cvar_t *sv_banFile; + +extern cvar_t *sv_protect; +extern cvar_t *sv_protectLog; + +#ifdef USE_VOIP +extern cvar_t *sv_voip; +extern cvar_t *sv_voipProtocol; +#endif + +extern cvar_t *sv_rsaAuth; + +extern cvar_t *sv_schachtmeisterPort; + +//=========================================================== + +// +// sv_main.c +// +struct leakyBucket_t { + netadrtype_t type; + + union { + byte _4[4]; + byte _6[16]; + } ipv; + + int lastTime; + signed char burst; + + long hash; + + leakyBucket_t *prev, *next; +}; + +extern leakyBucket_t outboundLeakyBucket; + +bool SVC_RateLimit(leakyBucket_t *bucket, int burst, int period); +bool SVC_RateLimitAddress(netadr_t from, int burst, int period); + +void SV_FinalMessage(const char *message); +void QDECL SV_SendServerCommand(client_t *cl, const char *fmt, ...) __attribute__((format(printf, 2, 3))); + +void SV_AddOperatorCommands(void); +void SV_RemoveOperatorCommands(void); + +void SV_MasterShutdown(void); +int SV_RateMsec(client_t *client); + +// +// sv_init.c +// +void SV_SetConfigstring(int index, const char *val); +void SV_GetConfigstring(int index, char *buffer, int bufferSize); +void SV_SetConfigstringRestrictions(int index, const clientList_t *clientList); +void SV_UpdateConfigstrings(client_t *client); + +void SV_SetUserinfo(int index, const char *val); +void SV_GetUserinfo(int index, char *buffer, int bufferSize); + +void SV_ChangeMaxClients(void); +void SV_SpawnServer(char *server); +void SV_WriteAttackLog(const char *log); + +#ifdef NDEBUG +#define SV_WriteAttackLogD(x) +#else +#define SV_WriteAttackLogD(x) SV_WriteAttackLog(x) +#endif + +// +// sv_client.c +// +void SV_GetChallenge(netadr_t from); + +void SV_DirectConnect(netadr_t from); + +void SV_ExecuteClientMessage(client_t *cl, msg_t *msg); +void SV_UserinfoChanged(client_t *cl); + +void SV_ClientEnterWorld(client_t *client, usercmd_t *cmd); +void SV_FreeClient(client_t *client); +void SV_DropClient(client_t *drop, const char *reason); + +void SV_ExecuteClientCommand(client_t *cl, const char *s, bool clientOK); +void SV_ClientThink(client_t *cl, usercmd_t *cmd); + +int SV_WriteDownloadToClient(client_t *cl, msg_t *msg); +int SV_SendDownloadMessages(void); +int SV_SendQueuedMessages(void); + +// +// sv_ccmds.c +// +void SV_Heartbeat_f(void); + +// +// sv_snapshot.c +// +void SV_AddServerCommand(client_t *client, const char *cmd); +void SV_UpdateServerCommandsToClient(client_t *client, msg_t *msg); +void SV_WriteFrameToClient(client_t *client, msg_t *msg); +void SV_SendMessageToClient(msg_t *msg, client_t *client); +void SV_SendClientMessages(void); +void SV_SendClientSnapshot(client_t *client); + +// +// sv_game.c +// +int SV_NumForGentity(sharedEntity_t *ent); +sharedEntity_t *SV_GentityNum(int num); +playerState_t *SV_GameClientNum(int num); +svEntity_t *SV_SvEntityForGentity(sharedEntity_t *gEnt); +sharedEntity_t *SV_GEntityForSvEntity(svEntity_t *svEnt); +void SV_InitGameProgs(void); +void SV_ShutdownGameProgs(void); +void SV_RestartGameProgs(void); +bool SV_inPVS(const vec3_t p1, const vec3_t p2); + +//============================================================ +// +// high level object sorting to reduce interaction tests +// + +void SV_ClearWorld(void); +// called after the world model has been loaded, before linking any entities + +void SV_UnlinkEntity(sharedEntity_t *ent); +// call before removing an entity, and before trying to move one, +// so it doesn't clip against itself + +void SV_LinkEntity(sharedEntity_t *ent); +// Needs to be called any time an entity changes origin, mins, maxs, +// or solid. Automatically unlinks if needed. +// sets ent->r.absmin and ent->r.absmax +// sets ent->leafnums[] for pvs determination even if the entity +// is not solid + +clipHandle_t SV_ClipHandleForEntity(const sharedEntity_t *ent); + +void SV_SectorList_f(void); + +int SV_AreaEntities(const vec3_t mins, const vec3_t maxs, int *entityList, int maxcount); +// fills in a table of entity numbers with entities that have bounding boxes +// that intersect the given area. It is possible for a non-axial bmodel +// to be returned that doesn't actually intersect the area on an exact +// test. +// returns the number of pointers filled in +// The world entity is never returned in this list. + +int SV_PointContents(const vec3_t p, int passEntityNum); +// returns the CONTENTS_* value from the world and all entities at the given point. + +void SV_Trace(trace_t *results, const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end, int passEntityNum, + int contentmask, traceType_t type); +// mins and maxs are relative + +// if the entire move stays in a solid volume, trace.allsolid will be set, +// trace.startsolid will be set, and trace.fraction will be 0 + +// if the starting point is in a solid, it will be allowed to move out +// to an open area + +// passEntityNum is explicitly excluded from clipping checks (normally ENTITYNUM_NONE) + +void SV_ClipToEntity(trace_t *trace, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, + int entityNum, int contentmask, traceType_t type); +// clip to a specific entity + +// +// sv_net_chan.c +// +void SV_Netchan_Transmit(client_t *client, msg_t *msg); +int SV_Netchan_TransmitNextFragment(client_t *client); +bool SV_Netchan_Process(client_t *client, msg_t *msg); +void SV_Netchan_FreeQueue(client_t *client); + +#endif diff --git a/src/server/sv_admin.cpp b/src/server/sv_admin.cpp new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/server/sv_admin.cpp diff --git a/src/server/sv_admin.h b/src/server/sv_admin.h new file mode 100644 index 0000000..b261457 --- /dev/null +++ b/src/server/sv_admin.h @@ -0,0 +1,61 @@ +// +// This file is part of Tremulous. +// Copyright © 2017 Victor Roemer (blowfish) <victor@badsec.org> +// Copyright (C) 2015-2019 GrangerHub +// +// 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 3 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, see <http://www.gnu.org/licenses/>. +// + +#pragma once + +#include <iostream> + +using std::string; + +typedef char fingerprint_t[64]; +typedef char guid_t[33]; +typedef char name_t[MAX_NAME_LENGTH]; +typedef char err_t[MAX_STRING_CHARS]; + +struct AdminFlag { + unsigned id; + name_t name; +}; + +struct AdminLevel { + name_t name; + admin_flags_t flags; + unsigned level; +}; + +struct Admin { + bool flag(const name_t flagname) + { } + + bool deny(const name_t flagname) + { } + + guid_t guid; + name_t name; + +private: + admin_flags_t flags; + admin_flags_t denied; + unsigned level; +}; + +class AdminMgr { + bool add(Admin&); + bool remove(Admin&); +} diff --git a/src/server/sv_ccmds.cpp b/src/server/sv_ccmds.cpp new file mode 100644 index 0000000..5c8902d --- /dev/null +++ b/src/server/sv_ccmds.cpp @@ -0,0 +1,441 @@ +/* +=========================================================================== +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/> + +=========================================================================== +*/ + +#include <arpa/inet.h> +#include "server.h" + +/* +=============================================================================== + +OPERATOR CONSOLE ONLY COMMANDS + +These commands can only be entered from stdin or by a remote operator datagram +=============================================================================== +*/ + +/* +================== +SV_Map_f + +Restart the server on a different map +================== +*/ +static void SV_Map_f( void ) { + const char *cmd; + const char *map; + bool cheat; + char expanded[MAX_QPATH]; + char mapname[MAX_QPATH]; + int a; + int i; + + map = Cmd_Argv(1); + if ( !map ) { + return; + } + + // make sure the level exists before trying to change, so that + // a typo at the server console won't end the game + Com_sprintf (expanded, sizeof(expanded), "maps/%s.bsp", map); + if ( FS_ReadFile (expanded, NULL) == -1 ) { + Com_Printf ("Can't find map %s\n", expanded); + return; + } + + cmd = Cmd_Argv(0); + if ( !Q_stricmp( cmd, "devmap" ) ) { + cheat = true; + } else { + cheat = false; + } + + // save the map name here cause on a map restart we reload the autogen.cfg + // and thus nuke the arguments of the map command + Q_strncpyz(mapname, map, sizeof(mapname)); + + // start up the map + SV_SpawnServer( mapname ); + + // set the cheat value + // if the level was started with "map <levelname>", then + // cheats will not be allowed. If started with "devmap <levelname>" + // then cheats will be allowed + if ( cheat ) { + Cvar_Set( "sv_cheats", "1" ); + } else { + Cvar_Set( "sv_cheats", "0" ); + } + + // This forces the local master server IP address cache + // to be updated on sending the next heartbeat + for( a = 0; a < 3; ++a ) + for( i = 0; i < MAX_MASTER_SERVERS; i++ ) + sv_masters[ a ][ i ]->modified = true; +} + +/* +================ +SV_MapRestart_f + +Completely restarts a level, but doesn't send a new gamestate to the clients. +This allows fair starts with variable load times. +================ +*/ +static void SV_MapRestart_f( void ) { + int i; + client_t *client; + char *denied; + int delay; + + // make sure we aren't restarting twice in the same frame + if ( com_frameTime == sv.serverId ) { + return; + } + + // make sure server is running + if ( !com_sv_running->integer ) { + Com_Printf( "Server is not running.\n" ); + return; + } + + if ( sv.restartTime ) { + return; + } + + if (Cmd_Argc() > 1 ) { + delay = atoi( Cmd_Argv(1) ); + } + else { + delay = 0; + } + if( delay && !Cvar_VariableValue("g_doWarmup") ) { + sv.restartTime = sv.time + delay * 1000; + SV_SetConfigstring( CS_WARMUP, va("%i", sv.restartTime) ); + return; + } + + // check for changes in variables that can't just be restarted + // check for maxclients change + if ( sv_maxclients->modified ) { + char mapname[MAX_QPATH]; + + Com_Printf( "variable change -- restarting.\n" ); + // restart the map the slow way + Q_strncpyz( mapname, Cvar_VariableString( "mapname" ), sizeof( mapname ) ); + + SV_SpawnServer( mapname ); + return; + } + + // toggle the server bit so clients can detect that a + // map_restart has happened + svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT; + + // generate a new serverid + // TTimo - don't update restartedserverId there, otherwise we won't deal correctly with multiple map_restart + sv.serverId = com_frameTime; + Cvar_Set( "sv_serverid", va("%i", sv.serverId ) ); + + // if a map_restart occurs while a client is changing maps, we need + // to give them the correct time so that when they finish loading + // they don't violate the backwards time check in cl_cgame.c + for (i=0 ; i<sv_maxclients->integer ; i++) { + if (svs.clients[i].state == CS_PRIMED) { + svs.clients[i].oldServerTime = sv.restartTime; + } + } + + // reset all the vm data in place without changing memory allocation + // note that we do NOT set sv.state = SS_LOADING, so configstrings that + // had been changed from their default values will generate broadcast updates + sv.state = SS_LOADING; + sv.restarting = true; + + SV_RestartGameProgs(); + + // run a few frames to allow everything to settle + for (i = 0; i < 3; i++) + { + VM_Call (sv.gvm, GAME_RUN_FRAME, sv.time); + sv.time += 100; + svs.time += 100; + } + + sv.state = SS_GAME; + sv.restarting = false; + + // connect and begin all the clients + for (i=0 ; i<sv_maxclients->integer ; i++) { + client = &svs.clients[i]; + + // send the new gamestate to all connected clients + if ( client->state < CS_CONNECTED) { + continue; + } + + // add the map_restart command + SV_AddServerCommand( client, "map_restart\n" ); + + // connect the client again, without the firstTime flag + denied = (char*)VM_ExplicitArgPtr( sv.gvm, VM_Call( sv.gvm, GAME_CLIENT_CONNECT, i, false ) ); + if ( denied ) { + // this generally shouldn't happen, because the client + // was connected before the level change + SV_DropClient( client, denied ); + Com_Printf( "SV_MapRestart_f(%d): dropped client %i - denied!\n", delay, i ); + continue; + } + + if(client->state == CS_ACTIVE) + SV_ClientEnterWorld(client, &client->lastUsercmd); + else + { + // If we don't reset client->lastUsercmd and are restarting during map load, + // the client will hang because we'll use the last Usercmd from the previous map, + // which is wrong obviously. + SV_ClientEnterWorld(client, NULL); + } + } + + // run another frame to allow things to look at all the players + VM_Call (sv.gvm, GAME_RUN_FRAME, sv.time); + sv.time += 100; + svs.time += 100; +} + + +//=============================================================== + +/** + * @brief SV_Status_f + */ +static void SV_Status_f(void) { + int i; + client_t *cl; + playerState_t *ps; + const char *s; + int ping; + unsigned int maxNameLength; + + // make sure server is running + if (!com_sv_running->integer) { + Com_Printf("Server is not running.\n"); + return; + } + + Com_Printf("cpu server utilization: %i %%\n" + "avg response time : %i ms\n" + "server time : %i\n" + "internal time : %i\n" + "map : %s\n\n" + "num score ping name lastmsg address qport rate lastConnectTime\n" + "--- ----- ---- ----------------------------------- ------- --------------------- ----- ----- ---------------\n", + ( int ) svs.stats.cpu, + ( int ) svs.stats.avg, + svs.time, + Sys_Milliseconds(), + sv_mapname->string); + + for (i = 0, cl = svs.clients ; i < sv_maxclients->integer ; i++, cl++) { + Com_Printf("%3i ", i); + ps = SV_GameClientNum(i); + Com_Printf("%5i ", ps->persistant[PERS_SCORE]); + + if (cl->state == CS_CONNECTED) { + Com_Printf("CNCT "); + } else if (cl->state == CS_ZOMBIE) { + Com_Printf("ZMBI "); + } else { + ping = cl->ping < 9999 ? cl->ping : 9999; + Com_Printf("%4i ", ping); + } + + s = NET_AdrToString(cl->netchan.remoteAddress); + + // extend the name length by couting extra color characters to keep well formated output + maxNameLength = sizeof(cl->name) + (strlen(cl->name) - Q_PrintStrlen(cl->name)) + 1; + + Com_Printf("%-*s %7i %-21s %5i %5i %i\n", maxNameLength, rc(cl->name), svs.time - cl->lastPacketTime, s, cl->netchan.qport, cl->rate, svs.time - cl->lastConnectTime); + } + + Com_Printf("\n"); +} + + +/* +================== +SV_Heartbeat_f + +Also called by SV_DropClient, SV_DirectConnect, and SV_SpawnServer +================== +*/ +void SV_Heartbeat_f( void ) { + svs.nextHeartbeatTime = -9999999; +} + + +/* +=========== +SV_Serverinfo_f + +Examine the serverinfo string +=========== +*/ +static void SV_Serverinfo_f( void ) { + // make sure server is running + if ( !com_sv_running->integer ) { + Com_Printf( "Server is not running.\n" ); + return; + } + + Com_Printf ("Server info settings:\n"); + Info_Print ( Cvar_InfoString( CVAR_SERVERINFO ) ); +} + + +/* +=========== +SV_Systeminfo_f + +Examine the systeminfo string +=========== +*/ +static void SV_Systeminfo_f( void ) { + // make sure server is running + if ( !com_sv_running->integer ) { + Com_Printf( "Server is not running.\n" ); + return; + } + + Com_Printf ("System info settings:\n"); + Info_Print ( Cvar_InfoString_Big( CVAR_SYSTEMINFO ) ); +} + + +/* +================= +SV_KillServer +================= +*/ +static void SV_KillServer_f( void ) { + SV_Shutdown( "killserver" ); +} + +static void SV_SMQ_f( void ) { + static qboolean schmResolved = qfalse; + static netadr_t schmAddress; + + if ( !schmResolved ) { + schmResolved = qtrue; + NET_StringToAdr( "127.0.0.1", &schmAddress, NA_IP ); + schmAddress.port = 1337; + } + + if ( sv_schachtmeisterPort->modified && + sv_schachtmeisterPort->integer >= 1 && sv_schachtmeisterPort->integer <= 65535 ) + { + schmAddress.port = htons(sv_schachtmeisterPort->integer); + } + + if ( Cmd_Argc() >= 3 && !Q_stricmp( Cmd_Argv( 1 ), "ipa" ) ) { // compatibility with out-of-date crapware conceived in the future + NET_OutOfBandPrint( NS_SERVER, schmAddress, "sm2query %s", Cmd_ArgsFrom( 2 ) ); + Com_Printf( "^3query [^7sm2query %s^3]\n", Cmd_ArgsFrom( 2 ) ); // DELME + } else { + char args[ MAX_STRING_CHARS ]; + char *p; + int s, i; + + p = args; + s = sizeof( args ); + + for ( i = 1; i < Cmd_Argc(); ++i ) + { + int l; + Com_sprintf( p, s, " \"%s\"", Cmd_Argv( i ) ); + l = strlen( p ); + s -= l; + p += l; + } + + NET_OutOfBandPrint( NS_SERVER, schmAddress, "sm2query%s", args ); + Com_Printf( "^3query [^7sm2query%s^3]\n", args ); // DELME + } +} + +//=========================================================== + +/* +================== +SV_CompleteMapName +================== +*/ +static void SV_CompleteMapName( char *args, int argNum ) { + if( argNum == 2 ) { + Field_CompleteFilename( "maps", "bsp", true, false ); + } +} + +/* +================== +SV_AddOperatorCommands +================== +*/ +void SV_AddOperatorCommands( void ) { + static bool initialized = false; + + if ( initialized ) { + return; + } + initialized = true; + + Cmd_AddCommand ("heartbeat", SV_Heartbeat_f); + Cmd_AddCommand ("status", SV_Status_f); + Cmd_AddCommand ("serverinfo", SV_Serverinfo_f); + Cmd_AddCommand ("systeminfo", SV_Systeminfo_f); + Cmd_AddCommand ("map_restart", SV_MapRestart_f); + Cmd_AddCommand ("sectorlist", SV_SectorList_f); + Cmd_AddCommand ("map", SV_Map_f); + Cmd_SetCommandCompletionFunc( "map", SV_CompleteMapName ); + Cmd_AddCommand ("devmap", SV_Map_f); + Cmd_SetCommandCompletionFunc( "devmap", SV_CompleteMapName ); + Cmd_AddCommand ("killserver", SV_KillServer_f); + Cmd_AddCommand ("smq", SV_SMQ_f); +} + +/* +================== +SV_RemoveOperatorCommands +================== +*/ +void SV_RemoveOperatorCommands( void ) { +#if 0 + // removing these won't let the server start again + Cmd_RemoveCommand ("heartbeat"); + Cmd_RemoveCommand ("serverinfo"); + Cmd_RemoveCommand ("systeminfo"); + Cmd_RemoveCommand ("map_restart"); + Cmd_RemoveCommand ("sectorlist"); +#endif +} 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 ); +// } +} diff --git a/src/server/sv_game.cpp b/src/server/sv_game.cpp new file mode 100644 index 0000000..23e5212 --- /dev/null +++ b/src/server/sv_game.cpp @@ -0,0 +1,602 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +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_game.c -- interface to the game dll + +#include "server.h" + +// these functions must be used instead of pointer arithmetic, because +// the game allocates gentities with private information after the server shared part +int SV_NumForGentity( sharedEntity_t *ent ) { + int num; + + num = ( (byte *)ent - (byte *)sv.gentities ) / sv.gentitySize; + + return num; +} + +sharedEntity_t *SV_GentityNum( int num ) { + sharedEntity_t *ent; + + ent = (sharedEntity_t *)((byte *)sv.gentities + sv.gentitySize*(num)); + + return ent; +} + +playerState_t *SV_GameClientNum( int num ) { + playerState_t *ps; + + ps = (playerState_t *)((byte *)sv.gameClients + sv.gameClientSize*(num)); + + return ps; +} + +svEntity_t *SV_SvEntityForGentity( sharedEntity_t *gEnt ) { + if ( !gEnt || gEnt->s.number < 0 || gEnt->s.number >= MAX_GENTITIES ) { + Com_Error( ERR_DROP, "SV_SvEntityForGentity: bad gEnt" ); + } + return &sv.svEntities[ gEnt->s.number ]; +} + +sharedEntity_t *SV_GEntityForSvEntity( svEntity_t *svEnt ) { + int num; + + num = svEnt - sv.svEntities; + return SV_GentityNum( num ); +} + +/* +=============== +SV_GameSendServerCommand + +Sends a command string to a client +=============== +*/ +void SV_GameSendServerCommand( int clientNum, const char *text ) { + if ( clientNum == -1 ) { + SV_SendServerCommand( NULL, "%s", text ); + } else { + if ( clientNum < 0 || clientNum >= sv_maxclients->integer ) { + return; + } + SV_SendServerCommand( svs.clients + clientNum, "%s", text ); + } +} + + +/* +=============== +SV_GameDropClient + +Disconnects the client with a message +=============== +*/ +void SV_GameDropClient( int clientNum, const char *reason ) { + if ( clientNum < 0 || clientNum >= sv_maxclients->integer ) { + return; + } + SV_DropClient( svs.clients + clientNum, reason ); +} + + +/* +================= +SV_SetBrushModel + +sets mins and maxs for inline bmodels +================= +*/ +void SV_SetBrushModel( sharedEntity_t *ent, const char *name ) { + clipHandle_t h; + vec3_t mins, maxs; + + if (!name) { + Com_Error( ERR_DROP, "SV_SetBrushModel: NULL" ); + } + + if (name[0] != '*') { + Com_Error( ERR_DROP, "SV_SetBrushModel: %s isn't a brush model", name ); + } + + + ent->s.modelindex = atoi( name + 1 ); + + h = CM_InlineModel( ent->s.modelindex ); + CM_ModelBounds( h, mins, maxs ); + VectorCopy (mins, ent->r.mins); + VectorCopy (maxs, ent->r.maxs); + ent->r.bmodel = qtrue; + + ent->r.contents = -1; // we don't know exactly what is in the brushes + + SV_LinkEntity( ent ); // FIXME: remove +} + + + +/* +================= +SV_inPVS + +Also checks portalareas so that doors block sight +================= +*/ +bool SV_inPVS (const vec3_t p1, const vec3_t p2) +{ + int leafnum; + int cluster; + int area1, area2; + byte *mask; + + leafnum = CM_PointLeafnum (p1); + cluster = CM_LeafCluster (leafnum); + area1 = CM_LeafArea (leafnum); + mask = CM_ClusterPVS (cluster); + + leafnum = CM_PointLeafnum (p2); + cluster = CM_LeafCluster (leafnum); + area2 = CM_LeafArea (leafnum); + + if ( mask && !(mask[cluster>>3] & (1<<(cluster&7))) ) + return false; + + if (!CM_AreasConnected (area1, area2)) + return false; // a door blocks sight + + return true; +} + + +/* +================= +SV_inPVSIgnorePortals + +Does NOT check portalareas +================= +*/ +bool SV_inPVSIgnorePortals( const vec3_t p1, const vec3_t p2) +{ + int leafnum; + int cluster; + byte *mask; + + leafnum = CM_PointLeafnum (p1); + cluster = CM_LeafCluster (leafnum); + mask = CM_ClusterPVS (cluster); + + leafnum = CM_PointLeafnum (p2); + cluster = CM_LeafCluster (leafnum); + + if ( mask && (!(mask[cluster>>3] & (1<<(cluster&7)) ) ) ) + return false; + + return true; +} + + +/* +======================== +SV_AdjustAreaPortalState +======================== +*/ +void SV_AdjustAreaPortalState( sharedEntity_t *ent, bool open ) { + svEntity_t *svEnt; + + svEnt = SV_SvEntityForGentity( ent ); + if ( svEnt->areanum2 == -1 ) { + return; + } + CM_AdjustAreaPortalState( svEnt->areanum, svEnt->areanum2, open ); +} + + +/* +================== +SV_EntityContact +================== +*/ +bool SV_EntityContact( vec3_t mins, vec3_t maxs, const sharedEntity_t *gEnt, traceType_t type ) { + const float *origin, *angles; + clipHandle_t ch; + trace_t trace; + + // check for exact collision + origin = gEnt->r.currentOrigin; + angles = gEnt->r.currentAngles; + + ch = SV_ClipHandleForEntity( gEnt ); + CM_TransformedBoxTrace ( &trace, vec3_origin, vec3_origin, mins, maxs, + ch, -1, origin, angles, type ); + + return trace.startsolid; +} + + +/* +=============== +SV_GetServerinfo + +=============== +*/ +void SV_GetServerinfo( char *buffer, int bufferSize ) { + if ( bufferSize < 1 ) { + Com_Error( ERR_DROP, "SV_GetServerinfo: bufferSize == %i", bufferSize ); + } + Q_strncpyz( buffer, Cvar_InfoString( CVAR_SERVERINFO ), bufferSize ); +} + +/* +=============== +SV_LocateGameData + +=============== +*/ +void SV_LocateGameData( sharedEntity_t *gEnts, int numGEntities, int sizeofGEntity_t, + playerState_t *clients, int sizeofGameClient ) { + sv.gentities = gEnts; + sv.gentitySize = sizeofGEntity_t; + sv.num_entities = numGEntities; + + sv.gameClients = clients; + sv.gameClientSize = sizeofGameClient; +} + + +/* +=============== +SV_GetUsercmd + +=============== +*/ +void SV_GetUsercmd( int clientNum, usercmd_t *cmd ) { + if ( clientNum < 0 || clientNum >= sv_maxclients->integer ) { + Com_Error( ERR_DROP, "SV_GetUsercmd: bad clientNum:%i", clientNum ); + } + *cmd = svs.clients[clientNum].lastUsercmd; +} + +//============================================== + +static int FloatAsInt( float f ) { + floatint_t fi; + fi.f = f; + return fi.i; +} + +/* +==================== +SV_GameSystemCalls + +The module is making a system call +==================== +*/ +intptr_t SV_GameSystemCalls( intptr_t *args ) { + switch( args[0] ) + { + case G_PRINT: + Com_Printf( "%s", (const char*)VMA(1) ); + return 0; + case G_ERROR: + Com_Error( ERR_DROP, "%s", (const char*)VMA(1) ); + return 0; + case G_MILLISECONDS: + return Sys_Milliseconds(); + case G_CVAR_REGISTER: + Cvar_Register( (vmCvar_t*)VMA(1), (const char*)VMA(2), (const char*)VMA(3), args[4] ); + return 0; + case G_CVAR_UPDATE: + Cvar_Update( (vmCvar_t*)VMA(1) ); + return 0; + case G_CVAR_SET: + Cvar_SetSafe( (const char *)VMA(1), (const char *)VMA(2) ); + return 0; + case G_CVAR_VARIABLE_INTEGER_VALUE: + return Cvar_VariableIntegerValue( (const char *)VMA(1) ); + case G_CVAR_VARIABLE_STRING_BUFFER: + Cvar_VariableStringBuffer( (const char*)VMA(1), (char*)VMA(2), args[3] ); + return 0; + case G_ARGC: + return Cmd_Argc(); + case G_ARGV: + Cmd_ArgvBuffer( args[1], (char*)VMA(2), args[3] ); + return 0; + case G_SEND_CONSOLE_COMMAND: + Cbuf_ExecuteText( args[1], (const char*)VMA(2) ); + return 0; + + case G_FS_FOPEN_FILE: + return FS_FOpenFileByMode( (const char*)VMA(1), (fileHandle_t*)VMA(2), (FS_Mode)args[3] ); + case G_FS_READ: + FS_Read( VMA(1), args[2], args[3] ); + return 0; + case G_FS_WRITE: + FS_Write( VMA(1), args[2], args[3] ); + return 0; + case G_FS_FCLOSE_FILE: + FS_FCloseFile( args[1] ); + return 0; + case G_FS_GETFILELIST: + return FS_GetFileList( (const char*)VMA(1), (const char*)VMA(2), (char*)VMA(3), args[4] ); + case G_FS_GETFILTEREDFILES: + return FS_GetFilteredFiles( (const char*)VMA(1), (const char*)VMA(2), (char*)VMA(3), (char*)VMA(4), args[5] ); + case G_FS_SEEK: + return FS_Seek( args[1], args[2], (FS_Origin)args[3] ); + case G_LOCATE_GAME_DATA: + SV_LocateGameData( (sharedEntity_t*)VMA(1), args[2], args[3], (playerState_t*)VMA(4), args[5] ); + return 0; + case G_DROP_CLIENT: + SV_GameDropClient( args[1], (const char*)VMA(2) ); + return 0; + case G_SEND_SERVER_COMMAND: + SV_GameSendServerCommand( args[1], (const char*)VMA(2) ); + return 0; + case G_LINKENTITY: + SV_LinkEntity( (sharedEntity_t*)VMA(1) ); + return 0; + case G_UNLINKENTITY: + SV_UnlinkEntity( (sharedEntity_t*)VMA(1) ); + return 0; + case G_ENTITIES_IN_BOX: + return SV_AreaEntities( (const vec_t*)VMA(1), (const vec_t*)VMA(2), (int*)VMA(3), args[4] ); + case G_ENTITY_CONTACT: + return SV_EntityContact( (vec_t*)VMA(1), (vec_t*)VMA(2), (const sharedEntity_t*)VMA(3), TT_AABB ); + case G_ENTITY_CONTACTCAPSULE: + return SV_EntityContact( (vec_t*)VMA(1), (vec_t*)VMA(2), (const sharedEntity_t*)VMA(3), TT_CAPSULE ); + case G_TRACE: + SV_Trace( (trace_t*)VMA(1), (const vec_t*)VMA(2), (vec_t*)VMA(3), (vec_t*)VMA(4), (const vec_t*)VMA(5), args[6], args[7], TT_AABB ); + return 0; + case G_TRACECAPSULE: + SV_Trace( (trace_t*)VMA(1), (const vec_t*)VMA(2), (vec_t*)VMA(3), (vec_t*)VMA(4), (const vec_t*)VMA(5), args[6], args[7], TT_CAPSULE ); + return 0; + case G_POINT_CONTENTS: + return SV_PointContents( (const vec_t*)VMA(1), args[2] ); + case G_SET_BRUSH_MODEL: + SV_SetBrushModel( (sharedEntity_t*)VMA(1), (const char*)VMA(2) ); + return 0; + case G_IN_PVS: + return SV_inPVS( (const vec_t*)VMA(1), (const vec_t*)VMA(2) ); + case G_IN_PVS_IGNORE_PORTALS: + return SV_inPVSIgnorePortals( (const vec_t*)VMA(1), (const vec_t*)VMA(2) ); + + case G_SET_CONFIGSTRING: + SV_SetConfigstring( args[1], (const char*)VMA(2) ); + return 0; + case G_GET_CONFIGSTRING: + SV_GetConfigstring( args[1], (char*)VMA(2), args[3] ); + return 0; + case G_SET_CONFIGSTRING_RESTRICTIONS: + SV_SetConfigstringRestrictions( args[1], (clientList_t*)VMA(2) ); + return 0; + case G_SET_USERINFO: + SV_SetUserinfo( args[1], (const char*)VMA(2) ); + return 0; + case G_GET_USERINFO: + SV_GetUserinfo( args[1], (char*)VMA(2), args[3] ); + return 0; + case G_GET_SERVERINFO: + SV_GetServerinfo( (char*)VMA(1), args[2] ); + return 0; + case G_ADJUST_AREA_PORTAL_STATE: + SV_AdjustAreaPortalState( (sharedEntity_t*)VMA(1), (bool)args[2] ); + return 0; + case G_AREAS_CONNECTED: + return CM_AreasConnected( args[1], args[2] ); + + case G_GET_USERCMD: + SV_GetUsercmd( args[1], (usercmd_t*)VMA(2) ); + return 0; + case G_GET_ENTITY_TOKEN: + { + const char *s; + + s = COM_Parse( &sv.entityParsePoint ); + Q_strncpyz( (char*)VMA(1), s, args[2] ); + if ( !sv.entityParsePoint && !s[0] ) { + return false; + } else { + return true; + } + } + + case G_REAL_TIME: + return Com_RealTime( (qtime_t*)VMA(1) ); + case G_SNAPVECTOR: + Q_SnapVector( (vec_t*)VMA(1) ); + return 0; + + case G_SEND_GAMESTAT: + return 0; + + //==================================== + + case G_PARSE_ADD_GLOBAL_DEFINE: + return Parse_AddGlobalDefine( (char*)VMA(1) ); + case G_PARSE_LOAD_SOURCE: + return Parse_LoadSourceHandle( (const char*)VMA(1) ); + case G_PARSE_FREE_SOURCE: + return Parse_FreeSourceHandle( args[1] ); + case G_PARSE_READ_TOKEN: + return Parse_ReadTokenHandle( args[1], (pc_token_t*)VMA(2) ); + case G_PARSE_SOURCE_FILE_AND_LINE: + return Parse_SourceFileAndLine( args[1], (char*)VMA(2), (int*)VMA(3) ); + + case G_ADDCOMMAND: + Cmd_AddCommand( (const char*)VMA(1), NULL ); + return 0; + case G_REMOVECOMMAND: + Cmd_RemoveCommand( (const char*)VMA(1) ); + return 0; + + case TRAP_MEMSET: + ::memset( VMA(1), args[2], args[3] ); + return 0; + + case TRAP_MEMCPY: + ::memcpy( VMA(1), VMA(2), args[3] ); + return 0; + + case TRAP_STRNCPY: + ::strncpy( (char*)VMA(1), (const char*)VMA(2), args[3] ); + return args[1]; + + case TRAP_SIN: + return FloatAsInt( sin( VMF(1) ) ); + + case TRAP_COS: + return FloatAsInt( cos( VMF(1) ) ); + + case TRAP_ATAN2: + return FloatAsInt( atan2( VMF(1), VMF(2) ) ); + + case TRAP_SQRT: + return FloatAsInt( sqrt( VMF(1) ) ); + + case TRAP_MATRIXMULTIPLY: + { + // XXX C++ is made this annoying + float (&in1)[3][3] = *reinterpret_cast<float (*)[3][3]>(VMA(1)); + float (&in2)[3][3] = *reinterpret_cast<float (*)[3][3]>(VMA(2)); + float (&in3)[3][3] = *reinterpret_cast<float (*)[3][3]>(VMA(3)); + MatrixMultiply( in1, in2, in3 ); + return 0; + } + + case TRAP_ANGLEVECTORS: + AngleVectors( (const vec_t*)VMA(1), (vec_t*)VMA(2), (vec_t*)VMA(3), (vec_t*)VMA(4) ); + return 0; + + case TRAP_PERPENDICULARVECTOR: + PerpendicularVector( (vec_t*)VMA(1), (const vec_t*)VMA(2) ); + return 0; + + case TRAP_FLOOR: + return FloatAsInt( floor( VMF(1) ) ); + + case TRAP_CEIL: + return FloatAsInt( ceil( VMF(1) ) ); + + default: + Com_Error( ERR_DROP, "Bad game system trap: %ld", (long int) args[0] ); + } + return 0; +} + +/* +=============== +SV_ShutdownGameProgs + +Called every time a map changes +=============== +*/ +void SV_ShutdownGameProgs( void ) { + if ( !sv.gvm ) { + return; + } + VM_Call( sv.gvm, GAME_SHUTDOWN, false ); + VM_Free( sv.gvm ); + sv.gvm = NULL; +} + +/* +================== +SV_InitGameVM + +Called for both a full init and a restart +================== +*/ +static void SV_InitGameVM( bool restart ) { + int i; + + // start the entity parsing at the beginning + sv.entityParsePoint = CM_EntityString(); + + // clear all gentity pointers that might still be set from + // a previous level + // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=522 + // now done before GAME_INIT call + for ( i = 0 ; i < sv_maxclients->integer ; i++ ) { + svs.clients[i].gentity = NULL; + } + + // use the current msec count for a random seed + // init for this gamestate + VM_Call (sv.gvm, GAME_INIT, sv.time, Com_Milliseconds(), restart); +} + + + +/* +=================== +SV_RestartGameProgs + +Called on a map_restart, but not on a normal map change +=================== +*/ +void SV_RestartGameProgs( void ) { + if ( !sv.gvm ) { + return; + } + VM_Call( sv.gvm, GAME_SHUTDOWN, true ); + + // do a restart instead of a free + sv.gvm = VM_Restart(sv.gvm, true); + if ( !sv.gvm ) { + Com_Error( ERR_FATAL, "VM_Restart on game failed" ); + } + + SV_InitGameVM( true ); +} + + +/* +=============== +SV_InitGameProgs + +Called on a normal map change, not on a map_restart +=============== +*/ +void SV_InitGameProgs( void ) { + // load the dll or bytecode + sv.gvm = VM_Create( "game", SV_GameSystemCalls, (vmInterpret_t)Cvar_VariableValue( "vm_game" ) ); + if ( !sv.gvm ) { + Com_Error( ERR_FATAL, "VM_Create on game failed" ); + } + + SV_InitGameVM( false ); +} + + +/* +==================== +SV_GameCommand + +See if the current console command is claimed by the game +==================== +*/ +bool SV_GameCommand( void ) { + if ( sv.state != SS_GAME ) { + return false; + } + + return (bool)VM_Call( sv.gvm, GAME_CONSOLE_COMMAND ); +} diff --git a/src/server/sv_init.cpp b/src/server/sv_init.cpp new file mode 100644 index 0000000..8c7729e --- /dev/null +++ b/src/server/sv_init.cpp @@ -0,0 +1,1004 @@ +/* +=========================================================================== +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/> + +=========================================================================== +*/ + +#include "server.h" + +#include "qcommon/cvar.h" + +// Attack log file is started when server is init (!= sv_running 1!) +// we even log attacks when the server is waiting for rcon and doesn't run a map +int attHandle = 0; // server attack log file handle + +char alternateInfos[2][2][BIG_INFO_STRING]; + +/* +=============== +SV_SendConfigstring + +Creates and sends the server command necessary to update the CS index for the +given client +=============== +*/ +static void SV_SendConfigstring(client_t *client, int i) +{ + const char *configstring; + int maxChunkSize = MAX_STRING_CHARS - 24; + int len; + + if (sv.configstrings[i].restricted && + Com_ClientListContains(&sv.configstrings[i].clientList, client - svs.clients)) + { + // Send a blank config string for this client if it's listed + SV_SendServerCommand(client, "cs %i \"\"\n", i); + return; + } + + if (i <= CS_SYSTEMINFO && client->netchan.alternateProtocol != 0) + { + configstring = alternateInfos[i][client->netchan.alternateProtocol - 1]; + } + else + { + configstring = sv.configstrings[i].s; + } + + len = strlen(configstring); + + if (len >= maxChunkSize) + { + int sent = 0; + int remaining = len; + const char *cmd; + char buf[MAX_STRING_CHARS]; + + while (remaining > 0) + { + if (sent == 0) + { + cmd = "bcs0"; + } + else if (remaining < maxChunkSize) + { + cmd = "bcs2"; + } + else + { + cmd = "bcs1"; + } + Q_strncpyz(buf, &configstring[sent], maxChunkSize); + + SV_SendServerCommand(client, "%s %i \"%s\"\n", cmd, i, buf); + + sent += (maxChunkSize - 1); + remaining -= (maxChunkSize - 1); + } + } + else + { + // standard cs, just send it + SV_SendServerCommand(client, "cs %i \"%s\"\n", i, configstring); + } +} + +/* +=============== +SV_UpdateConfigstrings + +Called when a client goes from CS_PRIMED to CS_ACTIVE. Updates all +Configstring indexes that have changed while the client was in CS_PRIMED +=============== +*/ +void SV_UpdateConfigstrings(client_t *client) +{ + for (int i = 0; i < MAX_CONFIGSTRINGS; i++) + { + // if the CS hasn't changed since we went to CS_PRIMED, ignore + if (!client->csUpdated[i]) continue; + + // do not always send server info to all clients + if (i == CS_SERVERINFO && client->gentity && (client->gentity->r.svFlags & SVF_NOSERVERINFO)) + { + continue; + } + + SV_SendConfigstring(client, i); + client->csUpdated[i] = false; + } +} + +/* +=============== +SV_SetConfigstring + +=============== +*/ +void SV_SetConfigstring(int idx, const char *val) +{ + bool modified[3] = {false, false, false}; + int i; + client_t *client; + + if (idx < 0 || idx >= MAX_CONFIGSTRINGS) + { + Com_Error(ERR_DROP, "SV_SetConfigstring: bad idx %i", idx); + } + + if (!val) + { + val = ""; + } + + if (idx <= CS_SYSTEMINFO) + { + for (i = 1; i < 3; ++i) + { + char info[BIG_INFO_STRING]; + + Q_strncpyz(info, val, sizeof(info)); + if (idx == CS_SERVERINFO) + { + Info_SetValueForKey_Big(info, "protocol", (i == 1 ? "70" : "69")); + } + else if (i == 2) + { + Info_SetValueForKey_Big(info, "sv_paks", Cvar_VariableString("sv_alternatePaks")); + Info_SetValueForKey_Big(info, "sv_pakNames", Cvar_VariableString("sv_alternatePakNames")); + Info_SetValueForKey_Big(info, "sv_referencedPaks", Cvar_VariableString("sv_referencedAlternatePaks")); + Info_SetValueForKey_Big( + info, "sv_referencedPakNames", Cvar_VariableString("sv_referencedAlternatePakNames")); + Info_SetValueForKey_Big(info, "cl_allowDownload", "1, you should set it yourself"); + if (!(sv_allowDownload->integer & DLF_NO_REDIRECT)) + { + Info_SetValueForKey_Big(info, "sv_wwwBaseURL", Cvar_VariableString("sv_dlUrl")); + Info_SetValueForKey_Big( + info, "sv_wwwDownload", Cvar_VariableString("1, you should set it yourself")); + } + } + + if (strcmp(info, alternateInfos[idx][i - 1])) + { + modified[i] = true; + strcpy(alternateInfos[idx][i - 1], info); + } + } + + if (strcmp(val, sv.configstrings[idx].s)) + { + modified[0] = true; + Z_Free(sv.configstrings[idx].s); + sv.configstrings[idx].s = CopyString(val); + } + + if (!modified[0] && !modified[1] && !modified[2]) + { + return; + } + } + else + { + // don't bother broadcasting an update if no change + if (!strcmp(val, sv.configstrings[idx].s)) + { + return; + } + + // change the string in sv + Z_Free(sv.configstrings[idx].s); + sv.configstrings[idx].s = CopyString(val); + } + + // send it to all the clients if we aren't + // spawning a new server + if (sv.state == SS_GAME || sv.restarting) + { + // send the data to all relevent clients + for (i = 0, client = svs.clients; i < sv_maxclients->integer; i++, client++) + { + if (idx <= CS_SYSTEMINFO && !modified[client->netchan.alternateProtocol]) + { + continue; + } + + if (client->state < CS_ACTIVE) + { + if (client->state == CS_PRIMED) client->csUpdated[idx] = true; + continue; + } + // do not always send server info to all clients + if (idx == CS_SERVERINFO && client->gentity && (client->gentity->r.svFlags & SVF_NOSERVERINFO)) + { + continue; + } + + SV_SendConfigstring(client, idx); + } + } +} + +/* +=============== +SV_GetConfigstring + +=============== +*/ +void SV_GetConfigstring(int idx, char *buffer, int bufferSize) +{ + if (bufferSize < 1) + { + Com_Error(ERR_DROP, "SV_GetConfigstring: bufferSize == %i", bufferSize); + } + if (idx < 0 || idx >= MAX_CONFIGSTRINGS) + { + Com_Error(ERR_DROP, "SV_GetConfigstring: bad idx %i", idx); + } + if (!sv.configstrings[idx].s) + { + buffer[0] = 0; + return; + } + + Q_strncpyz(buffer, sv.configstrings[idx].s, bufferSize); +} + +/* +=============== +SV_SetConfigstringRestrictions +=============== +*/ +void SV_SetConfigstringRestrictions(int idx, const clientList_t *clientList) +{ + int i; + clientList_t oldClientList = sv.configstrings[idx].clientList; + + sv.configstrings[idx].clientList = *clientList; + sv.configstrings[idx].restricted = true; + + for (i = 0; i < sv_maxclients->integer; i++) + { + if (svs.clients[i].state >= CS_CONNECTED) + { + if (Com_ClientListContains(&oldClientList, i) != Com_ClientListContains(clientList, i)) + { + // A client has left or joined the restricted list, so update + SV_SendConfigstring(&svs.clients[i], idx); + } + } + } +} + +/* +=============== +SV_SetUserinfo + +=============== +*/ +void SV_SetUserinfo(int idx, const char *val) +{ + if (idx < 0 || idx >= sv_maxclients->integer) + { + Com_Error(ERR_DROP, "SV_SetUserinfo: bad idx %i", idx); + } + + if (!val) + { + val = ""; + } + + Q_strncpyz(svs.clients[idx].userinfo, val, sizeof(svs.clients[idx].userinfo)); + Q_strncpyz(svs.clients[idx].name, Info_ValueForKey(val, "name"), sizeof(svs.clients[idx].name)); +} + +/* +=============== +SV_GetUserinfo + +=============== +*/ +void SV_GetUserinfo(int idx, char *buffer, int bufferSize) +{ + if (bufferSize < 1) + { + Com_Error(ERR_DROP, "SV_GetUserinfo: bufferSize == %i", bufferSize); + } + if (idx < 0 || idx >= sv_maxclients->integer) + { + Com_Error(ERR_DROP, "SV_GetUserinfo: bad idx %i", idx); + } + Q_strncpyz(buffer, svs.clients[idx].userinfo, bufferSize); +} + +/* +================ +SV_CreateBaseline + +Entity baselines are used to compress non-delta messages +to the clients -- only the fields that differ from the +baseline will be transmitted +================ +*/ +static void SV_CreateBaseline(void) +{ + sharedEntity_t *svent; + int entnum; + + for (entnum = 1; entnum < sv.num_entities; entnum++) + { + svent = SV_GentityNum(entnum); + if (!svent->r.linked) + { + continue; + } + svent->s.number = entnum; + + // + // take current state as baseline + // + sv.svEntities[entnum].baseline = svent->s; + } +} + +/* +=============== +SV_BoundMaxClients + +=============== +*/ +static void SV_BoundMaxClients(int minimum) +{ + // get the current maxclients value + Cvar_Get("sv_maxclients", "8", 0); + + sv_maxclients->modified = false; + + if (sv_maxclients->integer < minimum) + { + Cvar_Set("sv_maxclients", va("%i", minimum)); + } + else if (sv_maxclients->integer > MAX_CLIENTS) + { + Cvar_Set("sv_maxclients", va("%i", MAX_CLIENTS)); + } +} + +/* +=============== +SV_Startup + +Called when a host starts a map when it wasn't running +one before. Successive map or map_restart commands will +NOT cause this to be called, unless the game is exited to +the menu system first. +=============== +*/ +static void SV_Startup(void) +{ + if (svs.initialized) + { + Com_Error(ERR_FATAL, "SV_Startup: svs.initialized"); + } + SV_BoundMaxClients(1); + + svs.clients = (client_t *)Z_Malloc(sizeof(client_t) * sv_maxclients->integer); + if (com_dedicated->integer) + { + svs.numSnapshotEntities = sv_maxclients->integer * PACKET_BACKUP * MAX_SNAPSHOT_ENTITIES; + } + else + { + // we don't need nearly as many when playing locally + svs.numSnapshotEntities = sv_maxclients->integer * 4 * MAX_SNAPSHOT_ENTITIES; + } + svs.initialized = true; + + // Don't respect sv_killserver unless a server is actually running + if (sv_killserver->integer) + { + Cvar_Set("sv_killserver", "0"); + } + + Cvar_Set("sv_running", "1"); + + // Join the ipv6 multicast group now that a map is running so clients can scan for us on the local network. + NET_JoinMulticast6(); +} + +/* +================== +SV_ChangeMaxClients +================== +*/ +void SV_ChangeMaxClients(void) +{ + int oldMaxClients; + int i; + client_t *oldClients; + int count; + + // get the highest client number in use + count = 0; + for (i = 0; i < sv_maxclients->integer; i++) + { + if (svs.clients[i].state >= CS_CONNECTED) + { + if (i > count) count = i; + } + } + count++; + + oldMaxClients = sv_maxclients->integer; + // never go below the highest client number in use + SV_BoundMaxClients(count); + // if still the same + if (sv_maxclients->integer == oldMaxClients) + { + return; + } + + oldClients = (client_t *)Hunk_AllocateTempMemory(count * sizeof(client_t)); + // copy the clients to hunk memory + for (i = 0; i < count; i++) + { + if (svs.clients[i].state >= CS_CONNECTED) + { + oldClients[i] = svs.clients[i]; + } + else + { + ::memset(&oldClients[i], 0, sizeof(client_t)); + } + } + + // free old clients arrays + Z_Free(svs.clients); + + // allocate new clients + svs.clients = (client_t *)Z_Malloc(sv_maxclients->integer * sizeof(client_t)); + ::memset(svs.clients, 0, sv_maxclients->integer * sizeof(client_t)); + + // copy the clients over + for (i = 0; i < count; i++) + { + if (oldClients[i].state >= CS_CONNECTED) + { + svs.clients[i] = oldClients[i]; + } + } + + // free the old clients on the hunk + Hunk_FreeTempMemory(oldClients); + + // allocate new snapshot entities + if (com_dedicated->integer) + { + svs.numSnapshotEntities = sv_maxclients->integer * PACKET_BACKUP * MAX_SNAPSHOT_ENTITIES; + } + else + { + // we don't need nearly as many when playing locally + svs.numSnapshotEntities = sv_maxclients->integer * 4 * MAX_SNAPSHOT_ENTITIES; + } +} + +/* +================ +SV_ClearServer +================ +*/ +static void SV_ClearServer(void) +{ + int i; + + for (i = 0; i < MAX_CONFIGSTRINGS; i++) + { + if (i <= CS_SYSTEMINFO) + { + alternateInfos[i][0][0] = alternateInfos[i][1][0] = '\0'; + } + if (sv.configstrings[i].s) + { + Z_Free(sv.configstrings[i].s); + } + } + ::memset(&sv, 0, sizeof(sv)); +} + +/* +================ +SV_TouchCGame + +Touch the cgame.qvm so that a pure client can load it if it's in a seperate pk3 +================ +*/ +static void SV_TouchCGame(void) +{ + fileHandle_t f; + char filename[MAX_QPATH]; + + Com_sprintf(filename, sizeof(filename), "vm/%s.qvm", "cgame"); + FS_FOpenFileRead(filename, &f, false); + if (f) + { + FS_FCloseFile(f); + } +} + +/* +================ +SV_SpawnServer + +Change the server to a new map, taking all connected +clients along with it. +This is NOT called for map_restart +================ +*/ +void SV_SpawnServer(char *server) +{ + int i; + int checksum; + char systemInfo[16384]; + const char *p; + + // shut down the existing game if it is running + SV_ShutdownGameProgs(); + + Com_Printf("------ Server Initialization ------\n"); + Com_Printf("Server: %s\n", server); + + // if not running a dedicated server CL_MapLoading will connect the client to the server + // also print some status stuff + CL_MapLoading(); + + // make sure all the client stuff is unloaded + CL_ShutdownAll(false); + + // clear the whole hunk because we're (re)loading the server + Hunk_Clear(); + + // clear collision map data + CM_ClearMap(); + + // init client structures and svs.numSnapshotEntities + if (!Cvar_VariableValue("sv_running")) + { + SV_Startup(); + } + else + { + // check for maxclients change + if (sv_maxclients->modified) + { + SV_ChangeMaxClients(); + } + } + + // clear pak references + FS_ClearPakReferences(0); + + // allocate the snapshot entities on the hunk + svs.snapshotEntities = (entityState_t *)Hunk_Alloc(sizeof(entityState_t) * svs.numSnapshotEntities, h_high); + svs.nextSnapshotEntities = 0; + + // toggle the server bit so clients can detect that a + // server has changed + svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT; + + for (i = 0; i < sv_maxclients->integer; i++) + { + // save when the server started for each client already connected + if (svs.clients[i].state >= CS_CONNECTED) + { + svs.clients[i].oldServerTime = sv.time; + } + } + + // wipe the entire per-level structure + SV_ClearServer(); + for (i = 0; i < MAX_CONFIGSTRINGS; i++) + { + if (i <= CS_SYSTEMINFO) + { + alternateInfos[i][0][0] = alternateInfos[i][1][0] = '\0'; + } + sv.configstrings[i].s = CopyString(""); + sv.configstrings[i].restricted = false; + ::memset(&sv.configstrings[i].clientList, 0, sizeof(clientList_t)); + } + + // make sure we are not paused + Cvar_Set("cl_paused", "0"); + + // get a new checksum feed and restart the file system + sv.checksumFeed = (((int)rand() << 16) ^ rand()) ^ Com_Milliseconds(); + FS_Restart(sv.checksumFeed); + + // advertise GPP-compatible extensions + Cvar_Set("sv_gppExtension", "1"); + + CM_LoadMap(va("maps/%s.bsp", server), false, &checksum); + + // set serverinfo visible name + Cvar_Set("mapname", server); + + Cvar_Set("sv_mapChecksum", va("%i", checksum)); + + // serverid should be different each time + sv.serverId = com_frameTime; + sv.restartedServerId = sv.serverId; // I suppose the init here is just to be safe + sv.checksumFeedServerId = sv.serverId; + Cvar_Set("sv_serverid", va("%i", sv.serverId)); + + // clear physics interaction links + SV_ClearWorld(); + + // media configstring setting should be done during + // the loading stage, so connected clients don't have + // to load during actual gameplay + sv.state = SS_LOADING; + + // load and spawn all other entities + SV_InitGameProgs(); + + // run a few frames to allow everything to settle + for (i = 0; i < 3; i++) + { + VM_Call(sv.gvm, GAME_RUN_FRAME, sv.time); + sv.time += 100; + svs.time += 100; + } + + // create a baseline for more efficient communications + SV_CreateBaseline(); + + for (i = 0; i < sv_maxclients->integer; i++) + { + // send the new gamestate to all connected clients + if (svs.clients[i].state >= CS_CONNECTED) + { + char *denied; + + // connect the client again + denied = + (char *)VM_ExplicitArgPtr(sv.gvm, VM_Call(sv.gvm, GAME_CLIENT_CONNECT, i, false)); // firstTime = false + if (denied) + { + // this generally shouldn't happen, because the client + // was connected before the level change + SV_DropClient(&svs.clients[i], denied); + } + else + { + // when we get the next packet from a connected client, + // the new gamestate will be sent + svs.clients[i].state = CS_CONNECTED; + } + } + } + + // run another frame to allow things to look at all the players + VM_Call(sv.gvm, GAME_RUN_FRAME, sv.time); + sv.time += 100; + svs.time += 100; + + if (sv_pure->integer) + { + // the server sends these to the clients so they will only + // load pk3s also loaded at the server + p = FS_LoadedPakChecksums(false); + Cvar_Set("sv_paks", p); + p = FS_LoadedPakChecksums(true); + Cvar_Set("sv_alternatePaks", p); + if (strlen(p) == 0) + { + Com_Printf("WARNING: sv_pure set but no PK3 files loaded\n"); + } + p = FS_LoadedPakNames(false); + Cvar_Set("sv_pakNames", p); + p = FS_LoadedPakNames(true); + Cvar_Set("sv_alternatePakNames", p); + + // if a dedicated pure server we need to touch the cgame because it could be in a + // seperate pk3 file and the client will need to load the latest cgame.qvm + if (com_dedicated->integer) + { + SV_TouchCGame(); + } + } + else + { + Cvar_Set("sv_paks", ""); + Cvar_Set("sv_pakNames", ""); + Cvar_Set("sv_alternatePaks", ""); + Cvar_Set("sv_alternatePakNames", ""); + } + // the server sends these to the clients so they can figure + // out which pk3s should be auto-downloaded + p = FS_ReferencedPakChecksums(false); + Cvar_Set("sv_referencedPaks", p); + p = FS_ReferencedPakChecksums(true); + Cvar_Set("sv_referencedAlternatePaks", p); + p = FS_ReferencedPakNames(false); + Cvar_Set("sv_referencedPakNames", p); + p = FS_ReferencedPakNames(true); + Cvar_Set("sv_referencedAlternatePakNames", p); + + // save systeminfo and serverinfo strings + Q_strncpyz(systemInfo, Cvar_InfoString_Big(CVAR_SYSTEMINFO), sizeof(systemInfo)); + cvar_modifiedFlags &= ~CVAR_SYSTEMINFO; + SV_SetConfigstring(CS_SYSTEMINFO, systemInfo); + + SV_SetConfigstring(CS_SERVERINFO, Cvar_InfoString(CVAR_SERVERINFO)); + cvar_modifiedFlags &= ~CVAR_SERVERINFO; + + // any media configstring setting now should issue a warning + // and any configstring changes should be reliably transmitted + // to all clients + sv.state = SS_GAME; + + // send a heartbeat now so the master will get up to date info + SV_Heartbeat_f(); + + Hunk_SetMark(); + +#ifndef DEDICATED + if (com_dedicated->integer) + { + // restart renderer in order to show console for dedicated servers + // launched through the regular binary + CL_StartHunkUsers(true); + } +#endif + + Com_Printf("-----------------------------------\n"); +} + +/** + * @brief SV_WriteAttackLog + * @param[in] log + */ +void SV_WriteAttackLog(const char *log) +{ + if (attHandle > 0) + { + char string[512]; // 512 chars seem enough here + qtime_t time; + + Com_RealTime(&time); + Com_sprintf(string, sizeof(string), "%i/%i/%i %i:%i:%i %s", 1900 + time.tm_year, time.tm_mday, time.tm_mon + 1, time.tm_hour, time.tm_min, time.tm_sec, log); + (void) FS_Write(string, strlen(string), attHandle); + } + + if (sv_protect->integer & SVP_CONSOLE) + { + Com_Printf("%s", log); + } +} + +/** + * @brief SV_InitAttackLog + */ +void SV_InitAttackLog() +{ + if (sv_protectLog->string[0] == '\0') + { + Com_Printf("Not logging server attacks to disk.\n"); + } + else + { + // in sync so admins can check this at runtime + FS_FOpenFileByMode(sv_protectLog->string, &attHandle, FS_APPEND_SYNC); + + if (attHandle <= 0) + { + Com_Printf("WARNING: Couldn't open server attack logfile %s\n", sv_protectLog->string); + } + else + { + Com_Printf("Logging server attacks to %s\n", sv_protectLog->string); + SV_WriteAttackLog("-------------------------------------------------------------------------------\n"); + SV_WriteAttackLog("Start server attack log\n"); + SV_WriteAttackLog("-------------------------------------------------------------------------------\n"); + } + } +} + +/** + * @brief SV_CloseAttackLog + */ +void SV_CloseAttackLog() +{ + if (attHandle > 0) + { + SV_WriteAttackLog("-------------------------------------------------------------------------------\n"); + SV_WriteAttackLog("End server attack log\n"); + SV_WriteAttackLog("-------------------------------------------------------------------------------\n"); + Com_Printf("Server attack log closed \n"); + } + + FS_FCloseFile(attHandle); + + attHandle = 0; // local handle +} + +/* +=============== +SV_Init + +Only called at main exe startup, not for each game +=============== +*/ +void SV_Init(void) +{ + SV_AddOperatorCommands(); + + // serverinfo vars + Cvar_Get("timelimit", "0", CVAR_SERVERINFO); + Cvar_Get("sv_keywords", "", CVAR_SERVERINFO); + sv_mapname = Cvar_Get("mapname", "nomap", CVAR_SERVERINFO | CVAR_ROM); + sv_privateClients = Cvar_Get("sv_privateClients", "0", CVAR_SERVERINFO); + sv_hostname = Cvar_Get("sv_hostname", "noname", CVAR_SERVERINFO | CVAR_ARCHIVE); + sv_maxclients = Cvar_Get("sv_maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH); + + sv_minRate = Cvar_Get("sv_minRate", "0", CVAR_ARCHIVE | CVAR_SERVERINFO); + sv_maxRate = Cvar_Get("sv_maxRate", "0", CVAR_ARCHIVE | CVAR_SERVERINFO); + sv_dlRate = Cvar_Get("sv_dlRate", "100", CVAR_ARCHIVE | CVAR_SERVERINFO); + sv_minPing = Cvar_Get("sv_minPing", "0", CVAR_ARCHIVE | CVAR_SERVERINFO); + sv_maxPing = Cvar_Get("sv_maxPing", "0", CVAR_ARCHIVE | CVAR_SERVERINFO); + + // systeminfo + Cvar_Get("sv_cheats", "1", CVAR_SYSTEMINFO | CVAR_ROM); + sv_serverid = Cvar_Get("sv_serverid", "0", CVAR_SYSTEMINFO | CVAR_ROM); + sv_pure = Cvar_Get("sv_pure", "1", CVAR_SYSTEMINFO); +#ifdef USE_VOIP + sv_voip = Cvar_Get("sv_voip", "1", CVAR_LATCH); + Cvar_CheckRange(sv_voip, 0, 1, true); + sv_voipProtocol = Cvar_Get("sv_voipProtocol", sv_voip->integer ? "opus" : "", CVAR_SYSTEMINFO | CVAR_ROM); +#endif + Cvar_Get("sv_paks", "", CVAR_SYSTEMINFO | CVAR_ROM); + Cvar_Get("sv_pakNames", "", CVAR_SYSTEMINFO | CVAR_ROM); + Cvar_Get("sv_referencedPaks", "", CVAR_SYSTEMINFO | CVAR_ROM); + Cvar_Get("sv_referencedPakNames", "", CVAR_SYSTEMINFO | CVAR_ROM); + Cvar_Get("sv_alternatePaks", "", CVAR_ALTERNATE_SYSTEMINFO | CVAR_ROM); + Cvar_Get("sv_alternatePakNames", "", CVAR_ALTERNATE_SYSTEMINFO | CVAR_ROM); + Cvar_Get("sv_referencedAlternatePaks", "", CVAR_ALTERNATE_SYSTEMINFO | CVAR_ROM); + Cvar_Get("sv_referencedAlternatePakNames", "", CVAR_ALTERNATE_SYSTEMINFO | CVAR_ROM); + + // server vars + sv_rconPassword = Cvar_Get("rconPassword", "", CVAR_TEMP); + sv_privatePassword = Cvar_Get("sv_privatePassword", "", CVAR_TEMP); + sv_fps = Cvar_Get("sv_fps", "40", CVAR_TEMP); + sv_timeout = Cvar_Get("sv_timeout", "200", CVAR_TEMP); + sv_zombietime = Cvar_Get("sv_zombietime", "2", CVAR_TEMP); + + sv_allowDownload = Cvar_Get("sv_allowDownload", "0", CVAR_SERVERINFO); + Cvar_Get("sv_dlURL", "http://downloads.tremulous.net", CVAR_SERVERINFO | CVAR_ARCHIVE); + + sv_protect = Cvar_Get("sv_protect", "3", CVAR_ARCHIVE); + sv_protectLog = Cvar_Get("sv_protectLog", "sv_protect.log", CVAR_ARCHIVE); + SV_InitAttackLog(); + + for (int a = 0; a < 3; ++a) + { + sv_masters[a][0] = Cvar_Get(va("sv_%smaster1", (a == 2 ? "alt2" : a == 1 ? "alt1" : "")), MASTER_SERVER_NAME, 0); + for (int i = 1; i < MAX_MASTER_SERVERS; i++) + sv_masters[a][i] = Cvar_Get(va("sv_%smaster%d", (a == 2 ? "alt2" : a == 1 ? "alt1" : ""), i + 1), "", CVAR_ARCHIVE); + } + + sv_reconnectlimit = Cvar_Get("sv_reconnectlimit", "3", 0); + sv_showloss = Cvar_Get("sv_showloss", "0", 0); + sv_padPackets = Cvar_Get("sv_padPackets", "0", 0); + sv_killserver = Cvar_Get("sv_killserver", "0", 0); + sv_mapChecksum = Cvar_Get("sv_mapChecksum", "", CVAR_ROM); + sv_lanForceRate = Cvar_Get("sv_lanForceRate", "1", CVAR_ARCHIVE); + sv_rsaAuth = Cvar_Get("sv_rsaAuth", "1", CVAR_INIT | CVAR_PROTECTED); + sv_schachtmeisterPort = Cvar_Get ("sv_schachtmeisterPort", "1337", CVAR_ARCHIVE); +} + +/* +================== +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(const char *message) +{ + client_t *cl; + + // send it twice, ignoring rate + for (int j = 0; j < 2; j++) + { + int i; + for (i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++) + { + if (cl->state >= CS_CONNECTED) + { + // don't send a disconnect to a local client + if (cl->netchan.remoteAddress.type != NA_LOOPBACK) + { + SV_SendServerCommand(cl, "print \"%s\n\"\n", message); + SV_SendServerCommand(cl, "disconnect \"%s\"", message); + } + // force a snapshot to be sent + cl->lastSnapshotTime = 0; + SV_SendClientSnapshot(cl); + } + } + } +} + +/* +================ +SV_Shutdown + +Called when each game quits, +before Sys_Quit or Sys_Error +================ +*/ +void SV_Shutdown(const char *finalmsg) +{ + // close attack log + SV_CloseAttackLog(); + + if (!com_sv_running || !com_sv_running->integer) + { + return; + } + + Com_Printf("----- Server Shutdown (%s) -----\n", finalmsg); + + NET_LeaveMulticast6(); + + if (svs.clients && !com_errorEntered) + { + SV_FinalMessage(finalmsg); + } + + SV_RemoveOperatorCommands(); + SV_MasterShutdown(); + SV_ShutdownGameProgs(); + + // free current level + SV_ClearServer(); + + // free server static data + if (svs.clients) + { + for (int i = 0; i < sv_maxclients->integer; i++) + SV_FreeClient(&svs.clients[i]); + + Z_Free(svs.clients); + } + ::memset(&svs, 0, sizeof(svs)); + + Cvar_Set("sv_running", "0"); + + Com_Printf("---------------------------\n"); + + // disconnect any local clients + if (sv_killserver->integer != 2) CL_Disconnect(false); +} diff --git a/src/server/sv_main.cpp b/src/server/sv_main.cpp new file mode 100644 index 0000000..f5c3b98 --- /dev/null +++ b/src/server/sv_main.cpp @@ -0,0 +1,1551 @@ +/* +=========================================================================== +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/> + +=========================================================================== +*/ + +#include "server.h" + +#include <iostream> + +#ifdef USE_VOIP +cvar_t *sv_voip; +cvar_t *sv_voipProtocol; +#endif + +serverStatic_t svs; // persistant server info +server_t sv {}; // local server + +cvar_t *sv_fps = NULL; // time rate for running non-clients +cvar_t *sv_timeout; // seconds without any message +cvar_t *sv_zombietime; // seconds to sink messages after disconnect +cvar_t *sv_rconPassword; // password for remote server commands +cvar_t *sv_privatePassword; // password for the privateClient slots +cvar_t *sv_allowDownload; +cvar_t *sv_maxclients; + +cvar_t *sv_privateClients; // number of clients reserved for password +cvar_t *sv_hostname; +cvar_t *sv_masters[3][MAX_MASTER_SERVERS]; // master server IP addresses +cvar_t *sv_reconnectlimit; // minimum seconds between connect messages +cvar_t *sv_showloss; // report when usercmds are lost +cvar_t *sv_padPackets; // add nop bytes to messages +cvar_t *sv_killserver; // menu system can set to 1 to shut server down +cvar_t *sv_mapname; +cvar_t *sv_mapChecksum; +cvar_t *sv_serverid; +cvar_t *sv_minRate; +cvar_t *sv_maxRate; +cvar_t *sv_dlRate; +cvar_t *sv_minPing; +cvar_t *sv_maxPing; +cvar_t *sv_pure; +cvar_t *sv_lanForceRate; // dedicated 1 (LAN) server forces local client rates to 99999 (bug #491) +cvar_t *sv_banFile; + +cvar_t *sv_rsaAuth; + +cvar_t *sv_schachtmeisterPort; + +// server attack protection +cvar_t *sv_protect; // 0 - unprotected + // 1 - ioquake3 method (default) + // 2 - OpenWolf method + // 4 - prints attack info to console (when ioquake3 or OPenWolf method is set) +cvar_t *sv_protectLog; // name of log file + +/* +============================================================================= + +EVENT MESSAGES + +============================================================================= +*/ + +/* +=============== +SV_ExpandNewlines + +Converts newlines to "\n" so a line prints nicer +=============== +*/ +static char *SV_ExpandNewlines( char *in ) { + static char string[1024]; + int l; + + l = 0; + while ( *in && l < sizeof(string) - 3 ) { + if ( *in == '\n' ) { + string[l++] = '\\'; + string[l++] = 'n'; + } else { + string[l++] = *in; + } + in++; + } + string[l] = 0; + + return string; +} + +/* +====================== +SV_ReplacePendingServerCommands + +FIXME: This is ugly +====================== +*/ +#if 0 // unused +static bool SV_ReplacePendingServerCommands( client_t *client, const char *cmd ) +{ + int i, index, csnum1, csnum2; + + for ( i = client->reliableSent+1; i <= client->reliableSequence; i++ ) { + index = i & ( MAX_RELIABLE_COMMANDS - 1 ); + // + if ( !Q_strncmp(cmd, client->reliableCommands[ index ], strlen("cs")) ) + { + sscanf(cmd, "cs %i", &csnum1); + sscanf(client->reliableCommands[ index ], "cs %i", &csnum2); + if ( csnum1 == csnum2 ) + { + Q_strncpyz( client->reliableCommands[ index ], cmd, sizeof( client->reliableCommands[ index ] ) ); + return true; + } + } + } + return false; +} +#endif + +/* +====================== +SV_AddServerCommand + +The given command will be transmitted to the client, and is guaranteed to +not have future snapshot_t executed before it is executed +====================== +*/ +void SV_AddServerCommand( client_t *client, const char *cmd ) { + int index, i; + + // this is very ugly but it's also a waste to for instance send multiple config string updates + // for the same config string index in one snapshot +// if ( SV_ReplacePendingServerCommands( client, cmd ) ) { +// return; +// } + + // do not send commands until the gamestate has been sent + if( client->state < CS_PRIMED ) + return; + + client->reliableSequence++; + // if we would be losing an old command that hasn't been acknowledged, + // we must drop the connection + // we check == instead of >= so a broadcast print added by SV_DropClient() + // doesn't cause a recursive drop client + if ( client->reliableSequence - client->reliableAcknowledge == MAX_RELIABLE_COMMANDS + 1 ) { + Com_Printf( "===== pending server commands =====\n" ); + for ( i = client->reliableAcknowledge + 1 ; i <= client->reliableSequence ; i++ ) { + Com_Printf( "cmd %5d: %s\n", i, client->reliableCommands[ i & (MAX_RELIABLE_COMMANDS-1) ] ); + } + Com_Printf( "cmd %5d: %s\n", i, cmd ); + SV_DropClient( client, "Server command overflow" ); + return; + } + index = client->reliableSequence & ( MAX_RELIABLE_COMMANDS - 1 ); + Q_strncpyz( client->reliableCommands[ index ], cmd, sizeof( client->reliableCommands[ index ] ) ); +} + +/* +================= +SV_SendServerCommand + +Sends a reliable command string to be interpreted by +the client game module: "cp", "print", "chat", etc +A NULL client will broadcast to all clients +================= +*/ +void QDECL SV_SendServerCommand(client_t *cl, const char *fmt, ...) { + va_list argptr; + byte message[MAX_MSGLEN]; + client_t *client; + int j; + + va_start(argptr, fmt); + Q_vsnprintf((char*)message, sizeof(message), fmt,argptr); + va_end(argptr); + + // Fix to http://aluigi.altervista.org/adv/q3msgboom-adv.txt + // The actual cause of the bug is probably further downstream + // and should maybe be addressed later, but this certainly + // fixes the problem for now. + // Summary: The bug is that messages longer than 1022 are not + // allowed downstream and there is a buffer overflow issue + // affecting network traffic etc. Therefore, one way to stop + // this from happening is stopping the packet here. Ideally, + // we should increase the size of the downstream message. + if ( strlen ((char *)message) > 1022 ) { + SV_WriteAttackLog( va( "SV_SendServerCommand( %ld, %.20s... ) length %ld > 1022, " + "dropping to avoid server buffer overflow.\n", + cl - svs.clients, message, strlen( (char *)message ) ) ); + SV_WriteAttackLog( va( "Full message: [%s]\n", message ) ); + return; + } + + if ( cl != NULL ) { + SV_AddServerCommand( cl, (char *)message ); + return; + } + + // hack to echo broadcast prints to console + if ( com_dedicated->integer && !strncmp( (char *)message, "print", 5) ) { + Com_Printf ("broadcast: %s\n", SV_ExpandNewlines((char *)message) ); + } + + // send the data to all relevent clients + for (j = 0, client = svs.clients; j < sv_maxclients->integer ; j++, client++) { + SV_AddServerCommand( client, (char *)message ); + } +} + + +/* +============================================================================== + +MASTER SERVER FUNCTIONS + +============================================================================== +*/ + +/* +================ +SV_MasterHeartbeat + +Send a message to the masters every few minutes to +let it know we are alive, and log information. +We will also have a heartbeat sent when a server +changes from empty to non-empty, and full to non-full, +but not on every player enter or exit. +================ +*/ +#define HEARTBEAT_MSEC 300*1000 +void SV_MasterHeartbeat(const char *message) +{ + static netadr_t adrs[3][MAX_MASTER_SERVERS][2]; // [2] for v4 and v6 address for the same address string. + int a; + int i; + int res; + int netenabled; + int netAlternateProtocols; + + netenabled = Cvar_VariableIntegerValue("net_enabled"); + netAlternateProtocols = Cvar_VariableIntegerValue("net_alternateProtocols"); + + // "dedicated 1" is for lan play, "dedicated 2" is for inet public play + if (!com_dedicated || com_dedicated->integer != 2 || !(netenabled & (NET_ENABLEV4 | NET_ENABLEV6))) + return; // only dedicated servers send heartbeats + + // if not time yet, don't send anything + if ( svs.time < svs.nextHeartbeatTime ) + return; + + svs.nextHeartbeatTime = svs.time + HEARTBEAT_MSEC; + + for (a = 0; a < 3; ++a) + { + // indent + if(a == 0 && (netAlternateProtocols & NET_DISABLEPRIMPROTO)) + continue; + if(a == 1 && !(netAlternateProtocols & NET_ENABLEALT1PROTO)) + continue; + if(a == 2 && !(netAlternateProtocols & NET_ENABLEALT2PROTO)) + continue; + + // send to group masters + for (i = 0; i < MAX_MASTER_SERVERS; i++) + { + if(!sv_masters[a][i]->string[0]) + continue; + + // see if we haven't already resolved the name + // resolving usually causes hitches on win95, so only + // do it when needed + if(sv_masters[a][i]->modified || (adrs[a][i][0].type == NA_BAD && adrs[a][i][1].type == NA_BAD)) + { + sv_masters[a][i]->modified = false; + + if(netenabled & NET_ENABLEV4) + { + Com_Printf("Resolving %s (IPv4)\n", sv_masters[a][i]->string); + res = NET_StringToAdr(sv_masters[a][i]->string, &adrs[a][i][0], NA_IP); + adrs[a][i][0].alternateProtocol = a; + + if(res == 2) + { + // if no port was specified, use the default master port + adrs[a][i][0].port = BigShort(a == 2 ? ALT2PORT_MASTER : a == 1 ? ALT1PORT_MASTER : PORT_MASTER); + } + + if(res) + Com_Printf( "%s resolved to %s\n", sv_masters[a][i]->string, NET_AdrToStringwPort(adrs[a][i][0])); + else + Com_Printf( "%s has no IPv4 address.\n", sv_masters[a][i]->string); + } + + if(netenabled & NET_ENABLEV6) + { + Com_Printf("Resolving %s (IPv6)\n", sv_masters[a][i]->string); + res = NET_StringToAdr(sv_masters[a][i]->string, &adrs[a][i][1], NA_IP6); + adrs[a][i][1].alternateProtocol = a; + + if(res == 2) + { + // if no port was specified, use the default master port + adrs[a][i][1].port = BigShort(a == 2 ? ALT2PORT_MASTER : a == 1 ? ALT1PORT_MASTER : PORT_MASTER); + } + + if(res) + Com_Printf( "%s resolved to %s\n", sv_masters[a][i]->string, NET_AdrToStringwPort(adrs[a][i][1])); + else + Com_Printf( "%s has no IPv6 address.\n", sv_masters[a][i]->string); + } + + if(adrs[a][i][0].type == NA_BAD && adrs[a][i][1].type == NA_BAD) + { + Com_Printf("Couldn't resolve address: %s\n", sv_masters[a][i]->string); + Cvar_Set(sv_masters[a][i]->name, ""); + sv_masters[a][i]->modified = false; + continue; + } + } + + + Com_Printf ("Sending%s heartbeat to %s\n", (a == 2 ? " alternate-2" : a == 1 ? " alternate-1" : ""), sv_masters[a][i]->string ); + + // this command should be changed if the server info / status format + // ever incompatably changes + + if(adrs[a][i][0].type != NA_BAD) + NET_OutOfBandPrint( NS_SERVER, adrs[a][i][0], "heartbeat %s\n", message); + if(adrs[a][i][1].type != NA_BAD) + NET_OutOfBandPrint( NS_SERVER, adrs[a][i][1], "heartbeat %s\n", message); + } + // outdent + } +} + +/* +================= +SV_MasterShutdown + +Informs all masters that this server is going down +================= +*/ +void SV_MasterShutdown( void ) { + // send a heartbeat right now + svs.nextHeartbeatTime = -9999; + SV_MasterHeartbeat(HEARTBEAT_FOR_MASTER); + + // send it again to minimize chance of drops + svs.nextHeartbeatTime = -9999; + SV_MasterHeartbeat(HEARTBEAT_FOR_MASTER); + + // when the master tries to poll the server, it won't respond, so + // it will be removed from the list +} + +/* +============================================================================== + +CONNECTIONLESS COMMANDS + +============================================================================== +*/ + +// This is deliberately quite large to make it more of an effort to DoS +#define MAX_BUCKETS 16384 +#define MAX_HASHES 1024 + +static leakyBucket_t buckets[ MAX_BUCKETS ]; +static leakyBucket_t *bucketHashes[ MAX_HASHES ]; +leakyBucket_t outboundLeakyBucket; + +/* +================ +SVC_HashForAddress +================ +*/ +static long SVC_HashForAddress( netadr_t address ) { + byte *ip = NULL; + size_t size = 0; + int i; + long hash = 0; + + switch ( address.type ) { + case NA_IP: ip = address.ip; size = 4; break; + case NA_IP6: ip = address.ip6; size = 16; break; + default: break; + } + + for ( i = 0; i < size; i++ ) { + hash += (long)( ip[ i ] ) * ( i + 119 ); + } + + hash = ( hash ^ ( hash >> 10 ) ^ ( hash >> 20 ) ); + hash &= ( MAX_HASHES - 1 ); + + return hash; +} + +/* +================ +SVC_BucketForAddress + +Find or allocate a bucket for an address +================ +*/ +static leakyBucket_t *SVC_BucketForAddress( netadr_t address, int burst, int period ) { + leakyBucket_t *bucket = NULL; + long hash = SVC_HashForAddress( address ); + int now = Sys_Milliseconds(); + + for ( bucket = bucketHashes[ hash ]; bucket; bucket = bucket->next ) + { + switch ( bucket->type ) + { + case NA_IP: + if ( ::memcmp( bucket->ipv._4, address.ip, 4 ) == 0 ) + return bucket; + break; + + case NA_IP6: + if ( ::memcmp( bucket->ipv._6, address.ip6, 16 ) == 0 ) + return bucket; + break; + + default: + break; + } + } + + for ( int i = 0; i < MAX_BUCKETS; i++ ) + { + int interval; + + bucket = &buckets[ i ]; + interval = now - bucket->lastTime; + + // Reclaim expired buckets + if ( bucket->lastTime > 0 && ( interval > ( burst * period ) || + interval < 0 ) ) { + if ( bucket->prev != NULL ) { + bucket->prev->next = bucket->next; + } else { + bucketHashes[ bucket->hash ] = bucket->next; + } + + if ( bucket->next != NULL ) { + bucket->next->prev = bucket->prev; + } + + ::memset( bucket, 0, sizeof( leakyBucket_t ) ); + } + + if ( bucket->type == NA_BAD ) { + bucket->type = address.type; + switch ( address.type ) { + case NA_IP: ::memcpy( bucket->ipv._4, address.ip, 4 ); break; + case NA_IP6: ::memcpy( bucket->ipv._6, address.ip6, 16 ); break; + default: break; + } + + bucket->lastTime = now; + bucket->burst = 0; + bucket->hash = hash; + + // Add to the head of the relevant hash chain + bucket->next = bucketHashes[ hash ]; + if ( bucketHashes[ hash ] != NULL ) { + bucketHashes[ hash ]->prev = bucket; + } + + bucket->prev = NULL; + bucketHashes[ hash ] = bucket; + + return bucket; + } + } + + // Couldn't allocate a bucket for this address + // Write the info to the attack log since this is relevant information as the system is malfunctioning + SV_WriteAttackLogD(va("SVC_BucketForAddress: Could not allocate a bucket for client from %s\n", NET_AdrToString(address))); + + return NULL; +} + +/* +================ +SVC_RateLimit + * + * @param[in,out] bucket + * @param[in] burst + * @param[in] period + * @return + * + * @note Don't call if sv_protect 1 (SVP_IOQ3) flag is not set! +================ +*/ +bool SVC_RateLimit( leakyBucket_t *bucket, int burst, int period ) +{ + if ( bucket != NULL ) + { + int now = Sys_Milliseconds(); + int interval = now - bucket->lastTime; + int expired = interval / period; + int expiredRemainder = interval % period; + + if ( expired > bucket->burst || interval < 0 ) + { + bucket->burst = 0; + bucket->lastTime = now; + } + else + { + bucket->burst -= expired; + bucket->lastTime = now - expiredRemainder; + } + + if ( bucket->burst < burst ) + { + bucket->burst++; + return false; + } + else + { + SV_WriteAttackLogD(va("SVC_RateLimit: burst limit exceeded for bucket: %i limit: %i\n", bucket->burst, burst)); + } + } + + return true; +} + +/* +================ +SVC_RateLimitAddress + +Rate limit for a particular address +================ +*/ +bool SVC_RateLimitAddress( netadr_t from, int burst, int period ) +{ + leakyBucket_t *bucket = SVC_BucketForAddress( from, burst, period ); + return SVC_RateLimit( bucket, burst, period ); +} + +/* +================ +SVC_Status + +Responds with all the info that qplug or qspy can see about the server +and all connected players. Used for getting detailed information after +the simple info query. +================ +*/ +static void SVC_Status( netadr_t from ) { + char player[1024]; + char status[MAX_MSGLEN]; + int i; + client_t *cl; + playerState_t *ps; + int statusLength; + int playerLength; + char infostring[MAX_INFO_STRING]; + + if (sv_protect->integer & SVP_IOQ3) { + // Prevent using getstatus as an amplifier + if (SVC_RateLimitAddress(from, 10, 1000)) { + SV_WriteAttackLog(va("SVC_Status: rate limit from %s exceeded, dropping request\n", + NET_AdrToString(from))); + return; + } + + // Allow getstatus to be DoSed relatively easily, but prevent + // excess outbound bandwidth usage when being flooded inbound + if (SVC_RateLimit(&outboundLeakyBucket, 10, 100)) { + SV_WriteAttackLog("SVC_Status: rate limit exceeded, dropping request\n"); + return; + } + } + + // A maximum challenge length of 128 should be more than plenty. + if (strlen(Cmd_Argv(1)) > 128) { + SV_WriteAttackLog(va("SVC_Status: challenge length exceeded from %s, dropping request\n", NET_AdrToString(from))); + return; + } + + strcpy( infostring, Cvar_InfoString( CVAR_SERVERINFO ) ); + + // echo back the parameter to status. so master servers can use it as a challenge + // to prevent timed spoofed reply packets that add ghost servers + Info_SetValueForKey( infostring, "challenge", Cmd_Argv(1) ); + + if ( from.alternateProtocol != 0 ) + Info_SetValueForKey( infostring, "protocol", from.alternateProtocol == 2 ? "69" : "70" ); + + status[0] = 0; + statusLength = 0; + + for (i=0 ; i < sv_maxclients->integer ; i++) { + cl = &svs.clients[i]; + if ( cl->state >= CS_CONNECTED ) { + ps = SV_GameClientNum( i ); + Com_sprintf (player, sizeof(player), "%i %i \"%s\"\n", + ps->persistant[PERS_SCORE], cl->ping, cl->name); + playerLength = strlen(player); + if (statusLength + playerLength >= sizeof(status) ) { + break; // can't hold any more + } + strcpy (status + statusLength, player); + statusLength += playerLength; + } + } + + NET_OutOfBandPrint( NS_SERVER, from, "statusResponse\n%s\n%s", infostring, status ); +} + +/* +================ +SVC_Info + +Responds with a short info message that should be enough to determine +if a user is interested in a server to do a full status +================ +*/ +void SVC_Info( netadr_t from ) { + int i, count; + const char *gamedir; + char infostring[MAX_INFO_STRING]; + + if (sv_protect->integer & SVP_IOQ3) { + // Prevent using getinfo as an amplifier + if (SVC_RateLimitAddress(from, 10, 1000)) { + SV_WriteAttackLog(va("SVC_Info: rate limit from %s exceeded, dropping request\n", + NET_AdrToString(from))); + return; + } + + // Allow getinfo to be DoSed relatively easily, but prevent + // excess outbound bandwidth usage when being flooded inbound + if (SVC_RateLimit(&outboundLeakyBucket, 10, 100)) { + SV_WriteAttackLog("SVC_Info: rate limit exceeded, dropping request\n"); + return; + } + } + + // Check whether Cmd_Argv(1) has a sane length. This was not done in the original Quake3 version which led + // to the Infostring bug discovered by Luigi Auriemma. See http://aluigi.altervista.org/ for the advisory. + // A maximum challenge length of 128 should be more than plenty. + if (strlen(Cmd_Argv(1)) > 128) { + SV_WriteAttackLog(va("SVC_Info: challenge length from %s exceeded, dropping request\n", NET_AdrToString(from))); + return; + } + + // don't count privateclients + count = 0; + for ( i = sv_privateClients->integer ; i < sv_maxclients->integer ; i++ ) { + if ( svs.clients[i].state >= CS_CONNECTED ) { + count++; + } + } + + infostring[0] = 0; + + // echo back the parameter to status. so servers can use it as a challenge + // to prevent timed spoofed reply packets that add ghost servers + Info_SetValueForKey( infostring, "challenge", Cmd_Argv(1) ); + + Info_SetValueForKey( infostring, "protocol", va("%i", from.alternateProtocol == 2 ? 69 : from.alternateProtocol == 1 ? 70 : PROTOCOL_VERSION) ); + Info_SetValueForKey( infostring, "gamename", com_gamename->string ); + Info_SetValueForKey( infostring, "hostname", sv_hostname->string ); + Info_SetValueForKey( infostring, "mapname", sv_mapname->string ); + Info_SetValueForKey( infostring, "clients", va("%i", count) ); + Info_SetValueForKey( infostring, "sv_maxclients", + va("%i", sv_maxclients->integer - sv_privateClients->integer ) ); + Info_SetValueForKey( infostring, "pure", va("%i", sv_pure->integer ) ); + +#ifdef USE_VOIP + if (sv_voipProtocol->string && *sv_voipProtocol->string) { + Info_SetValueForKey( infostring, "voip", sv_voipProtocol->string ); + } +#endif + + if( sv_minPing->integer ) { + Info_SetValueForKey( infostring, "minPing", va("%i", sv_minPing->integer) ); + } + if( sv_maxPing->integer ) { + Info_SetValueForKey( infostring, "maxPing", va("%i", sv_maxPing->integer) ); + } + gamedir = Cvar_VariableString( "fs_game" ); + if( *gamedir ) { + Info_SetValueForKey( infostring, "game", gamedir ); + } + + NET_OutOfBandPrint( NS_SERVER, from, "infoResponse\n%s", infostring ); +} + +/* +================ +SVC_FlushRedirect + +================ +*/ +static void SV_FlushRedirect( char *outputbuf ) { + NET_OutOfBandPrint( NS_SERVER, svs.redirectAddress, "print\n%s", outputbuf ); +} + +/** + * @brief DRDoS stands for "Distributed Reflected Denial of Service". + * See here: http://www.lemuria.org/security/application-drdos.html + * + * If the address isn't NA_IP, it's automatically denied. + * + * @return false if we're good. + * otherwise true means we need to block. + * + * @note Don't call this if sv_protect 2 flag is not set! + */ +bool SV_CheckDRDoS(netadr_t from) { + int i; + int globalCount; + int specificCount; + int timeNow; + receipt_t *receipt; + netadr_t exactFrom; + int oldest; + int oldestTime; + static int lastGlobalLogTime = 0; + static int lastSpecificLogTime = 0; + + // Usually the network is smart enough to not allow incoming UDP packets + // with a source address being a spoofed LAN address. Even if that's not + // the case, sending packets to other hosts in the LAN is not a big deal. + // NA_LOOPBACK qualifies as a LAN address. + if (Sys_IsLANAddress(from)) { + return false; + } + + timeNow = svs.time; + exactFrom = from; + + // Time has wrapped + if (lastGlobalLogTime > timeNow || lastSpecificLogTime > timeNow) { + lastGlobalLogTime = 0; + lastSpecificLogTime = 0; + + // just setting time to 1 (cannot be 0 as then globalCount would not be counted) + for (i = 0; i < MAX_INFO_RECEIPTS; i++) { + if (svs.infoReceipts[i].time) { + svs.infoReceipts[i].time = 1; // hack it so we count globalCount correctly + } + } + } + + if (from.type == NA_IP) { + from.ip[3] = 0; // xx.xx.xx.0 + } else { + from.ip6[15] = 0; + } + + // Count receipts in last 2 seconds. + globalCount = 0; + specificCount = 0; + receipt = &svs.infoReceipts[0]; + oldest = 0; + oldestTime = 0x7fffffff; + for (i = 0; i < MAX_INFO_RECEIPTS; i++, receipt++) { + if (receipt->time + 2000 > timeNow) { + if (receipt->time) { + // When the server starts, all receipt times are at zero. Furthermore, + // svs.time is close to zero. We check that the receipt time is already + // set so that during the first two seconds after server starts, queries + // from the master servers don't get ignored. As a consequence a potentially + // unlimited number of getinfo+getstatus responses may be sent during the + // first frame of a server's life. + globalCount++; + } + if (NET_CompareBaseAdr(from, receipt->adr)) { + specificCount++; + } + } + if (receipt->time < oldestTime) { + oldestTime = receipt->time; + oldest = i; + } + } + + if (globalCount == MAX_INFO_RECEIPTS) { // All receipts happened in last 2 seconds. + if (lastGlobalLogTime + 1000 <= timeNow) { // Limit one log every second. + SV_WriteAttackLog("Detected flood of getinfo/getstatus connectionless packets\n"); + lastGlobalLogTime = timeNow; + } + + return true; + } + if (specificCount >= 3) { // Already sent 3 to this IP in last 2 seconds. + if (lastSpecificLogTime + 1000 <= timeNow) { // Limit one log every second. + SV_WriteAttackLog(va("Possible DRDoS attack to address %s, ignoring getinfo/getstatus connectionless packet\n", + NET_AdrToString(exactFrom))); + lastSpecificLogTime = timeNow; + } + + return true; + } + + receipt = &svs.infoReceipts[oldest]; + receipt->adr = from; + receipt->time = timeNow; + return false; +} + +/* +=============== +SVC_RemoteCommand + +An rcon packet arrived from the network. +Shift down the remaining args +Redirect all printfs +=============== +*/ +static void SVC_RemoteCommand( netadr_t from, msg_t *msg ) { + bool valid; + char remaining[1024]; + // TTimo - scaled down to accumulate, but not overflow anything network wise, print wise etc. + // (OOB messages are the bottleneck here) +#define SV_OUTPUTBUF_LENGTH (1024 - 16) + char sv_outputbuf[SV_OUTPUTBUF_LENGTH]; + char *cmd_aux; + + // Prevent using rcon as an amplifier and make dictionary attacks impractical + if ((sv_protect->integer & SVP_IOQ3) && SVC_RateLimitAddress(from, 10, 1000)) { + SV_WriteAttackLog(va("Bad rcon - rate limit from %s exceeded, dropping request\n", + NET_AdrToString(from))); + return; + } + + if ( !strlen( sv_rconPassword->string ) || + strcmp (Cmd_Argv(1), sv_rconPassword->string) ) { + static leakyBucket_t bucket; + + // Make DoS via rcon impractical + if ((sv_protect->integer & SVP_IOQ3) && SVC_RateLimit(&bucket, 10, 1000)) { + SV_WriteAttackLog("Bad rcon - rate limit exceeded, dropping request\n"); + return; + } + + valid = false; + Com_Printf ("Bad rcon from %s: %s\n", NET_AdrToString (from), Cmd_ArgsFrom(2) ); + } else { + valid = true; + Com_Printf ("Rcon from %s: %s\n", NET_AdrToString (from), Cmd_ArgsFrom(2) ); + SV_WriteAttackLog(va("Rcon from %s: %s\n", NET_AdrToString(from), Cmd_Argv(2))); + } + + // start redirecting all print outputs to the packet + svs.redirectAddress = from; + Com_BeginRedirect (sv_outputbuf, SV_OUTPUTBUF_LENGTH, SV_FlushRedirect); + + if ( !strlen( sv_rconPassword->string ) ) { + Com_Printf ("No rconpassword set on the server.\n"); + } else if ( !valid ) { + Com_Printf ("Bad rconpassword.\n"); + SV_WriteAttackLog(va("Bad rconpassword from %s\n", NET_AdrToString(from))); + } else { + remaining[0] = 0; + + // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=543 + // get the command directly, "rcon <pass> <command>" to avoid quoting issues + // extract the command by walking + // since the cmd formatting can fuckup (amount of spaces), using a dumb step by step parsing + cmd_aux = Cmd_Cmd(); + cmd_aux+=4; + while(cmd_aux[0]==' ') + cmd_aux++; + while(cmd_aux[0] && cmd_aux[0]!=' ') // password + cmd_aux++; + while(cmd_aux[0]==' ') + cmd_aux++; + + Q_strcat( remaining, sizeof(remaining), cmd_aux); + + Cmd_ExecuteString (remaining); + + } + + Com_EndRedirect (); +} + +static void SVC_SchachtmeisterResponse( netadr_t from ) { + + int tmp[ 4 ]; + + if ( !( from.type == NA_IP && from.ip[0] == 127 ) ) { + return; + } + + if ( Cmd_Argc() >= 2 && sscanf( Cmd_Argv( 1 ), "%i.%i.%i.%i", &tmp[ 0 ], &tmp[ 1 ], &tmp[ 2 ], &tmp[ 3 ] ) == 4 ) { // compatibility with out-of-date crapware conceived in the future + char cmdl[ MAX_STRING_CHARS ]; + Com_sprintf( cmdl, sizeof( cmdl ), "smr ipa %s", Cmd_ArgsFrom( 1 ) ); + Cmd_TokenizeString( cmdl ); + } else { + strcpy( Cmd_Argv( 0 ), "smr" ); + } + + SV_GameCommand(); +} + +/* +================= +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. +================= +*/ +static void SV_ConnectionlessPacket( netadr_t from, msg_t *msg ) { + char *s; + const char *c; + + MSG_BeginReadingOOB( msg ); + MSG_ReadLong( msg ); // skip the -1 marker + + if (!Q_strncmp("connect", (char *) &msg->data[4], 7)) { + Huff_Decompress(msg, 12); + } + + s = MSG_ReadStringLine( msg ); + Cmd_TokenizeString( s ); + + c = Cmd_Argv(0); + Com_DPrintf ("SV packet %s : %s\n", NET_AdrToString(from), c); + + if (!Q_stricmp(c, "getstatus")) { + if ((sv_protect->integer & SVP_OWOLF) && SV_CheckDRDoS(from)) { + return; + } + + SVC_Status( from ); + } else if (!Q_stricmp(c, "getinfo")) { + if ((sv_protect->integer & SVP_OWOLF) && SV_CheckDRDoS(from)) { + return; + } + + SVC_Info( from ); + } else if (!Q_stricmp(c, "getchallenge")) { + if ((sv_protect->integer & SVP_OWOLF) && SV_CheckDRDoS(from)) { + return; + } + + SV_GetChallenge(from); + } else if (!Q_stricmp(c, "connect")) { + SV_DirectConnect( from ); + } else if (!Q_stricmp(c, "rcon")) { + SVC_RemoteCommand( from, msg ); + } else if (!Q_stricmp(c, "disconnect")) { + // if a client starts up a local server, we may see some spurious + // server disconnect messages when their new server sees our final + // sequenced messages to the old client + } else if (!Q_stricmp(c, "sm2reply")) { + SVC_SchachtmeisterResponse( from ); + Com_Printf( "^2response [^7%s^2]\n", s ); + } else { + SV_WriteAttackLog(va("bad connectionless packet from %s:\n%s\n" // changed from Com_DPrintf to print in attack log + , NET_AdrToString(from), s)); // this was never reported to admins before so they might be confused + } // note: if protect log isn't set we do Com_Printf +} + +//============================================================================ + +/* +================= +SV_PacketEvent +================= +*/ +void SV_PacketEvent( netadr_t from, msg_t *msg ) { + int i; + client_t *cl; + int qport; + + // check for connectionless packet (0xffffffff) first + if ( msg->cursize >= 4 && *(int *)msg->data == -1) { + SV_ConnectionlessPacket( from, msg ); + return; + } + + // read the qport out of the message so we can fix up + // stupid address translating routers + MSG_BeginReadingOOB( msg ); + MSG_ReadLong( msg ); // sequence number + qport = MSG_ReadShort( msg ) & 0xffff; + + // find which client the message is from + 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 ) ) { + continue; + } + // it is possible to have multiple clients from a single IP + // address, so they are differentiated by the qport variable + if (cl->netchan.qport != qport) { + continue; + } + + // the IP port can't be used to differentiate them, because + // some address translating routers periodically change UDP + // port assignments + if (cl->netchan.remoteAddress.port != from.port) { + Com_Printf( "SV_PacketEvent: fixing up a translated port\n" ); + cl->netchan.remoteAddress.port = from.port; + } + + // make sure it is a valid, in sequence packet + if (SV_Netchan_Process(cl, msg)) { + // zombie clients still need to do the Netchan_Process + // to make sure they don't need to retransmit the final + // reliable message, but they don't do any other processing + if (cl->state != CS_ZOMBIE) { + cl->lastPacketTime = svs.time; // don't timeout + SV_ExecuteClientMessage( cl, msg ); + } + } + return; + } +} + + +/* +=================== +SV_CalcPings + +Updates the cl->ping variables +=================== +*/ +static void SV_CalcPings( void ) { + int i, j; + client_t *cl; + int total, count; + int delta; + playerState_t *ps; + + for (i=0 ; i < sv_maxclients->integer ; i++) { + cl = &svs.clients[i]; + if ( cl->state != CS_ACTIVE ) { + cl->ping = 999; + continue; + } + if ( !cl->gentity ) { + cl->ping = 999; + continue; + } + + total = 0; + count = 0; + for ( j = 0 ; j < PACKET_BACKUP ; j++ ) { + if ( cl->frames[j].messageAcked <= 0 ) { + continue; + } + delta = cl->frames[j].messageAcked - cl->frames[j].messageSent; + count++; + total += delta; + } + if (!count) { + cl->ping = 999; + } else { + cl->ping = total/count; + if ( cl->ping > 999 ) { + cl->ping = 999; + } + } + + // let the game dll know about the ping + ps = SV_GameClientNum( i ); + ps->ping = cl->ping; + } +} + +/* +================== +SV_CheckTimeouts + +If a packet has not been received from a client for timeout->integer +seconds, drop the conneciton. Server time is 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 +================== +*/ +static void SV_CheckTimeouts( void ) { + int i; + client_t *cl; + int droppoint; + int zombiepoint; + + droppoint = svs.time - 1000 * sv_timeout->integer; + zombiepoint = svs.time - 1000 * sv_zombietime->integer; + + for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) { + // message times may be wrong across a changelevel + if (cl->lastPacketTime > svs.time) { + cl->lastPacketTime = svs.time; + } + + if (cl->state == CS_ZOMBIE + && cl->lastPacketTime < zombiepoint) { + // using the client id cause the cl->name is empty at this point + Com_DPrintf( "Going from CS_ZOMBIE to CS_FREE for client %d\n", i ); + cl->state = CS_FREE; // can now be reused + continue; + } + if ( cl->state >= CS_CONNECTED && cl->lastPacketTime < droppoint) { + // wait several frames so a debugger session doesn't + // cause a timeout + if ( ++cl->timeoutCount > 5 ) { + SV_DropClient (cl, "timed out"); + cl->state = CS_FREE; // don't bother with zombie state + } + } else { + cl->timeoutCount = 0; + } + } +} + + +/* +================== +SV_CheckPaused +================== +*/ +static bool SV_CheckPaused( void ) { + int count; + client_t *cl; + int i; + + if ( !cl_paused->integer ) { + return false; + } + + // only pause if there is just a single client connected + count = 0; + for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) { + if ( cl->state >= CS_CONNECTED ) { + count++; + } + } + + if ( count > 1 ) { + // don't pause + if (sv_paused->integer) + Cvar_Set("sv_paused", "0"); + return false; + } + + if (!sv_paused->integer) + Cvar_Set("sv_paused", "1"); + return true; +} + +/* +================== +SV_FrameMsec +Return time in millseconds until processing of the next server frame. +================== +*/ +int SV_FrameMsec() +{ + if(sv_fps) + { + int frameMsec; + + frameMsec = 1000.0f / sv_fps->value; + + if(frameMsec < sv.timeResidual) + return 0; + else + return frameMsec - sv.timeResidual; + } + else + return 1; +} + +#define CPU_USAGE_WARNING 70 +#define FRAME_TIME_WARNING 30 + +/* +================== +SV_Frame + +Player movement occurs as a result of packet events, which +happen before SV_Frame is called +================== +*/ +void SV_Frame( int msec ) { + int frameMsec; + int startTime; + int frameStartTime = 0; + static int start, end; + + start = Sys_Milliseconds(); + svs.stats.idle += ( double )(start - end) / 1000; + + // the menu kills the server with this cvar + if ( sv_killserver->integer ) { + SV_Shutdown ("Server was killed"); + Cvar_Set( "sv_killserver", "0" ); + return; + } + + if (!com_sv_running->integer) + { + // Running as a server, but no map loaded +#ifdef DEDICATED + // Block until something interesting happens + Sys_Sleep(-1); +#endif + + return; + } + + // allow pause if only the local client is connected + if ( SV_CheckPaused() ) { + return; + } + + if (com_dedicated->integer) + { + frameStartTime = Sys_Milliseconds(); + } + + // if it isn't time for the next frame, do nothing + if ( sv_fps->integer < 1 ) { + Cvar_Set( "sv_fps", "10" ); + } + + frameMsec = 1000 / sv_fps->integer * com_timescale->value; + // don't let it scale below 1ms + if(frameMsec < 1) + { + Cvar_Set("timescale", va("%f", sv_fps->integer / 1000.0f)); + frameMsec = 1; + } + + sv.timeResidual += msec; + + // if time is about to hit the 32nd bit, kick all clients + // and clear sv.time, rather + // than checking for negative time wraparound everywhere. + // 2giga-milliseconds = 23 days, so it won't be too often + if ( svs.time > 0x70000000 ) { + SV_Shutdown( "Restarting server due to time wrapping" ); + Cbuf_AddText( va( "map \"%s\"\n", Cvar_VariableString( "mapname" ) ) ); + return; + } + // this can happen considerably earlier when lots of clients play and the map doesn't change + if ( svs.nextSnapshotEntities >= 0x7FFFFFFE - svs.numSnapshotEntities ) { + SV_Shutdown( "Restarting server due to numSnapshotEntities wrapping" ); + Cbuf_AddText( va( "map \"%s\"\n", Cvar_VariableString( "mapname" ) ) ); + return; + } + + if( sv.restartTime && sv.time >= sv.restartTime ) { + sv.restartTime = 0; + Cbuf_AddText( "map_restart 0\n" ); + return; + } + + // update infostrings if anything has been changed + if ( cvar_modifiedFlags & CVAR_SERVERINFO ) { + SV_SetConfigstring( CS_SERVERINFO, Cvar_InfoString( CVAR_SERVERINFO ) ); + cvar_modifiedFlags &= ~CVAR_SERVERINFO; + } + if ( cvar_modifiedFlags & CVAR_SYSTEMINFO ) { + SV_SetConfigstring( CS_SYSTEMINFO, Cvar_InfoString_Big( CVAR_SYSTEMINFO ) ); + cvar_modifiedFlags &= ~CVAR_SYSTEMINFO; + } + + if ( com_speeds->integer ) { + startTime = Sys_Milliseconds (); + } else { + startTime = 0; // quite a compiler warning + } + + // update ping based on the all received frames + SV_CalcPings(); + + // run the game simulation in chunks + while ( sv.timeResidual >= frameMsec ) { + sv.timeResidual -= frameMsec; + svs.time += frameMsec; + sv.time += frameMsec; + + // let everything in the world think and move + VM_Call (sv.gvm, GAME_RUN_FRAME, sv.time); + } + + if ( com_speeds->integer ) { + time_game = Sys_Milliseconds () - startTime; + } + + // check timeouts + SV_CheckTimeouts(); + + // send messages back to the clients + SV_SendClientMessages(); + + // send a heartbeat to the master if needed + SV_MasterHeartbeat(HEARTBEAT_FOR_MASTER); + + if (com_dedicated->integer) + { + int frameEndTime = Sys_Milliseconds(); + + svs.totalFrameTime += (frameEndTime - frameStartTime); + + // we may send warnings (similar to watchdog) to the game in case the frametime is unacceptable + //Com_Printf("FRAMETIME frame: %i total %i\n", frameEndTime - frameStartTime, svs.totalFrameTime); + + svs.currentFrameIndex++; + + //if( svs.currentFrameIndex % 50 == 0 ) + // Com_Printf( "currentFrameIndex: %i\n", svs.currentFrameIndex ); + + if (svs.currentFrameIndex == SERVER_PERFORMANCECOUNTER_FRAMES) + { + int averageFrameTime = svs.totalFrameTime / SERVER_PERFORMANCECOUNTER_FRAMES; + + svs.sampleTimes[svs.currentSampleIndex % SERVER_PERFORMANCECOUNTER_SAMPLES] = averageFrameTime; + svs.currentSampleIndex++; + + if (svs.currentSampleIndex > SERVER_PERFORMANCECOUNTER_SAMPLES) + { + int totalTime = 0, i; + + for (i = 0; i < SERVER_PERFORMANCECOUNTER_SAMPLES; i++) + { + totalTime += svs.sampleTimes[i]; + } + + if (!totalTime) + { + totalTime = 1; + } + + averageFrameTime = totalTime / SERVER_PERFORMANCECOUNTER_SAMPLES; + + svs.serverLoad = (int)((averageFrameTime / (float)frameMsec) * 100); + } + + //Com_Printf( "serverload: %i (%i/%i)\n", svs.serverLoad, averageFrameTime, frameMsec ); + + svs.totalFrameTime = 0; + svs.currentFrameIndex = 0; + } + } + else + { + svs.serverLoad = -1; + } + + // collect timing statistics + // - the above 2.60 performance thingy is just inaccurate (30 seconds 'stats') + // to give good warning messages and is only done for dedicated + end = Sys_Milliseconds(); + svs.stats.active += (( double )(end - start)) / 1000; + + if (++svs.stats.count == STATFRAMES) // 5 seconds + { + svs.stats.latched_active = svs.stats.active; + svs.stats.latched_idle = svs.stats.idle; + svs.stats.active = 0; + svs.stats.idle = 0; + svs.stats.count = 0; + + svs.stats.cpu = svs.stats.latched_active + svs.stats.latched_idle; + + if (svs.stats.cpu != 0.f) + { + svs.stats.cpu = 100 * svs.stats.latched_active / svs.stats.cpu; + } + + svs.stats.avg = 1000 * svs.stats.latched_active / STATFRAMES; + + // FIXME: add mail, IRC, player info etc for both warnings + // TODO: inspect/adjust these values and/or add cvars + if (svs.stats.cpu > CPU_USAGE_WARNING) + { + Com_Printf("^3WARNING: Server CPU has reached a critical usage of %i%%\n", (int) svs.stats.cpu); + } + + if (svs.stats.avg > FRAME_TIME_WARNING) + { + Com_Printf("^3WARNING: Average frame time has reached a critical value of %ims\n", (int) svs.stats.avg); + } + } +} + +/* +==================== +SV_RateMsec + +Return the number of msec until another message can be sent to +a client based on its rate settings +==================== +*/ + +#define UDPIP_HEADER_SIZE 28 +#define UDPIP6_HEADER_SIZE 48 + +int SV_RateMsec(client_t *client) +{ + int rate, rateMsec; + int messageSize; + + messageSize = client->netchan.lastSentSize; + rate = client->rate; + + if(sv_maxRate->integer) + { + if(sv_maxRate->integer < 1000) + Cvar_Set( "sv_MaxRate", "1000" ); + if(sv_maxRate->integer < rate) + rate = sv_maxRate->integer; + } + + if(sv_minRate->integer) + { + if(sv_minRate->integer < 1000) + Cvar_Set("sv_minRate", "1000"); + if(sv_minRate->integer > rate) + rate = sv_minRate->integer; + } + + if(client->netchan.remoteAddress.type == NA_IP6) + messageSize += UDPIP6_HEADER_SIZE; + else + messageSize += UDPIP_HEADER_SIZE; + + rate = (int)(rate * com_timescale->value); + if(rate < 1) + rate = 1; + rateMsec = messageSize * 1000 / rate; + rate = Sys_Milliseconds() - client->netchan.lastSentTime; + + if(rate > rateMsec) + return 0; + else + return rateMsec - rate; +} + +/* +==================== +SV_SendQueuedPackets + +Send download messages and queued packets in the time that we're idle, i.e. +not computing a server frame or sending client snapshots. +Return the time in msec until we expect to be called next +==================== +*/ + +int SV_SendQueuedPackets() +{ + int numBlocks; + int dlStart, deltaT, delayT; + static int dlNextRound = 0; + int timeVal = INT_MAX; + + // Send out fragmented packets now that we're idle + delayT = SV_SendQueuedMessages(); + if(delayT >= 0) + timeVal = delayT; + + if(sv_dlRate->integer) + { + // Rate limiting. This is very imprecise for high + // download rates due to millisecond timedelta resolution + dlStart = Sys_Milliseconds(); + deltaT = dlNextRound - dlStart; + + if(deltaT > 0) + { + if(deltaT < timeVal) + timeVal = deltaT + 1; + } + else + { + numBlocks = SV_SendDownloadMessages(); + + if(numBlocks) + { + // There are active downloads + deltaT = Sys_Milliseconds() - dlStart; + + delayT = 1000 * numBlocks * MAX_DOWNLOAD_BLKSIZE; + delayT /= sv_dlRate->integer * 1024; + + if(delayT <= deltaT + 1) + { + // Sending the last round of download messages + // took too long for given rate, don't wait for + // next round, but always enforce a 1ms delay + // between DL message rounds so we don't hog + // all of the bandwidth. This will result in an + // effective maximum rate of 1MB/s per user, but the + // low download window size limits this anyways. + if(timeVal > 2) + timeVal = 2; + + dlNextRound = dlStart + deltaT + 1; + } + else + { + dlNextRound = dlStart + delayT; + delayT -= deltaT; + + if(delayT < timeVal) + timeVal = delayT; + } + } + } + } + else + { + if(SV_SendDownloadMessages()) + timeVal = 0; + } + + return timeVal; +} diff --git a/src/server/sv_net_chan.cpp b/src/server/sv_net_chan.cpp new file mode 100644 index 0000000..f8d9b7e --- /dev/null +++ b/src/server/sv_net_chan.cpp @@ -0,0 +1,259 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +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/> + +=========================================================================== +*/ + +#include "server.h" + +#include "qcommon/q_shared.h" +#include "qcommon/msg.h" +#include "qcommon/net.h" +#include "qcommon/qcommon.h" + +/* +============== +SV_Netchan_Encode + + // first four bytes of the data are always: + long reliableAcknowledge; + +============== +*/ +static void SV_Netchan_Encode( client_t *client, msg_t *msg ) { + long i, index; + byte key, *string; + int srdc, sbit; + bool soob; + + if ( msg->cursize < SV_ENCODE_START ) { + return; + } + + srdc = msg->readcount; + sbit = msg->bit; + soob = msg->oob; + + msg->bit = 0; + msg->readcount = 0; + msg->oob = false; + + /* reliableAcknowledge = */ MSG_ReadLong(msg); + + msg->oob = soob; + msg->bit = sbit; + msg->readcount = srdc; + + string = (byte *)client->lastClientCommandString; + index = 0; + // xor the client challenge with the netchan sequence number + key = client->challenge ^ client->netchan.outgoingSequence; + for (i = SV_ENCODE_START; i < msg->cursize; i++) { + // modify the key with the last received and with this message acknowledged client command + if (!string[index]) + index = 0; + if ( string[index] > 127 || (client->netchan.alternateProtocol == 2 && string[index] == '%')) + { + key ^= '.' << (i & 1); + } + else + { + key ^= string[index] << (i & 1); + } + index++; + // encode the data with this key + *(msg->data + i) = *(msg->data + i) ^ key; + } +} + +/* +============== +SV_Netchan_Decode + + // first 12 bytes of the data are always: + long serverId; + long messageAcknowledge; + long reliableAcknowledge; + +============== +*/ +static void SV_Netchan_Decode( client_t *client, msg_t *msg ) { + int serverId, messageAcknowledge, reliableAcknowledge; + int i, index, srdc, sbit; + bool soob; + byte key, *string; + + srdc = msg->readcount; + sbit = msg->bit; + soob = msg->oob; + + msg->oob = false; + + serverId = MSG_ReadLong(msg); + messageAcknowledge = MSG_ReadLong(msg); + reliableAcknowledge = MSG_ReadLong(msg); + + msg->oob = soob; + msg->bit = sbit; + msg->readcount = srdc; + + string = (byte *)client->reliableCommands[ reliableAcknowledge & (MAX_RELIABLE_COMMANDS-1) ]; + index = 0; + + key = client->challenge ^ serverId ^ messageAcknowledge; + for (i = msg->readcount + SV_DECODE_START; i < msg->cursize; i++) { + // modify the key with the last sent and acknowledged server command + if (!string[index]) + index = 0; + if (string[index] > 127 || (client->netchan.alternateProtocol == 2 && string[index] == '%')) { + key ^= '.' << (i & 1); + } + else { + key ^= string[index] << (i & 1); + } + index++; + // decode the data with this key + *(msg->data + i) = *(msg->data + i) ^ key; + } +} + +/* +================= +SV_Netchan_FreeQueue +================= +*/ +void SV_Netchan_FreeQueue(client_t *client) +{ + netchan_buffer_t *netbuf, *next; + + for(netbuf = client->netchan_start_queue; netbuf; netbuf = next) + { + next = netbuf->next; + Z_Free(netbuf); + } + + client->netchan_start_queue = NULL; + client->netchan_end_queue = &client->netchan_start_queue; +} + +/* +================= +SV_Netchan_TransmitNextInQueue +================= +*/ +void SV_Netchan_TransmitNextInQueue(client_t *client) +{ + netchan_buffer_t *netbuf; + + Com_DPrintf("#462 Netchan_TransmitNextFragment: popping a queued message for transmit\n"); + netbuf = client->netchan_start_queue; + + Netchan_Transmit(&client->netchan, netbuf->msg.cursize, netbuf->msg.data); + + // pop from queue + client->netchan_start_queue = netbuf->next; + if(!client->netchan_start_queue) + { + Com_DPrintf("#462 Netchan_TransmitNextFragment: emptied queue\n"); + client->netchan_end_queue = &client->netchan_start_queue; + } + else + Com_DPrintf("#462 Netchan_TransmitNextFragment: remaining queued message\n"); + + Z_Free(netbuf); +} + +/* +================= +SV_Netchan_TransmitNextFragment +Transmit the next fragment and the next queued packet +Return number of ms until next message can be sent based on throughput given by client rate, +-1 if no packet was sent. +================= +*/ + +int SV_Netchan_TransmitNextFragment(client_t *client) +{ + if(client->netchan.unsentFragments) + { + Netchan_TransmitNextFragment(&client->netchan); + return SV_RateMsec(client); + } + else if(client->netchan_start_queue) + { + SV_Netchan_TransmitNextInQueue(client); + return SV_RateMsec(client); + } + + return -1; +} + + +/* +=============== +SV_Netchan_Transmit +TTimo +https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=462 +if there are some unsent fragments (which may happen if the snapshots +and the gamestate are fragmenting, and collide on send for instance) +then buffer them and make sure they get sent in correct order +================ +*/ + +void SV_Netchan_Transmit( client_t *client, msg_t *msg) +{ + MSG_WriteByte( msg, svc_EOF ); + + if(client->netchan.unsentFragments || client->netchan_start_queue) + { + netchan_buffer_t *netbuf; + Com_DPrintf("#462 SV_Netchan_Transmit: unsent fragments, stacked\n"); + netbuf = (netchan_buffer_t *) Z_Malloc(sizeof(netchan_buffer_t)); + // store the msg, we can't store it encoded, as the encoding depends on stuff we still have to finish sending + MSG_Copy(&netbuf->msg, netbuf->msgBuffer, sizeof( netbuf->msgBuffer ), msg); + netbuf->next = NULL; + // insert it in the queue, the message will be encoded and sent later + *client->netchan_end_queue = netbuf; + client->netchan_end_queue = &(*client->netchan_end_queue)->next; + } + else + { + if (client->netchan.alternateProtocol != 0) + SV_Netchan_Encode( client, msg ); + Netchan_Transmit( &client->netchan, msg->cursize, msg->data ); + } +} + +/* +================= +Netchan_SV_Process +================= +*/ +bool SV_Netchan_Process( client_t *client, msg_t *msg ) +{ + bool ret = Netchan_Process( &client->netchan, msg ); + if (!ret) return false; + + if (client->netchan.alternateProtocol != 0) + SV_Netchan_Decode( client, msg ); + + return true; +} diff --git a/src/server/sv_snapshot.cpp b/src/server/sv_snapshot.cpp new file mode 100644 index 0000000..07dd210 --- /dev/null +++ b/src/server/sv_snapshot.cpp @@ -0,0 +1,749 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +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/> + +=========================================================================== +*/ + +#include "server.h" + +/* +============================================================================= + +Delta encode a client frame onto the network channel + +A normal server packet will look like: + +4 sequence number (high bit set if an oversize fragment) +<optional reliable commands> +1 svc_snapshot +4 last client reliable command +4 serverTime +1 lastframe for delta compression +1 snapFlags +1 areaBytes +<areabytes> +<playerstate> +<packetentities> + +============================================================================= +*/ + +/* +============= +SV_EmitPacketEntities + +Writes a delta update of an entityState_t list to the message. +============= +*/ +static void SV_EmitPacketEntities(int alternateProtocol, clientSnapshot_t *from, clientSnapshot_t *to, msg_t *msg) +{ + entityState_t *oldent, *newent; + int oldindex, newindex; + int oldnum, newnum; + int from_num_entities; + + // generate the delta update + if (!from) + { + from_num_entities = 0; + } + else + { + from_num_entities = from->num_entities; + } + + newent = NULL; + oldent = NULL; + newindex = 0; + oldindex = 0; + while (newindex < to->num_entities || oldindex < from_num_entities) + { + if (newindex >= to->num_entities) + { + newnum = 9999; + } + else + { + newent = &svs.snapshotEntities[(to->first_entity + newindex) % svs.numSnapshotEntities]; + newnum = newent->number; + } + + if (oldindex >= from_num_entities) + { + oldnum = 9999; + } + else + { + oldent = &svs.snapshotEntities[(from->first_entity + oldindex) % svs.numSnapshotEntities]; + oldnum = oldent->number; + } + + if (newnum == oldnum) + { + // delta update from old position + // because the force parm is false, this will not result + // in any bytes being emited if the entity has not changed at all + MSG_WriteDeltaEntity(alternateProtocol, msg, oldent, newent, false); + oldindex++; + newindex++; + continue; + } + + if (newnum < oldnum) + { + // this is a new entity, send it from the baseline + MSG_WriteDeltaEntity(alternateProtocol, msg, &sv.svEntities[newnum].baseline, newent, true); + newindex++; + continue; + } + + if (newnum > oldnum) + { + // the old entity isn't present in the new message + MSG_WriteDeltaEntity(alternateProtocol, msg, oldent, NULL, true); + oldindex++; + continue; + } + } + + MSG_WriteBits(msg, (MAX_GENTITIES - 1), GENTITYNUM_BITS); // end of packetentities +} + +/* +================== +SV_WriteSnapshotToClient +================== +*/ +static void SV_WriteSnapshotToClient(client_t *client, msg_t *msg) +{ + clientSnapshot_t *frame, *oldframe; + int lastframe; + int i; + int snapFlags; + + // this is the snapshot we are creating + frame = &client->frames[client->netchan.outgoingSequence & PACKET_MASK]; + + // try to use a previous frame as the source for delta compressing the snapshot + if (client->deltaMessage <= 0 || client->state != CS_ACTIVE) + { + // client is asking for a retransmit + oldframe = NULL; + lastframe = 0; + } + else if (client->netchan.outgoingSequence - client->deltaMessage >= (PACKET_BACKUP - 3)) + { + // client hasn't gotten a good message through in a long time + Com_DPrintf("%s: Delta request from out of date packet.\n", client->name); + oldframe = NULL; + lastframe = 0; + } + else + { + // we have a valid snapshot to delta from + oldframe = &client->frames[client->deltaMessage & PACKET_MASK]; + lastframe = client->netchan.outgoingSequence - client->deltaMessage; + + // the snapshot's entities may still have rolled off the buffer, though + if (oldframe->first_entity <= svs.nextSnapshotEntities - svs.numSnapshotEntities) + { + Com_DPrintf("%s: Delta request from out of date entities.\n", client->name); + oldframe = NULL; + lastframe = 0; + } + } + + MSG_WriteByte(msg, svc_snapshot); + + // NOTE, MRE: now sent at the start of every message from server to client + // let the client know which reliable clientCommands we have received + // MSG_WriteLong( msg, client->lastClientCommand ); + + // send over the current server time so the client can drift + // its view of time to try to match + if (client->oldServerTime) + { + // The server has not yet got an acknowledgement of the + // new gamestate from this client, so continue to send it + // a time as if the server has not restarted. Note from + // the client's perspective this time is strictly speaking + // incorrect, but since it'll be busy loading a map at + // the time it doesn't really matter. + MSG_WriteLong(msg, sv.time + client->oldServerTime); + } + else + { + MSG_WriteLong(msg, sv.time); + } + + // what we are delta'ing from + MSG_WriteByte(msg, lastframe); + + snapFlags = svs.snapFlagServerBit; + if (client->rateDelayed) + { + snapFlags |= SNAPFLAG_RATE_DELAYED; + } + if (client->state != CS_ACTIVE) + { + snapFlags |= SNAPFLAG_NOT_ACTIVE; + } + + MSG_WriteByte(msg, snapFlags); + + // send over the areabits + MSG_WriteByte(msg, frame->areabytes); + MSG_WriteData(msg, frame->areabits, frame->areabytes); + + // delta encode the playerstate + if (oldframe) + { + MSG_WriteDeltaPlayerstate(client->netchan.alternateProtocol, msg, &oldframe->ps, &frame->ps); + } + else + { + MSG_WriteDeltaPlayerstate(client->netchan.alternateProtocol, msg, NULL, &frame->ps); + } + + // delta encode the entities + SV_EmitPacketEntities(client->netchan.alternateProtocol, oldframe, frame, msg); + + // padding for rate debugging + if (sv_padPackets->integer) + { + for (i = 0; i < sv_padPackets->integer; i++) + { + MSG_WriteByte(msg, svc_nop); + } + } +} + +/* +================== +SV_UpdateServerCommandsToClient + +(re)send all server commands the client hasn't acknowledged yet +================== +*/ +void SV_UpdateServerCommandsToClient(client_t *client, msg_t *msg) +{ + int i; + + // write any unacknowledged serverCommands + for (i = client->reliableAcknowledge + 1; i <= client->reliableSequence; i++) + { + MSG_WriteByte(msg, svc_serverCommand); + MSG_WriteLong(msg, i); + MSG_WriteString(msg, client->reliableCommands[i & (MAX_RELIABLE_COMMANDS - 1)]); + } + client->reliableSent = client->reliableSequence; +} + +/* +============================================================================= + +Build a client snapshot structure + +============================================================================= +*/ + +typedef struct { + int numSnapshotEntities; + int snapshotEntities[MAX_SNAPSHOT_ENTITIES]; +} snapshotEntityNumbers_t; + +/* +======================= +SV_QsortEntityNumbers +======================= +*/ +static int QDECL SV_QsortEntityNumbers(const void *a, const void *b) +{ + int *ea, *eb; + + ea = (int *)a; + eb = (int *)b; + + if (*ea == *eb) + { + Com_Error(ERR_DROP, "SV_QsortEntityStates: duplicated entity"); + } + + if (*ea < *eb) + { + return -1; + } + + return 1; +} + +/* +=============== +SV_AddEntToSnapshot +=============== +*/ +static void SV_AddEntToSnapshot(svEntity_t *svEnt, sharedEntity_t *gEnt, snapshotEntityNumbers_t *eNums) +{ + // if we have already added this entity to this snapshot, don't add again + if (svEnt->snapshotCounter == sv.snapshotCounter) + { + return; + } + svEnt->snapshotCounter = sv.snapshotCounter; + + // if we are full, silently discard entities + if (eNums->numSnapshotEntities == MAX_SNAPSHOT_ENTITIES) + { + return; + } + + eNums->snapshotEntities[eNums->numSnapshotEntities] = gEnt->s.number; + eNums->numSnapshotEntities++; +} + +/* +=============== +SV_AddEntitiesVisibleFromPoint +=============== +*/ +static void SV_AddEntitiesVisibleFromPoint(vec3_t origin, clientSnapshot_t *frame, snapshotEntityNumbers_t *eNums) +{ + int e, i; + sharedEntity_t *ent; + svEntity_t *svEnt; + int l; + int clientarea, clientcluster; + int leafnum; + byte *clientpvs; + byte *bitvector; + + // during an error shutdown message we may need to transmit + // the shutdown message after the server has shutdown, so + // specfically check for it + if (!sv.state) + { + return; + } + + leafnum = CM_PointLeafnum(origin); + clientarea = CM_LeafArea(leafnum); + clientcluster = CM_LeafCluster(leafnum); + + // calculate the visible areas + frame->areabytes = CM_WriteAreaBits(frame->areabits, clientarea); + + clientpvs = CM_ClusterPVS(clientcluster); + + for (e = 0; e < sv.num_entities; e++) + { + ent = SV_GentityNum(e); + + // never send entities that aren't linked in + if (!ent->r.linked) + { + continue; + } + + if (ent->s.number != e) + { + Com_DPrintf("FIXING ENT->S.NUMBER!!!\n"); + ent->s.number = e; + } + + // entities can be flagged to explicitly not be sent to the client + if (ent->r.svFlags & SVF_NOCLIENT) + { + continue; + } + + // entities can be flagged to be sent to only one client + if (ent->r.svFlags & SVF_SINGLECLIENT) + { + if (ent->r.singleClient != frame->ps.clientNum) + { + continue; + } + } + // entities can be flagged to be sent to everyone but one client + if (ent->r.svFlags & SVF_NOTSINGLECLIENT) + { + if (ent->r.singleClient == frame->ps.clientNum) + { + continue; + } + } + // entities can be flagged to be sent to a given mask of clients + if (ent->r.svFlags & SVF_CLIENTMASK) + { + if (frame->ps.clientNum >= 32) + { + if (~ent->r.hack.generic1 & (1 << (frame->ps.clientNum - 32))) continue; + } + else + { + if (~ent->r.singleClient & (1 << frame->ps.clientNum)) continue; + } + } + + svEnt = SV_SvEntityForGentity(ent); + + // don't double add an entity through portals + if (svEnt->snapshotCounter == sv.snapshotCounter) + { + continue; + } + + // broadcast entities are always sent + if (ent->r.svFlags & SVF_BROADCAST) + { + SV_AddEntToSnapshot(svEnt, ent, eNums); + continue; + } + + // ignore if not touching a PV leaf + // check area + if (!CM_AreasConnected(clientarea, svEnt->areanum)) + { + // doors can legally straddle two areas, so + // we may need to check another one + if (!CM_AreasConnected(clientarea, svEnt->areanum2)) + { + continue; // blocked by a door + } + } + + bitvector = clientpvs; + + // check individual leafs + if (!svEnt->numClusters) + { + continue; + } + l = 0; + for (i = 0; i < svEnt->numClusters; i++) + { + l = svEnt->clusternums[i]; + if (bitvector[l >> 3] & (1 << (l & 7))) + { + break; + } + } + + // if we haven't found it to be visible, + // check overflow clusters that coudln't be stored + if (i == svEnt->numClusters) + { + if (svEnt->lastCluster) + { + for (; l <= svEnt->lastCluster; l++) + { + if (bitvector[l >> 3] & (1 << (l & 7))) + { + break; + } + } + if (l == svEnt->lastCluster) + { + continue; // not visible + } + } + else + { + continue; + } + } + + // add it + SV_AddEntToSnapshot(svEnt, ent, eNums); + + // if it's a portal entity, add everything visible from its camera position + if (ent->r.svFlags & SVF_PORTAL) + { + if (ent->s.generic1) + { + vec3_t dir; + VectorSubtract(ent->r.currentOrigin, origin, dir); + if (VectorLengthSquared(dir) > (float)ent->s.generic1 * ent->s.generic1) + { + continue; + } + } + SV_AddEntitiesVisibleFromPoint(ent->s.origin2, frame, eNums); + } + } +} + +/* +============= +SV_BuildClientSnapshot + +Decides which entities are going to be visible to the client, and +copies off the playerstate and areabits. + +This properly handles multiple recursive portals, but the render +currently doesn't. + +For viewing through other player's eyes, clent can be something other than client->gentity +============= +*/ +static void SV_BuildClientSnapshot(client_t *client) +{ + vec3_t org; + clientSnapshot_t *frame; + snapshotEntityNumbers_t entityNumbers; + int i; + sharedEntity_t *ent; + entityState_t *state; + svEntity_t *svEnt; + sharedEntity_t *clent; + int clientNum; + playerState_t *ps; + + // bump the counter used to prevent double adding + sv.snapshotCounter++; + + // this is the frame we are creating + frame = &client->frames[client->netchan.outgoingSequence & PACKET_MASK]; + + // clear everything in this snapshot + entityNumbers.numSnapshotEntities = 0; + ::memset(frame->areabits, 0, sizeof(frame->areabits)); + + // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=62 + frame->num_entities = 0; + + clent = client->gentity; + if (!clent || client->state == CS_ZOMBIE) + { + return; + } + + // grab the current playerState_t + ps = SV_GameClientNum(client - svs.clients); + frame->ps = *ps; + + // never send client's own entity, because it can + // be regenerated from the playerstate + clientNum = frame->ps.clientNum; + if (clientNum < 0 || clientNum >= MAX_GENTITIES) + { + Com_Error(ERR_DROP, "SV_SvEntityForGentity: bad gEnt"); + } + svEnt = &sv.svEntities[clientNum]; + + svEnt->snapshotCounter = sv.snapshotCounter; + + // find the client's viewpoint + VectorCopy(ps->origin, org); + org[2] += ps->viewheight; + + // add all the entities directly visible to the eye, which + // may include portal entities that merge other viewpoints + SV_AddEntitiesVisibleFromPoint(org, frame, &entityNumbers); + + // if there were portals visible, there may be out of order entities + // in the list which will need to be resorted for the delta compression + // to work correctly. This also catches the error condition + // of an entity being included twice. + qsort(entityNumbers.snapshotEntities, entityNumbers.numSnapshotEntities, sizeof(entityNumbers.snapshotEntities[0]), + SV_QsortEntityNumbers); + + // now that all viewpoint's areabits have been OR'd together, invert + // all of them to make it a mask vector, which is what the renderer wants + for (i = 0; i < MAX_MAP_AREA_BYTES / 4; i++) + { + ((int *)frame->areabits)[i] = ((int *)frame->areabits)[i] ^ -1; + } + + // copy the entity states out + frame->num_entities = 0; + frame->first_entity = svs.nextSnapshotEntities; + for (i = 0; i < entityNumbers.numSnapshotEntities; i++) + { + ent = SV_GentityNum(entityNumbers.snapshotEntities[i]); + state = &svs.snapshotEntities[svs.nextSnapshotEntities % svs.numSnapshotEntities]; + *state = ent->s; + svs.nextSnapshotEntities++; + // this should never hit, map should always be restarted first in SV_Frame + if (svs.nextSnapshotEntities >= 0x7FFFFFFE) + { + Com_Error(ERR_FATAL, "svs.nextSnapshotEntities wrapped"); + } + frame->num_entities++; + } +} + +#ifdef USE_VOIP +/* +================== +SV_WriteVoipToClient + +Check to see if there is any VoIP queued for a client, and send if there is. +================== +*/ +static void SV_WriteVoipToClient(client_t *cl, msg_t *msg) +{ + int totalbytes = 0; + int i; + voipServerPacket_t *packet; + + if (cl->queuedVoipPackets) + { + // Write as many VoIP packets as we reasonably can... + for (i = 0; i < cl->queuedVoipPackets; i++) + { + packet = cl->voipPacket[(i + cl->queuedVoipIndex) % ARRAY_LEN(cl->voipPacket)]; + + if (!*cl->downloadName) + { + totalbytes += packet->len; + if (totalbytes > (msg->maxsize - msg->cursize) / 2) break; + + if (cl->netchan.alternateProtocol != 0) MSG_WriteByte(msg, svc_EOF); + MSG_WriteByte(msg, svc_voipSpeex); + if (cl->netchan.alternateProtocol != 0) MSG_WriteByte(msg, svc_voipSpeex + 1); + MSG_WriteShort(msg, packet->sender); + MSG_WriteByte(msg, (byte)packet->generation); + MSG_WriteLong(msg, packet->sequence); + MSG_WriteByte(msg, packet->frames); + MSG_WriteShort(msg, packet->len); + if (cl->netchan.alternateProtocol == 0) MSG_WriteBits(msg, packet->flags, VOIP_FLAGCNT); + MSG_WriteData(msg, packet->data, packet->len); + } + + Z_Free(packet); + } + + cl->queuedVoipPackets -= i; + cl->queuedVoipIndex += i; + cl->queuedVoipIndex %= ARRAY_LEN(cl->voipPacket); + } +} +#endif + +/* +======================= +SV_SendMessageToClient + +Called by SV_SendClientSnapshot and SV_SendClientGameState +======================= +*/ +void SV_SendMessageToClient(msg_t *msg, client_t *client) +{ + // record information about the message + client->frames[client->netchan.outgoingSequence & PACKET_MASK].messageSize = msg->cursize; + client->frames[client->netchan.outgoingSequence & PACKET_MASK].messageSent = svs.time; + client->frames[client->netchan.outgoingSequence & PACKET_MASK].messageAcked = -1; + + // send the datagram + SV_Netchan_Transmit(client, msg); +} + +/* +======================= +SV_SendClientSnapshot + +Also called by SV_FinalMessage + +======================= +*/ +void SV_SendClientSnapshot(client_t *client) +{ + byte msg_buf[MAX_MSGLEN]; + msg_t msg; + + // build the snapshot + SV_BuildClientSnapshot(client); + + MSG_Init(&msg, msg_buf, sizeof(msg_buf)); + msg.allowoverflow = true; + + // NOTE, MRE: all server->client messages now acknowledge + // let the client know which reliable clientCommands we have received + MSG_WriteLong(&msg, client->lastClientCommand); + + // (re)send any reliable server commands + SV_UpdateServerCommandsToClient(client, &msg); + + // send over all the relevant entityState_t + // and the playerState_t + SV_WriteSnapshotToClient(client, &msg); + +#ifdef USE_VOIP + SV_WriteVoipToClient(client, &msg); +#endif + + // check for overflow + if (msg.overflowed) + { + Com_Printf("WARNING: msg overflowed for %s\n", client->name); + MSG_Clear(&msg); + } + + SV_SendMessageToClient(&msg, client); +} + +/* +======================= +SV_SendClientMessages +======================= +*/ +void SV_SendClientMessages(void) +{ + int i; + client_t *c; + + // send a message to each connected client + for (i = 0; i < sv_maxclients->integer; i++) + { + c = &svs.clients[i]; + + if (!c->state) continue; // not connected + + if (svs.time - c->lastSnapshotTime < c->snapshotMsec * com_timescale->value) continue; // It's not time yet + + if (*c->downloadName) continue; // Client is downloading, don't send snapshots + + if (c->netchan.unsentFragments || c->netchan_start_queue) + { + c->rateDelayed = true; + continue; // Drop this snapshot if the packet queue is still full or delta compression will break + } + + if (!(c->netchan.remoteAddress.type == NA_LOOPBACK || + (sv_lanForceRate->integer && Sys_IsLANAddress(c->netchan.remoteAddress)))) + { + // rate control for clients not on LAN + + if (SV_RateMsec(c) > 0) + { + // Not enough time since last packet passed through the line + c->rateDelayed = true; + continue; + } + } + + // generate and send a new message + SV_SendClientSnapshot(c); + c->lastSnapshotTime = svs.time; + c->rateDelayed = false; + } +} diff --git a/src/server/sv_world.cpp b/src/server/sv_world.cpp new file mode 100644 index 0000000..fd0710e --- /dev/null +++ b/src/server/sv_world.cpp @@ -0,0 +1,745 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2013 Darklegion Development +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/> + +=========================================================================== +*/ + +// world.c -- world query functions + +#include "server.h" + +/* +================ +SV_ClipHandleForEntity + +Returns a headnode that can be used for testing or clipping to a +given entity. If the entity is a bsp model, the headnode will +be returned, otherwise a custom box tree will be constructed. +================ +*/ +clipHandle_t SV_ClipHandleForEntity(const sharedEntity_t *ent) +{ + if (ent->r.bmodel) + { + // explicit hulls in the BSP model + return CM_InlineModel(ent->s.modelindex); + } + if (ent->r.svFlags & SVF_CAPSULE) + { + // create a temp capsule from bounding box sizes + return CM_TempBoxModel(ent->r.mins, ent->r.maxs, true); + } + + // create a temp tree from bounding box sizes + return CM_TempBoxModel(ent->r.mins, ent->r.maxs, qfalse); +} + +/* +=============================================================================== + +ENTITY CHECKING + +To avoid linearly searching through lists of entities during environment testing, +the world is carved up with an evenly spaced, axially aligned bsp tree. Entities +are kept in chains either at the final leafs, or at the first node that splits +them, which prevents having to deal with multiple fragments of a single entity. + +=============================================================================== +*/ + +struct worldSector_t { + int axis; // -1 = leaf node + float dist; + worldSector_t *children[2]; + svEntity_t *entities; +}; + +#define AREA_DEPTH 4 +#define AREA_NODES 64 + +worldSector_t sv_worldSectors[AREA_NODES]; +int sv_numworldSectors; + +/* +=============== +SV_SectorList_f +=============== +*/ +void SV_SectorList_f(void) +{ + int i, c; + worldSector_t *sec; + svEntity_t *ent; + + for (i = 0; i < AREA_NODES; i++) + { + sec = &sv_worldSectors[i]; + + c = 0; + for (ent = sec->entities; ent; ent = ent->nextEntityInWorldSector) + { + c++; + } + Com_Printf("sector %i: %i entities\n", i, c); + } +} + +/* +=============== +SV_CreateworldSector + +Builds a uniformly subdivided tree for the given world size +=============== +*/ +static worldSector_t *SV_CreateworldSector(int depth, vec3_t mins, vec3_t maxs) +{ + worldSector_t *anode; + vec3_t size; + vec3_t mins1, maxs1, mins2, maxs2; + + anode = &sv_worldSectors[sv_numworldSectors]; + sv_numworldSectors++; + + if (depth == AREA_DEPTH) + { + anode->axis = -1; + anode->children[0] = anode->children[1] = NULL; + return anode; + } + + VectorSubtract(maxs, mins, size); + if (size[0] > size[1]) + { + anode->axis = 0; + } + else + { + anode->axis = 1; + } + + anode->dist = 0.5 * (maxs[anode->axis] + mins[anode->axis]); + VectorCopy(mins, mins1); + VectorCopy(mins, mins2); + VectorCopy(maxs, maxs1); + VectorCopy(maxs, maxs2); + + maxs1[anode->axis] = mins2[anode->axis] = anode->dist; + + anode->children[0] = SV_CreateworldSector(depth + 1, mins2, maxs2); + anode->children[1] = SV_CreateworldSector(depth + 1, mins1, maxs1); + + return anode; +} + +/* +=============== +SV_ClearWorld + +=============== +*/ +void SV_ClearWorld(void) +{ + clipHandle_t h; + vec3_t mins, maxs; + + ::memset(sv_worldSectors, 0, sizeof(sv_worldSectors)); + sv_numworldSectors = 0; + + // get world map bounds + h = CM_InlineModel(0); + CM_ModelBounds(h, mins, maxs); + SV_CreateworldSector(0, mins, maxs); +} + +/* +=============== +SV_UnlinkEntity + +=============== +*/ +void SV_UnlinkEntity(sharedEntity_t *gEnt) +{ + svEntity_t *ent; + svEntity_t *scan; + worldSector_t *ws; + + ent = SV_SvEntityForGentity(gEnt); + + gEnt->r.linked = qfalse; + + ws = ent->worldSector; + if (!ws) + { + return; // not linked in anywhere + } + ent->worldSector = NULL; + + if (ws->entities == ent) + { + ws->entities = ent->nextEntityInWorldSector; + return; + } + + for (scan = ws->entities; scan; scan = scan->nextEntityInWorldSector) + { + if (scan->nextEntityInWorldSector == ent) + { + scan->nextEntityInWorldSector = ent->nextEntityInWorldSector; + return; + } + } + + Com_Printf("WARNING: SV_UnlinkEntity: not found in worldSector\n"); +} + +/* +=============== +SV_LinkEntity + +=============== +*/ +#define MAX_TOTAL_ENT_LEAFS 128 +void SV_LinkEntity(sharedEntity_t *gEnt) +{ + worldSector_t *node; + int leafs[MAX_TOTAL_ENT_LEAFS]; + int cluster; + int num_leafs; + int i, j, k; + int area; + int lastLeaf; + float *origin, *angles; + svEntity_t *ent; + + ent = SV_SvEntityForGentity(gEnt); + + if (ent->worldSector) + { + SV_UnlinkEntity(gEnt); // unlink from old position + } + + // encode the size into the entityState_t for client prediction + if (gEnt->r.bmodel) + { + gEnt->s.solid = SOLID_BMODEL; // a solid_box will never create this value + } + else if (gEnt->r.contents & (CONTENTS_SOLID | CONTENTS_BODY)) + { + // assume that x/y are equal and symetric + i = gEnt->r.maxs[0]; + if (i < 1) i = 1; + if (i > 255) i = 255; + + // z is not symetric + j = (-gEnt->r.mins[2]); + if (j < 1) j = 1; + if (j > 255) j = 255; + + // and z maxs can be negative... + k = (gEnt->r.maxs[2] + 32); + if (k < 1) k = 1; + if (k > 255) k = 255; + + gEnt->s.solid = (k << 16) | (j << 8) | i; + } + else + { + gEnt->s.solid = 0; + } + + // get the position + origin = gEnt->r.currentOrigin; + angles = gEnt->r.currentAngles; + + // set the abs box + if (gEnt->r.bmodel && (angles[0] || angles[1] || angles[2])) + { + // expand for rotation + float max; + + max = RadiusFromBounds(gEnt->r.mins, gEnt->r.maxs); + for (i = 0; i < 3; i++) + { + gEnt->r.absmin[i] = origin[i] - max; + gEnt->r.absmax[i] = origin[i] + max; + } + } + else + { + // normal + VectorAdd(origin, gEnt->r.mins, gEnt->r.absmin); + VectorAdd(origin, gEnt->r.maxs, gEnt->r.absmax); + } + + // because movement is clipped an epsilon away from an actual edge, + // we must fully check even when bounding boxes don't quite touch + gEnt->r.absmin[0] -= 1; + gEnt->r.absmin[1] -= 1; + gEnt->r.absmin[2] -= 1; + gEnt->r.absmax[0] += 1; + gEnt->r.absmax[1] += 1; + gEnt->r.absmax[2] += 1; + + // link to PVS leafs + ent->numClusters = 0; + ent->lastCluster = 0; + ent->areanum = -1; + ent->areanum2 = -1; + + // get all leafs, including solids + num_leafs = CM_BoxLeafnums(gEnt->r.absmin, gEnt->r.absmax, leafs, MAX_TOTAL_ENT_LEAFS, &lastLeaf); + + // if none of the leafs were inside the map, the + // entity is outside the world and can be considered unlinked + if (!num_leafs) + { + return; + } + + // set areas, even from clusters that don't fit in the entity array + for (i = 0; i < num_leafs; i++) + { + area = CM_LeafArea(leafs[i]); + if (area != -1) + { + // doors may legally straggle two areas, + // but nothing should evern need more than that + if (ent->areanum != -1 && ent->areanum != area) + { + if (ent->areanum2 != -1 && ent->areanum2 != area && sv.state == SS_LOADING) + { + Com_DPrintf("Object %i touching 3 areas at %f %f %f\n", gEnt->s.number, gEnt->r.absmin[0], + gEnt->r.absmin[1], gEnt->r.absmin[2]); + } + ent->areanum2 = area; + } + else + { + ent->areanum = area; + } + } + } + + // store as many explicit clusters as we can + ent->numClusters = 0; + for (i = 0; i < num_leafs; i++) + { + cluster = CM_LeafCluster(leafs[i]); + if (cluster != -1) + { + ent->clusternums[ent->numClusters++] = cluster; + if (ent->numClusters == MAX_ENT_CLUSTERS) + { + break; + } + } + } + + // store off a last cluster if we need to + if (i != num_leafs) + { + ent->lastCluster = CM_LeafCluster(lastLeaf); + } + + gEnt->r.linkcount++; + + // find the first world sector node that the ent's box crosses + node = sv_worldSectors; + for ( ;; ) + { + if (node->axis == -1) + break; + + if (gEnt->r.absmin[node->axis] > node->dist) + node = node->children[0]; + else if (gEnt->r.absmax[node->axis] < node->dist) + node = node->children[1]; + else + break; // crosses the node + } + + // link it in + ent->worldSector = node; + ent->nextEntityInWorldSector = node->entities; + node->entities = ent; + + gEnt->r.linked = qtrue; +} + +/* +============================================================================ + +AREA QUERY + +Fills in a list of all entities who's absmin / absmax intersects the given +bounds. This does NOT mean that they actually touch in the case of bmodels. +============================================================================ +*/ + +struct areaParms_t { + const float *mins; + const float *maxs; + int *list; + int count; + int maxcount; +}; + +/* +==================== +SV_AreaEntities_r + +==================== +*/ +static void SV_AreaEntities_r(worldSector_t *node, areaParms_t *ap) +{ + svEntity_t *check, *next; + sharedEntity_t *gcheck; + + for (check = node->entities; check; check = next) + { + next = check->nextEntityInWorldSector; + + gcheck = SV_GEntityForSvEntity(check); + + if (gcheck->r.absmin[0] > ap->maxs[0] || gcheck->r.absmin[1] > ap->maxs[1] || + gcheck->r.absmin[2] > ap->maxs[2] || gcheck->r.absmax[0] < ap->mins[0] || + gcheck->r.absmax[1] < ap->mins[1] || gcheck->r.absmax[2] < ap->mins[2]) + { + continue; + } + + if (ap->count == ap->maxcount) + { + Com_Printf("SV_AreaEntities: MAXCOUNT\n"); + return; + } + + ap->list[ap->count] = check - sv.svEntities; + ap->count++; + } + + if (node->axis == -1) + { + return; // terminal node + } + + // recurse down both sides + if (ap->maxs[node->axis] > node->dist) + { + SV_AreaEntities_r(node->children[0], ap); + } + if (ap->mins[node->axis] < node->dist) + { + SV_AreaEntities_r(node->children[1], ap); + } +} + +/* +================ +SV_AreaEntities +================ +*/ +int SV_AreaEntities(const vec3_t mins, const vec3_t maxs, int *entityList, int maxcount) +{ + areaParms_t ap; + + ap.mins = mins; + ap.maxs = maxs; + ap.list = entityList; + ap.count = 0; + ap.maxcount = maxcount; + + SV_AreaEntities_r(sv_worldSectors, &ap); + + return ap.count; +} + +//=========================================================================== + +struct moveclip_t { + vec3_t boxmins; + vec3_t boxmaxs; // enclose the test object along entire move + const float *mins; + const float *maxs; // size of the moving object + const float *start; + vec3_t end; + trace_t trace; + int passEntityNum; + int contentmask; + traceType_t collisionType; +}; + +/* +==================== +SV_ClipToEntity + +==================== +*/ +void SV_ClipToEntity(trace_t *trace, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, + int entityNum, int contentmask, traceType_t type) +{ + sharedEntity_t *touch; + clipHandle_t clipHandle; + float *origin, *angles; + + touch = SV_GentityNum(entityNum); + + ::memset(trace, 0, sizeof(trace_t)); + + // if it doesn't have any brushes of a type we + // are looking for, ignore it + if (!(contentmask & touch->r.contents)) + { + trace->fraction = 1.0; + return; + } + + // might intersect, so do an exact clip + clipHandle = SV_ClipHandleForEntity(touch); + + origin = touch->r.currentOrigin; + angles = touch->r.currentAngles; + + if (!touch->r.bmodel) + { + angles = vec3_origin; // boxes don't rotate + } + + CM_TransformedBoxTrace(trace, (float *)start, (float *)end, (float *)mins, (float *)maxs, clipHandle, contentmask, + origin, angles, type); + + if (trace->fraction < 1) + { + trace->entityNum = touch->s.number; + } +} + +/* +==================== +SV_ClipMoveToEntities + +==================== +*/ +static void SV_ClipMoveToEntities(moveclip_t *clip) +{ + int i, num; + int touchlist[MAX_GENTITIES]; + sharedEntity_t *touch; + int passOwnerNum; + trace_t trace; + clipHandle_t clipHandle; + float *origin, *angles; + + num = SV_AreaEntities(clip->boxmins, clip->boxmaxs, touchlist, MAX_GENTITIES); + + if (clip->passEntityNum != ENTITYNUM_NONE) + { + passOwnerNum = (SV_GentityNum(clip->passEntityNum))->r.ownerNum; + if (passOwnerNum == ENTITYNUM_NONE) + { + passOwnerNum = -1; + } + } + else + { + passOwnerNum = -1; + } + + for (i = 0; i < num; i++) + { + if (clip->trace.allsolid) + { + return; + } + touch = SV_GentityNum(touchlist[i]); + + // see if we should ignore this entity + if (clip->passEntityNum != ENTITYNUM_NONE) + { + if (touchlist[i] == clip->passEntityNum) + { + continue; // don't clip against the pass entity + } + if (touch->r.ownerNum == clip->passEntityNum) + { + continue; // don't clip against own missiles + } + if (touch->r.ownerNum == passOwnerNum) + { + continue; // don't clip against other missiles from our owner + } + } + + // if it doesn't have any brushes of a type we + // are looking for, ignore it + if (!(clip->contentmask & touch->r.contents)) + { + continue; + } + + // might intersect, so do an exact clip + clipHandle = SV_ClipHandleForEntity(touch); + + origin = touch->r.currentOrigin; + angles = touch->r.currentAngles; + + if (!touch->r.bmodel) + { + angles = vec3_origin; // boxes don't rotate + } + + CM_TransformedBoxTrace(&trace, (float *)clip->start, (float *)clip->end, (float *)clip->mins, + (float *)clip->maxs, clipHandle, clip->contentmask, origin, angles, clip->collisionType); + + if (trace.allsolid) + { + clip->trace.allsolid = qtrue; + trace.entityNum = touch->s.number; + } + else if (trace.startsolid) + { + clip->trace.startsolid = qtrue; + trace.entityNum = touch->s.number; + } + + if (trace.fraction < clip->trace.fraction) + { + int oldStart; + + // make sure we keep a startsolid from a previous trace + oldStart = clip->trace.startsolid; + + trace.entityNum = touch->s.number; + clip->trace = trace; + clip->trace.startsolid |= oldStart; + } + } +} + +/* +================== +SV_Trace + +Moves the given mins/maxs volume through the world from start to end. +passEntityNum and entities owned by passEntityNum are explicitly not checked. +================== +*/ +void SV_Trace(trace_t *results, const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end, int passEntityNum, + int contentmask, traceType_t type) +{ + moveclip_t clip; + int i; + + if (!mins) + { + mins = vec3_origin; + } + if (!maxs) + { + maxs = vec3_origin; + } + + ::memset(&clip, 0, sizeof(moveclip_t)); + + // clip to world + CM_BoxTrace(&clip.trace, start, end, mins, maxs, 0, contentmask, type); + clip.trace.entityNum = clip.trace.fraction != 1.0 ? ENTITYNUM_WORLD : ENTITYNUM_NONE; + if (clip.trace.fraction == 0) + { + *results = clip.trace; + return; // blocked immediately by the world + } + + clip.contentmask = contentmask; + clip.start = start; + // VectorCopy( clip.trace.endpos, clip.end ); + VectorCopy(end, clip.end); + clip.mins = mins; + clip.maxs = maxs; + clip.passEntityNum = passEntityNum; + clip.collisionType = type; + + // create the bounding box of the entire move + // we can limit it to the part of the move not + // already clipped off by the world, which can be + // a significant savings for line of sight and shot traces + for (i = 0; i < 3; i++) + { + if (end[i] > start[i]) + { + clip.boxmins[i] = clip.start[i] + clip.mins[i] - 1; + clip.boxmaxs[i] = clip.end[i] + clip.maxs[i] + 1; + } + else + { + clip.boxmins[i] = clip.end[i] + clip.mins[i] - 1; + clip.boxmaxs[i] = clip.start[i] + clip.maxs[i] + 1; + } + } + + // clip to other solid entities + SV_ClipMoveToEntities(&clip); + + *results = clip.trace; +} + +/* +============= +SV_PointContents +============= +*/ +int SV_PointContents(const vec3_t p, int passEntityNum) +{ + int touch[MAX_GENTITIES]; + sharedEntity_t *hit; + int i, num; + int contents, c2; + clipHandle_t clipHandle; + float *angles; + + // get base contents from world + contents = CM_PointContents(p, 0); + + // or in contents from all the other entities + num = SV_AreaEntities(p, p, touch, MAX_GENTITIES); + + for (i = 0; i < num; i++) + { + if (touch[i] == passEntityNum) + { + continue; + } + hit = SV_GentityNum(touch[i]); + // might intersect, so do an exact clip + clipHandle = SV_ClipHandleForEntity(hit); + angles = hit->r.currentAngles; + if (!hit->r.bmodel) + { + angles = vec3_origin; // boxes don't rotate + } + + c2 = CM_TransformedPointContents(p, clipHandle, hit->r.currentOrigin, angles); + + contents |= c2; + } + + return contents; +} |