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