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