Annotation of botnow/BNC.pm, Revision 1.3
1.1 bountyht 1: #!/usr/bin/perl
2:
3: package BNC;
4:
5: use strict;
6: use warnings;
7: use OpenBSD::Pledge;
8: use OpenBSD::Unveil;
9: use MIME::Base64;
10: use Digest::SHA qw(sha256_hex);
11: use lib './';
12: require "SQLite.pm";
13: require "Hash.pm";
14: require "DNS.pm";
15: require "Mail.pm";
16:
17: my %conf = %main::conf;
18: my $chans = $conf{chans};
19: my $teamchans = $conf{teamchans};
20: my @teamchans = split /[,\s]+/m, $teamchans;
21: my $staff = $conf{staff};
22: my $zncdir = $conf{zncdir};
23: my $znclog = $conf{znclog} || "$zncdir/.znc/moddata/adminlog/znc.log";
24: my $hostname = $conf{hostname};
25: my $terms = $conf{terms};
26: my @logs;
27: my $expires = $conf{expires};
28: my $sslport = $conf{sslport};
29: my $plainport = $conf{plainport};
30: my $mailfrom = $conf{mailfrom};
31: my $mailname = $conf{mailname};
32: my $zncconfpath = $conf{zncconfpath} || "$zncdir/.znc/configs/znc.conf";
33: my $znctree = { Node => "root" };
34:
35: use constant {
36: NONE => 0,
37: ERRORS => 1,
38: WARNINGS => 2,
39: ALL => 3,
40: };
41:
42: `doas chown znc:daemon /home/znc/home/znc/.znc/configs/znc.conf`;
43: `doas chmod g+r /home/znc/home/znc/.znc/`;
44: my @zncconf = main::readarray($zncconfpath);
45: $znctree;
46: my @users;
47: foreach my $line (@zncconf) {
48: if ($line =~ /<User (.*)>/) {
49: push(@users, $1);
50: }
51: }
52: #$znctree = parseml($znctree, @zncconf);
53: main::cbind("pub", "-", "bnc", \&mbnc);
54: main::cbind("msg", "-", "bnc", \&mbnc);
55: main::cbind("msg", "-", "regex", \&mregex);
56: main::cbind("msg", "-", "foreach", \&mforeach);
57: main::cbind("msgm", "-", "*", \&mcontrolpanel);
58: main::cbind("msg", "-", "taillog", \&mtaillog);
59: main::cbind("msg", "-", "lastseen", \&mlastseen);
60:
61: sub init {
62: #znc.conf file
63: unveil("$zncconfpath", "r") or die "Unable to unveil $!";
64: #dependencies for figlet
65: unveil("/usr/local/bin/figlet", "rx") or die "Unable to unveil $!";
66: unveil("/usr/lib/libc.so.95.1", "r") or die "Unable to unveil $!";
67: unveil("/usr/libexec/ld.so", "r") or die "Unable to unveil $!";
68: unveil("/usr/bin/tail", "rx") or die "Unable to unveil $!";
69: #znc.log file
70: unveil("$znclog", "r") or die "Unable to unveil $!";
71: #print treeget($znctree, "AnonIPLimit")."\n";
72: #print treeget($znctree, "ServerThrottle")."\n";
73: #print treeget($znctree, "ConnectDelay")."\n";
74: #print "treeget\n";
75: #print Dumper \treeget($znctree, "User", "Node");
76: #print Dumper \treeget($znctree, "User", "Network", "Node");
77: }
78:
79: # parseml($tree, @lines)
80: # tree is a reference to a hash
81: # returns hash ref of tree
82: sub parseml {
83: my ($tree, @lines) = @_;
84: #if (scalar(@lines) == 0) { return $tree; }
85: while (scalar(@lines) > 0) {
86: my $line = shift(@lines);
87: if ($line =~ /^\s*([^=<>\s]+)\s*=\s*([^=<>]+)\s*$/) {
88: my ($tag, $val) = ($1, $2);
89: $tree->{$tag} = $val;
90: } elsif ($line =~ /^\/\//) { # skip comments
91: } elsif ($line =~ /^\s*$/) { # skip blank lines
92: } elsif ($line =~ /^\s*<([^>\s\/]+)\s*([^>\/]*)>\s*$/) {
93: my ($tag, $val) = ($1, $2);
94: if (!defined($tree->{$tag})) { $tree->{$tag} = []; }
95: my @newlines;
96: while (scalar(@lines) > 0) {
97: my $line = shift(@lines);
98: if ($line =~ /^\s*<\/$tag>\s*$/) {
99: my $subtree = parseml({ Node => $val }, @newlines);
100: push(@{$tree->{$tag}}, $subtree);
101: return parseml($tree, @lines);
102: }
103: push(@newlines, $line);
104: }
105: } else { print "ERROR: $line\n"; }
106: #TODO ERRORS not defined??
107: # } else { main::debug(ERRORS, "ERROR: $line"); }
108: }
109: return $tree;
110: }
111:
112: #Returns array of all values
113: #treeget($tree, "User");
114: #treeget($tree, "MaFFia Network");
115: sub treeget {
116: my ($tree, @keys) = @_;
117: my $subtree;
118: my @rest = @keys;
119: my $key = shift(@rest);
120: $subtree = $tree->{$key};
121: if (!defined($subtree)) {
122: return ("Undefined");
123: } elsif (ref($subtree) eq 'HASH') {
124: return treeget($subtree, @rest);
125: } elsif (ref($subtree) eq 'ARRAY') {
126: my @array = @{$subtree};
127: my @ret;
128: foreach my $hashref (@array) {
129: push(@ret, treeget($hashref, @rest));
130: }
131: return @ret;
132: #my @array = @{$subtree};
133: #print Dumper treeget($hashref, @rest);
134: #print Dumper treeget({$key => $subtree}, @rest);
135: #return (treeget($hashref, @rest), treeget({$key => $subtree}, @rest));
136: } else {
137: return ($subtree);
138: }
139: }
140:
141: sub mbnc {
142: my ($bot, $nick, $host, $hand, @args) = @_;
143: my ($chan, $text);
144: if (@args == 2) {
145: ($chan, $text) = ($args[0], $args[1]);
146: } else { $text = $args[0]; }
147: my $hostmask = "$nick!$host";
148: if (defined($chan) && $chans =~ /$chan/) {
149: main::putserv($bot, "PRIVMSG $chan :$nick: Please check private message");
150: }
151: if ($text =~ /^$/) {
152: main::putserv($bot, "PRIVMSG $nick :Type !help for new instructions");
153: foreach my $chan (@teamchans) {
154: main::putservlocalnet($bot, "PRIVMSG $chan :Help *$nick* on ".$bot->{name});
155: }
156: return;
157: } elsif (main::isstaff($bot, $nick) && $text =~ /^delete\s+([[:ascii:]]+)/) {
158: my $username = $1;
159: if (SQLite::deleterows("bnc", "username", $username)) {
160: main::putserv($bot, "PRIVMSG *controlpanel :deluser $username");
161: foreach my $chan (@teamchans) {
162: main::putserv($bot, "PRIVMSG $chan :$username deleted");
163: }
164: }
165: return;
166: } elsif ($staff =~ /$nick/ && $text =~ /^cloneuser$/i) {
167: main::putserv($bot, "PRIVMSG *controlpanel :deluser cloneuser");
168: sleep 3;
169: main::putserv($bot, "PRIVMSG *controlpanel :get Nick cloneuser");
170: }
171: ### TODO: Check duplicate emails ###
172: my @rows = SQLite::selectrows("irc", "hostmask", $hostmask);
173: foreach my $row (@rows) {
174: my $password = SQLite::get("bnc", "ircid", $row->{id}, "password");
175: if (defined($password)) {
176: main::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help.");
177: return;
178: }
179: }
180: if ($text =~ /^captcha\s+([[:alnum:]]+)/) {
181: my $text = $1;
182: # TODO avoid using host mask because cloaking can cause problems
183: my $ircid = SQLite::id("irc", "nick", $nick, $expires);
184: my $captcha = SQLite::get("bnc", "ircid", $ircid, "captcha");
185: if ($text ne $captcha) {
186: main::putserv($bot, "PRIVMSG $nick :Wrong captcha. To get a new captcha, type !bnc <username> <email>");
187: return;
188: }
189: my $pass = Hash::newpass();
190: chomp(my $encrypted = `encrypt $pass`);
191: my $username = SQLite::get("bnc", "ircid", $ircid, "username");
192: my $email = SQLite::get("bnc", "ircid", $ircid, "email");
193: my $hashirc = SQLite::get("irc", "id", $ircid, "hashid");
194: my $bindhost = "$username.$hostname";
195: SQLite::set("bnc", "ircid", $ircid, "password", $encrypted);
196: if (DNS::nextdns($username)) {
197: sleep(2);
198: createbnc($bot, $username, $pass, $bindhost);
199: main::putserv($bot, "PRIVMSG $nick :Check your email!");
200: mailbnc($username, $email, $pass, "bouncer", $hashirc);
201: #www($newnick, $reply, $password, "bouncer");
202: } else {
203: foreach my $chan (@teamchans) {
204: main::putserv($bot, "PRIVMSG $chan :Assigning bindhost $bindhost failed");
205: }
206: }
207: return;
208: } elsif ($text =~ /^([[:alnum:]]+)\s+([[:ascii:]]+)/) {
209: my ($username, $email) = ($1, $2);
210: # my @users = treeget($znctree, "User", "Node");
211: foreach my $user (@users) {
212: if ($user eq $username) {
213: main::putserv($bot, "PRIVMSG $nick :Sorry, username taken. Please contact staff if you need help.");
1.3 ! bountyht 214: return;
1.1 bountyht 215: }
216: }
217: #my $captcha = join'', map +(0..9,'a'..'z','A'..'Z')[rand(10+26*2)], 1..4;
218: my $captcha = int(rand(999));
219: my $ircid = int(rand(9223372036854775807));
220: my $hashid = sha256_hex("$ircid");
221: SQLite::set("irc", "id", $ircid, "localtime", time());
222: SQLite::set("irc", "id", $ircid, "hashid", sha256_hex($ircid));
223: SQLite::set("irc", "id", $ircid, "date", main::date());
224: SQLite::set("irc", "id", $ircid, "hostmask", $hostmask);
225: SQLite::set("irc", "id", $ircid, "nick", $nick);
226: SQLite::set("bnc", "ircid", $ircid, "username", $username);
227: SQLite::set("bnc", "ircid", $ircid, "email", $email);
228: SQLite::set("bnc", "ircid", $ircid, "captcha", $captcha);
229: SQLite::set("bnc", "ircid", $ircid, "hashid", $hashid);
230: main::whois($bot->{sock}, $nick);
231: main::ctcp($bot->{sock}, $nick);
232: main::putserv($bot, "PRIVMSG $nick :".`figlet $captcha`);
233: main::putserv($bot, "PRIVMSG $nick :https://$hostname/$hashid/captcha.png");
234: main::putserv($bot, "PRIVMSG $nick :https://$hostname/register.php?hashirc=$hashid");
235: main::putserv($bot, "PRIVMSG $nick :Type !bnc captcha <text>");
236: foreach my $chan (@teamchans) {
1.2 bountyht 237: main::putservlocalnet($bot, "PRIVMSG $chan :$nick\'s on $bot->{name} bnc captcha is $captcha");
1.1 bountyht 238: }
239: } else {
240: main::putserv($bot, "PRIVMSG $nick :Invalid username or email. Type !bnc <username> <email> to try again.");
241: foreach my $chan (@teamchans) {
242: main::putservlocalnet($bot, "PRIVMSG $chan :Help *$nick* on ".$bot->{name});
243: }
244: }
245: }
246:
247: sub mregex {
248: my ($bot, $nick, $host, $hand, $text) = @_;
249: if (!main::isstaff($bot, $nick)) { return; }
250: if ($text =~ /^ips?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
251: my $ips = $1; # space-separated list of IPs
252: main::putserv($bot, "PRIVMSG $nick :".regexlist($ips));
253: } elsif ($text =~ /^users?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
254: my $users = $1; # space-separated list of usernames
255: main::putserv($bot, "PRIVMSG $nick :".regexlist($users));
256: } elsif ($text =~ /^[-_()|0-9A-Za-z:,\.?*\s]{3,}$/) {
257: my @lines = regex($text);
258: foreach my $l (@lines) { print "$l\n"; }
259: }
260: }
261: sub mforeach {
262: my ($bot, $nick, $host, $hand, $text) = @_;
263: if ($staff !~ /$nick/) { return; }
264: if ($text =~ /^network\s+del\s+([[:graph:]]+)\s+(#[[:graph:]]+)$/) {
265: my ($user, $chan) = ($1, $2);
266: foreach my $n (@main::networks) {
267: main::putserv($bot, "PRIVMSG *controlpanel :delchan $user $n->{name} $chan");
268: }
269: }
270: }
271:
272: sub mcontrolpanel {
273: my ($bot, $nick, $host, $hand, @args) = @_;
274: my ($chan, $text);
275: if (@args == 2) {
276: ($chan, $text) = ($args[0], $args[1]);
277: } else { $text = $args[0]; }
278: my $hostmask = "$nick!$host";
279: if($hostmask eq '*controlpanel!znc@znc.in') {
280: if ($text =~ /^Error: User \[cloneuser\] does not exist/) {
281: createclone($bot);
282: foreach my $chan (@teamchans) {
283: main::putserv($bot, "PRIVMSG $chan :Cloneuser created");
284: }
285: } elsif ($text =~ /^User (.*) added!$/) {
286: main::debug(ALL, "User $1 created");
287: } elsif ($text =~ /^Password has been changed!$/) {
288: main::debug(ALL, "Password changed");
289: } elsif ($text =~ /^Queued network (.*) of user (.*) for a reconnect.$/) {
290: main::debug(ALL, "$2 now connecting to $1...");
291: } elsif ($text =~ /^Admin = false/) {
292: foreach my $chan (@teamchans) {
293: main::putserv($bot, "PRIVMSG $chan :ERROR: $nick is not admin");
294: }
295: die "ERROR: $nick is not admin";
296: } elsif ($text =~ /^Admin = true/) {
297: main::debug(ALL, "$nick is ZNC admin");
298: } elsif ($text =~ /(.*) = (.*)/) {
299: my ($key, $val) = ($1, $2);
300: main::debug(ALL, "ZNC: $key => $val");
301: } else {
302: main::debug(ERRORS, "Unexpected 290 BNC.pm: $hostmask $text");
303: }
304: }
305: }
306: sub loadlog {
307: open(my $fh, '<', "$znclog") or die "Could not read file 'znc.log' $!";
308: chomp(@logs = <$fh>);
309: close $fh;
310: }
311:
312: # return all lines matching a pattern
313: sub regex {
314: my ($pattern) = @_;
315: if (!@logs) { loadlog(); }
316: return grep(/$pattern/, @logs);
317: }
318:
319: # given a list of IPs, return matching users
320: # or given a list of users, return matching IPs
321: sub regexlist {
322: my ($items) = @_;
323: my @items = split /[,\s]+/m, $items;
324: my $pattern = "(".join('|', @items).")";
325: if (!@logs) { loadlog(); }
326: my @matches = grep(/$pattern/, @logs);
327: my @results;
328: foreach my $match (@matches) {
329: if ($match =~ /^\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[([^]\/]+)(\/[^]]+)?\] connected to ZNC from (.*)/) {
330: my ($user, $ip) = ($1, $3);
331: if ($items =~ /[.:]/) { # items are IP addresses
332: push(@results, $user);
333: } else { # items are users
334: push(@results, $ip);
335: }
336: }
337: }
338: my @sorted = sort @results;
339: @results = do { my %seen; grep { !$seen{$_}++ } @sorted }; # uniq
340: return join(' ', @results);
341: }
342:
343: sub createclone {
344: my ($bot) = @_;
345: my $socket = $bot->{sock};
346: my $password = Hash::newpass();
347: my $msg = <<"EOF";
348: adduser cloneuser $password
349: set Nick cloneuser cloneuser
350: set Altnick cloneuser cloneuser_
351: set Ident cloneuser cloneuser
352: set RealName cloneuser cloneuser
353: set MaxNetworks cloneuser 1000
354: set ChanBufferSize cloneuser 1000
355: set MaxQueryBuffers cloneuser 1000
356: set QueryBufferSize cloneuser 1000
357: set NoTrafficTimeout cloneuser 600
358: set QuitMsg cloneuser IRCNow and Forever!
359: set RealName cloneuser cloneuser
360: set DenySetBindHost cloneuser true
361: set Timezone cloneuser US/Pacific
362: LoadModule cloneuser controlpanel
363: LoadModule cloneuser chansaver
364: EOF
365: #LoadModule cloneuser buffextras
366: main::putserv($bot, "PRIVMSG *controlpanel :$msg");
367: foreach my $n (@main::networks) {
368: my $net = $n->{name};
369: my $server = $n->{server};
370: my $port = $n->{port};
371: my $trustcerts = $n->{trustcerts};
372: $msg = <<"EOF";
373: addnetwork cloneuser $net
374: addserver cloneuser $net $server $port
375: disconnect cloneuser $net
376: EOF
377: if ($trustcerts) {
378: $msg .= "SetNetwork TrustAllCerts cloneuser $net True\r\n";
379: }
380: my @chans = split /[,\s]+/m, $chans;
381: foreach my $chan (@chans) {
382: $msg .= "addchan cloneuser $net $chan\r\n";
383: }
384: main::putserv($bot, "PRIVMSG *controlpanel :$msg");
385: }
386: }
387:
388: sub createbnc {
389: my ($bot, $username, $password, $bindhost) = @_;
390: my $netname = $bot->{name};
391: my $msg = <<"EOF";
392: cloneuser cloneuser $username
393: set Nick $username $username
394: set Altnick $username ${username}_
395: set Ident $username $username
396: set RealName $username $username
397: set Password $username $password
398: set MaxNetworks $username 1000
399: set ChanBufferSize $username 1000
400: set MaxQueryBuffers $username 1000
401: set QueryBufferSize $username 1000
402: set NoTrafficTimeout $username 600
403: set QuitMsg $username IRCNow and Forever!
404: set BindHost $username $bindhost
405: set DCCBindHost $username $bindhost
406: set DenySetBindHost $username true
407: reconnect $username $netname
408: EOF
409: #set Language $username en-US
410: main::putserv($bot, "PRIVMSG *controlpanel :$msg");
411: return 1;
412: }
413: sub mailbnc {
414: my( $username, $email, $password, $service, $hashirc )=@_;
415: my $passhash = sha256_hex("$username");
416:
417: my $body = <<"EOF";
418: You created a bouncer!
419:
420: Username: $username
421: Password: $password
422: Server: $hostname
423: Port: $sslport for SSL (secure connection)
424: Port: $plainport for plaintext
425:
426: *IMPORTANT*: Verify your email address:
427:
428: https://$hostname/register.php?hashirc=$hashirc
429:
430: You *MUST* click on the link or your account will be deleted.
431:
432: IRCNow
433: EOF
434: Mail::mail($mailfrom, $email, $mailname, "Verify IRCNow Account", $body);
435: }
436:
437: sub mtaillog {
438: my ($bot, $nick, $host, $hand, @args) = @_;
439: my ($chan, $text);
440: if (@args == 2) {
441: ($chan, $text) = ($args[0], $args[1]);
442: } else { $text = $args[0]; }
443: my $hostmask = "$nick!$host";
444: open(my $fh, "-|", "/usr/bin/tail", "-f", $znclog) or die "could not start tail: $!";
445: while (my $line = <$fh>) {
446: foreach my $chan (@teamchans) {
447: main::putserv($bot, "PRIVMSG $chan :$line");
448: }
449: }
450: }
451:
452: sub mlastseen {
453: my ($bot, $nick, $host, $hand, @args) = @_;
454: my ($chan, $text);
455: if (@args == 2) {
456: ($chan, $text) = ($args[0], $args[1]);
457: } else { $text = $args[0]; }
458: my $hostmask = "$nick!$host";
459: if (!@logs) { loadlog(); }
460: my @users = treeget($znctree, "User", "Node");
461: foreach my $user (@users) {
462: my @lines = grep(/^\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[$user\] connected to ZNC from [.0-9a-fA-F:]+/, @logs);
463: if (scalar(@lines) == 0) {
464: foreach my $chan (@teamchans) {
465: main::putserv($bot, "PRIVMSG $chan :$user never logged in");
466: }
467: next;
468: }
469: my $recent = pop(@lines);
470: if ($recent =~ /^\[(\d{4}-\d\d-\d\d) \d\d:\d\d:\d\d\] \[$user\] connected to ZNC from [.0-9a-fA-F:]+/) {
471: my $date = $1;
472: foreach my $chan (@teamchans) {
473: main::putserv($bot, "PRIVMSG $chan :$user $date");
474: }
475: }
476: }
477: }
478: #sub resend {
479: # my ($bot, $newnick, $email) = @_;
480: # my $password = newpass();
481: # sendmsg($bot, "*controlpanel", "set Password $newnick $password");
482: # mailverify($newnick, $email, $password, "bouncer");
483: # sendmsg($bot, "$newnick", "Email sent");
484: #}
485:
486: # if ($reply =~ /^!resend ([-_0-9a-zA-Z]+) ([-_0-9a-zA-Z]+@[-_0-9a-zA-Z]+\.[-_0-9a-zA-Z]+)$/i) {
487: # my ($newnick, $email) = ($1, $2);
488: # my $password = newpass();
489: # resend($bot, $newnick, $email);
490: # }
491:
492: #sub resetznc {
493: #
494: #AnonIPLimit 10000
495: #AuthOnlyViaModule false
496: #ConnectDelay 0
497: #HideVersion true
498: #LoadModule
499: #ServerThrottle
500: #1337 209.141.38.137
501: #31337 209.141.38.137
502: #1337 2605:6400:20:5cc::
503: #31337 2605:6400:20:5cc::
504: #1337 127.0.0.1
505: #1338 127.0.0.1
506: #}
507: #
508: #alias Provides bouncer-side command alias support.
509: #autoreply Reply to queries when you are away
510: #block_motd Block the MOTD from IRC so it's not sent to your client(s).
511: #bouncedcc Bounces DCC transfers through ZNC instead of sending them directly to the user.
512: #clientnotify Notifies you when another IRC client logs into or out of your account. Configurable.
513: #ctcpflood Don't forward CTCP floods to clients
514: #dcc This module allows you to transfer files to and from ZNC
515: #perform Keeps a list of commands to be executed when ZNC connects to IRC.
516: #webadmin Web based administration module.
517:
518:
519: 1; # MUST BE LAST STATEMENT IN FILE
CVSweb