]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/ipban.qc
ipban:
[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_sane(cvar_string("g_ban_sync_uri"));
35         for(i = 0; i < n; ++i)
36                 uri_get(strcat(argv(i), uri), 0); // 0 = "discard" callback target
37 }
38
39 void OnlineBanList_SendUnban(string ip)
40 {
41         string uri;
42         float i, n;
43
44         uri = strcat(     "?action=unban&hostname=", uri_escape(cvar_string("hostname")));
45         uri = strcat(uri, "&ip=", uri_escape(ip));
46
47         n = tokenize_sane(cvar_string("g_ban_sync_uri"));
48         for(i = 0; i < n; ++i)
49                 uri_get(strcat(argv(i), uri), 0); // 0 = "discard" callback target
50 }
51
52 string OnlineBanList_Servers;
53 float OnlineBanList_Timeout;
54 float OnlineBanList_Requests;
55
56 void OnlineBanList_URI_Get_Callback(float status, string data)
57 {
58         float n, i, j, l;
59         string ip;
60         float timeleft;
61         string reason;
62         string serverip;
63         float syncinterval;
64
65         if(OnlineBanList_Requests <= 0)
66                 return;
67
68         --OnlineBanList_Requests;
69
70         if(time > OnlineBanList_Timeout)
71                 return;
72
73         syncinterval = cvar("g_ban_sync_interval");
74         if(syncinterval == 0)
75                 return;
76         if(syncinterval > 0)
77                 syncinterval *= 60;
78
79         if(status != 0)
80         {
81                 print("Error receiving the online ban list: Status is ", ftos(status), "\n");
82                 return;
83         }
84
85         if(substring(data, 0, 1) == "<")
86         {
87                 print("Error receiving the online ban list: Received HTML instead of a ban list: ");
88                 return;
89         }
90
91         if(strstrofs(data, "\r", 0) != -1)
92         {
93                 print("Error receiving the online ban list: Received carriage returns: ");
94                 return;
95         }
96
97         n = tokenizebyseparator(data, "\n");
98         if(mod(n, 4) != 0)
99         {
100                 print("Error receiving the online ban list: Received invalid item count: ");
101                 return;
102         }
103
104         for(i = 0; i < n; i += 4)
105         {
106                 ip = argv(i);
107                 timeleft = stof(argv(i + 1));
108                 reason = argv(i + 2);
109                 serverip = argv(i + 3);
110
111                 timeleft -= 15;
112                 if(timeleft < 0)
113                         continue;
114
115                 l = strlen(ip);
116                 for(j = 0; j < l; ++j)
117                         if(strstrofs("0123456789.", substring(ip, j, 1), 0) == -1)
118                         {
119                                 print("Invalid character ", substring(ip, j, 1), " in IP address ", ip, ". Skipping this ban.\n");
120                                 goto skip;
121                         }
122
123                 if(cvar("g_ban_sync_trusted_servers_verify"))
124                         if((strstrofs(strcat(";", OnlineBanList_Servers, ";"), strcat(";", serverip, ";"), 0) == -1))
125                                 goto skip;
126
127                 if(syncinterval > 0)
128                         timeleft = min(syncinterval + 15, timeleft);
129                         // 15 seconds for safety
130                         // the ban will be prolonged on the next sync
131                 Ban_Insert(ip, timeleft, strcat("ban synced from ", serverip), 0);
132                 print("Ban list syncing: accepted ban of ", ip, " by ", serverip, ": ", reason, "\n");
133
134                 continue;
135 :skip
136         }
137 }
138
139 void OnlineBanList_Think()
140 {
141         float argc;
142         string uri;
143         float i, n;
144         
145         if(cvar_string("g_ban_sync_uri") == "")
146                 return;
147         if(cvar("g_ban_sync_interval") == 0) // < 0 is okay, it means "sync on level start only"
148                 return;
149         argc = tokenize_sane(cvar_string("g_ban_sync_trusted_servers"));
150         if(argc == 0)
151                 return;
152
153         if(OnlineBanList_Requests == 0) // only if there is no ongoing request!
154         {
155                 if(OnlineBanList_Servers)
156                         strunzone(OnlineBanList_Servers);
157                 OnlineBanList_Servers = argv(0);
158                 for(i = 1; i < argc; ++i)
159                         OnlineBanList_Servers = strcat(OnlineBanList_Servers, ";", argv(i));
160                 OnlineBanList_Servers = strzone(OnlineBanList_Servers);
161                 
162                 uri = strcat(     "?action=list&hostname=", uri_escape(cvar_string("hostname")));
163                 uri = strcat(uri, "&servers=", uri_escape(OnlineBanList_Servers));
164
165                 OnlineBanList_Timeout = time + 10;
166
167                 n = tokenize_sane(cvar_string("g_ban_sync_uri"));
168                 for(i = 0; i < n; ++i)
169                 {
170                         ++OnlineBanList_Requests;
171                         uri_get(strcat(argv(i), uri), 1); // 1 = "banlist" callback target
172                 }
173         }
174         
175         if(cvar("g_ban_sync_interval") > 0)
176                 self.nextthink = time + max(60, cvar("g_ban_sync_interval") * 60);
177 }
178
179 #define BAN_MAX 64
180 float ban_loaded;
181 string ban_ip[BAN_MAX];
182 float ban_expire[BAN_MAX];
183 float ban_count;
184
185 string ban_ip1;
186 string ban_ip2;
187 string ban_ip3;
188 string ban_ip4;
189
190 void Ban_SaveBans()
191 {
192         string out;
193         float i;
194
195         if(!ban_loaded)
196                 return;
197
198         // version of list
199         out = "1";
200         for(i = 0; i < ban_count; ++i)
201         {
202                 if(time > ban_expire[i])
203                         continue;
204                 out = strcat(out, " ", ban_ip[i]);
205                 out = strcat(out, " ", ftos(ban_expire[i] - time));
206         }
207         if(strlen(out) <= 1) // no real entries
208                 cvar_set("g_banned_list", "");
209         else
210                 cvar_set("g_banned_list", out);
211 }
212
213 float Ban_Delete(float i)
214 {
215         if(i < 0)
216                 return FALSE;
217         if(i >= ban_count)
218                 return FALSE;
219         if(ban_expire[i] == 0)
220                 return FALSE;
221         if(ban_expire[i] > 0)
222         {
223                 OnlineBanList_SendUnban(ban_ip[i]);
224                 strunzone(ban_ip[i]);
225         }
226         ban_expire[i] = 0;
227         ban_ip[i] = "";
228         Ban_SaveBans();
229         return TRUE;
230 }
231
232 void Ban_LoadBans()
233 {
234         float i, n;
235         for(i = 0; i < ban_count; ++i)
236                 Ban_Delete(i);
237         ban_count = 0;
238         ban_loaded = TRUE;
239         n = tokenize_sane(cvar_string("g_banned_list"));
240         if(stof(argv(0)) == 1)
241         {
242                 ban_count = (n - 1) / 2;
243                 for(i = 0; i < ban_count; ++i)
244                 {
245                         ban_ip[i] = strzone(argv(2*i+1));
246                         ban_expire[i] = time + stof(argv(2*i+2));
247                 }
248         }
249
250         entity e;
251         e = spawn();
252         e.classname = "bansyncer";
253         e.think = OnlineBanList_Think;
254         e.nextthink = time + 1;
255 }
256
257 void Ban_View()
258 {
259         float i;
260         string msg;
261         for(i = 0; i < ban_count; ++i)
262         {
263                 if(time > ban_expire[i])
264                         continue;
265                 msg = strcat("#", ftos(i), ": ");
266                 msg = strcat(msg, ban_ip[i], " is still banned for ");
267                 msg = strcat(msg, ftos(ban_expire[i] - time), " seconds");
268                 print(msg, "\n");
269         }
270 }
271
272 float Ban_GetClientIP(entity client)
273 {
274         float n;
275         n = tokenizebyseparator(client.netaddress, ".");
276         if(n != 4)
277                 return FALSE;
278         ban_ip1 = strcat1(argv(0));
279         ban_ip2 = strcat(ban_ip1, ".", argv(1));
280         ban_ip3 = strcat(ban_ip2, ".", argv(2));
281         ban_ip4 = strcat(ban_ip3, ".", argv(3));
282         return TRUE;
283 }
284
285 float Ban_IsClientBanned(entity client, float idx)
286 {
287         float i, b, e;
288         if(!ban_loaded)
289                 Ban_LoadBans();
290         if(!Ban_GetClientIP(client))
291                 return FALSE;
292         if(idx < 0)
293         {
294                 b = 0;
295                 e = ban_count;
296         }
297         else
298         {
299                 b = idx;
300                 e = idx + 1;
301         }
302         for(i = b; i < e; ++i)
303         {
304                 string s;
305                 if(time > ban_expire[i])
306                         continue;
307                 s = ban_ip[i];
308                 if(ban_ip1 == s) return TRUE;
309                 if(ban_ip2 == s) return TRUE;
310                 if(ban_ip3 == s) return TRUE;
311                 if(ban_ip4 == s) return TRUE;
312         }
313         return FALSE;
314 }
315
316 float Ban_MaybeEnforceBan(entity client)
317 {
318         if(Ban_IsClientBanned(client, -1))
319         {
320                 string s;
321                 s = strcat("^1NOTE:^7 banned client ", client.netaddress, " just tried to enter\n");
322                 dropclient(client);
323                 bprint(s);
324                 return TRUE;
325         }
326         return FALSE;
327 }
328
329 float Ban_Insert(string ip, float bantime, string reason, float dosync)
330 {
331         float i;
332         float j;
333         float bestscore;
334         entity e;
335         string s;
336
337         // already banned?
338         for(i = 0; i < ban_count; ++i)
339                 if(ban_ip[i] == ip)
340                 {
341                         // prolong the ban
342                         if(time + bantime > ban_expire[i])
343                         {
344                                 ban_expire[i] = time + bantime;
345                                 print(ip, "'s ban has been prolonged to ", ftos(bantime), " seconds from now\n");
346                         }
347
348                         // and abort
349                         if(dosync)
350                                 if(reason != "")
351                                         if(substring(reason, 0, 1) != "~") // like IRC: unauthenticated banner
352                                                 OnlineBanList_SendBan(ip, bantime, reason);
353
354                         return FALSE;
355                 }
356         // do we have a free slot?
357         for(i = 0; i < ban_count; ++i)
358                 if(time > ban_expire[i])
359                         break;
360         // no free slot? Then look for the one who would get unbanned next
361         if(i >= BAN_MAX)
362         {
363                 i = 0;
364                 bestscore = ban_expire[i];
365                 for(j = 1; j < ban_count; ++j)
366                 {
367                         if(ban_expire[j] < bestscore)
368                         {
369                                 i = j;
370                                 bestscore = ban_expire[i];
371                         }
372                 }
373         }
374         // if we replace someone, will we be banned longer than him (so long-term
375         // bans never get overridden by short-term bans)
376         if(ban_expire[i] > time + bantime)
377                 return FALSE;
378         // okay, insert our new victim as i
379         Ban_Delete(i);
380         print(ip, " has been banned for ", ftos(bantime), " seconds\n");
381         ban_expire[i] = time + bantime;
382         ban_ip[i] = strzone(ip);
383         ban_count = max(ban_count, i + 1);
384
385         Ban_SaveBans();
386
387         // Enforce our new ban
388         s = "";
389         FOR_EACH_REALCLIENT(e)
390                 if(Ban_IsClientBanned(e, i))
391                 {
392                         if(reason != "")
393                         {
394                                 if(s == "")
395                                         reason = strcat(reason, ": affects ");
396                                 else
397                                         reason = strcat(reason, ", ");
398                                 reason = strcat(reason, e.netname);
399                         }
400                         s = strcat(s, "^1NOTE:^7 banned client ", e.netname, "^7 has to go\n");
401                         dropclient(e);
402                 }
403         bprint(s);
404
405         // and abort
406         if(dosync)
407                 if(reason != "")
408                         if(substring(reason, 0, 1) != "~") // like IRC: unauthenticated banner
409                                 OnlineBanList_SendBan(ip, bantime, reason);
410
411         return TRUE;
412 }
413
414 void Ban_KickBanClient(entity client, float bantime, float masksize, string reason)
415 {
416         if(!Ban_GetClientIP(client))
417         {
418                 sprint(client, strcat("Kickbanned: ", reason, "\n"));
419                 dropclient(client);
420                 return;
421         }
422         // now ban him
423         switch(masksize)
424         {
425                 case 1:
426                         Ban_Insert(ban_ip1, bantime, reason, 1);
427                         break;
428                 case 2:
429                         Ban_Insert(ban_ip2, bantime, reason, 1);
430                         break;
431                 case 3:
432                         Ban_Insert(ban_ip3, bantime, reason, 1);
433                         break;
434                 default:
435                         Ban_Insert(ban_ip4, bantime, reason, 1);
436                         break;
437         }
438         /*
439          * not needed, as we enforce the ban in Ban_Insert anyway
440         // and kick him
441         sprint(client, strcat("Kickbanned: ", reason, "\n"));
442         dropclient(client);
443          */
444 }
445
446 float GameCommand_Ban(string command)
447 {
448         float argc;
449         float bantime;
450         entity client;
451         float entno;
452         float masksize;
453         string reason;
454         float reasonarg;
455
456         argc = tokenize_sane(command);
457         if(argv(0) == "help")
458         {
459                 print("  kickban # n m p reason - kickban player n for m seconds, using mask size p (1 to 4)\n");
460                 print("  ban ip m reason - ban an IP or range (incomplete IP, like 1.2.3) for m seconds\n");
461                 print("  bans - list all existing bans\n");
462                 print("  unban n - delete the entry #n from the bans list\n");
463                 return TRUE;
464         }
465         if(argv(0) == "kickban")
466         {
467 #define INITARG(c) reasonarg = c
468 #define GETARG(v,d) if((argc > reasonarg) && ((v = stof(argv(reasonarg))) != 0)) ++reasonarg; else v = d
469 #define RESTARG(v) if(argc > reasonarg) v = substring(command, argv_start_index(reasonarg), strlen(command) - argv_start_index(reasonarg)); else v = ""
470                 if(argc >= 3)
471                 {
472                         entno = stof(argv(2));
473                         if(entno > maxclients || entno < 1)
474                                 return TRUE;
475                         client = edict_num(entno);
476
477                         INITARG(3);
478                         GETARG(bantime, cvar("g_ban_default_bantime"));
479                         GETARG(masksize, cvar("g_ban_default_masksize"));
480                         RESTARG(reason);
481
482                         Ban_KickBanClient(client, bantime, masksize, reason);
483                         return TRUE;
484                 }
485         }
486         else if(argv(0) == "ban")
487         {
488                 if(argc >= 2)
489                 {
490                         string ip;
491                         ip = argv(1);
492
493                         INITARG(2);
494                         GETARG(bantime, cvar("g_ban_default_bantime"));
495                         RESTARG(reason);
496
497                         Ban_Insert(ip, bantime, reason, 1);
498                         return TRUE;
499                 }
500 #undef INITARG
501 #undef GETARG
502 #undef RESTARG
503         }
504         else if(argv(0) == "bans")
505         {
506                 Ban_View();
507                 return TRUE;
508         }
509         else if(argv(0) == "unban")
510         {
511                 if(argc >= 2)
512                 {
513                         float who;
514                         who = stof(argv(1));
515                         Ban_Delete(who);
516                         return TRUE;
517                 }
518         }
519         return FALSE;
520 }