Annotation of botnow/Shell.pm, Revision 1.1
1.1 ! bountyht 1: #!/usr/bin/perl
! 2:
! 3: package Shell;
! 4:
! 5: use strict;
! 6: use warnings;
! 7: use OpenBSD::Pledge;
! 8: use OpenBSD::Unveil;
! 9: use MIME::Base64;
! 10: use Data::Dumper;
! 11: use Digest::SHA qw(sha256_hex);
! 12: use lib './';
! 13: require "SQLite.pm";
! 14: require "Hash.pm";
! 15:
! 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 $captchaURL = "https://example.com/captcha.php?vhost=";
! 22: my $hostname = $conf{hostname};
! 23: my $terms = $conf{terms};
! 24: my $expires = $conf{expires};
! 25: my $mailfrom = $conf{mailfrom};
! 26: my $mailname = $conf{mailname};
! 27: my $passpath = "/etc/passwd";
! 28: my $httpdconfpath = "/etc/httpd.conf";
! 29: my $acmeconfpath = "/etc/acme-client.conf";
! 30: main::cbind("pub", "-", "shell", \&mshell);
! 31: main::cbind("msg", "-", "shell", \&mshell);
! 32:
! 33: sub init {
! 34: #dependencies for figlet
! 35: unveil("/usr/local/bin/figlet", "rx") or die "Unable to unveil $!";
! 36: unveil("/usr/lib/libc.so.95.1", "r") or die "Unable to unveil $!";
! 37: unveil("/usr/libexec/ld.so", "r") or die "Unable to unveil $!";
! 38: #dependencies for shell account
! 39: unveil($passpath, "r") or die "Unable to unveil $!";
! 40: unveil($httpdconfpath, "rwxc") or die "Unable to unveil $!";
! 41: unveil($acmeconfpath, "rwxc") or die "Unable to unveil $!";
! 42: unveil("/usr/sbin/chown", "rx") or die "Unable to unveil $!";
! 43: unveil("/bin/chmod", "rx") or die "Unable to unveil $!";
! 44: unveil("/usr/sbin/groupadd", "rx") or die "Unable to unveil $!";
! 45: unveil("/usr/sbin/useradd", "rx") or die "Unable to unveil $!";
! 46: unveil("/usr/sbin/groupdel", "rx") or die "Unable to unveil $!";
! 47: unveil("/usr/sbin/userdel", "rx") or die "Unable to unveil $!";
! 48: unveil("/bin/mkdir", "rx") or die "Unable to unveil $!";
! 49: unveil("/bin/ln", "rx") or die "Unable to unveil $!";
! 50: unveil("/usr/sbin/acme-client", "rx") or die "Unable to unveil $!";
! 51: unveil("/bin/rm", "rx") or die "Unable to unveil $!";
! 52: unveil("/bin/mv", "rx") or die "Unable to unveil $!";
! 53: unveil("/home/", "rwxc") or die "Unable to unveil $!";
! 54: }
! 55:
! 56: # !shell <username> <email>
! 57: # !shell captcha <captcha>
! 58: sub mshell {
! 59: my ($bot, $nick, $host, $hand, @args) = @_;
! 60: my ($chan, $text);
! 61: if (@args == 2) {
! 62: ($chan, $text) = ($args[0], $args[1]);
! 63: } else { $text = $args[0]; }
! 64: my $hostmask = "$nick!$host";
! 65: if (defined($chan) && $chans =~ /$chan/) {
! 66: main::putserv($bot, "PRIVMSG $chan :$nick: Please check private message");
! 67: }
! 68: if ($text =~ /^$/) {
! 69: main::putserv($bot, "PRIVMSG $nick :Type !help for new instructions");
! 70: foreach my $chan (@teamchans) {
! 71: main::putservlocalnet($bot, "PRIVMSG $chan :Help shell *$nick* on ".$bot->{name});
! 72: }
! 73: return;
! 74: } elsif (main::isstaff($bot, $nick) && $text =~ /^delete\s+([[:ascii:]]+)/) {
! 75: my $username = $1;
! 76: if (SQLite::deleterows("shell", "username", $username)) {
! 77: # TODO delete shell
! 78: deleteshell($bot, $username);
! 79: foreach my $chan (@teamchans) {
! 80: main::putserv($bot, "PRIVMSG $chan :$username deleted");
! 81: }
! 82: }
! 83: return;
! 84: }
! 85: ### TODO: Check duplicate emails ###
! 86: my @rows = SQLite::selectrows("irc", "nick", $nick);
! 87: foreach my $row (@rows) {
! 88: my $password = SQLite::get("shell", "ircid", $row->{id}, "password");
! 89: if (defined($password)) {
! 90: main::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help.");
! 91: return;
! 92: }
! 93: }
! 94: if ($text =~ /^lastseen\s+([[:alnum:]]+)/) {
! 95: }
! 96: if ($text =~ /^captcha\s+([[:alnum:]]+)/) {
! 97: my $text = $1;
! 98: my $ircid = SQLite::id("irc", "nick", $nick, $expires);
! 99: if (!defined($ircid)) { die "undefined ircid"; }
! 100: my $captcha = SQLite::get("shell", "ircid", $ircid, "captcha");
! 101: if ($text ne $captcha) {
! 102: main::putserv($bot, "PRIVMSG $nick :Wrong captcha. To get a new captcha, type !shell <username> <email>");
! 103: return;
! 104: }
! 105: my $pass = Hash::newpass();
! 106: chomp(my $encrypted = `encrypt $pass`);
! 107: my $username = SQLite::get("shell", "ircid", $ircid, "username");
! 108: my $email = SQLite::get("shell", "ircid", $ircid, "email");
! 109: my $version = SQLite::get("shell", "ircid", $ircid, "version");
! 110: my $bindhost = "$username.$hostname";
! 111: SQLite::set("shell", "ircid", $ircid, "password", $encrypted);
! 112: if (DNS::nextdns($username)) {
! 113: sleep(2);
! 114: createshell($bot, $username, $pass, $bindhost);
! 115: mailshell($username, $email, $pass, "shell", $version);
! 116: main::putserv($bot, "PRIVMSG $nick :Check your email!");
! 117:
! 118: #www($newnick, $reply, $password, "bouncer");
! 119: } else {
! 120: foreach my $chan (@teamchans) {
! 121: main::putserv($bot, "PRIVMSG $chan :Assigning bindhost $bindhost failed");
! 122: }
! 123: }
! 124: return;
! 125: } elsif ($text =~ /^([[:alnum:]]+)\s+([[:ascii:]]+)/) {
! 126: my ($username, $email) = ($1, $2);
! 127: my @users = col($passpath, 1, ":");
! 128: my @matches = grep(/^$username$/i, @users);
! 129: if (scalar(@matches) > 0) {
! 130: main::putserv($bot, "PRIVMSG $nick :Sorry, username taken. Please choose another username, or contact staff for help.");
! 131: return;
! 132: }
! 133: # my $captcha = join'', map +(0..9,'a'..'z','A'..'Z')[rand(10+26*2)], 1..4;
! 134: my $captcha = int(rand(999));
! 135: my $ircid = int(rand(2147483647));
! 136: SQLite::set("irc", "id", $ircid, "localtime", time());
! 137: SQLite::set("irc", "id", $ircid, "date", main::date());
! 138: SQLite::set("irc", "id", $ircid, "hostmask", $hostmask);
! 139: SQLite::set("irc", "id", $ircid, "nick", $nick);
! 140: SQLite::set("shell", "ircid", $ircid, "username", $username);
! 141: SQLite::set("shell", "ircid", $ircid, "email", $email);
! 142: SQLite::set("shell", "ircid", $ircid, "captcha", $captcha);
! 143: main::whois($bot->{sock}, $nick);
! 144: main::ctcp($bot->{sock}, $nick);
! 145: main::putserv($bot, "PRIVMSG $nick :".`figlet $captcha`);
! 146: main::putserv($bot, "PRIVMSG $nick :$captchaURL".encode_base64($captcha));
! 147: main::putserv($bot, "PRIVMSG $nick :Type !shell captcha <text>");
! 148: foreach my $chan (@teamchans) {
! 149: main::putservlocalnet($bot, "PRIVMSG $chan :$nick\'s captcha on $bot->{name} is $captcha");
! 150: }
! 151: } else {
! 152: main::putserv($bot, "PRIVMSG $nick :Invalid username or email. Type !shell <username> <email> to try again.");
! 153: foreach my $chan (@teamchans) {
! 154: main::putserv($bot, "PRIVMSG $chan :Help *$nick* on ".$bot->{name});
! 155: }
! 156: }
! 157: }
! 158: sub mailshell {
! 159: my( $username, $email, $password, $service, $version )=@_;
! 160: my $passhash = sha256_hex("$username");
! 161: my $versionhash = encode_base64($version);
! 162: my $ports;
! 163: my $body = <<"EOF";
! 164: You created a shell account!
! 165:
! 166: Username: $username
! 167: Password: $password
! 168: Server: $hostname
! 169: SSH Port: 22
! 170: Your Ports: $ports for plaintext
! 171:
! 172: *IMPORTANT*: Verify your email address:
! 173:
! 174: https://www.$hostname/register.php?id=$passhash&version=$versionhash
! 175:
! 176: You *MUST* click on the link or your account will be deleted.
! 177:
! 178: IRCNow
! 179: EOF
! 180: Mail::mail($mailfrom, $email, $mailname, "Verify IRCNow Account", $body);
! 181: }
! 182:
! 183:
! 184: #sub mregex {
! 185: # my ($bot, $nick, $host, $hand, $text) = @_;
! 186: # if ($staff !~ /$nick/) { return; }
! 187: # if ($text =~ /^ips?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
! 188: # my $ips = $1; # space-separated list of IPs
! 189: # main::putserv($bot, "PRIVMSG $nick :".regexlist($ips));
! 190: # } elsif ($text =~ /^users?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
! 191: # my $users = $1; # space-separated list of usernames
! 192: # main::putserv($bot, "PRIVMSG $nick :".regexlist($users));
! 193: # } elsif ($text =~ /^[-_()|0-9A-Za-z:,\.?*\s]{3,}$/) {
! 194: # my @lines = regex($text);
! 195: # foreach my $l (@lines) { print "$l\n"; }
! 196: # }
! 197: #}
! 198: #sub mforeach {
! 199: # my ($bot, $nick, $host, $hand, $text) = @_;
! 200: # if ($staff !~ /$nick/) { return; }
! 201: # if ($text =~ /^network\s+del\s+([[:graph:]]+)\s+(#[[:graph:]]+)$/) {
! 202: # my ($user, $chan) = ($1, $2);
! 203: # foreach my $n (@main::networks) {
! 204: # main::putserv($bot, "PRIVMSG *controlpanel :delchan $user $n->{name} $chan");
! 205: # }
! 206: # }
! 207: #}
! 208:
! 209: #sub loadlog {
! 210: # open(my $fh, '<', "$authlog") or die "Could not read file 'authlog' $!";
! 211: # chomp(@logs = <$fh>);
! 212: # close $fh;
! 213: #}
! 214:
! 215: # return all lines matching a pattern
! 216: #sub regex {
! 217: # my ($pattern) = @_;
! 218: # if (!@logs) { loadlog(); }
! 219: # return grep(/$pattern/, @logs);
! 220: #}
! 221:
! 222: # given a list of IPs, return matching users
! 223: # or given a list of users, return matching IPs
! 224: #sub regexlist {
! 225: # my ($items) = @_;
! 226: # my @items = split /[,\s]+/m, $items;
! 227: # my $pattern = "(".join('|', @items).")";
! 228: # if (!@logs) { loadlog(); }
! 229: # my @matches = grep(/$pattern/, @logs);
! 230: # my @results;
! 231: # foreach my $match (@matches) {
! 232: # if ($match =~ /^\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[([^]\/]+)(\/[^]]+)?\] connected to ZNC from (.*)/) {
! 233: # my ($user, $ip) = ($1, $3);
! 234: # if ($items =~ /[.:]/) { # items are IP addresses
! 235: # push(@results, $user);
! 236: # } else { # items are users
! 237: # push(@results, $ip);
! 238: # }
! 239: # }
! 240: # }
! 241: # my @sorted = sort @results;
! 242: # @results = do { my %seen; grep { !$seen{$_}++ } @sorted }; # uniq
! 243: # return join(' ', @results);
! 244: #}
! 245:
! 246: sub createshell {
! 247: my ($bot, $username, $password, $bindhost) = @_;
! 248: my $netname = $bot->{name};
! 249: system "doas groupadd $username";
! 250: system "doas adduser -batch $username $username $username `encrypt $password`";
! 251: system "doas chmod 700 /home/$username /home/$username/.ssh";
! 252: system "doas chmod 600 /home/$username/{.Xdefaults,.cshrc,.cvsrc,.login,.mailrc,.profile}";
! 253: system "doas mkdir /var/www/htdocs/$username";
! 254: system "doas ln -s /var/www/htdocs/$username /home/$username/htdocs";
! 255: system "doas chown -R $username:www /var/www/htdocs/$username /home/$username/htdocs";
! 256: system "doas chmod -R o-rx /var/www/htdocs/$username /home/$username/htdocs";
! 257: system "doas chmod -R g+rwx /var/www/htdocs/$username /home/$username/htdocs";
! 258: my $lusername = lc $username;
! 259: my $block = <<"EOF";
! 260: server "$lusername.$hostname" {
! 261: listen on * port 80
! 262: listen on * port 8001
! 263: location "/.well-known/acme-challenge/*" {
! 264: root "/acme"
! 265: request strip 2
! 266: }
! 267: location "*.php" {
! 268: fastcgi socket "/run/php-fpm.sock"
! 269: }
! 270: root "/htdocs/$username"
! 271: }
! 272: EOF
! 273: main::appendfile($httpdconfpath, $block);
! 274: $block = <<"EOF";
! 275: domain "$lusername.$hostname" {
! 276: domain key "/etc/ssl/private/$lusername.$hostname.key"
! 277: domain full chain certificate "/etc/ssl/$lusername.$hostname.fullchain.pem"
! 278: sign with letsencrypt
! 279: }
! 280: EOF
! 281: main::appendfile($acmeconfpath, $block);
! 282:
! 283: system "doas rcctl reload httpd";
! 284: system "doas mv /etc/ssl/private/$hostname.key /etc/ssl/private/l.k";
! 285: system "doas acme-client -F $lusername.$hostname";
! 286: system "doas ln -s /etc/ssl/crt/$lusername.$hostname.fullchain.pem /etc/ssl/$lusername.$hostname.crt";
! 287: system "doas mv /etc/ssl/private/l.k /etc/ssl/private/$hostname.key";
! 288: #edquota $username
! 289: return 1;
! 290: }
! 291:
! 292: sub deleteshell {
! 293: my ($bot, $username, $bindhost) = @_;
! 294: my $netname = $bot->{name};
! 295: my $lusername = lc $username;
! 296: system "doas groupdel $username";
! 297: system "doas userdel $username";
! 298: system "doas rm -f /etc/ssl/$lusername.$hostname.crt /etc/ssl/$lusername.$hostname.fullchain.pem /etc/ssl/private/$lusername.$hostname.key";
! 299: my $httpdconf = main::readstr($httpdconfpath);
! 300: my $block = <<"EOF";
! 301: server "$lusername.$hostname" {
! 302: listen on * port 80
! 303: location "/.well-known/acme-challenge/*" {
! 304: root "/acme"
! 305: request strip 2
! 306: }
! 307: location "*.php" {
! 308: fastcgi socket "/run/php-fpm.sock"
! 309: }
! 310: root "/htdocs/$username"
! 311: }
! 312: EOF
! 313: $block =~ s/{/\\{/gm;
! 314: $block =~ s/}/\\}/gm;
! 315: $block =~ s/\./\\./gm;
! 316: $block =~ s/\*/\\*/gm;
! 317: $httpdconf =~ s{$block}{}gm;
! 318: print $httpdconf;
! 319: main::writefile($httpdconfpath, $httpdconf);
! 320:
! 321: my $acmeconf = main::readstr($acmeconfpath);
! 322: $block = <<"EOF";
! 323: domain "$lusername.$hostname" {
! 324: domain key "/etc/ssl/private/$lusername.$hostname.key"
! 325: domain full chain certificate "/etc/ssl/$lusername.$hostname.fullchain.pem"
! 326: sign with letsencrypt
! 327: }
! 328: EOF
! 329: $block =~ s/{/\\{/gm;
! 330: $block =~ s/}/\\}/gm;
! 331: $block =~ s/\./\\./gm;
! 332: $block =~ s/\*/\\*/gm;
! 333: $acmeconf =~ s{$block}{}gm;
! 334: main::writefile($acmeconfpath, $acmeconf);
! 335: return 1;
! 336: }
! 337:
! 338: #TODO Fix for $i
! 339: # Return column $i from $filename as an array with file separator $FS
! 340: sub col {
! 341: my ($filename, $i, $FS) = @_;
! 342: my @rows = main::readarray($filename);
! 343: my @results;
! 344: foreach my $row (@rows) {
! 345: if ($row =~ /^(.*?)$FS/) {
! 346: push(@results, $1);
! 347: }
! 348: }
! 349: return @results;
! 350: }
! 351: #unveil("./newacct", "rx") or die "Unable to unveil $!";
! 352: 1; # MUST BE LAST STATEMENT IN FILE
CVSweb