]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/ipban.qc
healthbars for sprites (currently unused)
[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                 return;
184         if(cvar("g_ban_sync_interval") == 0) // < 0 is okay, it means "sync on level start only"
185                 return;
186         argc = tokenize_console(cvar_string("g_ban_sync_trusted_servers"));
187         if(argc == 0)
188                 return;
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 }
216
217 #define BAN_MAX 64
218 float ban_loaded;
219 string ban_ip[BAN_MAX];
220 float ban_expire[BAN_MAX];
221 float ban_count;
222
223 string ban_ip1;
224 string ban_ip2;
225 string ban_ip3;
226 string ban_ip4;
227
228 void Ban_SaveBans()
229 {
230         string out;
231         float i;
232
233         if(!ban_loaded)
234                 return;
235
236         // version of list
237         out = "1";
238         for(i = 0; i < ban_count; ++i)
239         {
240                 if(time > ban_expire[i])
241                         continue;
242                 out = strcat(out, " ", ban_ip[i]);
243                 out = strcat(out, " ", ftos(ban_expire[i] - time));
244         }
245         if(strlen(out) <= 1) // no real entries
246                 cvar_set("g_banned_list", "");
247         else
248                 cvar_set("g_banned_list", out);
249 }
250
251 float Ban_Delete(float i)
252 {
253         if(i < 0)
254                 return FALSE;
255         if(i >= ban_count)
256                 return FALSE;
257         if(ban_expire[i] == 0)
258                 return FALSE;
259         if(ban_expire[i] > 0)
260         {
261                 OnlineBanList_SendUnban(ban_ip[i]);
262                 strunzone(ban_ip[i]);
263         }
264         ban_expire[i] = 0;
265         ban_ip[i] = "";
266         Ban_SaveBans();
267         return TRUE;
268 }
269
270 void Ban_LoadBans()
271 {
272         float i, n;
273         for(i = 0; i < ban_count; ++i)
274                 Ban_Delete(i);
275         ban_count = 0;
276         ban_loaded = TRUE;
277         n = tokenize_console(cvar_string("g_banned_list"));
278         if(stof(argv(0)) == 1)
279         {
280                 ban_count = (n - 1) / 2;
281                 for(i = 0; i < ban_count; ++i)
282                 {
283                         ban_ip[i] = strzone(argv(2*i+1));
284                         ban_expire[i] = time + stof(argv(2*i+2));
285                 }
286         }
287
288         entity e;
289         e = spawn();
290         e.classname = "bansyncer";
291         e.think = OnlineBanList_Think;
292         e.nextthink = time + 1;
293 }
294
295 void Ban_View()
296 {
297         float i;
298         string msg;
299         for(i = 0; i < ban_count; ++i)
300         {
301                 if(time > ban_expire[i])
302                         continue;
303                 msg = strcat("#", ftos(i), ": ");
304                 msg = strcat(msg, ban_ip[i], " is still banned for ");
305                 msg = strcat(msg, ftos(ban_expire[i] - time), " seconds");
306                 print(msg, "\n");
307         }
308 }
309
310 float Ban_GetClientIP(entity client)
311 {
312         // we can't use tokenizing here, as this is called during ban list parsing
313         float i1, i2, i3, i4;
314         string s;
315
316         s = client.netaddress;
317         
318         i1 = strstrofs(s, ".", 0);
319         if(i1 < 0)
320                 return FALSE;
321         i2 = strstrofs(s, ".", i1 + 1);
322         if(i2 < 0)
323                 return FALSE;
324         i3 = strstrofs(s, ".", i2 + 1);
325         if(i3 < 0)
326                 return FALSE;
327         i4 = strstrofs(s, ".", i3 + 1);
328         if(i4 >= 0)
329                 return FALSE;
330         
331         ban_ip1 = substring(s, 0, i1);
332         ban_ip2 = substring(s, 0, i2);
333         ban_ip3 = substring(s, 0, i3);
334         ban_ip4 = strcat1(s);
335
336         return TRUE;
337 }
338
339 float Ban_IsClientBanned(entity client, float idx)
340 {
341         float i, b, e;
342         if(!ban_loaded)
343                 Ban_LoadBans();
344         if(!Ban_GetClientIP(client))
345                 return FALSE;
346         if(idx < 0)
347         {
348                 b = 0;
349                 e = ban_count;
350         }
351         else
352         {
353                 b = idx;
354                 e = idx + 1;
355         }
356         for(i = b; i < e; ++i)
357         {
358                 string s;
359                 if(time > ban_expire[i])
360                         continue;
361                 s = ban_ip[i];
362                 if(ban_ip1 == s) return TRUE;
363                 if(ban_ip2 == s) return TRUE;
364                 if(ban_ip3 == s) return TRUE;
365                 if(ban_ip4 == s) return TRUE;
366         }
367         return FALSE;
368 }
369
370 float Ban_MaybeEnforceBan(entity client)
371 {
372         if(Ban_IsClientBanned(client, -1))
373         {
374                 string s;
375                 s = strcat("^1NOTE:^7 banned client ", client.netaddress, " just tried to enter\n");
376                 dropclient(client);
377                 bprint(s);
378                 return TRUE;
379         }
380         return FALSE;
381 }
382
383 string Ban_Enforce(float i, string reason)
384 {
385         string s;
386         entity e;
387
388         // Enforce our new ban
389         s = "";
390         FOR_EACH_REALCLIENT(e)
391                 if(Ban_IsClientBanned(e, i))
392                 {
393                         if(reason != "")
394                         {
395                                 if(s == "")
396                                         reason = strcat(reason, ": affects ");
397                                 else
398                                         reason = strcat(reason, ", ");
399                                 reason = strcat(reason, e.netname);
400                         }
401                         s = strcat(s, "^1NOTE:^7 banned client ", e.netname, "^7 has to go\n");
402                         dropclient(e);
403                 }
404         bprint(s);
405
406         return reason;
407 }
408
409 float Ban_Insert(string ip, float bantime, string reason, float dosync)
410 {
411         float i;
412         float j;
413         float bestscore;
414
415         // already banned?
416         for(i = 0; i < ban_count; ++i)
417                 if(ban_ip[i] == ip)
418                 {
419                         // prolong the ban
420                         if(time + bantime > ban_expire[i])
421                         {
422                                 ban_expire[i] = time + bantime;
423                                 dprint(ip, "'s ban has been prolonged to ", ftos(bantime), " seconds from now\n");
424                         }
425                         else
426                                 dprint(ip, "'s ban is still active until ", ftos(ban_expire[i] - time), " seconds from now\n");
427
428                         // and enforce
429                         reason = Ban_Enforce(i, reason);
430
431                         // and abort
432                         if(dosync)
433                                 if(reason != "")
434                                         if(substring(reason, 0, 1) != "~") // like IRC: unauthenticated banner
435                                                 OnlineBanList_SendBan(ip, bantime, reason);
436
437                         return FALSE;
438                 }
439
440         // do we have a free slot?
441         for(i = 0; i < ban_count; ++i)
442                 if(time > ban_expire[i])
443                         break;
444         // no free slot? Then look for the one who would get unbanned next
445         if(i >= BAN_MAX)
446         {
447                 i = 0;
448                 bestscore = ban_expire[i];
449                 for(j = 1; j < ban_count; ++j)
450                 {
451                         if(ban_expire[j] < bestscore)
452                         {
453                                 i = j;
454                                 bestscore = ban_expire[i];
455                         }
456                 }
457         }
458         // if we replace someone, will we be banned longer than him (so long-term
459         // bans never get overridden by short-term bans)
460         if(i < ban_count)
461         if(ban_expire[i] > time + bantime)
462         {
463                 print(ip, " could not get banned due to no free ban slot\n");
464                 return FALSE;
465         }
466         // okay, insert our new victim as i
467         Ban_Delete(i);
468         dprint(ip, " has been banned for ", ftos(bantime), " seconds\n");
469         ban_expire[i] = time + bantime;
470         ban_ip[i] = strzone(ip);
471         ban_count = max(ban_count, i + 1);
472
473         Ban_SaveBans();
474
475         reason = Ban_Enforce(i, reason);
476
477         // and abort
478         if(dosync)
479                 if(reason != "")
480                         if(substring(reason, 0, 1) != "~") // like IRC: unauthenticated banner
481                                 OnlineBanList_SendBan(ip, bantime, reason);
482
483         return TRUE;
484 }
485
486 void Ban_KickBanClient(entity client, float bantime, float masksize, string reason)
487 {
488         if(!Ban_GetClientIP(client))
489         {
490                 sprint(client, strcat("Kickbanned: ", reason, "\n"));
491                 dropclient(client);
492                 return;
493         }
494         // now ban him
495         switch(masksize)
496         {
497                 case 1:
498                         Ban_Insert(ban_ip1, bantime, reason, 1);
499                         break;
500                 case 2:
501                         Ban_Insert(ban_ip2, bantime, reason, 1);
502                         break;
503                 case 3:
504                         Ban_Insert(ban_ip3, bantime, reason, 1);
505                         break;
506                 default:
507                         Ban_Insert(ban_ip4, bantime, reason, 1);
508                         break;
509         }
510         /*
511          * not needed, as we enforce the ban in Ban_Insert anyway
512         // and kick him
513         sprint(client, strcat("Kickbanned: ", reason, "\n"));
514         dropclient(client);
515          */
516 }
517
518 float GameCommand_Ban(string command)
519 {
520         float argc;
521         float bantime;
522         entity client;
523         float entno;
524         float masksize;
525         string reason;
526         float reasonarg;
527
528         argc = tokenize_console(command);
529         if(argv(0) == "help")
530         {
531                 print("  kickban # n m p reason - kickban player n for m seconds, using mask size p (1 to 4)\n");
532                 print("  ban ip m reason - ban an IP or range (incomplete IP, like 1.2.3) for m seconds\n");
533                 print("  bans - list all existing bans\n");
534                 print("  unban n - delete the entry #n from the bans list\n");
535                 return TRUE;
536         }
537         if(argv(0) == "kickban")
538         {
539 #define INITARG(c) reasonarg = c
540 #define GETARG(v,d) if((argc > reasonarg) && ((v = stof(argv(reasonarg))) != 0)) ++reasonarg; else v = d
541 #define RESTARG(v) if(argc > reasonarg) v = substring(command, argv_start_index(reasonarg), strlen(command) - argv_start_index(reasonarg)); else v = ""
542                 if(argc >= 3)
543                 {
544                         entno = stof(argv(2));
545                         if(entno > maxclients || entno < 1)
546                                 return TRUE;
547                         client = edict_num(entno);
548
549                         INITARG(3);
550                         GETARG(bantime, cvar("g_ban_default_bantime"));
551                         GETARG(masksize, cvar("g_ban_default_masksize"));
552                         RESTARG(reason);
553
554                         Ban_KickBanClient(client, bantime, masksize, reason);
555                         return TRUE;
556                 }
557         }
558         else if(argv(0) == "ban")
559         {
560                 if(argc >= 2)
561                 {
562                         string ip;
563                         ip = argv(1);
564
565                         INITARG(2);
566                         GETARG(bantime, cvar("g_ban_default_bantime"));
567                         RESTARG(reason);
568
569                         Ban_Insert(ip, bantime, reason, 1);
570                         return TRUE;
571                 }
572 #undef INITARG
573 #undef GETARG
574 #undef RESTARG
575         }
576         else if(argv(0) == "bans")
577         {
578                 Ban_View();
579                 return TRUE;
580         }
581         else if(argv(0) == "unban")
582         {
583                 if(argc >= 2)
584                 {
585                         float who;
586                         who = stof(argv(1));
587                         Ban_Delete(who);
588                         return TRUE;
589                 }
590         }
591         return FALSE;
592 }