]> de.git.xonotic.org Git - voretournament/voretournament.git/blob - data/qcsrc/server/ipban.qc
Fix my last fix
[voretournament/voretournament.git] / data / qcsrc / server / ipban.qc
1 /*\r
2  * Protocol of online ban list:\r
3  *\r
4  * - Reporting a ban:\r
5  *     GET g_ban_sync_uri?action=ban&hostname=...&ip=xxx.xxx.xxx&duration=nnnn&reason=...................\r
6  *     (IP 1, 2, 3, or 4 octets, 3 octets for example is a /24 mask)\r
7  * - Removing a ban:\r
8  *     GET g_ban_sync_uri?action=unban&hostname=...&ip=xxx.xxx.xxx\r
9  * - Querying the ban list\r
10  *     GET g_ban_sync_uri?action=list&hostname=...&servers=xxx.xxx.xxx.xxx;xxx.xxx.xxx.xxx;...\r
11  *     \r
12  *     shows the bans from the listed servers, and possibly others.\r
13  *     Format of a ban is ASCII plain text, four lines per ban, delimited by\r
14  *     newline ONLY (no carriage return):\r
15  *\r
16  *     IP address (also 1, 2, 3, or 4 octets, delimited by dot)\r
17  *     time left in seconds\r
18  *     reason of the ban\r
19  *     server IP that registered the ban\r
20  */\r
21 \r
22 float Ban_Insert(string ip, float bantime, string reason, float dosync);\r
23 \r
24 void OnlineBanList_SendBan(string ip, float bantime, string reason)\r
25 {\r
26         string uri;\r
27         float i, n;\r
28 \r
29         uri = strcat(     "action=ban&hostname=", uri_escape(cvar_string("hostname")));\r
30         uri = strcat(uri, "&ip=", uri_escape(ip));\r
31         uri = strcat(uri, "&duration=", ftos(bantime));\r
32         uri = strcat(uri, "&reason=", uri_escape(reason));\r
33 \r
34         n = tokenize_console(cvar_string("g_ban_sync_uri"));\r
35         if(n >= MAX_IPBAN_URIS)\r
36                 n = MAX_IPBAN_URIS;\r
37         for(i = 0; i < n; ++i)\r
38         {\r
39                 if(strstrofs(argv(i), "?", 0) >= 0)\r
40                         uri_get(strcat(argv(i), "&", uri), URI_GET_DISCARD); // 0 = "discard" callback target\r
41                 else\r
42                         uri_get(strcat(argv(i), "?", uri), URI_GET_DISCARD); // 0 = "discard" callback target\r
43         }\r
44 }\r
45 \r
46 void OnlineBanList_SendUnban(string ip)\r
47 {\r
48         string uri;\r
49         float i, n;\r
50 \r
51         uri = strcat(     "action=unban&hostname=", uri_escape(cvar_string("hostname")));\r
52         uri = strcat(uri, "&ip=", uri_escape(ip));\r
53 \r
54         n = tokenize_console(cvar_string("g_ban_sync_uri"));\r
55         if(n >= MAX_IPBAN_URIS)\r
56                 n = MAX_IPBAN_URIS;\r
57         for(i = 0; i < n; ++i)\r
58         {\r
59                 if(strstrofs(argv(i), "?", 0) >= 0)\r
60                         uri_get(strcat(argv(i), "&", uri), URI_GET_DISCARD); // 0 = "discard" callback target\r
61                 else\r
62                         uri_get(strcat(argv(i), "?", uri), URI_GET_DISCARD); // 0 = "discard" callback target\r
63         }\r
64 }\r
65 \r
66 string OnlineBanList_Servers;\r
67 float OnlineBanList_Timeout;\r
68 float OnlineBanList_RequestWaiting[MAX_IPBAN_URIS];\r
69 \r
70 void OnlineBanList_URI_Get_Callback(float id, float status, string data)\r
71 {\r
72         float n, i, j, l;\r
73         string ip;\r
74         float timeleft;\r
75         string reason;\r
76         string serverip;\r
77         float syncinterval;\r
78         string uri;\r
79 \r
80         id -= URI_GET_IPBAN;\r
81 \r
82         if(id >= MAX_IPBAN_URIS)\r
83         {\r
84                 print("Received ban list for invalid ID\n");\r
85                 return;\r
86         }\r
87 \r
88         tokenize_console(cvar_string("g_ban_sync_uri"));\r
89         uri = argv(id);\r
90 \r
91         print("Received ban list from ", uri, ": ");\r
92 \r
93         if(OnlineBanList_RequestWaiting[id] == 0)\r
94         {\r
95                 print("rejected (unexpected)\n");\r
96                 return;\r
97         }\r
98 \r
99         OnlineBanList_RequestWaiting[id] = 0;\r
100 \r
101         if(time > OnlineBanList_Timeout)\r
102         {\r
103                 print("rejected (too late)\n");\r
104                 return;\r
105         }\r
106 \r
107         syncinterval = cvar("g_ban_sync_interval");\r
108         if(syncinterval == 0)\r
109         {\r
110                 print("rejected (syncing disabled)\n");\r
111                 return;\r
112         }\r
113         if(syncinterval > 0)\r
114                 syncinterval *= 60;\r
115         \r
116         if(status != 0)\r
117         {\r
118                 print("error: status is ", ftos(status), "\n");\r
119                 return;\r
120         }\r
121 \r
122         if(substring(data, 0, 1) == "<")\r
123         {\r
124                 print("error: received HTML instead of a ban list\n");\r
125                 return;\r
126         }\r
127 \r
128         if(strstrofs(data, "\r", 0) != -1)\r
129         {\r
130                 print("error: received carriage returns\n");\r
131                 return;\r
132         }\r
133 \r
134         if(data == "")\r
135                 n = 0;\r
136         else\r
137                 n = tokenizebyseparator(data, "\n");\r
138 \r
139         if(mod(n, 4) != 0)\r
140         {\r
141                 print("error: received invalid item count: ", ftos(n), "\n");\r
142                 return;\r
143         }\r
144 \r
145         print("OK, ", ftos(n / 4), " items\n");\r
146 \r
147         for(i = 0; i < n; i += 4)\r
148         {\r
149                 ip = argv(i);\r
150                 timeleft = stof(argv(i + 1));\r
151                 reason = argv(i + 2);\r
152                 serverip = argv(i + 3);\r
153 \r
154                 dprint("received ban list item ", ftos(i / 4), ": ip=", ip);\r
155                 dprint(" timeleft=", ftos(timeleft), " reason=", reason);\r
156                 dprint(" serverip=", serverip, "\n");\r
157 \r
158                 timeleft -= 1.5 * cvar("g_ban_sync_timeout");\r
159                 if(timeleft < 0)\r
160                         continue;\r
161 \r
162                 l = strlen(ip);\r
163                 for(j = 0; j < l; ++j)\r
164                         if(strstrofs("0123456789.", substring(ip, j, 1), 0) == -1)\r
165                         {\r
166                                 print("Invalid character ", substring(ip, j, 1), " in IP address ", ip, ". Skipping this ban.\n");\r
167                                 goto skip;\r
168                         }\r
169 \r
170                 if(cvar("g_ban_sync_trusted_servers_verify"))\r
171                         if((strstrofs(strcat(";", OnlineBanList_Servers, ";"), strcat(";", serverip, ";"), 0) == -1))\r
172                                 continue;\r
173 \r
174                 if(syncinterval > 0)\r
175                         timeleft = min(syncinterval + (OnlineBanList_Timeout - time) + 5, timeleft);\r
176                         // the ban will be prolonged on the next sync\r
177                         // or expire 5 seconds after the next timeout\r
178                 Ban_Insert(ip, timeleft, strcat("ban synced from ", serverip, " at ", uri), 0);\r
179                 print("Ban list syncing: accepted ban of ", ip, " by ", serverip, " at ", uri, ": ");\r
180                 print(reason, "\n");\r
181 \r
182 :skip\r
183         }\r
184 }\r
185 \r
186 void OnlineBanList_Think()\r
187 {\r
188         float argc;\r
189         string uri;\r
190         float i, n;\r
191         \r
192         if(cvar_string("g_ban_sync_uri") == "")\r
193                 goto killme;\r
194         if(cvar("g_ban_sync_interval") == 0) // < 0 is okay, it means "sync on level start only"\r
195                 goto killme;\r
196         argc = tokenize_console(cvar_string("g_ban_sync_trusted_servers"));\r
197         if(argc == 0)\r
198                 goto killme;\r
199 \r
200         if(OnlineBanList_Servers)\r
201                 strunzone(OnlineBanList_Servers);\r
202         OnlineBanList_Servers = argv(0);\r
203         for(i = 1; i < argc; ++i)\r
204                 OnlineBanList_Servers = strcat(OnlineBanList_Servers, ";", argv(i));\r
205         OnlineBanList_Servers = strzone(OnlineBanList_Servers);\r
206         \r
207         uri = strcat(     "action=list&hostname=", uri_escape(cvar_string("hostname")));\r
208         uri = strcat(uri, "&servers=", uri_escape(OnlineBanList_Servers));\r
209 \r
210         OnlineBanList_Timeout = time + cvar("g_ban_sync_timeout");\r
211 \r
212         n = tokenize_console(cvar_string("g_ban_sync_uri"));\r
213         if(n >= MAX_IPBAN_URIS)\r
214                 n = MAX_IPBAN_URIS;\r
215         for(i = 0; i < n; ++i)\r
216         {\r
217                 if(OnlineBanList_RequestWaiting[i])\r
218                         continue;\r
219                 OnlineBanList_RequestWaiting[i] = 1;\r
220                 if(strstrofs(argv(i), "?", 0) >= 0)\r
221                         uri_get(strcat(argv(i), "&", uri), URI_GET_IPBAN + i); // 1000 = "banlist" callback target\r
222                 else\r
223                         uri_get(strcat(argv(i), "?", uri), URI_GET_IPBAN + i); // 1000 = "banlist" callback target\r
224         }\r
225         \r
226         if(cvar("g_ban_sync_interval") > 0)\r
227                 self.nextthink = time + max(60, cvar("g_ban_sync_interval") * 60);\r
228         else\r
229                 goto killme;\r
230         return;\r
231 \r
232 :killme\r
233         remove(self);\r
234 }\r
235 \r
236 #define BAN_MAX 256\r
237 float ban_loaded;\r
238 string ban_ip[BAN_MAX];\r
239 float ban_expire[BAN_MAX];\r
240 float ban_count;\r
241 \r
242 string ban_ip1;\r
243 string ban_ip2;\r
244 string ban_ip3;\r
245 string ban_ip4;\r
246 #ifdef UID\r
247 string ban_uid;\r
248 #endif\r
249 \r
250 void Ban_SaveBans()\r
251 {\r
252         string out;\r
253         float i;\r
254 \r
255         if(!ban_loaded)\r
256                 return;\r
257 \r
258         // version of list\r
259         out = "1";\r
260         for(i = 0; i < ban_count; ++i)\r
261         {\r
262                 if(time > ban_expire[i])\r
263                         continue;\r
264                 out = strcat(out, " ", ban_ip[i]);\r
265                 out = strcat(out, " ", ftos(ban_expire[i] - time));\r
266         }\r
267         if(strlen(out) <= 1) // no real entries\r
268                 cvar_set("g_banned_list", "");\r
269         else\r
270                 cvar_set("g_banned_list", out);\r
271 }\r
272 \r
273 float Ban_Delete(float i)\r
274 {\r
275         if(i < 0)\r
276                 return FALSE;\r
277         if(i >= ban_count)\r
278                 return FALSE;\r
279         if(ban_expire[i] == 0)\r
280                 return FALSE;\r
281         if(ban_expire[i] > 0)\r
282         {\r
283                 OnlineBanList_SendUnban(ban_ip[i]);\r
284                 strunzone(ban_ip[i]);\r
285         }\r
286         ban_expire[i] = 0;\r
287         ban_ip[i] = "";\r
288         Ban_SaveBans();\r
289         return TRUE;\r
290 }\r
291 \r
292 void Ban_LoadBans()\r
293 {\r
294         float i, n;\r
295         for(i = 0; i < ban_count; ++i)\r
296                 Ban_Delete(i);\r
297         ban_count = 0;\r
298         ban_loaded = TRUE;\r
299         n = tokenize_console(cvar_string("g_banned_list"));\r
300         if(stof(argv(0)) == 1)\r
301         {\r
302                 ban_count = (n - 1) / 2;\r
303                 for(i = 0; i < ban_count; ++i)\r
304                 {\r
305                         ban_ip[i] = strzone(argv(2*i+1));\r
306                         ban_expire[i] = time + stof(argv(2*i+2));\r
307                 }\r
308         }\r
309 \r
310         entity e;\r
311         e = spawn();\r
312         e.classname = "bansyncer";\r
313         e.think = OnlineBanList_Think;\r
314         e.nextthink = time + 1;\r
315 }\r
316 \r
317 void Ban_View()\r
318 {\r
319         float i;\r
320         string msg;\r
321         for(i = 0; i < ban_count; ++i)\r
322         {\r
323                 if(time > ban_expire[i])\r
324                         continue;\r
325                 msg = strcat("#", ftos(i), ": ");\r
326                 msg = strcat(msg, ban_ip[i], " is still banned for ");\r
327                 msg = strcat(msg, ftos(ban_expire[i] - time), " seconds");\r
328                 print(msg, "\n");\r
329         }\r
330 }\r
331 \r
332 float Ban_GetClientIP(entity client)\r
333 {\r
334         // we can't use tokenizing here, as this is called during ban list parsing\r
335         float i1, i2, i3, i4;\r
336         string s;\r
337 \r
338         s = client.netaddress;\r
339         \r
340         i1 = strstrofs(s, ".", 0);\r
341         if(i1 < 0)\r
342                 return FALSE;\r
343         i2 = strstrofs(s, ".", i1 + 1);\r
344         if(i2 < 0)\r
345                 return FALSE;\r
346         i3 = strstrofs(s, ".", i2 + 1);\r
347         if(i3 < 0)\r
348                 return FALSE;\r
349         i4 = strstrofs(s, ".", i3 + 1);\r
350         if(i4 >= 0)\r
351                 return FALSE;\r
352         \r
353         ban_ip1 = substring(s, 0, i1);\r
354         ban_ip2 = substring(s, 0, i2);\r
355         ban_ip3 = substring(s, 0, i3);\r
356         ban_ip4 = strcat1(s);\r
357 #ifdef UID\r
358         ban_uid = client.uid;\r
359 #endif\r
360 \r
361         return TRUE;\r
362 }\r
363 \r
364 float Ban_IsClientBanned(entity client, float idx)\r
365 {\r
366         float i, b, e;\r
367         if(!ban_loaded)\r
368                 Ban_LoadBans();\r
369         if(!Ban_GetClientIP(client))\r
370                 return FALSE;\r
371         if(idx < 0)\r
372         {\r
373                 b = 0;\r
374                 e = ban_count;\r
375         }\r
376         else\r
377         {\r
378                 b = idx;\r
379                 e = idx + 1;\r
380         }\r
381         for(i = b; i < e; ++i)\r
382         {\r
383                 string s;\r
384                 if(time > ban_expire[i])\r
385                         continue;\r
386                 s = ban_ip[i];\r
387                 if(ban_ip1 == s) return TRUE;\r
388                 if(ban_ip2 == s) return TRUE;\r
389                 if(ban_ip3 == s) return TRUE;\r
390                 if(ban_ip4 == s) return TRUE;\r
391 #ifdef UID\r
392                 if(ban_uid == s) return TRUE;\r
393 #endif\r
394         }\r
395         return FALSE;\r
396 }\r
397 \r
398 float Ban_MaybeEnforceBan(entity client)\r
399 {\r
400         if(Ban_IsClientBanned(client, -1))\r
401         {\r
402                 string s;\r
403                 s = strcat("^1NOTE:^7 banned client ", client.netaddress, " just tried to enter\n");\r
404                 dropclient(client);\r
405                 bprint(s);\r
406                 return TRUE;\r
407         }\r
408         return FALSE;\r
409 }\r
410 \r
411 string Ban_Enforce(float i, string reason)\r
412 {\r
413         string s;\r
414         entity e;\r
415 \r
416         // Enforce our new ban\r
417         s = "";\r
418         FOR_EACH_REALCLIENT(e)\r
419                 if(Ban_IsClientBanned(e, i))\r
420                 {\r
421                         if(reason != "")\r
422                         {\r
423                                 if(s == "")\r
424                                         reason = strcat(reason, ": affects ");\r
425                                 else\r
426                                         reason = strcat(reason, ", ");\r
427                                 reason = strcat(reason, e.netname);\r
428                         }\r
429                         s = strcat(s, "^1NOTE:^7 banned client ", e.netname, "^7 has to go\n");\r
430                         dropclient(e);\r
431                 }\r
432         bprint(s);\r
433 \r
434         return reason;\r
435 }\r
436 \r
437 float Ban_Insert(string ip, float bantime, string reason, float dosync)\r
438 {\r
439         float i;\r
440         float j;\r
441         float bestscore;\r
442 \r
443         // already banned?\r
444         for(i = 0; i < ban_count; ++i)\r
445                 if(ban_ip[i] == ip)\r
446                 {\r
447                         // prolong the ban\r
448                         if(time + bantime > ban_expire[i])\r
449                         {\r
450                                 ban_expire[i] = time + bantime;\r
451                                 dprint(ip, "'s ban has been prolonged to ", ftos(bantime), " seconds from now\n");\r
452                         }\r
453                         else\r
454                                 dprint(ip, "'s ban is still active until ", ftos(ban_expire[i] - time), " seconds from now\n");\r
455 \r
456                         // and enforce\r
457                         reason = Ban_Enforce(i, reason);\r
458 \r
459                         // and abort\r
460                         if(dosync)\r
461                                 if(reason != "")\r
462                                         if(substring(reason, 0, 1) != "~") // like IRC: unauthenticated banner\r
463                                                 OnlineBanList_SendBan(ip, bantime, reason);\r
464 \r
465                         return FALSE;\r
466                 }\r
467 \r
468         // do we have a free slot?\r
469         for(i = 0; i < ban_count; ++i)\r
470                 if(time > ban_expire[i])\r
471                         break;\r
472         // no free slot? Then look for the one who would get unbanned next\r
473         if(i >= BAN_MAX)\r
474         {\r
475                 i = 0;\r
476                 bestscore = ban_expire[i];\r
477                 for(j = 1; j < ban_count; ++j)\r
478                 {\r
479                         if(ban_expire[j] < bestscore)\r
480                         {\r
481                                 i = j;\r
482                                 bestscore = ban_expire[i];\r
483                         }\r
484                 }\r
485         }\r
486         // if we replace someone, will we be banned longer than him (so long-term\r
487         // bans never get overridden by short-term bans)\r
488         if(i < ban_count)\r
489         if(ban_expire[i] > time + bantime)\r
490         {\r
491                 print(ip, " could not get banned due to no free ban slot\n");\r
492                 return FALSE;\r
493         }\r
494         // okay, insert our new victim as i\r
495         Ban_Delete(i);\r
496         dprint(ip, " has been banned for ", ftos(bantime), " seconds\n");\r
497         ban_expire[i] = time + bantime;\r
498         ban_ip[i] = strzone(ip);\r
499         ban_count = max(ban_count, i + 1);\r
500 \r
501         Ban_SaveBans();\r
502 \r
503         reason = Ban_Enforce(i, reason);\r
504 \r
505         // and abort\r
506         if(dosync)\r
507                 if(reason != "")\r
508                         if(substring(reason, 0, 1) != "~") // like IRC: unauthenticated banner\r
509                                 OnlineBanList_SendBan(ip, bantime, reason);\r
510 \r
511         return TRUE;\r
512 }\r
513 \r
514 void Ban_KickBanClient(entity client, float bantime, float masksize, string reason)\r
515 {\r
516         if(!Ban_GetClientIP(client))\r
517         {\r
518                 sprint(client, strcat("Kickbanned: ", reason, "\n"));\r
519                 dropclient(client);\r
520                 return;\r
521         }\r
522         // now ban him\r
523         switch(masksize)\r
524         {\r
525                 case 1:\r
526                         Ban_Insert(ban_ip1, bantime, reason, 1);\r
527                         break;\r
528                 case 2:\r
529                         Ban_Insert(ban_ip2, bantime, reason, 1);\r
530                         break;\r
531                 case 3:\r
532                         Ban_Insert(ban_ip3, bantime, reason, 1);\r
533                         break;\r
534                 case 4:\r
535                 default:\r
536                         Ban_Insert(ban_ip4, bantime, reason, 1);\r
537                         break;\r
538 #ifdef UID\r
539                 case 0:\r
540                         Ban_Insert(ban_uid, bantime, reason, 1);\r
541                         break;\r
542 #endif\r
543         }\r
544         /*\r
545          * not needed, as we enforce the ban in Ban_Insert anyway\r
546         // and kick him\r
547         sprint(client, strcat("Kickbanned: ", reason, "\n"));\r
548         dropclient(client);\r
549          */\r
550 }\r
551 \r
552 float GameCommand_Ban(string command)\r
553 {\r
554         float argc;\r
555         float bantime;\r
556         entity client;\r
557         float entno;\r
558         float masksize;\r
559         string reason;\r
560         float reasonarg;\r
561 \r
562         argc = tokenize_console(command);\r
563         if(argv(0) == "help")\r
564         {\r
565                 print("  kickban # n m p reason - kickban player n for m seconds, using mask size p (1 to 4)\n");\r
566                 print("  ban ip m reason - ban an IP or range (incomplete IP, like 1.2.3) for m seconds\n");\r
567                 print("  bans - list all existing bans\n");\r
568                 print("  unban n - delete the entry #n from the bans list\n");\r
569                 return TRUE;\r
570         }\r
571         if(argv(0) == "kickban")\r
572         {\r
573 #define INITARG(c) reasonarg = c\r
574 #define GETARG(v,d) if((argc > reasonarg) && ((v = stof(argv(reasonarg))) != 0)) ++reasonarg; else v = d\r
575 #define RESTARG(v) if(argc > reasonarg) v = substring(command, argv_start_index(reasonarg), strlen(command) - argv_start_index(reasonarg)); else v = ""\r
576                 if(argc >= 3)\r
577                 {\r
578                         entno = stof(argv(2));\r
579                         if(entno > maxclients || entno < 1)\r
580                                 return TRUE;\r
581                         client = edict_num(entno);\r
582 \r
583                         INITARG(3);\r
584                         GETARG(bantime, cvar("g_ban_default_bantime"));\r
585                         GETARG(masksize, cvar("g_ban_default_masksize"));\r
586                         RESTARG(reason);\r
587 \r
588                         Ban_KickBanClient(client, bantime, masksize, reason);\r
589                         return TRUE;\r
590                 }\r
591         }\r
592         else if(argv(0) == "ban")\r
593         {\r
594                 if(argc >= 2)\r
595                 {\r
596                         string ip;\r
597                         ip = argv(1);\r
598 \r
599                         INITARG(2);\r
600                         GETARG(bantime, cvar("g_ban_default_bantime"));\r
601                         RESTARG(reason);\r
602 \r
603                         Ban_Insert(ip, bantime, reason, 1);\r
604                         return TRUE;\r
605                 }\r
606 #undef INITARG\r
607 #undef GETARG\r
608 #undef RESTARG\r
609         }\r
610         else if(argv(0) == "bans")\r
611         {\r
612                 Ban_View();\r
613                 return TRUE;\r
614         }\r
615         else if(argv(0) == "unban")\r
616         {\r
617                 if(argc >= 2)\r
618                 {\r
619                         float who;\r
620                         who = stof(argv(1));\r
621                         Ban_Delete(who);\r
622                         return TRUE;\r
623                 }\r
624         }\r
625         return FALSE;\r
626 }\r