Commit Diff


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 <username> <email>");
+			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 <text>");
+		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 <username> <email> 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 <username> <email>
+# !shell captcha <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 <username> <email>");
+			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 <text>");
+		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 <username> <email> 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 <username> <email>");
+			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 <text>");
+		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