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