10 use Digest::SHA qw(sha256_hex);
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};
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" };
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);
47 foreach my $line (@zncconf) {
48 if ($line =~ /<User (.*)>/) {
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);
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 $!";
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";
75 #print Dumper \treeget($znctree, "User", "Node");
76 #print Dumper \treeget($znctree, "User", "Network", "Node");
79 # parseml($tree, @lines)
80 # tree is a reference to a hash
81 # returns hash ref of tree
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);
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} = []; }
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);
103 push(@newlines, $line);
105 } else { print "ERROR: $line\n"; }
106 #TODO ERRORS not defined??
107 # } else { main::debug(ERRORS, "ERROR: $line"); }
112 #Returns array of all values
113 #treeget($tree, "User");
114 #treeget($tree, "MaFFia Network");
116 my ($tree, @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};
128 foreach my $hashref (@array) {
129 push(@ret, treeget($hashref, @rest));
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));
142 my ($bot, $nick, $host, $hand, @args) = @_;
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");
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});
157 } elsif (main::isstaff($bot, $nick) && $text =~ /^delete\s+([[:ascii:]]+)/) {
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");
166 } elsif ($staff =~ /$nick/ && $text =~ /^cloneuser$/i) {
167 main::putserv($bot, "PRIVMSG *controlpanel :deluser cloneuser");
169 main::putserv($bot, "PRIVMSG *controlpanel :get Nick cloneuser");
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.");
180 if ($text =~ /^captcha\s+([[:alnum:]]+)/) {
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>");
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)) {
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");
203 foreach my $chan (@teamchans) {
204 main::putserv($bot, "PRIVMSG $chan :Assigning bindhost $bindhost failed");
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.");
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) {
237 main::putservlocalnet($bot, "PRIVMSG $chan :$nick\'s on $bot->{name} bnc captcha is $captcha");
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});
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"; }
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");
273 my ($bot, $nick, $host, $hand, @args) = @_;
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/) {
282 foreach my $chan (@teamchans) {
283 main::putserv($bot, "PRIVMSG $chan :Cloneuser created");
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");
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");
302 main::debug(ERRORS, "Unexpected 290 BNC.pm: $hostmask $text");
307 open(my $fh, '<', "$znclog") or die "Could not read file 'znc.log' $!";
308 chomp(@logs = <$fh>);
312 # return all lines matching a pattern
315 if (!@logs) { loadlog(); }
316 return grep(/$pattern/, @logs);
319 # given a list of IPs, return matching users
320 # or given a list of users, return matching IPs
323 my @items = split /[,\s]+/m, $items;
324 my $pattern = "(".join('|', @items).")";
325 if (!@logs) { loadlog(); }
326 my @matches = grep(/$pattern/, @logs);
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
338 my @sorted = sort @results;
339 @results = do { my %seen; grep { !$seen{$_}++ } @sorted }; # uniq
340 return join(' ', @results);
345 my $socket = $bot->{sock};
346 my $password = Hash::newpass();
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
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};
373 addnetwork cloneuser $net
374 addserver cloneuser $net $server $port
375 disconnect cloneuser $net
378 $msg .= "SetNetwork TrustAllCerts cloneuser $net True\r\n";
380 my @chans = split /[,\s]+/m, $chans;
381 foreach my $chan (@chans) {
382 $msg .= "addchan cloneuser $net $chan\r\n";
384 main::putserv($bot, "PRIVMSG *controlpanel :$msg");
389 my ($bot, $username, $password, $bindhost) = @_;
390 my $netname = $bot->{name};
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
409 #set Language $username en-US
410 main::putserv($bot, "PRIVMSG *controlpanel :$msg");
414 my( $username, $email, $password, $service, $hashirc )=@_;
415 my $passhash = sha256_hex("$username");
418 You created a bouncer!
423 Port: $sslport for SSL (secure connection)
424 Port: $plainport for plaintext
426 *IMPORTANT*: Verify your email address:
428 https://$hostname/register.php?hashirc=$hashirc
430 You *MUST* click on the link or your account will be deleted.
434 Mail::mail($mailfrom, $email, $mailname, "Verify IRCNow Account", $body);
438 my ($bot, $nick, $host, $hand, @args) = @_;
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");
453 my ($bot, $nick, $host, $hand, @args) = @_;
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");
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:]+/) {
472 foreach my $chan (@teamchans) {
473 main::putserv($bot, "PRIVMSG $chan :$user $date");
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");
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);
495 #AuthOnlyViaModule false
501 #31337 209.141.38.137
502 #1337 2605:6400:20:5cc::
503 #31337 2605:6400:20:5cc::
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.
519 1; # MUST BE LAST STATEMENT IN FILE