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