Blob


1 #!/usr/bin/perl
3 package BNC;
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";
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" };
35 use constant {
36 NONE => 0,
37 ERRORS => 1,
38 WARNINGS => 2,
39 ALL => 3,
40 };
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);
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 }
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);
103 push(@newlines, $line);
105 } else { print "ERROR: $line\n"; }
106 #TODO ERRORS not defined??
107 # } else { main::debug(ERRORS, "ERROR: $line"); }
109 return $tree;
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));
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);
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");
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});
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");
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");
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;
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;
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");
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.");
214 return;
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");
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});
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"; }
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");
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");
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");
301 } else {
302 main::debug(ERRORS, "Unexpected 290 BNC.pm: $hostmask $text");
306 sub loadlog {
307 open(my $fh, '<', "$znclog") or die "Could not read file 'znc.log' $!";
308 chomp(@logs = <$fh>);
309 close $fh;
312 # return all lines matching a pattern
313 sub regex {
314 my ($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
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);
338 my @sorted = sort @results;
339 @results = do { my %seen; grep { !$seen{$_}++ } @sorted }; # uniq
340 return join(' ', @results);
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";
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");
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;
413 sub mailbnc {
414 my( $username, $email, $password, $service, $hashirc )=@_;
415 my $passhash = sha256_hex("$username");
417 my $body = <<"EOF";
418 You created a bouncer!
420 Username: $username
421 Password: $password
422 Server: $hostname
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.
432 IRCNow
433 EOF
434 Mail::mail($mailfrom, $email, $mailname, "Verify IRCNow Account", $body);
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");
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");
467 next;
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");
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 #}
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 # }
492 #sub resetznc {
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 #}
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