]> git.xonotic.org Git - xonotic/darkplaces.git/blob - cl_cmd.c
(Round 6) Break up host_cmd.c
[xonotic/darkplaces.git] / cl_cmd.c
1 /*
2 Copyright (C) 1996-1997 Id Software, Inc.
3
4 This program is free software; you can redistribute it and/or
5 modify it under the terms of the GNU General Public License
6 as published by the Free Software Foundation; either version 2
7 of the License, or (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12
13 See the GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
18
19 */
20
21 #include "quakedef.h"
22
23 // for secure rcon authentication
24 #include "hmac.h"
25 #include "mdfour.h"
26 #include <time.h>
27
28 cvar_t rcon_password = {CVAR_CLIENT | CVAR_SERVER | CVAR_PRIVATE, "rcon_password", "", "password to authenticate rcon commands; NOTE: changing rcon_secure clears rcon_password, so set rcon_secure always before rcon_password; may be set to a string of the form user1:pass1 user2:pass2 user3:pass3 to allow multiple user accounts - the client then has to specify ONE of these combinations"};
29 cvar_t rcon_secure = {CVAR_CLIENT | CVAR_SERVER, "rcon_secure", "0", "force secure rcon authentication (1 = time based, 2 = challenge based); NOTE: changing rcon_secure clears rcon_password, so set rcon_secure always before rcon_password"};
30 cvar_t rcon_secure_challengetimeout = {CVAR_CLIENT, "rcon_secure_challengetimeout", "5", "challenge-based secure rcon: time out requests if no challenge came within this time interval"};
31 cvar_t rcon_address = {CVAR_CLIENT, "rcon_address", "", "server address to send rcon commands to (when not connected to a server)"};
32 cvar_t cl_name = {CVAR_CLIENT | CVAR_SAVE | CVAR_USERINFO, "name", "player", "change your player name"};
33 cvar_t cl_topcolor = {CVAR_CLIENT | CVAR_SAVE | CVAR_USERINFO, "topcolor", "0", "change the color of your shirt"};
34 cvar_t cl_bottomcolor = {CVAR_CLIENT | CVAR_SAVE | CVAR_USERINFO, "bottomcolor", "0", "change the color of your pants"};
35
36 /*
37 ===================
38 CL_ForwardToServer
39
40 Sends an entire command string over to the server, unprocessed
41 ===================
42 */
43 void CL_ForwardToServer (const char *s)
44 {
45         char temp[128];
46         if (cls.state != ca_connected)
47         {
48                 Con_Printf("Can't \"%s\", not connected\n", s);
49                 return;
50         }
51
52         if (!cls.netcon)
53                 return;
54
55         // LadyHavoc: thanks to Fuh for bringing the pure evil of SZ_Print to my
56         // attention, it has been eradicated from here, its only (former) use in
57         // all of darkplaces.
58         if (cls.protocol == PROTOCOL_QUAKEWORLD)
59                 MSG_WriteByte(&cls.netcon->message, qw_clc_stringcmd);
60         else
61                 MSG_WriteByte(&cls.netcon->message, clc_stringcmd);
62         if ((!strncmp(s, "say ", 4) || !strncmp(s, "say_team ", 9)) && cl_locs_enable.integer)
63         {
64                 // say/say_team commands can replace % character codes with status info
65                 while (*s)
66                 {
67                         if (*s == '%' && s[1])
68                         {
69                                 // handle proquake message macros
70                                 temp[0] = 0;
71                                 switch (s[1])
72                                 {
73                                 case 'l': // current location
74                                         CL_Locs_FindLocationName(temp, sizeof(temp), cl.movement_origin);
75                                         break;
76                                 case 'h': // current health
77                                         dpsnprintf(temp, sizeof(temp), "%i", cl.stats[STAT_HEALTH]);
78                                         break;
79                                 case 'a': // current armor
80                                         dpsnprintf(temp, sizeof(temp), "%i", cl.stats[STAT_ARMOR]);
81                                         break;
82                                 case 'x': // current rockets
83                                         dpsnprintf(temp, sizeof(temp), "%i", cl.stats[STAT_ROCKETS]);
84                                         break;
85                                 case 'c': // current cells
86                                         dpsnprintf(temp, sizeof(temp), "%i", cl.stats[STAT_CELLS]);
87                                         break;
88                                 // silly proquake macros
89                                 case 'd': // loc at last death
90                                         CL_Locs_FindLocationName(temp, sizeof(temp), cl.lastdeathorigin);
91                                         break;
92                                 case 't': // current time
93                                         dpsnprintf(temp, sizeof(temp), "%.0f:%.0f", floor(cl.time / 60), cl.time - floor(cl.time / 60) * 60);
94                                         break;
95                                 case 'r': // rocket launcher status ("I have RL", "I need rockets", "I need RL")
96                                         if (!(cl.stats[STAT_ITEMS] & IT_ROCKET_LAUNCHER))
97                                                 dpsnprintf(temp, sizeof(temp), "I need RL");
98                                         else if (!cl.stats[STAT_ROCKETS])
99                                                 dpsnprintf(temp, sizeof(temp), "I need rockets");
100                                         else
101                                                 dpsnprintf(temp, sizeof(temp), "I have RL");
102                                         break;
103                                 case 'p': // powerup status (outputs "quad" "pent" and "eyes" according to status)
104                                         if (cl.stats[STAT_ITEMS] & IT_QUAD)
105                                         {
106                                                 if (temp[0])
107                                                         strlcat(temp, " ", sizeof(temp));
108                                                 strlcat(temp, "quad", sizeof(temp));
109                                         }
110                                         if (cl.stats[STAT_ITEMS] & IT_INVULNERABILITY)
111                                         {
112                                                 if (temp[0])
113                                                         strlcat(temp, " ", sizeof(temp));
114                                                 strlcat(temp, "pent", sizeof(temp));
115                                         }
116                                         if (cl.stats[STAT_ITEMS] & IT_INVISIBILITY)
117                                         {
118                                                 if (temp[0])
119                                                         strlcat(temp, " ", sizeof(temp));
120                                                 strlcat(temp, "eyes", sizeof(temp));
121                                         }
122                                         break;
123                                 case 'w': // weapon status (outputs "SSG:NG:SNG:GL:RL:LG" with the text between : characters omitted if you lack the weapon)
124                                         if (cl.stats[STAT_ITEMS] & IT_SUPER_SHOTGUN)
125                                                 strlcat(temp, "SSG", sizeof(temp));
126                                         strlcat(temp, ":", sizeof(temp));
127                                         if (cl.stats[STAT_ITEMS] & IT_NAILGUN)
128                                                 strlcat(temp, "NG", sizeof(temp));
129                                         strlcat(temp, ":", sizeof(temp));
130                                         if (cl.stats[STAT_ITEMS] & IT_SUPER_NAILGUN)
131                                                 strlcat(temp, "SNG", sizeof(temp));
132                                         strlcat(temp, ":", sizeof(temp));
133                                         if (cl.stats[STAT_ITEMS] & IT_GRENADE_LAUNCHER)
134                                                 strlcat(temp, "GL", sizeof(temp));
135                                         strlcat(temp, ":", sizeof(temp));
136                                         if (cl.stats[STAT_ITEMS] & IT_ROCKET_LAUNCHER)
137                                                 strlcat(temp, "RL", sizeof(temp));
138                                         strlcat(temp, ":", sizeof(temp));
139                                         if (cl.stats[STAT_ITEMS] & IT_LIGHTNING)
140                                                 strlcat(temp, "LG", sizeof(temp));
141                                         break;
142                                 default:
143                                         // not a recognized macro, print it as-is...
144                                         temp[0] = s[0];
145                                         temp[1] = s[1];
146                                         temp[2] = 0;
147                                         break;
148                                 }
149                                 // write the resulting text
150                                 SZ_Write(&cls.netcon->message, (unsigned char *)temp, (int)strlen(temp));
151                                 s += 2;
152                                 continue;
153                         }
154                         MSG_WriteByte(&cls.netcon->message, *s);
155                         s++;
156                 }
157                 MSG_WriteByte(&cls.netcon->message, 0);
158         }
159         else // any other command is passed on as-is
160                 SZ_Write(&cls.netcon->message, (const unsigned char *)s, (int)strlen(s) + 1);
161 }
162
163 void CL_ForwardToServer_f (cmd_state_t *cmd)
164 {
165         const char *s;
166         char vabuf[1024];
167         if (!strcasecmp(Cmd_Argv(cmd, 0), "cmd"))
168         {
169                 // we want to strip off "cmd", so just send the args
170                 s = Cmd_Argc(cmd) > 1 ? Cmd_Args(cmd) : "";
171         }
172         else
173         {
174                 // we need to keep the command name, so send Cmd_Argv(cmd, 0), a space and then Cmd_Args(cmd)
175                 s = va(vabuf, sizeof(vabuf), "%s %s", Cmd_Argv(cmd, 0), Cmd_Argc(cmd) > 1 ? Cmd_Args(cmd) : "");
176         }
177         // don't send an empty forward message if the user tries "cmd" by itself
178         if (!s || !*s)
179                 return;
180         CL_ForwardToServer(s);
181 }
182
183 /*
184 ==================
185 CL_Color_f
186 ==================
187 */
188 cvar_t cl_color = {CVAR_READONLY | CVAR_CLIENT | CVAR_SAVE, "_cl_color", "0", "internal storage cvar for current player colors (changed by color command)"};
189
190 // Ignore the callbacks so this two-to-three way synchronization doesn't cause an infinite loop.
191 static void CL_Color_c(cvar_t *var)
192 {
193         char vabuf[1024];
194         
195         Cvar_Set_NoCallback(&cl_topcolor, va(vabuf, sizeof(vabuf), "%i", ((var->integer >> 4) & 15)));
196         Cvar_Set_NoCallback(&cl_bottomcolor, va(vabuf, sizeof(vabuf), "%i", (var->integer & 15)));
197 }
198
199 static void CL_Topcolor_c(cvar_t *var)
200 {
201         char vabuf[1024];
202         
203         Cvar_Set_NoCallback(&cl_color, va(vabuf, sizeof(vabuf), "%i", var->integer*16 + cl_bottomcolor.integer));
204 }
205
206 static void CL_Bottomcolor_c(cvar_t *var)
207 {
208         char vabuf[1024];
209
210         Cvar_Set_NoCallback(&cl_color, va(vabuf, sizeof(vabuf), "%i", cl_topcolor.integer*16 + var->integer));
211 }
212
213 static void CL_Color_f(cmd_state_t *cmd)
214 {
215         int top, bottom;
216
217         if (Cmd_Argc(cmd) == 1)
218         {
219                 if (cmd->source == src_command)
220                 {
221                         Con_Printf("\"color\" is \"%i %i\"\n", cl_topcolor.integer, cl_bottomcolor.integer);
222                         Con_Print("color <0-15> [0-15]\n");
223                 }
224                 return;
225         }
226
227         if (Cmd_Argc(cmd) == 2)
228                 top = bottom = atoi(Cmd_Argv(cmd, 1));
229         else
230         {
231                 top = atoi(Cmd_Argv(cmd, 1));
232                 bottom = atoi(Cmd_Argv(cmd, 2));
233         }
234         /*
235          * This is just a convenient way to change topcolor and bottomcolor
236          * We can't change cl_color from here directly because topcolor and
237          * bottomcolor may be changed separately and do not call this function.
238          * So it has to be changed when the userinfo strings are updated, which
239          * happens twice here. Perhaps find a cleaner way?
240          */
241
242         top = top >= 0 ? top : cl_topcolor.integer;
243         bottom = bottom >= 0 ? bottom : cl_bottomcolor.integer;
244
245         top &= 15;
246         bottom &= 15;
247
248         // LadyHavoc: allowing skin colormaps 14 and 15 by commenting this out
249         //if (top > 13)
250         //      top = 13;
251         //if (bottom > 13)
252         //      bottom = 13;
253
254         if (cmd->source == src_command)
255         {
256                 Cvar_SetValueQuick(&cl_topcolor, top);
257                 Cvar_SetValueQuick(&cl_bottomcolor, bottom);
258                 return;
259         }
260 }
261
262 /*
263 ====================
264 CL_Packet_f
265
266 packet <destination> <contents>
267
268 Contents allows \n escape character
269 ====================
270 */
271 static void CL_Packet_f(cmd_state_t *cmd) // credit: taken from QuakeWorld
272 {
273         char send[2048];
274         int i, l;
275         const char *in;
276         char *out;
277         lhnetaddress_t address;
278         lhnetsocket_t *mysocket;
279
280         if (Cmd_Argc(cmd) != 3)
281         {
282                 Con_Printf ("packet <destination> <contents>\n");
283                 return;
284         }
285
286         if (!LHNETADDRESS_FromString (&address, Cmd_Argv(cmd, 1), sv_netport.integer))
287         {
288                 Con_Printf ("Bad address\n");
289                 return;
290         }
291
292         in = Cmd_Argv(cmd, 2);
293         out = send+4;
294         send[0] = send[1] = send[2] = send[3] = -1;
295
296         l = (int)strlen (in);
297         for (i=0 ; i<l ; i++)
298         {
299                 if (out >= send + sizeof(send) - 1)
300                         break;
301                 if (in[i] == '\\' && in[i+1] == 'n')
302                 {
303                         *out++ = '\n';
304                         i++;
305                 }
306                 else if (in[i] == '\\' && in[i+1] == '0')
307                 {
308                         *out++ = '\0';
309                         i++;
310                 }
311                 else if (in[i] == '\\' && in[i+1] == 't')
312                 {
313                         *out++ = '\t';
314                         i++;
315                 }
316                 else if (in[i] == '\\' && in[i+1] == 'r')
317                 {
318                         *out++ = '\r';
319                         i++;
320                 }
321                 else if (in[i] == '\\' && in[i+1] == '"')
322                 {
323                         *out++ = '\"';
324                         i++;
325                 }
326                 else
327                         *out++ = in[i];
328         }
329
330         mysocket = NetConn_ChooseClientSocketForAddress(&address);
331         if (!mysocket)
332                 mysocket = NetConn_ChooseServerSocketForAddress(&address);
333         if (mysocket)
334                 NetConn_Write(mysocket, send, out - send, &address);
335 }
336
337 /*
338 =====================
339 CL_PQRcon_f
340
341 ProQuake rcon support
342 =====================
343 */
344 static void CL_PQRcon_f(cmd_state_t *cmd)
345 {
346         int n;
347         const char *e;
348         lhnetsocket_t *mysocket;
349
350         if (Cmd_Argc(cmd) == 1)
351         {
352                 Con_Printf("%s: Usage: %s command\n", Cmd_Argv(cmd, 0), Cmd_Argv(cmd, 0));
353                 return;
354         }
355
356         if (!rcon_password.string || !rcon_password.string[0] || rcon_secure.integer > 0)
357         {
358                 Con_Printf ("You must set rcon_password before issuing an pqrcon command, and rcon_secure must be 0.\n");
359                 return;
360         }
361
362         e = strchr(rcon_password.string, ' ');
363         n = e ? e-rcon_password.string : (int)strlen(rcon_password.string);
364
365         if (cls.netcon)
366                 cls.rcon_address = cls.netcon->peeraddress;
367         else
368         {
369                 if (!rcon_address.string[0])
370                 {
371                         Con_Printf ("You must either be connected, or set the rcon_address cvar to issue rcon commands\n");
372                         return;
373                 }
374                 LHNETADDRESS_FromString(&cls.rcon_address, rcon_address.string, sv_netport.integer);
375         }
376         mysocket = NetConn_ChooseClientSocketForAddress(&cls.rcon_address);
377         if (mysocket)
378         {
379                 sizebuf_t buf;
380                 unsigned char bufdata[64];
381                 buf.data = bufdata;
382                 SZ_Clear(&buf);
383                 MSG_WriteLong(&buf, 0);
384                 MSG_WriteByte(&buf, CCREQ_RCON);
385                 SZ_Write(&buf, (const unsigned char*)rcon_password.string, n);
386                 MSG_WriteByte(&buf, 0); // terminate the (possibly partial) string
387                 MSG_WriteString(&buf, Cmd_Args(cmd));
388                 StoreBigLong(buf.data, NETFLAG_CTL | (buf.cursize & NETFLAG_LENGTH_MASK));
389                 NetConn_Write(mysocket, buf.data, buf.cursize, &cls.rcon_address);
390                 SZ_Clear(&buf);
391         }
392 }
393
394 /*
395 =====================
396 CL_Rcon_f
397
398   Send the rest of the command line over as
399   an unconnected command.
400 =====================
401 */
402 static void CL_Rcon_f(cmd_state_t *cmd) // credit: taken from QuakeWorld
403 {
404         int i, n;
405         const char *e;
406         lhnetsocket_t *mysocket;
407
408         if (Cmd_Argc(cmd) == 1)
409         {
410                 Con_Printf("%s: Usage: %s command\n", Cmd_Argv(cmd, 0), Cmd_Argv(cmd, 0));
411                 return;
412         }
413
414         if (!rcon_password.string || !rcon_password.string[0])
415         {
416                 Con_Printf ("You must set rcon_password before issuing an rcon command.\n");
417                 return;
418         }
419
420         e = strchr(rcon_password.string, ' ');
421         n = e ? e-rcon_password.string : (int)strlen(rcon_password.string);
422
423         if (cls.netcon)
424                 cls.rcon_address = cls.netcon->peeraddress;
425         else
426         {
427                 if (!rcon_address.string[0])
428                 {
429                         Con_Printf ("You must either be connected, or set the rcon_address cvar to issue rcon commands\n");
430                         return;
431                 }
432                 LHNETADDRESS_FromString(&cls.rcon_address, rcon_address.string, sv_netport.integer);
433         }
434         mysocket = NetConn_ChooseClientSocketForAddress(&cls.rcon_address);
435         if (mysocket && Cmd_Args(cmd)[0])
436         {
437                 // simply put together the rcon packet and send it
438                 if(Cmd_Argv(cmd, 0)[0] == 's' || rcon_secure.integer > 1)
439                 {
440                         if(cls.rcon_commands[cls.rcon_ringpos][0])
441                         {
442                                 char s[128];
443                                 LHNETADDRESS_ToString(&cls.rcon_addresses[cls.rcon_ringpos], s, sizeof(s), true);
444                                 Con_Printf("rcon to %s (for command %s) failed: too many buffered commands (possibly increase MAX_RCONS)\n", s, cls.rcon_commands[cls.rcon_ringpos]);
445                                 cls.rcon_commands[cls.rcon_ringpos][0] = 0;
446                                 --cls.rcon_trying;
447                         }
448                         for (i = 0;i < MAX_RCONS;i++)
449                                 if(cls.rcon_commands[i][0])
450                                         if (!LHNETADDRESS_Compare(&cls.rcon_address, &cls.rcon_addresses[i]))
451                                                 break;
452                         ++cls.rcon_trying;
453                         if(i >= MAX_RCONS)
454                                 NetConn_WriteString(mysocket, "\377\377\377\377getchallenge", &cls.rcon_address); // otherwise we'll request the challenge later
455                         strlcpy(cls.rcon_commands[cls.rcon_ringpos], Cmd_Args(cmd), sizeof(cls.rcon_commands[cls.rcon_ringpos]));
456                         cls.rcon_addresses[cls.rcon_ringpos] = cls.rcon_address;
457                         cls.rcon_timeout[cls.rcon_ringpos] = host.realtime + rcon_secure_challengetimeout.value;
458                         cls.rcon_ringpos = (cls.rcon_ringpos + 1) % MAX_RCONS;
459                 }
460                 else if(rcon_secure.integer > 0)
461                 {
462                         char buf[1500];
463                         char argbuf[1500];
464                         dpsnprintf(argbuf, sizeof(argbuf), "%ld.%06d %s", (long) time(NULL), (int) (rand() % 1000000), Cmd_Args(cmd));
465                         memcpy(buf, "\377\377\377\377srcon HMAC-MD4 TIME ", 24);
466                         if(HMAC_MDFOUR_16BYTES((unsigned char *) (buf + 24), (unsigned char *) argbuf, (int)strlen(argbuf), (unsigned char *) rcon_password.string, n))
467                         {
468                                 buf[40] = ' ';
469                                 strlcpy(buf + 41, argbuf, sizeof(buf) - 41);
470                                 NetConn_Write(mysocket, buf, 41 + (int)strlen(buf + 41), &cls.rcon_address);
471                         }
472                 }
473                 else
474                 {
475                         char buf[1500];
476                         memcpy(buf, "\377\377\377\377", 4);
477                         dpsnprintf(buf+4, sizeof(buf)-4, "rcon %.*s %s",  n, rcon_password.string, Cmd_Args(cmd));
478                         NetConn_WriteString(mysocket, buf, &cls.rcon_address);
479                 }
480         }
481 }
482
483 static void CL_RCon_ClearPassword_c(cvar_t *var)
484 {
485         // whenever rcon_secure is changed to 0, clear rcon_password for
486         // security reasons (prevents a send-rcon-password-as-plaintext
487         // attack based on NQ protocol session takeover and svc_stufftext)
488         if(var->integer <= 0)
489                 Cvar_SetQuick(&rcon_password, "");
490 }
491
492 void CL_InitCommands(void)
493 {
494         Cvar_RegisterVariable(&cl_color);
495         Cvar_RegisterCallback(&cl_color, CL_Color_c);
496         Cvar_RegisterVariable(&cl_topcolor);
497         Cvar_RegisterCallback(&cl_topcolor, CL_Topcolor_c);
498         Cvar_RegisterVariable(&cl_bottomcolor);
499         Cvar_RegisterCallback(&cl_bottomcolor, CL_Bottomcolor_c);
500         Cvar_RegisterVariable(&rcon_address);
501         Cvar_RegisterVariable(&rcon_secure);
502         Cvar_RegisterCallback(&rcon_secure, CL_RCon_ClearPassword_c);
503         Cvar_RegisterVariable(&rcon_secure_challengetimeout);
504
505         Cmd_AddCommand(CMD_CLIENT | CMD_CLIENT_FROM_SERVER, "cmd", CL_ForwardToServer_f, "send a console commandline to the server (used by some mods)");
506         Cmd_AddCommand(CMD_CLIENT, "color", CL_Color_f, "change your player shirt and pants colors");
507         Cmd_AddCommand(CMD_CLIENT, "rcon", CL_Rcon_f, "sends a command to the server console (if your rcon_password matches the server's rcon_password), or to the address specified by rcon_address when not connected (again rcon_password must match the server's); note: if rcon_secure is set, client and server clocks must be synced e.g. via NTP");
508         Cmd_AddCommand(CMD_CLIENT, "srcon", CL_Rcon_f, "sends a command to the server console (if your rcon_password matches the server's rcon_password), or to the address specified by rcon_address when not connected (again rcon_password must match the server's); this always works as if rcon_secure is set; note: client and server clocks must be synced e.g. via NTP");
509         Cmd_AddCommand(CMD_CLIENT, "pqrcon", CL_PQRcon_f, "sends a command to a proquake server console (if your rcon_password matches the server's rcon_password), or to the address specified by rcon_address when not connected (again rcon_password must match the server's)");
510         Cmd_AddCommand(CMD_CLIENT, "packet", CL_Packet_f, "send a packet to the specified address:port containing a text string");
511         
512 }