9 use Digest::SHA qw(sha256_hex);
16 my %conf = %main::conf;
17 my $chans = $conf{chans};
18 my $teamchans = $conf{teamchans};
19 my @teamchans = split /[,\s]+/m, $teamchans;
20 my $staff = $conf{staff};
21 my $zncdir = $conf{zncdir};
22 my $znclog = $conf{znclog} || "$zncdir/.znc/moddata/adminlog/znc.log";
23 my $hostname = $conf{hostname};
24 my $bnchostname = $conf{bnchostname};
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 $approval = $conf{approval};
33 my $webpanel = $conf{webpanel};
34 # File containing IRC networks
35 my $netpath = "networks";
45 `doas chmod g+r /home/znc/home/znc/.znc/`;
47 main::cbind("pub", "-", "bnc", \&mbnc);
48 main::cbind("msg", "-", "bnc", \&mbnc);
49 main::cbind("msg", "-", "regex", \&mregex);
50 main::cbind("msg", "-", "foreach", \&mforeach);
51 main::cbind("msgm", "-", "*", \&mcontrolpanel);
52 main::cbind("msg", "-", "taillog", \&mtaillog);
53 main::cbind("msg", "-", "lastseen", \&mlastseen);
56 unveil("/usr/local/bin/figlet", "rx") or die "Unable to unveil $!";
57 unveil("/usr/lib/libc.so.95.1", "r") or die "Unable to unveil $!";
58 unveil("/usr/libexec/ld.so", "r") or die "Unable to unveil $!";
59 unveil("/usr/bin/tail", "rx") or die "Unable to unveil $!";
60 unveil("$znclog", "r") or die "unable to unveil $!";
61 unveil("$netpath", "r") or die "Unable to unveil $!";
63 @networks = readnetworks($netpath);
65 # networks must be sorted to avoid multiple connections
66 @networks = sort @networks;
69 # Return list of networks from filename
70 # To add multiple servers for a single network, simply create a new entry with
71 # the same net name; znc ignores addnetwork commands when a network already exists
74 my @lines = main::readarray($filename);
76 foreach my $line (@lines) {
77 if ($line =~ /^#/ or $line =~ /^\s*$/) { # skip comments and whitespace
79 } elsif ($line =~ /^\s*([-a-zA-Z0-9]+)\s*([-_.:a-zA-Z0-9]+)\s*(~|\+)?([0-9]+)\s*$/) {
80 my ($name, $server, $port) = ($1, $2, $4);
84 } elsif ($3 eq "~") { # Use SSL but trust all certs
87 } else { # Use SSL and verify certs
91 push(@networks, {"name" => $name, "server" => $server, "port" => $port, "trustcerts" => $trustcerts });
93 die "network format invalid: $line\n";
100 my ($bot, $nick, $host, $hand, @args) = @_;
103 ($chan, $text) = ($args[0], $args[1]);
104 } else { $text = $args[0]; }
105 my $hostmask = "$nick!$host";
106 if (defined($chan) && $chans =~ /$chan/) {
107 main::putserv($bot, "PRIVMSG $chan :$nick: Please check private message");
110 main::putserv($bot, "PRIVMSG $nick :Type !help for new instructions");
111 foreach my $chan (@teamchans) {
112 main::putservlocalnet($bot, "PRIVMSG $chan :$staff: Help *$nick* on network ".$bot->{name});
115 } elsif (main::isstaff($bot, $nick) && $text =~ /^delete\s+([[:ascii:]]+)/) {
117 if (SQLite::deleterows("bnc", "username", $username)) {
118 main::putserv($bot, "PRIVMSG *controlpanel :deluser $username");
119 foreach my $chan (@teamchans) {
120 main::putserv($bot, "PRIVMSG $chan :$username deleted");
124 } elsif (main::isstaff($bot, $nick) && $text =~ /^approve\s+([[:ascii:]]+)/) {
126 if (SQLite::selectrows("bnc", "username", $username)) {
127 my $email = SQLite::get("bnc", "username", $username, "email");
128 my $password = Hash::newpass();
129 main::putserv($bot,"privmsg *controlpanel :set Password $username $password");
130 main::putserv($bot, "PRIVMSG *blockuser :unblock $username");
131 mailbncApproved($username,$email,$password);
132 foreach my $chan (@teamchans) {
133 main::putserv($bot, "PRIVMSG $chan :$username bnc approved");
136 main::putserv($bot, "PRIVMSG $chan :$username hasn't requested a bnc account");
139 } elsif ($staff =~ /$nick/ && $text =~ /^cloneuser$/i) {
140 main::putserv($bot, "PRIVMSG *controlpanel :deluser cloneuser");
142 main::putserv($bot, "PRIVMSG *controlpanel :get Nick cloneuser");
144 ### Check duplicate hostmasks ###
145 my @rows = SQLite::selectrows("irc", "hostmask", $hostmask);
146 foreach my $row (@rows) {
147 my $password = SQLite::get("bnc", "ircid", $row->{id}, "password");
148 if (defined($password)) {
149 main::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help.");
154 if ($text =~ /^captcha\s+([[:alnum:]]+)/) {
156 # TODO avoid using host mask because cloaking can cause problems
157 my $ircid = SQLite::id("irc", "nick", $nick, $expires);
158 my $captcha = SQLite::get("bnc", "ircid", $ircid, "captcha");
159 if ($text ne $captcha) {
160 main::putserv($bot, "PRIVMSG $nick :Wrong captcha. To get a new captcha, type !bnc <username> <email>");
163 my $pass = Hash::newpass();
164 chomp(my $encrypted = `encrypt $pass`);
165 my $username = SQLite::get("bnc", "ircid", $ircid, "username");
166 my $email = SQLite::get("bnc", "ircid", $ircid, "email");
167 my $hashirc = SQLite::get("irc", "id", $ircid, "hashid");
168 my $bindhost = "$username.$hostname";
169 SQLite::set("bnc", "ircid", $ircid, "password", $encrypted);
170 if (DNS::nextdns($username)) {
172 createbnc($bot, $username, $pass, $bindhost);
173 if ($approval eq "true") {
174 main::putserv($bot, "PRIVMSG *blockuser :block $username");
175 main::putserv($bot, "PRIVMSG $nick :Your account has been created but must be approved by your admins ($staff) before it can be used. Please reply to the email and contact staff over IRC.");
176 mailbncPending($username, $email);
177 foreach my $chan (@teamchans) {
178 main::putservlocalnet($bot, "PRIVMSG $chan :$staff: To approve $nick, you must type !bnc approve $username");
181 main::putserv($bot, "PRIVMSG $nick :Check your email! Please reply to the email and contact staff over IRC.");
182 mailbncApproved($username,$email,$pass);
184 foreach my $chan (@teamchans) {
185 main::putservlocalnet($bot, "PRIVMSG $chan :$staff: $nick\'s bnc registration of $username on $bot->{name} was successful, *but* you *must* help him connect. Most users are unable to connect. Show him https://wiki.ircnow.org/?n=Bouncer.Bouncer and give him connection instructions");
187 #www($newnick, $reply, $password, "bouncer");
189 foreach my $chan (@teamchans) {
190 main::putserv($bot, "PRIVMSG $chan :Assigning bindhost $bindhost failed");
194 } elsif ($text =~ /^([[:alnum:]]+)\s+([[:ascii:]]+)/) {
195 my ($username, $email) = ($1, $2);
196 my @userrows = SQLite::selectrows("bnc", "username", $username);
197 foreach my $row (@userrows) {
198 my $password = SQLite::get("bnc", "ircid", $row->{id}, "password");
199 if (defined($password)) {
200 main::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help.");
204 my @emailrows = SQLite::selectrows("bnc", "email", $email);
205 foreach my $row (@userrows) {
206 my $password = SQLite::get("bnc", "ircid", $row->{id}, "password");
207 if (defined($password)) {
208 main::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help.");
213 # my @users = treeget($znctree, "User", "Node");
214 foreach my $user (@users) {
215 if ($user eq $username) {
216 main::putserv($bot, "PRIVMSG $nick :Sorry, username taken. Please contact staff if you need help.");
221 #my $captcha = join'', map +(0..9,'a'..'z','A'..'Z')[rand(10+26*2)], 1..4;
222 my $captcha = int(rand(999));
223 my $ircid = int(rand(9223372036854775807));
224 my $hashid = sha256_hex("$ircid");
225 SQLite::set("irc", "id", $ircid, "localtime", time());
226 SQLite::set("irc", "id", $ircid, "hashid", sha256_hex($ircid));
227 SQLite::set("irc", "id", $ircid, "date", main::date());
228 SQLite::set("irc", "id", $ircid, "hostmask", $hostmask);
229 SQLite::set("irc", "id", $ircid, "nick", $nick);
230 SQLite::set("bnc", "ircid", $ircid, "username", $username);
231 SQLite::set("bnc", "ircid", $ircid, "email", $email);
232 SQLite::set("bnc", "ircid", $ircid, "captcha", $captcha);
233 SQLite::set("bnc", "ircid", $ircid, "hashid", $hashid);
234 main::whois($bot->{sock}, $nick);
235 main::ctcp($bot->{sock}, $nick);
236 main::putserv($bot, "PRIVMSG $nick :".`figlet $captcha`);
237 #main::putserv($bot, "PRIVMSG $nick :https://$hostname/$hashid/captcha.png");
238 #main::putserv($bot, "PRIVMSG $nick :https://$hostname/register.php?hashirc=$hashid");
239 main::putserv($bot, "PRIVMSG $nick :Type !bnc captcha <text>");
240 foreach my $chan (@teamchans) {
241 main::putservlocalnet($bot, "PRIVMSG $chan :$nick\'s on $bot->{name} bnc captcha is $captcha");
244 main::putserv($bot, "PRIVMSG $nick :Invalid username or email. Type !bnc <username> <email> to try again.");
245 foreach my $chan (@teamchans) {
246 main::putservlocalnet($bot, "PRIVMSG $chan :$staff: Help *$nick* on network ".$bot->{name});
252 my ($bot, $nick, $host, $hand, $text) = @_;
253 if (!main::isstaff($bot, $nick)) { return; }
254 if ($text =~ /^ips?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
255 my $ips = $1; # space-separated list of IPs
256 main::putserv($bot, "PRIVMSG $nick :".regexlist($ips));
257 } elsif ($text =~ /^users?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
258 my $users = $1; # space-separated list of usernames
259 main::putserv($bot, "PRIVMSG $nick :".regexlist($users));
260 } elsif ($text =~ /^[-_()|0-9A-Za-z:,\.?*\s]{3,}$/) {
261 my @lines = regex($text);
262 foreach my $l (@lines) { print "$l\n"; }
266 my ($bot, $nick, $host, $hand, $text) = @_;
267 if ($staff !~ /$nick/) { return; }
268 if ($text =~ /^network\s+del\s+([[:graph:]]+)\s+(#[[:graph:]]+)$/) {
269 my ($user, $chan) = ($1, $2);
270 foreach my $n (@networks) {
271 main::putserv($bot, "PRIVMSG *controlpanel :delchan $user $n->{name} $chan");
277 my ($bot, $nick, $host, $hand, @args) = @_;
280 ($chan, $text) = ($args[0], $args[1]);
281 } else { $text = $args[0]; }
282 my $hostmask = "$nick!$host";
283 if($hostmask eq '*controlpanel!znc@znc.in') {
284 if ($text =~ /^Error: User \[cloneuser\] does not exist/) {
286 main::putserv($bot, "PRIVMSG *status :loadmod blockuser");
287 foreach my $chan (@teamchans) {
288 main::putserv($bot, "PRIVMSG $chan :Cloneuser created");
290 } elsif ($text =~ /^User (.*) added!$/) {
291 main::debug(ALL, "User $1 created");
292 } elsif ($text =~ /^Password has been changed!$/) {
293 main::debug(ALL, "Password changed");
294 } elsif ($text =~ /^Queued network (.*) of user (.*) for a reconnect.$/) {
295 main::debug(ALL, "$2 now connecting to $1...");
296 } elsif ($text =~ /^Admin = false/) {
297 foreach my $chan (@teamchans) {
298 main::putserv($bot, "PRIVMSG $chan :ERROR: $nick is not admin");
300 die "ERROR: $nick is not admin";
301 } elsif ($text =~ /^Admin = true/) {
302 main::debug(ALL, "$nick is ZNC admin");
303 } elsif ($text =~ /(.*) = (.*)/) {
304 my ($key, $val) = ($1, $2);
305 main::debug(ALL, "ZNC: $key => $val");
307 main::debug(ERRORS, "Unexpected 290 BNC.pm: $hostmask $text");
312 open(my $fh, '<', "$znclog") or die "Could not read znc log file: '$znclog' $!";
313 chomp(@logs = <$fh>);
317 # return all lines matching a pattern
320 if (!@logs) { loadlog(); }
321 return grep(/$pattern/, @logs);
324 # given a list of IPs, return matching users
325 # or given a list of users, return matching IPs
328 my @items = split /[,\s]+/m, $items;
329 my $pattern = "(".join('|', @items).")";
330 if (!@logs) { loadlog(); }
331 my @matches = grep(/$pattern/, @logs);
333 foreach my $match (@matches) {
334 if ($match =~ /^\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[([^]\/]+)(\/[^]]+)?\] connected to ZNC from (.*)/) {
335 my ($user, $ip) = ($1, $3);
336 if ($items =~ /[.:]/) { # items are IP addresses
337 push(@results, $user);
338 } else { # items are users
343 my @sorted = sort @results;
344 @results = do { my %seen; grep { !$seen{$_}++ } @sorted }; # uniq
345 return join(' ', @results);
350 my $socket = $bot->{sock};
351 my $password = Hash::newpass();
353 adduser cloneuser $password
354 set Nick cloneuser cloneuser
355 set Altnick cloneuser cloneuser_
356 set Ident cloneuser cloneuser
357 set RealName cloneuser cloneuser
358 set MaxNetworks cloneuser 1000
359 set ChanBufferSize cloneuser 1000
360 set MaxQueryBuffers cloneuser 1000
361 set QueryBufferSize cloneuser 1000
362 set NoTrafficTimeout cloneuser 600
363 set QuitMsg cloneuser IRCNow and Forever!
364 set RealName cloneuser cloneuser
365 set DenySetBindHost cloneuser true
366 set Timezone cloneuser US/Pacific
367 LoadModule cloneuser controlpanel
368 LoadModule cloneuser chansaver
370 main::putserv($bot, "PRIVMSG *controlpanel :$msg");
371 foreach my $n (@networks) {
372 my $net = $n->{name};
373 my $server = $n->{server};
374 my $port = $n->{port};
375 my $trustcerts = $n->{trustcerts};
377 addnetwork cloneuser $net
378 addserver cloneuser $net $server $port
379 disconnect cloneuser $net
382 $msg .= "SetNetwork TrustAllCerts cloneuser $net True\r\n";
384 my @chans = split /[,\s]+/m, $chans;
385 foreach my $chan (@chans) {
386 $msg .= "addchan cloneuser $net $chan\r\n";
388 main::putserv($bot, "PRIVMSG *controlpanel :$msg");
393 my ($bot, $username, $password, $bindhost) = @_;
394 my $netname = $bot->{name};
396 cloneuser cloneuser $username
397 set Nick $username $username
398 set Altnick $username ${username}_
399 set Ident $username $username
400 set RealName $username $username
401 set Password $username $password
402 set MaxNetworks $username 1000
403 set ChanBufferSize $username 1000
404 set MaxQueryBuffers $username 1000
405 set QueryBufferSize $username 1000
406 set NoTrafficTimeout $username 600
407 set QuitMsg $username IRCNow and Forever!
408 set BindHost $username $bindhost
409 set DCCBindHost $username $bindhost
410 set DenySetBindHost $username true
411 reconnect $username $netname
413 #set Language $username en-US
414 main::putserv($bot, "PRIVMSG *controlpanel :$msg");
417 sub mailbncApproved {
418 my( $username, $email, $password)=@_;
420 Welcome to $conf{localnet}!
422 You created a bouncer:
427 Port: $sslport for SSL (secure connection)
428 Port: $plainport for plaintext
431 Connection Instructions: https://wiki.ircnow.org/?n=Bouncer.Bouncer
438 main::mail($mailfrom, $email, $mailname, "Your $conf{localnet} Bouncer", $body);
442 my( $username, $email)=@_;
444 Welcome to $conf{localnet}!
446 Your bouncer needs to be approved by staff ($staff).
447 Please reply to this email and contact them over IRC.
449 Once $staff have been contacted, they will send you login
450 instructions. This may take up to 48 hours.
455 main::mail($mailfrom, $email, $mailname, "Confirm $conf{localnet} Bouncer", $body);
459 my ($bot, $nick, $host, $hand, @args) = @_;
462 ($chan, $text) = ($args[0], $args[1]);
463 } else { $text = $args[0]; }
464 my $hostmask = "$nick!$host";
465 open(my $fh, "-|", "/usr/bin/tail", '-10', $znclog) or die "could not tail $znclog: $!";
466 while (my $line = <$fh>) {
467 main::putserv($bot, "PRIVMSG $nick :$line");
472 my ($bot, $nick, $host, $hand, @args) = @_;
475 ($chan, $text) = ($args[0], $args[1]);
476 } else { $text = $args[0]; }
477 my $hostmask = "$nick!$host";
478 if (!@logs) { loadlog(); }
479 #my @users = treeget($znctree, "User", "Node");
480 foreach my $user (@users) {
481 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);
482 if (scalar(@lines) == 0) {
483 main::putserv($bot, "PRIVMSG $nick :$user never logged in");
486 my $recent = pop(@lines);
487 if ($recent =~ /^\[(\d{4}-\d\d-\d\d) \d\d:\d\d:\d\d\] \[$user\] connected to ZNC from [.0-9a-fA-F:]+/) {
489 main::putserv($bot, "PRIVMSG $nick :$user $date");
494 # my ($bot, $newnick, $email) = @_;
495 # my $password = newpass();
496 # sendmsg($bot, "*controlpanel", "set Password $newnick $password");
497 # mailverify($newnick, $email, $password, "bouncer");
498 # sendmsg($bot, "$newnick", "Email sent");
501 #`doas chown znc:daemon /home/znc/home/znc/.znc/configs/znc.conf`;
503 # if ($reply =~ /^!resend ([-_0-9a-zA-Z]+) ([-_0-9a-zA-Z]+@[-_0-9a-zA-Z]+\.[-_0-9a-zA-Z]+)$/i) {
504 # my ($newnick, $email) = ($1, $2);
505 # my $password = newpass();
506 # resend($bot, $newnick, $email);
512 #AuthOnlyViaModule false
518 #31337 209.141.38.137
519 #1337 2605:6400:20:5cc::
520 #31337 2605:6400:20:5cc::
525 #alias Provides bouncer-side command alias support.
526 #autoreply Reply to queries when you are away
527 #block_motd Block the MOTD from IRC so it's not sent to your client(s).
528 #bouncedcc Bounces DCC transfers through ZNC instead of sending them directly to the user.
529 #clientnotify Notifies you when another IRC client logs into or out of your account. Configurable.
530 #ctcpflood Don't forward CTCP floods to clients
531 #dcc This module allows you to transfer files to and from ZNC
532 #perform Keeps a list of commands to be executed when ZNC connects to IRC.
533 #webadmin Web based administration module.
535 #my $zncconfpath = $conf{zncconfpath} || "$zncdir/.znc/configs/znc.conf";
536 #my $znctree = { Node => "root" };
538 #unveil("$zncconfpath", "r") or die "Unable to unveil $!";
539 #dependencies for figlet
541 #unveil("$znclog", "r") or die "Unable to unveil $!";
542 #print treeget($znctree, "AnonIPLimit")."\n";
543 #print treeget($znctree, "ServerThrottle")."\n";
544 #print treeget($znctree, "ConnectDelay")."\n";
546 #print Dumper \treeget($znctree, "User", "Node");
547 #print Dumper \treeget($znctree, "User", "Network", "Node");
548 #my @zncconf = main::readarray($zncconfpath);
550 #foreach my $line (@zncconf) {
551 # if ($line =~ /<User (.*)>/) {
555 #$znctree = parseml($znctree, @zncconf);
557 ## parseml($tree, @lines)
558 ## tree is a reference to a hash
559 ## returns hash ref of tree
561 # my ($tree, @lines) = @_;
562 # #if (scalar(@lines) == 0) { return $tree; }
563 # while (scalar(@lines) > 0) {
564 # my $line = shift(@lines);
565 # if ($line =~ /^\s*([^=<>\s]+)\s*=\s*([^=<>]+)\s*$/) {
566 # my ($tag, $val) = ($1, $2);
567 # $tree->{$tag} = $val;
568 # } elsif ($line =~ /^\/\//) { # skip comments
569 # } elsif ($line =~ /^\s*$/) { # skip blank lines
570 # } elsif ($line =~ /^\s*<([^>\s\/]+)\s*([^>\/]*)>\s*$/) {
571 # my ($tag, $val) = ($1, $2);
572 # if (!defined($tree->{$tag})) { $tree->{$tag} = []; }
574 # while (scalar(@lines) > 0) {
575 # my $line = shift(@lines);
576 # if ($line =~ /^\s*<\/$tag>\s*$/) {
577 # my $subtree = parseml({ Node => $val }, @newlines);
578 # push(@{$tree->{$tag}}, $subtree);
579 # return parseml($tree, @lines);
581 # push(@newlines, $line);
583 # } else { print "ERROR: $line\n"; }
584 # #TODO ERRORS not defined??
585 ## } else { main::debug(ERRORS, "ERROR: $line"); }
590 ##Returns array of all values
591 ##treeget($tree, "User");
593 # my ($tree, @keys) = @_;
596 # my $key = shift(@rest);
597 # $subtree = $tree->{$key};
598 # if (!defined($subtree)) {
599 # return ("Undefined");
600 # } elsif (ref($subtree) eq 'HASH') {
601 # return treeget($subtree, @rest);
602 # } elsif (ref($subtree) eq 'ARRAY') {
603 # my @array = @{$subtree};
605 # foreach my $hashref (@array) {
606 # push(@ret, treeget($hashref, @rest));
609 # #my @array = @{$subtree};
610 # #print Dumper treeget($hashref, @rest);
611 # #print Dumper treeget({$key => $subtree}, @rest);
612 # #return (treeget($hashref, @rest), treeget({$key => $subtree}, @rest));
619 1; # MUST BE LAST STATEMENT IN FILE