+void NetConn_ClearFlood(lhnetaddress_t *peeraddress, server_floodaddress_t *floodlist, size_t floodlength)
+{
+ size_t floodslotnum;
+ lhnetaddress_t noportpeeraddress;
+ // see if this is a connect flood
+ noportpeeraddress = *peeraddress;
+ LHNETADDRESS_SetPort(&noportpeeraddress, 0);
+ for (floodslotnum = 0;floodslotnum < floodlength;floodslotnum++)
+ {
+ if (floodlist[floodslotnum].lasttime && LHNETADDRESS_Compare(&noportpeeraddress, &floodlist[floodslotnum].address) == 0)
+ {
+ // this address matches an ongoing flood address
+ // remove the ban
+ floodlist[floodslotnum].address.addresstype = LHNETADDRESSTYPE_NONE;
+ floodlist[floodslotnum].lasttime = 0;
+ //Con_Printf("Flood cleared!\n");
+ }
+ }
+}
+
+typedef qbool (*rcon_matchfunc_t) (lhnetaddress_t *peeraddress, const char *password, const char *hash, const char *s, int slen);
+
+static qbool hmac_mdfour_time_matching(lhnetaddress_t *peeraddress, const char *password, const char *hash, const char *s, int slen)
+{
+ char mdfourbuf[16];
+ long t1, t2;
+
+ if (!password[0]) {
+ Con_Print(CON_ERROR "LOGIC ERROR: RCon_Authenticate should never call the comparator with an empty password. Please report.\n");
+ return false;
+ }
+
+ t1 = (long) time(NULL);
+ t2 = strtol(s, NULL, 0);
+ if(labs(t1 - t2) > rcon_secure_maxdiff.integer)
+ return false;
+
+ if(!HMAC_MDFOUR_16BYTES((unsigned char *) mdfourbuf, (unsigned char *) s, slen, (unsigned char *) password, (int)strlen(password)))
+ return false;
+
+ return !memcmp(mdfourbuf, hash, 16);
+}
+
+static qbool hmac_mdfour_challenge_matching(lhnetaddress_t *peeraddress, const char *password, const char *hash, const char *s, int slen)
+{
+ char mdfourbuf[16];
+ int i;
+
+ if (!password[0]) {
+ Con_Print(CON_ERROR "LOGIC ERROR: RCon_Authenticate should never call the comparator with an empty password. Please report.\n");
+ return false;
+ }
+
+ if(slen < (int)(sizeof(challenges[0].string)) - 1)
+ return false;
+
+ // validate the challenge
+ for (i = 0;i < MAX_CHALLENGES;i++)
+ if(challenges[i].time > 0)
+ if (!LHNETADDRESS_Compare(peeraddress, &challenges[i].address) && !strncmp(challenges[i].string, s, sizeof(challenges[0].string) - 1))
+ break;
+ // if the challenge is not recognized, drop the packet
+ if (i == MAX_CHALLENGES)
+ return false;
+
+ if(!HMAC_MDFOUR_16BYTES((unsigned char *) mdfourbuf, (unsigned char *) s, slen, (unsigned char *) password, (int)strlen(password)))
+ return false;
+
+ if(memcmp(mdfourbuf, hash, 16))
+ return false;
+
+ // unmark challenge to prevent replay attacks
+ challenges[i].time = 0;
+
+ return true;
+}
+
+static qbool plaintext_matching(lhnetaddress_t *peeraddress, const char *password, const char *hash, const char *s, int slen)
+{
+ if (!password[0]) {
+ Con_Print(CON_ERROR "LOGIC ERROR: RCon_Authenticate should never call the comparator with an empty password. Please report.\n");
+ return false;
+ }
+
+ return !strcmp(password, hash);
+}
+
+/// returns a string describing the user level, or NULL for auth failure
+static const char *RCon_Authenticate(lhnetaddress_t *peeraddress, const char *password, const char *s, const char *endpos, rcon_matchfunc_t comparator, const char *cs, int cslen)
+{
+ const char *text, *userpass_start, *userpass_end, *userpass_startpass;
+ static char buf[MAX_INPUTLINE];
+ qbool hasquotes;
+ qbool restricted = false;
+ qbool have_usernames = false;
+ static char vabuf[1024];
+
+ userpass_start = rcon_password.string;
+ while((userpass_end = strchr(userpass_start, ' ')))
+ {
+ have_usernames = true;
+ strlcpy(buf, userpass_start, ((size_t)(userpass_end-userpass_start) >= sizeof(buf)) ? (int)(sizeof(buf)) : (int)(userpass_end-userpass_start+1));
+ if(buf[0]) // Ignore empty entries due to leading/duplicate space.
+ if(comparator(peeraddress, buf, password, cs, cslen))
+ goto allow;
+ userpass_start = userpass_end + 1;
+ }
+ if(userpass_start[0]) // Ignore empty trailing entry due to trailing space or password not set.
+ {
+ userpass_end = userpass_start + strlen(userpass_start);
+ if(comparator(peeraddress, userpass_start, password, cs, cslen))
+ goto allow;
+ }
+
+ restricted = true;
+ have_usernames = false;
+ userpass_start = rcon_restricted_password.string;
+ while((userpass_end = strchr(userpass_start, ' ')))
+ {
+ have_usernames = true;
+ strlcpy(buf, userpass_start, ((size_t)(userpass_end-userpass_start) >= sizeof(buf)) ? (int)(sizeof(buf)) : (int)(userpass_end-userpass_start+1));
+ if(buf[0]) // Ignore empty entries due to leading/duplicate space.
+ if(comparator(peeraddress, buf, password, cs, cslen))
+ goto check;
+ userpass_start = userpass_end + 1;
+ }
+ if(userpass_start[0]) // Ignore empty trailing entry due to trailing space or password not set.
+ {
+ userpass_end = userpass_start + strlen(userpass_start);
+ if(comparator(peeraddress, userpass_start, password, cs, cslen))
+ goto check;
+ }
+
+ return NULL; // DENIED
+
+check:
+ for(text = s; text != endpos; ++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)
+ {
+ size_t l = strlen(s);
+ if(l)
+ {
+ hasquotes = (strchr(s, '"') != NULL);
+ // sorry, we can't allow these substrings in wildcard expressions,
+ // as they can mess with the argument counts
+ text = rcon_restricted_commands.string;
+ while(COM_ParseToken_Console(&text))
+ {
+ // com_token now contains a pattern to check for...
+ if(strchr(com_token, '*') || strchr(com_token, '?')) // wildcard expression, * can only match a SINGLE argument
+ {
+ if(!hasquotes)
+ if(matchpattern_with_separator(s, com_token, true, " ", true)) // note how we excluded tab, newline etc. above
+ goto match;
+ }
+ else if(strchr(com_token, ' ')) // multi-arg expression? must match in whole
+ {
+ if(!strcmp(com_token, s))
+ goto match;
+ }
+ else // single-arg expression? must match the beginning of the command
+ {
+ if(!strcmp(com_token, s))
+ goto match;
+ if(!memcmp(va(vabuf, sizeof(vabuf), "%s ", com_token), s, strlen(com_token) + 1))
+ goto match;
+ }
+ }
+ // if we got here, nothing matched!
+ return NULL;
+ }
+match:
+ s += l + 1;
+ }
+
+allow:
+ userpass_startpass = strchr(userpass_start, ':');
+ if(have_usernames && userpass_startpass && userpass_startpass < userpass_end)
+ return va(vabuf, sizeof(vabuf), "%srcon (username %.*s)", restricted ? "restricted " : "", (int)(userpass_startpass-userpass_start), userpass_start);
+
+ return va(vabuf, sizeof(vabuf), "%srcon", restricted ? "restricted " : "");
+}
+
+static void RCon_Execute(lhnetsocket_t *mysocket, lhnetaddress_t *peeraddress, const char *addressstring2, const char *userlevel, const char *s, const char *endpos, qbool proquakeprotocol)
+{
+ 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, proquakeprotocol);
+ while(s != endpos)
+ {
+ size_t l = strlen(s);
+ if(l)
+ {
+ client_t *host_client_save = host_client;
+ Cmd_ExecuteString(cmd_local, s, src_local, true);
+ 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);
+ }
+}
+
+static int NetConn_ServerParsePacket(lhnetsocket_t *mysocket, unsigned char *data, int length, lhnetaddress_t *peeraddress)
+{
+ int i, ret, clientnum, best;
+ double besttime;
+ char *string, response[2800], addressstring2[128];
+ static char stringbuf[16384]; // server only
+ qbool islocal = (LHNETADDRESS_GetAddressType(peeraddress) == LHNETADDRESSTYPE_LOOP);
+ char senddata[NET_HEADERSIZE+NET_MAXMESSAGE+CRYPTO_HEADERSIZE];
+ size_t sendlength, response_len;
+ char infostringvalue[MAX_INPUTLINE];
+
+ if (!sv.active)
+ return false;
+
+ // convert the address to a string incase we need it
+ LHNETADDRESS_ToString(peeraddress, addressstring2, sizeof(addressstring2), true);
+
+ // see if we can identify the sender as a local player
+ // (this is necessary for rcon to send a reliable reply if the client is
+ // actually on the server, not sending remotely)
+ for (i = 0, host_client = svs.clients;i < svs.maxclients;i++, host_client++)
+ if (host_client->netconnection && host_client->netconnection->mysocket == mysocket && !LHNETADDRESS_Compare(&host_client->netconnection->peeraddress, peeraddress))
+ break;
+ if (i == svs.maxclients)
+ host_client = NULL;
+
+ if (length >= 5 && data[0] == 255 && data[1] == 255 && data[2] == 255 && data[3] == 255)
+ {
+ // received a command string - strip off the packaging and put it
+ // into our string buffer with NULL termination
+ data += 4;
+ length -= 4;
+ length = min(length, (int)sizeof(stringbuf) - 1);
+ memcpy(stringbuf, data, length);
+ stringbuf[length] = 0;
+ string = stringbuf;
+
+ if (developer_extra.integer)
+ {
+ Con_Printf("NetConn_ServerParsePacket: %s sent us a command:\n", addressstring2);
+ Com_HexDumpToConsole(data, length);
+ }
+
+ sendlength = sizeof(senddata) - 4;
+ switch(Crypto_ServerParsePacket(string, length, senddata+4, &sendlength, peeraddress))
+ {
+ case CRYPTO_NOMATCH:
+ // nothing to do
+ break;
+ case CRYPTO_MATCH:
+ if(sendlength)
+ {
+ memcpy(senddata, "\377\377\377\377", 4);
+ NetConn_Write(mysocket, senddata, (int)sendlength+4, peeraddress);
+ }
+ break;
+ case CRYPTO_DISCARD:
+ if(sendlength)
+ {
+ memcpy(senddata, "\377\377\377\377", 4);
+ NetConn_Write(mysocket, senddata, (int)sendlength+4, peeraddress);
+ }
+ return true;
+ break;
+ case CRYPTO_REPLACE:
+ string = senddata+4;
+ length = (int)sendlength;
+ break;
+ }
+
+ if (length >= 12 && !memcmp(string, "getchallenge", 12) && (islocal || sv_public.integer > -3))
+ {
+ for (i = 0, best = 0, besttime = host.realtime;i < MAX_CHALLENGES;i++)
+ {
+ if(challenges[i].time > 0)
+ if (!LHNETADDRESS_Compare(peeraddress, &challenges[i].address))
+ break;
+ if (besttime > challenges[i].time)
+ besttime = challenges[best = i].time;
+ }
+ // if we did not find an exact match, choose the oldest and
+ // update address and string
+ if (i == MAX_CHALLENGES)
+ {
+ i = best;
+ challenges[i].address = *peeraddress;
+ NetConn_BuildChallengeString(challenges[i].string, sizeof(challenges[i].string));
+ }
+ else
+ {
+ // flood control: drop if requesting challenge too often
+ if(challenges[i].time > host.realtime - net_challengefloodblockingtimeout.value)
+ return true;
+ }
+ challenges[i].time = host.realtime;
+ // send the challenge
+ memcpy(response, "\377\377\377\377", 4);
+ dpsnprintf(response+4, sizeof(response)-4, "challenge %s", challenges[i].string);
+ response_len = strlen(response) + 1;
+ Crypto_ServerAppendToChallenge(string, length, response, &response_len, sizeof(response));
+ NetConn_Write(mysocket, response, (int)response_len, peeraddress);
+ return true;
+ }
+ if (length > 8 && !memcmp(string, "connect\\", 8))
+ {
+ char *s;
+ client_t *client;
+ crypto_t *crypto = Crypto_ServerGetInstance(peeraddress);
+ string += 7;
+ length -= 7;
+
+ if(crypto && crypto->authenticated)
+ {
+ // no need to check challenge
+ if(crypto_developer.integer)
+ {
+ Con_Printf("%s connection to %s is being established: client is %s@%s%.*s, I am %.*s@%s%.*s\n",
+ crypto->use_aes ? "Encrypted" : "Authenticated",
+ addressstring2,
+ crypto->client_idfp[0] ? crypto->client_idfp : "-",
+ (crypto->client_issigned || !crypto->client_keyfp[0]) ? "" : "~",
+ crypto_keyfp_recommended_length, crypto->client_keyfp[0] ? crypto->client_keyfp : "-",
+ crypto_keyfp_recommended_length, crypto->server_idfp[0] ? crypto->server_idfp : "-",
+ (crypto->server_issigned || !crypto->server_keyfp[0]) ? "" : "~",
+ crypto_keyfp_recommended_length, crypto->server_keyfp[0] ? crypto->server_keyfp : "-"
+ );
+ }
+ }
+ else
+ {
+ if ((s = InfoString_GetValue(string, "challenge", infostringvalue, sizeof(infostringvalue))))
+ {
+ // validate the challenge
+ for (i = 0;i < MAX_CHALLENGES;i++)
+ if(challenges[i].time > 0)
+ if (!LHNETADDRESS_Compare(peeraddress, &challenges[i].address) && !strcmp(challenges[i].string, s))
+ break;
+ // if the challenge is not recognized, drop the packet
+ if (i == MAX_CHALLENGES)
+ return true;
+ }
+ }
+
+ if((s = InfoString_GetValue(string, "message", infostringvalue, sizeof(infostringvalue))))
+ Con_DPrintf("Connecting client %s sent us the message: %s\n", addressstring2, s);
+
+ if(!(islocal || sv_public.integer > -2))
+ {
+ if (developer_extra.integer)
+ Con_Printf("Datagram_ParseConnectionless: sending \"reject %s\" to %s.\n", sv_public_rejectreason.string, addressstring2);
+ memcpy(response, "\377\377\377\377", 4);
+ dpsnprintf(response+4, sizeof(response)-4, "reject %s", sv_public_rejectreason.string);
+ NetConn_WriteString(mysocket, response, peeraddress);
+ return true;
+ }
+
+ // check engine protocol
+ if(!(s = InfoString_GetValue(string, "protocol", infostringvalue, sizeof(infostringvalue))) || strcmp(s, "darkplaces 3"))
+ {
+ if (developer_extra.integer)
+ Con_Printf("Datagram_ParseConnectionless: sending \"reject Wrong game protocol.\" to %s.\n", addressstring2);
+ NetConn_WriteString(mysocket, "\377\377\377\377reject Wrong game protocol.", peeraddress);
+ return true;
+ }
+
+ // see if this is a duplicate connection request or a disconnected
+ // client who is rejoining to the same client slot
+ for (clientnum = 0, client = svs.clients;clientnum < svs.maxclients;clientnum++, client++)
+ {
+ if (client->netconnection && LHNETADDRESS_Compare(peeraddress, &client->netconnection->peeraddress) == 0)
+ {
+ // this is a known client...
+ if(crypto && crypto->authenticated)
+ {
+ // reject if changing key!
+ if(client->netconnection->crypto.authenticated)
+ {
+ if(
+ strcmp(client->netconnection->crypto.client_idfp, crypto->client_idfp)
+ ||
+ strcmp(client->netconnection->crypto.server_idfp, crypto->server_idfp)
+ ||
+ strcmp(client->netconnection->crypto.client_keyfp, crypto->client_keyfp)
+ ||
+ strcmp(client->netconnection->crypto.server_keyfp, crypto->server_keyfp)
+ )
+ {
+ if (developer_extra.integer)
+ Con_Printf("Datagram_ParseConnectionless: sending \"reject Attempt to change key of crypto.\" to %s.\n", addressstring2);
+ NetConn_WriteString(mysocket, "\377\377\377\377reject Attempt to change key of crypto.", peeraddress);
+ return true;
+ }
+ }
+ }
+ else
+ {
+ // reject if downgrading!
+ if(client->netconnection->crypto.authenticated)
+ {
+ if (developer_extra.integer)
+ Con_Printf("Datagram_ParseConnectionless: sending \"reject Attempt to downgrade crypto.\" to %s.\n", addressstring2);
+ NetConn_WriteString(mysocket, "\377\377\377\377reject Attempt to downgrade crypto.", peeraddress);
+ return true;
+ }
+ }
+ if (client->begun)
+ {
+ // client crashed and is coming back,
+ // keep their stuff intact
+ if (developer_extra.integer)
+ Con_Printf("Datagram_ParseConnectionless: sending \"accept\" to %s.\n", addressstring2);
+ NetConn_WriteString(mysocket, "\377\377\377\377accept", peeraddress);
+ if(crypto && crypto->authenticated)
+ Crypto_FinishInstance(&client->netconnection->crypto, crypto);
+ SV_SendServerinfo(client);
+ }
+ else
+ {
+ // client is still trying to connect,
+ // so we send a duplicate reply
+ if (developer_extra.integer)
+ Con_Printf("Datagram_ParseConnectionless: sending duplicate accept to %s.\n", addressstring2);
+ if(crypto && crypto->authenticated)
+ Crypto_FinishInstance(&client->netconnection->crypto, crypto);
+ NetConn_WriteString(mysocket, "\377\377\377\377accept", peeraddress);
+ }
+ return true;
+ }
+ }
+
+ if (NetConn_PreventFlood(peeraddress, sv.connectfloodaddresses, sizeof(sv.connectfloodaddresses) / sizeof(sv.connectfloodaddresses[0]), net_connectfloodblockingtimeout.value, true))
+ return true;
+
+ // find an empty client slot for this new client
+ for (clientnum = 0, client = svs.clients;clientnum < svs.maxclients;clientnum++, client++)
+ {
+ netconn_t *conn;
+ if (!client->active && (conn = NetConn_Open(mysocket, peeraddress)))
+ {
+ // allocated connection
+ if (developer_extra.integer)
+ Con_Printf("Datagram_ParseConnectionless: sending \"accept\" to %s.\n", conn->address);
+ NetConn_WriteString(mysocket, "\377\377\377\377accept", peeraddress);
+ // now set up the client
+ if(crypto && crypto->authenticated)
+ Crypto_FinishInstance(&conn->crypto, crypto);
+ SV_ConnectClient(clientnum, conn);
+ NetConn_Heartbeat(1);
+ return true;
+ }
+ }
+
+ // no empty slots found - server is full
+ if (developer_extra.integer)
+ Con_Printf("Datagram_ParseConnectionless: sending \"reject Server is full.\" to %s.\n", addressstring2);
+ NetConn_WriteString(mysocket, "\377\377\377\377reject Server is full.", peeraddress);
+
+ return true;
+ }
+ if (length >= 7 && !memcmp(string, "getinfo", 7) && (islocal || sv_public.integer > -1))
+ {
+ const char *challenge = NULL;
+
+ if (NetConn_PreventFlood(peeraddress, sv.getstatusfloodaddresses, sizeof(sv.getstatusfloodaddresses) / sizeof(sv.getstatusfloodaddresses[0]), net_getstatusfloodblockingtimeout.value, false))
+ return true;
+
+ // If there was a challenge in the getinfo message
+ if (length > 8 && string[7] == ' ')
+ challenge = string + 8;
+
+ if (NetConn_BuildStatusResponse(challenge, response, sizeof(response), false))
+ {
+ if (developer_extra.integer)
+ Con_DPrintf("Sending reply to master %s - %s\n", addressstring2, response);
+ NetConn_WriteString(mysocket, response, peeraddress);
+ }
+ return true;
+ }
+ if (length >= 9 && !memcmp(string, "getstatus", 9) && (islocal || sv_public.integer > -1))
+ {
+ const char *challenge = NULL;
+
+ if (NetConn_PreventFlood(peeraddress, sv.getstatusfloodaddresses, sizeof(sv.getstatusfloodaddresses) / sizeof(sv.getstatusfloodaddresses[0]), net_getstatusfloodblockingtimeout.value, false))
+ return true;
+
+ // If there was a challenge in the getinfo message
+ if (length > 10 && string[9] == ' ')
+ challenge = string + 10;
+
+ if (NetConn_BuildStatusResponse(challenge, response, sizeof(response), true))
+ {
+ if (developer_extra.integer)
+ Con_DPrintf("Sending reply to client %s - %s\n", addressstring2, response);
+ NetConn_WriteString(mysocket, response, peeraddress);
+ }
+ 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(rcon_secure.integer > 1)
+ return true;