#include "quakedef.h"
#include "lhnet.h"
+// for secure rcon authentication
+#include "hmac.h"
+#include "mdfour.h"
+#include <time.h>
+
#define QWMASTER_PORT 27000
#define DPMASTER_PORT 27950
{CVAR_SAVE, "sv_master2", "", "user-chosen master server 2"},
{CVAR_SAVE, "sv_master3", "", "user-chosen master server 3"},
{CVAR_SAVE, "sv_master4", "", "user-chosen master server 4"},
- {0, "sv_masterextra1", "ghdigital.com", "default master server 1 (admin: LordHavoc)"}, // admin: LordHavoc
- {0, "sv_masterextra2", "dpmaster.deathmask.net", "default master server 2 (admin: Willis)"}, // admin: Willis
- {0, "sv_masterextra3", "dpmaster.tchr.no", "default master server 3 (admin: tChr)"}, // admin: tChr
+ {0, "sv_masterextra1", "69.59.212.88", "ghdigital.com - default master server 1 (admin: LordHavoc)"}, // admin: LordHavoc
+ {0, "sv_masterextra2", "64.22.107.125", "dpmaster.deathmask.net - default master server 2 (admin: Willis)"}, // admin: Willis
+ {0, "sv_masterextra3", "92.62.40.6", "dpmaster.tchr.no - default master server 3 (admin: tChr)"}, // admin: tChr
{0, NULL, NULL, NULL}
};
static unsigned char net_message_buf[NET_MAXMESSAGE];
cvar_t net_messagetimeout = {0, "net_messagetimeout","300", "drops players who have not sent any packets for this many seconds"};
-cvar_t net_connecttimeout = {0, "net_connecttimeout","10", "after requesting a connection, the client must reply within this many seconds or be dropped (cuts down on connect floods)"};
+cvar_t net_connecttimeout = {0, "net_connecttimeout","15", "after requesting a connection, the client must reply within this many seconds or be dropped (cuts down on connect floods). Must be above 10 seconds."};
cvar_t net_connectfloodblockingtimeout = {0, "net_connectfloodblockingtimeout", "5", "when a connection packet is received, it will block all future connect packets from that IP address for this many seconds (cuts down on connect floods)"};
cvar_t hostname = {CVAR_SAVE, "hostname", "UNNAMED", "server message to show in server browser"};
cvar_t developer_networking = {0, "developer_networking", "0", "prints all received and sent packets (recommended only for debugging)"};
static cvar_t gameversion = {0, "gameversion", "0", "version of game data (mod-specific), when client and server gameversion mismatch in the server browser the server is shown as incompatible"};
static cvar_t rcon_restricted_password = {CVAR_PRIVATE, "rcon_restricted_password", "", "password to authenticate rcon commands in restricted mode"};
static cvar_t rcon_restricted_commands = {0, "rcon_restricted_commands", "", "allowed commands for rcon when the restricted mode password was used"};
+static cvar_t rcon_secure_maxdiff = {0, "rcon_secure_maxdiff", "5", "maximum time difference between rcon request and server system clock (to protect against replay attack)"};
+extern cvar_t rcon_secure;
/* statistic counters */
static int packetsSent = 0;
char rejectreason[32];
cls.connect_trying = false;
string += 7;
- length = max(length - 7, (int)sizeof(rejectreason) - 1);
+ length = min(length - 7, (int)sizeof(rejectreason) - 1);
memcpy(rejectreason, string, length);
rejectreason[length] = 0;
M_Update_Return_Reason(rejectreason);
// (div0) build the full response only if possible; better a getinfo response than no response at all if getstatus won't fit
static qboolean NetConn_BuildStatusResponse(const char* challenge, char* out_msg, size_t out_size, qboolean fullstatus)
{
- const char *qcstatus = NULL;
+ char qcstatus[256];
unsigned int nb_clients = 0, nb_bots = 0, i;
int length;
+ char teambuf[3];
SV_VM_Begin();
}
}
+ *qcstatus = 0;
if(prog->globaloffsets.worldstatus >= 0)
{
const char *str = PRVM_G_STRING(prog->globaloffsets.worldstatus);
{
char *p;
const char *q;
- qcstatus = p = Mem_Alloc(tempmempool, strlen(str) + 1);
+ p = qcstatus;
for(q = str; *q; ++q)
- if(*q != '\\')
+ if(*q != '\\' && *q != '\n')
*p++ = *q;
*p = 0;
}
fullstatus ? "statusResponse" : "infoResponse",
gamename, com_modname, gameversion.integer, svs.maxclients,
nb_clients, nb_bots, sv.name, hostname.string, NET_PROTOCOL_VERSION,
- qcstatus ? "\\qcstatus\\" : "", qcstatus ? qcstatus : "",
+ *qcstatus ? "\\qcstatus\\" : "", qcstatus,
challenge ? "\\challenge\\" : "", challenge ? challenge : "",
fullstatus ? "\n" : "");
- if(qcstatus)
- {
- Mem_Free((char *)qcstatus);
- qcstatus = NULL;
- }
-
// Make sure it fits in the buffer
if (length < 0)
goto bad;
break;
}
} while (curchar != '\0');
+ cleanname[cleanind] = 0; // cleanind is always a valid index even at this point
pingvalue = (int)(cl->ping * 1000.0f);
if(cl->netconnection)
else
pingvalue = 0;
+ *qcstatus = 0;
if(prog->fieldoffsets.clientstatus >= 0)
{
const char *str = PRVM_E_STRING(PRVM_EDICT_NUM(i + 1), prog->fieldoffsets.clientstatus);
{
char *p;
const char *q;
- qcstatus = p = Mem_Alloc(tempmempool, strlen(str) + 1);
- for(q = str; *q; ++q)
- if(*q != '\\' && *q != ' ')
+ p = qcstatus;
+ for(q = str; *q && p != qcstatus + sizeof(qcstatus) - 1; ++q)
+ if(*q != '\\' && *q != '"' && !ISWHITESPACE(*q))
*p++ = *q;
*p = 0;
}
}
- if(qcstatus)
+ if ((gamemode == GAME_NEXUIZ) && (teamplay.integer > 0))
{
- length = dpsnprintf(ptr, left, "%s %d \"%s\"\n",
+ if(cl->frags == -666) // spectator
+ strlcpy(teambuf, " 0", sizeof(teambuf));
+ else if(cl->colors == 0x44) // red team
+ strlcpy(teambuf, " 1", sizeof(teambuf));
+ else if(cl->colors == 0xDD) // blue team
+ strlcpy(teambuf, " 2", sizeof(teambuf));
+ else if(cl->colors == 0xCC) // yellow team
+ strlcpy(teambuf, " 3", sizeof(teambuf));
+ else if(cl->colors == 0x99) // pink team
+ strlcpy(teambuf, " 4", sizeof(teambuf));
+ else
+ strlcpy(teambuf, " 0", sizeof(teambuf));
+ }
+ else
+ *teambuf = 0;
+
+ // note: team number is inserted according to SoF2 protocol
+ if(*qcstatus)
+ length = dpsnprintf(ptr, left, "%s %d%s \"%s\"\n",
qcstatus,
pingvalue,
+ teambuf,
cleanname);
- Mem_Free((char *)qcstatus);
- qcstatus = NULL;
- }
else
- length = dpsnprintf(ptr, left, "%d %d \"%s\"\n",
+ length = dpsnprintf(ptr, left, "%d %d%s \"%s\"\n",
cl->frags,
pingvalue,
+ teambuf,
cleanname);
if(length < 0)
}
}
+typedef qboolean (*rcon_matchfunc_t) (const char *password, const char *hash, const char *s, int slen);
+
+qboolean hmac_mdfour_matching(const char *password, const char *hash, const char *s, int slen)
+{
+ char mdfourbuf[16];
+ long t1, t2;
+
+ t1 = (long) time(NULL);
+ t2 = strtol(s, NULL, 0);
+ if(abs(t1 - t2) > rcon_secure_maxdiff.integer)
+ return false;
+
+ if(!HMAC_MDFOUR_16BYTES((unsigned char *) mdfourbuf, (unsigned char *) s, slen, (unsigned char *) password, strlen(password)))
+ return false;
+
+ return !memcmp(mdfourbuf, hash, 16);
+}
+
+qboolean plaintext_matching(const char *password, const char *hash, const char *s, int slen)
+{
+ return !strcmp(password, hash);
+}
+
// returns a string describing the user level, or NULL for auth failure
-const char *RCon_Authenticate(const char *password, const char *s, const char *endpos)
+const char *RCon_Authenticate(const char *password, const char *s, const char *endpos, rcon_matchfunc_t comparator, const char *cs, int cslen)
{
const char *text;
qboolean hasquotes;
- if(!strcmp(rcon_password.string, password))
+ if(comparator(rcon_password.string, password, cs, cslen))
return "rcon";
- if(strcmp(rcon_restricted_password.string, password))
+ if(!comparator(rcon_restricted_password.string, password, cs, cslen))
return NULL;
for(text = s; text != endpos; ++text)
- if(*text > 0 && (*text < ' ' || *text == ';'))
+ if((signed char) *text > 0 && ((signed char) *text < (signed char) ' ' || *text == ';'))
return NULL; // block possible exploits against the parser/alias expansion
while(s != endpos)
return "restricted rcon";
}
+void RCon_Execute(lhnetsocket_t *mysocket, lhnetaddress_t *peeraddress, const char *addressstring2, const char *userlevel, const char *s, const char *endpos)
+{
+ if(userlevel)
+ {
+ // looks like a legitimate rcon command with the correct password
+ const char *s_ptr = s;
+ Con_Printf("server received %s command from %s: ", userlevel, host_client ? host_client->name : addressstring2);
+ while(s_ptr != endpos)
+ {
+ size_t l = strlen(s_ptr);
+ if(l)
+ Con_Printf(" %s;", s_ptr);
+ s_ptr += l + 1;
+ }
+ Con_Printf("\n");
+
+ if (!host_client || !host_client->netconnection || LHNETADDRESS_GetAddressType(&host_client->netconnection->peeraddress) != LHNETADDRESSTYPE_LOOP)
+ Con_Rcon_Redirect_Init(mysocket, peeraddress);
+ while(s != endpos)
+ {
+ size_t l = strlen(s);
+ if(l)
+ {
+ client_t *host_client_save = host_client;
+ Cmd_ExecuteString(s, src_command);
+ host_client = host_client_save;
+ // in case it is a command that changes host_client (like restart)
+ }
+ s += l + 1;
+ }
+ Con_Rcon_Redirect_End();
+ }
+ else
+ {
+ Con_Printf("server denied rcon access to %s\n", host_client ? host_client->name : addressstring2);
+ }
+}
+
extern void SV_SendServerinfo (client_t *client);
static int NetConn_ServerParsePacket(lhnetsocket_t *mysocket, unsigned char *data, int length, lhnetaddress_t *peeraddress)
{
return true;
// check engine protocol
- if (strcmp(SearchInfostring(string, "protocol"), "darkplaces 3"))
+ if(!(s = SearchInfostring(string, "protocol")) || strcmp(s, "darkplaces 3"))
{
if (developer.integer >= 10)
Con_Printf("Datagram_ParseConnectionless: sending \"reject Wrong game protocol.\" to %s.\n", addressstring2);
}
return true;
}
+ if (length >= 37 && !memcmp(string, "srcon HMAC-MD4 TIME ", 20))
+ {
+ char *password = string + 20;
+ char *timeval = string + 37;
+ char *s = strchr(timeval, ' ');
+ char *endpos = string + length + 1; // one behind the NUL, so adding strlen+1 will eventually reach it
+ const char *userlevel;
+ if(!s)
+ return true; // invalid packet
+ ++s;
+
+ userlevel = RCon_Authenticate(password, s, endpos, hmac_mdfour_matching, timeval, endpos - timeval - 1); // not including the appended \0 into the HMAC
+ RCon_Execute(mysocket, peeraddress, addressstring2, userlevel, s, endpos);
+ return true;
+ }
if (length >= 5 && !memcmp(string, "rcon ", 5))
{
int i;
char *s = string + 5;
char *endpos = string + length + 1; // one behind the NUL, so adding strlen+1 will eventually reach it
char password[64];
- for (i = 0;*s > ' ';s++)
+
+ if(rcon_secure.integer)
+ return true;
+
+ for (i = 0;!ISWHITESPACE(*s);s++)
if (i < (int)sizeof(password) - 1)
password[i++] = *s;
- if(*s <= ' ' && s != endpos) // skip leading ugly space
+ if(ISWHITESPACE(*s) && s != endpos) // skip leading ugly space
++s;
password[i] = 0;
- if (password[0] > ' ')
+ if (!ISWHITESPACE(password[0]))
{
- const char *userlevel = RCon_Authenticate(password, s, endpos);
- if(userlevel)
- {
- // looks like a legitimate rcon command with the correct password
- char *s_ptr = s;
- Con_Printf("server received %s command from %s: ", userlevel, host_client ? host_client->name : addressstring2);
- while(s_ptr != endpos)
- {
- size_t l = strlen(s_ptr);
- if(l)
- Con_Printf(" %s;", s_ptr);
- s_ptr += l + 1;
- }
- Con_Printf("\n");
-
- if (!host_client || !host_client->netconnection || LHNETADDRESS_GetAddressType(&host_client->netconnection->peeraddress) != LHNETADDRESSTYPE_LOOP)
- Con_Rcon_Redirect_Init(mysocket, peeraddress);
- while(s != endpos)
- {
- size_t l = strlen(s);
- if(l)
- {
- client_t *host_client_save = host_client;
- Cmd_ExecuteString(s, src_command);
- host_client = host_client_save;
- // in case it is a command that changes host_client (like restart)
- }
- s += l + 1;
- }
- Con_Rcon_Redirect_End();
- }
- else
- {
- Con_Printf("server denied rcon access to %s\n", host_client ? host_client->name : addressstring2);
- }
+ const char *userlevel = RCon_Authenticate(password, s, endpos, plaintext_matching, NULL, 0);
+ RCon_Execute(mysocket, peeraddress, addressstring2, userlevel, s, endpos);
}
return true;
}
Cmd_AddCommand("heartbeat", Net_Heartbeat_f, "send a heartbeat to the master server (updates your server information)");
Cvar_RegisterVariable(&rcon_restricted_password);
Cvar_RegisterVariable(&rcon_restricted_commands);
+ Cvar_RegisterVariable(&rcon_secure_maxdiff);
Cvar_RegisterVariable(&net_slist_queriespersecond);
Cvar_RegisterVariable(&net_slist_queriesperframe);
Cvar_RegisterVariable(&net_slist_timeout);