commit 0f1839cb8eb6c873a99d1568e34d96d450767862 from: Izzy Blacklock date: Mon Aug 14 01:26:54 2023 UTC Initial addition of BotNow::* modules to IRCNOW::Acct::* commit - 508e42881039eebebd6f2b001a42579950242266 commit + 0f1839cb8eb6c873a99d1568e34d96d450767862 blob - /dev/null blob + cf0e088ca4e13e263abd86615d38a43500cfc497 (mode 644) --- /dev/null +++ lib/IRCNOW/Acct/Mail.pm @@ -0,0 +1,279 @@ +package BotNow::Mail; + +use strict; +use warnings; +use OpenBSD::Pledge; +use OpenBSD::Unveil; +use lib qw(./lib); +use IRCNOW::IO qw(readarray writefile); +use IRCNOW::IO::IRC; +use BotNow::Hash; +use File::Copy qw(copy); +use MIME::Base64; +use Digest::SHA qw(sha256_hex); + +my %conf = %main::conf; +my $chans = $conf{chans}; +my $staff = $conf{staff}; +my $mailhostname = $conf{mailhostname}; +my $mailfrom = $conf{mailfrom}; +my $mailname = $conf{mailname}; +my $imapport = $conf{imapport}; +my $smtpport = $conf{smtpport}; +my $teamchans = $conf{teamchans}; +my @teamchans = split /[,\s]+/m, $teamchans; +my $webmail = $conf{webmail}; +my $approval = $conf{approval}; +my $expires = $conf{expires}; +my $passwdpath = "/etc/mail/passwd"; +my $virtualspath = "/etc/mail/virtuals"; +my $senderspath = "/etc/mail/users"; +my @users; + +IRCNOW::IO::IRC::cbind("msg", "-", "mail", \&mmail); + +sub init { + #dependencies for encrypt + unveil("/usr/bin/encrypt", "rx") or die "Unable to unveil $!"; + #dependencies for mail + unveil("/usr/sbin/sendmail", "rx") or die "Unable to unveil $!"; + unveil($passwdpath, "rwc") or die "Unable to unveil $!"; + unveil($virtualspath, "rwc") or die "Unable to unveil $!"; + unveil($senderspath, "rwc") or die "Unable to unveil $!"; + unveil("$passwdpath.bak", "rwc") or die "Unable to unveil $!"; + unveil("$virtualspath.bak", "rwc") or die "Unable to unveil $!"; + unveil("$senderspath.bak", "rwc") or die "Unable to unveil $!"; + unveil("/usr/lib/libutil.so.13.1", "r") or die "Unable to unveil $!"; + unveil("/bin/sh", "rx") or die "Unable to unveil $!"; +} + +sub mmail { + my ($bot, $nick, $host, $hand, @args) = @_; + my ($chan, $text); + if (@args == 2) { + ($chan, $text) = ($args[0], $args[1]); + } else { $text = $args[0]; } + my $hostmask = "$nick!$host"; + if (defined($chan) && $chans =~ /$chan/) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $chan :$nick: Please check private message"); + } + if ($text =~ /^$/) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Type !help for new instructions"); + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putservlocalnet($bot, "PRIVMSG $chan :$staff: Help *$nick* on network ".$bot->{name}." with mail"); + } + return; + } elsif (IRCNOW::IO::IRC::isstaff($bot, $nick) && $text =~ /^delete\s+([[:ascii:]]+)/) { + my $username = $1; + if (BotNow::SQLite::deleterows("mail", "username", $username)) { + deletemail($username); + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $chan :$username email deleted"); + } + } + return; + } elsif (IRCNOW::IO::IRC::isstaff($bot, $nick) && $text =~ /^approve\s+([[:ascii:]]+)/) { + my $username = $1; + my @passwd = readarray($passwdpath); + foreach my $line (@passwd) { + $line =~ s/^#(${username}\@${mailhostname}.*)/$1/; + } + # trailing newline necessary + `doas touch $passwdpath.bak`; + `doas chmod g+w $passwdpath.bak`; + writefile("$passwdpath.bak", join("\n", @passwd)."\n"); + copy "${passwdpath}.bak", $passwdpath; + + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $chan :$username mail approved"); + } + return; + } + ### Check duplicate hostmasks ### + my @rows = BotNow::SQLite::selectrows("irc", "hostmask", $hostmask); + foreach my $row (@rows) { + my $password = BotNow::SQLite::get("mail", "ircid", $row->{id}, "password"); + if (defined($password)) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help."); + return; + } + } + + if ($text =~ /^captcha\s+([[:alnum:]]+)/) { + my $text = $1; + # TODO avoid using host mask because cloaking can cause problems + my $ircid = BotNow::SQLite::id("irc", "nick", $nick, $expires); + my $captcha = BotNow::SQLite::get("mail", "ircid", $ircid, "captcha"); + if ($text ne $captcha) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Wrong captcha. To get a new captcha, type !mail "); + return; + } + my $pass = BotNot::Hash::newpass(); + chomp(my $encrypted = `encrypt $pass`); + my $username = BotNow::SQLite::get("mail", "ircid", $ircid, "username"); + my $email = BotNow::SQLite::get("mail", "ircid", $ircid, "email"); + my $hashirc = BotNow::SQLite::get("irc", "id", $ircid, "hashid"); + BotNow::SQLite::set("mail", "ircid", $ircid, "password", $encrypted); + sleep(2); + createmail($pass, $username); + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Check your email!"); + sleep(5); + mailmail($username, $pass, $email); + if ($approval) { + my @passwd = readarray($passwdpath); + foreach my $line (@passwd) { + $line =~ s/^(${username}\@${mailhostname}.*)/#$1/; + } + # trailing newline necessary + `doas touch $passwdpath.bak`; + `doas chmod g+w $passwdpath.bak`; + writefile("$passwdpath.bak", join("\n", @passwd)."\n"); + copy "${passwdpath}.bak", $passwdpath; + + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Your account has been created but must be manually approved by your admins ($staff) before it can be used."); + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putservlocalnet($bot, "PRIVMSG $chan :$staff: $nick\'s account $username must be manually unblocked before it can be used."); + } + } + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putservlocalnet($bot, "PRIVMSG $chan :$staff: $nick\'s mail registration of $username\@$mailhostname on $bot->{name} was successful, but you *must* help him to connect. Most users are unable to connect. Show him https://wiki.ircnow.org/?n=Email.Email"); + } + #www($newnick, $reply, $password, "bouncer"); + return; + } elsif ($text =~ /^([[:alnum:]]+)\s+([[:ascii:]]+)/) { + my ($username, $email) = ($1, $2); + my @userrows = BotNow::SQLite::selectrows("mail", "username", $username); + foreach my $row (@userrows) { + my $password = BotNow::SQLite::get("mail", "ircid", $row->{id}, "password"); + if (defined($password)) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help."); + return; + } + } + my @emailrows = BotNow::SQLite::selectrows("mail", "email", $email); + foreach my $row (@userrows) { + my $password = BotNow::SQLite::get("mail", "ircid", $row->{id}, "password"); + if (defined($password)) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help."); + return; + } + } + +# my @users = treeget($znctree, "User", "Node"); + foreach my $user (@users) { + if ($user eq $username) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Sorry, username taken. Please contact staff if you need help."); + return; + } + } + + #my $captcha = join'', map +(0..9,'a'..'z','A'..'Z')[rand(10+26*2)], 1..4; + my $captcha = int(rand(999)); + my $ircid = int(rand(9223372036854775807)); + my $hashid = sha256_hex("$ircid"); + BotNow::SQLite::set("irc", "id", $ircid, "localtime", time()); + BotNow::SQLite::set("irc", "id", $ircid, "hashid", sha256_hex($ircid)); + BotNow::SQLite::set("irc", "id", $ircid, "date", IRCNOW::IO::data()); + BotNow::SQLite::set("irc", "id", $ircid, "hostmask", $hostmask); + BotNow::SQLite::set("irc", "id", $ircid, "nick", $nick); + BotNow::SQLite::set("mail", "ircid", $ircid, "username", $username); + BotNow::SQLite::set("mail", "ircid", $ircid, "email", $email); + BotNow::SQLite::set("mail", "ircid", $ircid, "captcha", $captcha); + BotNow::SQLite::set("mail", "ircid", $ircid, "hashid", $hashid); + IRCNOW::IO::IRC::whois($bot->{sock}, $nick); + IRCNOW::IO::IRC::ctcp($bot->{sock}, $nick); + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :".`figlet $captcha`); +#IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :https://$hostname/$hashid/captcha.png"); +#IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :https://$hostname/register.php?hashirc=$hashid"); + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Type !mail captcha "); + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putservlocalnet($bot, "PRIVMSG $chan :$nick\'s on $bot->{name} mail captcha is $captcha"); + } + } else { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Invalid username or email. Type !mail to try again."); + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putservlocalnet($bot, "PRIVMSG $chan :$staff: Help *$nick* on network ".$bot->{name}." with mail"); + } + } +} + +sub mailmail { + my( $username, $password, $email )=@_; + my $approvemsg; + if ($approval eq "true") { + $approvemsg = <<"EOF"; + +*IMPORTANT*: Your account has been created but it has not yet been +approved. To get your account approved, please contact your admins +$staff on IRC and by email. + +EOF + } +my $body = <<"EOF"; +Welcome to IRCNow! + +You created an email account: + +Username: $username\@$mailhostname +Password: $password +Server: $mailhostname +IMAP Port: $imapport (STARTTLS) +SMTP Port: $smtpport (STARTTLS) +Webpanel: $webmail +$approvemsg +*IMPORTANT*: Verify your email address: + +Please reply to this email to indicate you have received the email. You must +reply in order to keep your account. + +Connection Instructions: https://wiki.ircnow.org/?n=Email.Email + +IRCNow +EOF + IRCNOW::IO::mail($mailfrom, $email, $mailname, "Verify IRCNow Account", $body); +} + + +sub createmail { + my ($password, $username) = @_; + chomp(my $encrypted = `encrypt $password`); + my $line = "${username}\@$mailhostname:${encrypted}::::::userdb_quota_rule=*:storage=1G"; + $line =~ s{\$}{\\\$}g; + my $line2 = "${username}\@$mailhostname vmail"; + my $line3 = "${username}\@$mailhostname: ${username}\@$mailhostname"; + `doas sh -c 'echo $line >> $passwdpath'`; + `doas sh -c 'echo $line2 >> $virtualspath'`; + `doas sh -c 'echo $line3 >> $senderspath'`; + `doas smtpctl update table passwd`; + `doas smtpctl update table virtuals`; + `doas smtpctl update table users`; + `doas rcctl reload dovecot`; +} + +sub deletemail { + my ($username) = @_; + my @passwd = readarray($passwdpath); + my @virtuals = readarray($virtualspath); + my @senders = readarray($senderspath); + @passwd = grep !/^${username}\@${mailhostname}/, @passwd; + @virtuals = grep !/^${username}\@${mailhostname}/, @virtuals; + @senders = grep !/^${username}\@${mailhostname}/, @senders; + + # trailing newline necessary + `doas touch $passwdpath.bak`; + `doas touch $virtualspath.bak`; + `doas touch $senderspath.bak`; + `doas chmod g+w $passwdpath.bak $virtualspath.bak $senderspath.bak`; + writefile("$passwdpath.bak", join("\n", @passwd)."\n"); + copy "${passwdpath}.bak", $passwdpath; + writefile("$virtualspath.bak", join("\n", @virtuals)."\n"); + copy "${virtualspath}.bak", $virtualspath; + writefile("$senderspath.bak", join("\n", @senders)."\n"); + copy "${senderspath}.bak", $senderspath; + + `doas smtpctl update table passwd`; + `doas smtpctl update table virtuals`; + `doas smtpctl update table users`; + `doas rcctl reload dovecot`; +} +1; # MUST BE LAST STATEMENT IN FILE blob - /dev/null blob + 803e30c732c0446fce3b6e12349424af16d133fa (mode 644) --- /dev/null +++ lib/IRCNOW/Acct/Sh.pm @@ -0,0 +1,41 @@ +package BotNow::Shell; + +use strict; +use warnings; +use OpenBSD::Pledge; +use OpenBSD::Unveil; +use lib qw(./lib); +use IRCNOW::IO qw(readarray); + +use Data::Dumper; + +my $authlog = "/var/log/authlog"; +my $etcpasswd = "/etc/master.passwd"; +my @etcpasswd = readarray($etcpasswd); +my @users; +foreach my $line (@etcpasswd) { + if ($line =~ /^([^:]+):[^:]+:([^:]+)/) { + my ($username, $uid) = ($1, $2); + if ($uid > 1000) { + push(@users, $username); + } + } +} +my @files = ("/var/log/authlog"); +push(@files, glob q("/var/log/authlog.?")); +push(@files, glob q("/var/log/authlog.1?")); +foreach my $user (@users) { + my $lastseen; + foreach my $file (@files) { + my @logs = readarray($file); + my @seen = grep(/$user/, @logs); + if (scalar(@seen) && $seen[0] =~ /^(\w+ \d+ \d\d:\d\d:\d\d)/) { + $lastseen = $1; + print "$user => $lastseen\n"; + last; + } + } + if (!defined($lastseen)) { + print "$user => Never logged in\n"; + } +} blob - /dev/null blob + b29fc08961862de6e3c59c2afb6ba166869f76ea (mode 644) --- /dev/null +++ lib/IRCNOW/Acct/Shell.pm @@ -0,0 +1,449 @@ +package BotNow::Shell; + +use strict; +use warnings; +use OpenBSD::Pledge; +use OpenBSD::Unveil; + +use MIME::Base64; +use Digest::SHA qw(sha256_hex); + +use lib './lib'; +use IRCNOW::IO qw(:FILEIO :DEBUG); +use IRCNOW::IO::IRC; +use BotNow::SQLite; +use BotNow::Hash; + +use Data::Dumper; + +my %conf = %main::conf; +my $chans = $conf{chans}; +my $teamchans = $conf{teamchans}; +my @teamchans = split /[,\s]+/m, $teamchans; +my $staff = $conf{staff}; +my $captchaURL = "https://example.com/captcha.php?vhost="; +my $hostname = $conf{hostname}; +my $terms = $conf{terms}; +my $expires = $conf{expires}; +my $mailfrom = $conf{mailfrom}; +my $mailname = $conf{mailname}; +my $approval = $conf{approval}; +my $loginclass = $conf{loginclass} || "freeshell"; +my $passpath = "/etc/passwd"; +my $httpdconfpath = "/etc/httpd.conf"; +my $acmeconfpath = "/etc/acme-client.conf"; +my $pfconfpath = "/etc/pf.conf"; +my $relaydconfpath = "/etc/relayd.conf"; +my $startPort; +my $endPort; + +use constant { + NONE => 0, + ERRORS => 1, + WARNINGS => 2, + ALL => 3, +}; + +IRCNOW::IO::IRC::cbind("pub", "-", "shell", \&mshell); +IRCNOW::IO::IRC::cbind("msg", "-", "shell", \&mshell); + +sub init { + #dependencies for figlet + unveil("/usr/local/bin/figlet", "rx") or die "Unable to unveil $!"; + unveil("/usr/lib/libc.so.95.1", "r") or die "Unable to unveil $!"; + unveil("/usr/libexec/ld.so", "r") or die "Unable to unveil $!"; + #dependencies for shell account + unveil($passpath, "r") or die "Unable to unveil $!"; + unveil($httpdconfpath, "rwxc") or die "Unable to unveil $!"; + unveil($acmeconfpath, "rwxc") or die "Unable to unveil $!"; + unveil($pfconfpath, "rwxc") or die "Unable to unveil $!"; + unveil($relaydconfpath, "rwxc") or die "Unable to unveil $!"; + unveil("/usr/sbin/chown", "rx") or die "Unable to unveil $!"; + unveil("/bin/chmod", "rx") or die "Unable to unveil $!"; + unveil("/usr/sbin/groupadd", "rx") or die "Unable to unveil $!"; + unveil("/usr/sbin/useradd", "rx") or die "Unable to unveil $!"; + unveil("/usr/sbin/usermod", "rx") or die "Unable to unveil $!"; + unveil("/usr/sbin/groupdel", "rx") or die "Unable to unveil $!"; + unveil("/usr/sbin/userdel", "rx") or die "Unable to unveil $!"; + unveil("/bin/mkdir", "rx") or die "Unable to unveil $!"; + unveil("/bin/ln", "rx") or die "Unable to unveil $!"; + unveil("/usr/sbin/acme-client", "rx") or die "Unable to unveil $!"; + unveil("/bin/rm", "rx") or die "Unable to unveil $!"; + unveil("/bin/mv", "rx") or die "Unable to unveil $!"; + unveil("/home/", "rwxc") or die "Unable to unveil $!"; +} + +# !shell +# !shell captcha +sub mshell { + my ($bot, $nick, $host, $hand, @args) = @_; + my ($chan, $text); + if (@args == 2) { + ($chan, $text) = ($args[0], $args[1]); + } else { $text = $args[0]; } + my $hostmask = "$nick!$host"; + if (defined($chan) && $chans =~ /$chan/) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $chan :$nick: Please check private message"); + } + if ($text =~ /^$/) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Type !help for new instructions"); + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putservlocalnet($bot, "PRIVMSG $chan :$staff: Help *$nick* on network ".$bot->{name}." with shell registration"); + } + return; + } elsif (IRCNOW::IO::IRC::isstaff($bot, $nick) && $text =~ /^delete\s+([[:ascii:]]+)/) { + my $username = $1; + if (BotNow::SQLite::deleterows("shell", "username", $username)) { + # TODO delete shell + deleteshell($username); + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $chan :$username deleted"); + } + } + return; + } elsif (IRCNOW::IO::IRC::isstaff($bot, $nick) && $text =~ /^approve\s+([[:ascii:]]+)/) { + my $username = $1; + system "doas usermod -U $username"; + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $chan :$username approved"); + } + return; + } + ### TODO: Check duplicate emails ### + my @rows = BotNow::SQLite::selectrows("irc", "nick", $nick); + foreach my $row (@rows) { + my $password = BotNow::SQLite::get("shell", "ircid", $row->{id}, "password"); + if (defined($password)) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help."); + return; + } + } + if ($text =~ /^lastseen\s+([[:alnum:]]+)/) { + } + if ($text =~ /^captcha\s+([[:alnum:]]+)/) { + my $text = $1; + my $ircid = BotNow::SQLite::id("irc", "nick", $nick, $expires); + if (!defined($ircid)) { die "undefined ircid"; } + my $captcha = BotNow::SQLite::get("shell", "ircid", $ircid, "captcha"); + if ($text ne $captcha) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Wrong captcha. To get a new captcha, type !shell "); + return; + } + my $pass = BotNow::Hash::newpass(); + chomp(my $encrypted = `encrypt $pass`); + my $username = BotNow::SQLite::get("shell", "ircid", $ircid, "username"); + my $email = BotNow::SQLite::get("shell", "ircid", $ircid, "email"); + my $version = BotNow::SQLite::get("shell", "ircid", $ircid, "version"); + my $bindhost = "$username.$hostname"; + BotNow::SQLite::set("shell", "ircid", $ircid, "password", $encrypted); + if (DNS::nextdns($username)) { + sleep(2); + createshell($username, $pass, $bindhost); + mailshell($username, $email, $pass, "shell", $version); + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Check your email!"); + if ($approval eq "true") { + system "doas usermod -Z $username"; + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Your account has been created but must be manually approved by your admins ($staff) before it can be used."); + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putservlocalnet($bot, "PRIVMSG $chan :$staff: $nick\'s account $username must be manually unblocked before it can be used."); + } + } + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putservlocalnet($bot, "PRIVMSG $chan :$staff: $nick\'s shell 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=Shell.Shell"); + } + + + #www($newnick, $reply, $password, "bouncer"); + } else { + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $chan :Assigning bindhost $bindhost failed"); + } + } + return; + } elsif ($text =~ /^([[:alnum:]]+)\s+([[:ascii:]]+)/) { + my ($username, $email) = ($1, $2); + my @users = col($passpath, 1, ":"); + my @matches = grep(/^$username$/i, @users); + if (scalar(@matches) > 0) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Sorry, username taken. Please choose another username, or contact staff for help."); + return; + } + # my $captcha = join'', map +(0..9,'a'..'z','A'..'Z')[rand(10+26*2)], 1..4; + my $captcha = int(rand(999)); + my $ircid = int(rand(2147483647)); + BotNow::SQLite::set("irc", "id", $ircid, "localtime", time()); + BotNow::SQLite::set("irc", "id", $ircid, "date", BotNow::IO::date()); + BotNow::SQLite::set("irc", "id", $ircid, "hostmask", $hostmask); + BotNow::SQLite::set("irc", "id", $ircid, "nick", $nick); + BotNow::SQLite::set("shell", "ircid", $ircid, "username", $username); + BotNow::SQLite::set("shell", "ircid", $ircid, "email", $email); + BotNow::SQLite::set("shell", "ircid", $ircid, "captcha", $captcha); + IRCNOW::IO::IRC::whois($bot->{sock}, $nick); + IRCNOW::IO::IRC::ctcp($bot->{sock}, $nick); + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :".`figlet $captcha`); + # IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :$captchaURL".encode_base64($captcha)); + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Type !shell captcha "); + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putservlocalnet($bot, "PRIVMSG $chan :$nick\'s captcha on $bot->{name} is $captcha"); + } + } else { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Invalid username or email. Type !shell to try again."); + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $chan :$staff: Help *$nick* on network ".$bot->{name}." with shell registration"); + } + } +} +sub mailshell { + my( $username, $email, $password, $service, $version )=@_; + my $passhash = sha256_hex("$username"); + my $versionhash = encode_base64($version); + my $approvemsg; + if ($approval eq "true") { + $approvemsg = <<"EOF"; + +*IMPORTANT*: Your account has been created but it has not yet been +approved. To get your account approved, please contact your admins +$staff on IRC and by email. + +EOF + } + + my $body = <<"EOF"; +You created a shell account! + +Username: $username +Password: $password +Server: $hostname +SSH Port: 22 +Your Ports: $startPort to $endPort + +To customize your vhost, connect to ask in $chans +$approvemsg +*IMPORTANT*: Verify your email address: + +Please reply to this email to indicate you have received the email. You must +reply in order to keep your account. + +IRCNow +EOF + BotNow::IO::mail($mailfrom, $email, $mailname, "Verify IRCNow Account", $body); +} + + +#sub mregex { +# my ($bot, $nick, $host, $hand, $text) = @_; +# if ($staff !~ /$nick/) { return; } +# if ($text =~ /^ips?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) { +# my $ips = $1; # space-separated list of IPs +# IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :".regexlist($ips)); +# } elsif ($text =~ /^users?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) { +# my $users = $1; # space-separated list of usernames +# IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :".regexlist($users)); +# } elsif ($text =~ /^[-_()|0-9A-Za-z:,\.?*\s]{3,}$/) { +# my @lines = regex($text); +# foreach my $l (@lines) { print "$l\n"; } +# } +#} +#sub mforeach { +# my ($bot, $nick, $host, $hand, $text) = @_; +# if ($staff !~ /$nick/) { return; } +# if ($text =~ /^network\s+del\s+([[:graph:]]+)\s+(#[[:graph:]]+)$/) { +# my ($user, $chan) = ($1, $2); +# foreach my $n (@main::networks) { +# IRCNOW::IO::IRC::putserv($bot, "PRIVMSG *controlpanel :delchan $user $n->{name} $chan"); +# } +# } +#} + +#sub loadlog { +# open(my $fh, '<', "$authlog") or die "Could not read file 'authlog' $!"; +# chomp(@logs = <$fh>); +# close $fh; +#} + +# return all lines matching a pattern +#sub regex { +# my ($pattern) = @_; +# if (!@logs) { loadlog(); } +# return grep(/$pattern/, @logs); +#} + +# given a list of IPs, return matching users +# or given a list of users, return matching IPs +#sub regexlist { +# my ($items) = @_; +# my @items = split /[,\s]+/m, $items; +# my $pattern = "(".join('|', @items).")"; +# if (!@logs) { loadlog(); } +# my @matches = grep(/$pattern/, @logs); +# my @results; +# foreach my $match (@matches) { +# if ($match =~ /^\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[([^]\/]+)(\/[^]]+)?\] connected to ZNC from (.*)/) { +# my ($user, $ip) = ($1, $3); +# if ($items =~ /[.:]/) { # items are IP addresses +# push(@results, $user); +# } else { # items are users +# push(@results, $ip); +# } +# } +# } +# my @sorted = sort @results; +# @results = do { my %seen; grep { !$seen{$_}++ } @sorted }; # uniq +# return join(' ', @results); +#} + +sub createshell { + my ($username, $password, $bindhost) = @_; + system "doas groupadd $username"; + system "doas adduser -batch $username $username $username `encrypt $password`"; + system "doas chmod 700 /home/$username /home/$username/.ssh"; + system "doas chmod 600 /home/$username/{.Xdefaults,.cshrc,.cvsrc,.login,.mailrc,.profile}"; + system "doas mkdir /var/www/htdocs/$username"; + system "doas ln -s /var/www/htdocs/$username /home/$username/htdocs"; + system "doas chown -R $username:www /var/www/htdocs/$username /home/$username/htdocs"; + system "doas chmod -R o-rx /var/www/htdocs/$username /home/$username/htdocs"; + system "doas chmod -R g+rwx /var/www/htdocs/$username /home/$username/htdocs"; + system "doas chown root:wheel $httpdconfpath $pfconfpath $acmeconfpath $relaydconfpath"; + system "doas chmod g+rw $httpdconfpath $pfconfpath $acmeconfpath $relaydconfpath"; + my $lusername = lc $username; + my $block = <<"EOF"; +server "$lusername.$hostname" { + listen on * port 80 + location "/.well-known/acme-challenge/*" { + root "/acme" + request strip 2 + } + location "*.php" { + fastcgi socket "/run/php-fpm.sock" + } + root "/htdocs/$username" +} +EOF + appendfile($httpdconfpath, $block); + $block = <<"EOF"; +domain "$lusername.$hostname" { + domain key "/etc/ssl/private/$lusername.$hostname.key" + domain full chain certificate "/etc/ssl/$lusername.$hostname.crt" + sign with letsencrypt +} +EOF + appendfile($acmeconfpath, $block); + configurepf($username); + system "doas rcctl reload httpd"; + system "doas acme-client -F $lusername.$hostname"; + system "doas ln -s /etc/ssl/$lusername.$hostname.crt /etc/ssl/$lusername.$hostname.fullchain.pem"; + system "doas pfctl -f /etc/pf.conf"; + configurerelayd($username); + $block = <<"EOF"; +~ * * * * acme-client $lusername.$hostname && rcctl reload relayd +EOF + system "echo $block | doas crontab -"; + system "doas usermod -L $loginclass $username"; +#edquota $username + return 1; +} + +sub deleteshell { + my ($username, $bindhost) = @_; + my $lusername = lc $username; + system "doas chown root:wheel $httpdconfpath $pfconfpath $acmeconfpath $relaydconfpath"; + system "doas chmod g+rw $httpdconfpath $pfconfpath $acmeconfpath $relaydconfpath"; + system "doas groupdel $username"; + system "doas userdel $username"; + system "doas rm -f /etc/ssl/$lusername.$hostname.crt /etc/ssl/$lusername.$hostname.fullchain.pem /etc/ssl/private/$lusername.$hostname.key"; + my $httpdconf = readstr($httpdconfpath); + my $block = <<"EOF"; +server "$lusername.$hostname" { + listen on * port 80 + location "/.well-known/acme-challenge/*" { + root "/acme" + request strip 2 + } + location "*.php" { + fastcgi socket "/run/php-fpm.sock" + } + root "/htdocs/$username" +} +EOF + $block =~ s/{/\\{/gm; + $block =~ s/}/\\}/gm; + $block =~ s/\./\\./gm; + $block =~ s/\*/\\*/gm; + $httpdconf =~ s{$block}{}gm; + print $httpdconf; + writefile($httpdconfpath, $httpdconf); + + my $acmeconf = readstr($acmeconfpath); + $block = <<"EOF"; +domain "$lusername.$hostname" { + domain key "/etc/ssl/private/$lusername.$hostname.key" + domain full chain certificate "/etc/ssl/$lusername.$hostname.fullchain.pem" + sign with letsencrypt +} +EOF + $block =~ s/{/\\{/gm; + $block =~ s/}/\\}/gm; + $block =~ s/\./\\./gm; + $block =~ s/\*/\\*/gm; + $acmeconf =~ s{$block}{}gm; + writefile($acmeconfpath, $acmeconf); + return 1; +} + +#TODO Fix for $i +# Return column $i from $filename as an array with file separator $FS +sub col { + my ($filename, $i, $FS) = @_; + my @rows = readarray($filename); + my @results; + foreach my $row (@rows) { + if ($row =~ /^(.*?)$FS/) { + push(@results, $1); + } + } + return @results; +} + +sub configurepf { + my $username = shift; + my @read = split('\n', readstr($pfconfpath) ); + + my $previousline = ""; + my @pfcontent; + foreach my $line(@read) + { + my $currline = $line; + if( $currline ne "# end user ports") { + $previousline = $currline; + } else { + #pass in proto {tcp udp} to port {31361:31370} user {JL} + if( $previousline =~ /(\d*):(\d*)/ ) { + my $startport = ( $1 + 10 ); + my $endport = ( $2 + 10 ); + my $insert = "pass in proto {tcp udp} to port {$startport:$endport} user {$username}"; + push(@pfcontent, $insert); + $startPort = $startport; + $endPort = $endport; + } + } + push(@pfcontent, $currline) + } + writefile("$pfconfpath", join("\n",@pfcontent)) +} + +sub configurerelayd { + my ($username) = @_; + my $block = "tls { keypair $username.$hostname }"; + my $relaydconf = readstr($relaydconfpath); + my $newconf; + if ($relaydconf =~ /^.*tls\s+{\s+keypair\s+[.0-9a-zA-Z]+\s*}/m) { + $newconf = "$`$&\n\t$block$'"; + } else { + $newconf = $relaydconf; + debug(ERRORS, "ERROR: regex can't match tls { keypair \$username.$hostname }"); + } + writefile($relaydconfpath, $newconf); +} + +#unveil("./newacct", "rx") or die "Unable to unveil $!"; +1; # MUST BE LAST STATEMENT IN FILE blob - /dev/null blob + 74f111b30cdf483d670c95dd133b06cb00ccc4c1 (mode 644) --- /dev/null +++ lib/IRCNOW/Acct/VPN.pm @@ -0,0 +1,154 @@ +package BotNow::VPN; + +use strict; +use warnings; +use OpenBSD::Pledge; +use OpenBSD::Unveil; +use lib qw(./lib); +use IRCNOW::IO qw(:DEBUG readarray); +use IRCNOW::IO::IRC; +use BotNow::DNS; +use BotNow::SQLite; + +my %conf = %main::conf; +my $chans = $conf{chans}; +my $teamchans = $conf{teamchans}; +my @teamchans = split /[,\s]+/m, $teamchans; +my $staff = $conf{staff}; +my $expires = $conf{expires}; +my $ikedconf = $conf{ikedconf} || "/etc/iked.conf"; +# File containing IRC networks +my $netpath = "networks"; +my @networks; + +IRCNOW::IO::IRC::cbind("pub", "-", "vpn", \&vpn); +IRCNOW::IO::IRC::cbind("msg", "-", "vpn", \&vpn); + +sub init { +# unveil("/usr/bin/rcctl", "rx") or die "Unable to unveil $!"; + unveil($ikedconf, "crx") or die "Unable to unveil $!"; +} + +sub vpn { + my ($bot, $nick, $host, $hand, @args) = @_; + my ($chan, $text); + if (@args == 2) { + ($chan, $text) = ($args[0], $args[1]); + } else { $text = $args[0]; } + my $hostmask = "$nick!$host"; + if (defined($chan) && $chans =~ /$chan/) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $chan :$nick: Please check private message"); + } + if ($text =~ /^$/) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Type !help for new instructions"); + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putservlocalnet($bot, "PRIVMSG $chan :$staff: Help *$nick* on network".$bot->{name}); + } + return; + } + my @rows = BotNow::SQLite::selectrows("irc", "nick", $nick); + foreach my $row (@rows) { + my $password = BotNow::SQLite::get("vpn", "ircid", $row->{id}, "password"); + if (defined($password)) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help."); + return; + } + } + if ($text =~ /^captcha\s+([[:alnum:]]+)/) { + my $text = $1; + my $ircid = BotNow::SQLite::id("irc", "nick", $nick, $expires); + if (!defined($ircid)) { die "undefined ircid"; } + my $captcha = BotNow::SQLite::get("vpn", "ircid", $ircid, "captcha"); + if ($text ne $captcha) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Wrong captcha. To get a new captcha, type !vpn "); + return; + } + my $pass = Hash::newpass(); + chomp(my $encrypted = `encrypt $pass`); + my $username = BotNow::SQLite::get("vpn", "ircid", $ircid, "username"); + my $email = BotNow::SQLite::get("vpn", "ircid", $ircid, "email"); + my $version = BotNow::SQLite::get("vpn", "ircid", $ircid, "version"); + BotNow::SQLite::set("vpn", "ircid", $ircid, "password", $encrypted); + + createvpn($username, $pass); + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putservlocalnet($bot, "PRIVMSG $chan :$staff: vpn created for $username"); + } + my $msg = <<"EOF"; +Your vpn account has been created! Username: $username with password: $pass +Our official support channel is #vpn. To connect, please follow these instructions: +https://wiki.ircnow.org/Vpn/Vpn +EOF + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :$msg"); + } elsif ($text =~ /^([[:alnum:]]+)\s+([[:ascii:]]+)/) { + my ($username, $email) = ($1, $2); + if ($staff !~ /$nick/) { + return; + } + my @users = col($ikedconf); + my @matches = grep(/^$username$/i, @users); + if (scalar(@matches) > 0) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Sorry, username taken. Please choose another username, or contact staff for help."); + return; + } + + my $captcha = int(rand(999)); + my $ircid = int(rand(2147483647)); + BotNow::SQLite::set("irc", "id", $ircid, "localtime", time()); + BotNow::SQLite::set("irc", "id", $ircid, "date", BotNow::IO::date()); + BotNow::SQLite::set("irc", "id", $ircid, "hostmask", $hostmask); + BotNow::SQLite::set("irc", "id", $ircid, "nick", $nick); + BotNow::SQLite::set("vpn", "ircid", $ircid, "username", $username); + BotNow::SQLite::set("vpn", "ircid", $ircid, "email", $email); + BotNow::SQLite::set("vpn", "ircid", $ircid, "captcha", $captcha); + IRCNOW::IO::IRC::whois($bot->{sock}, $nick); + IRCNOW::IO::IRC::ctcp($bot->{sock}, $nick); + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :".`figlet $captcha`); + # IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :$captchaURL".encode_base64($captcha)); + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :Type !vpn captcha "); + foreach my $chan (@teamchans) { + IRCNOW::IO::IRC::putservlocalnet($bot, "PRIVMSG $chan :$nick\'s captcha on $bot->{name} is $captcha"); + } + } +} +sub createvpn { + my ($username, $password) = @_; + `doas sh -c 'echo "user $username $password" >> /etc/iked.conf'`; + `doas rcctl reload iked`; +} +sub col { + my ($filename) = @_; + my @rows = readarray($filename); + my @results; + foreach my $row (@rows) { + if ($row =~ /^user (.*?) /) { + push(@results, $1); + } + } + return @results; +} + +#sub init { +#} +# if ($reply =~ /^!vpn (.*) ([-_0-9a-zA-Z]+)$/i) { +# my $ircnick = $1; +# my $newnick = $2; +# if ($staff !~ /$sender/) { +# return; +# } +# my $password = newpass(); +# createvpn($password, $newnick); +# sendmsg($bot, $sender, "vpn created for $newnick"); +#my $msg = <<"EOF"; +#Your vpn account has been created! Username: $newnick with password: $password +#Our official support channel is #vpn. To connect, please follow these instructions: https://ircnow.org/kb/doku.php?id=vpn:vpn . +#EOF +# sendmsg($bot, $ircnick, $msg); +# } +#sub createvpn { +# my ($password, $username) = @_; +# `doas sh -c 'echo "user '$username' '$password'" >> /etc/iked.conf'`; +# `doas rcctl reload iked`; +#} + +1; # MUST BE LAST STATEMENT IN FILE