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