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