#!/usr/bin/perl use strict; use warnings; use IO::Socket; use IO::Select; use OpenBSD::Pledge; use OpenBSD::Unveil; use File::Copy qw(copy); use Digest::SHA qw(sha256_hex); use MIME::Base64; use Time::HiRes qw ( setitimer ITIMER_VIRTUAL time ); use DBI; use DBD::SQLite; my $confpath = "botnow.conf"; my %conf; foreach my $line (readfile($confpath)) { if ($line =~ /^#/ or $line =~ /^\s*$/) { # skip comments and whitespace next; } elsif ($line =~ /^([-_a-zA-Z0-9]+)\s*=\s*([[:print:]]+)$/) { $conf{$1} = $2; } else { die "ERROR: botnow.conf: $line"; } } # Name of local network my $localnet = $conf{localnet} || "ircnow"; # Internal IPv4 address and plaintext port my $host = $conf{host} || "127.0.0.1"; my $port = $conf{port} || 1337; # Bouncer hostname chomp(my $hostname = $conf{hostname} || `hostname`); # External IPv4 address, plaintext and ssl port my $ip4 = $conf{ip4} || host($hostname,4); my $plainport = $conf{plainport} || 1337; my $sslport = $conf{sslport} || 31337; # Nick and password of bot -- Make sure to add to oper block my $nick = $conf{nick} || "botnow"; my $pass = $conf{pass} or die "ERROR: botnow.conf: pass"; # Comma-separated list of channels for requesting bouncers my $chans = $conf{chans} || "#ircnow"; # Number of words in password my $passlength = $conf{passlength} || 3; # Comma-separated list of staff nicks; comment out to avoid highlights my $staff = $conf{staff}; # Comma-separated list of team channels on localnet; comment out to disable my $teamchans = $conf{teamchans}; # Mail from address my $mailfrom = $conf{mailfrom} or die "ERROR: botnow.conf: mailfrom"; my $mailname = $conf{mailname}; if (!defined($mailname)) { if ($mailfrom =~ /^([^@]+)@/) { $mailname = $1; } else { die "ERROR: botnow.conf mailname"; } } # DNS zone directory my $zonedir = $conf{zonedir} || "/var/nsd/zones/master/"; # rDNS keys from Stallion in BuyVM my $key = $conf{key} or die "ERROR: botnow.conf: key"; my $hash = $conf{hash} or die "ERROR: botnow.conf: hash"; # ZNC install directory my $zncdir = $conf{zncdir} || "/home/znc/home/znc"; # Network Interface Config File my $hostnameif = $conf{hostnameif} || "/etc/hostname.vio0"; # Verbosity: 0 (no errors), 1 (errors), 2 (warnings), 3 (diagnostic) my $verbose = $conf{verbose} || 1; # Terms of Service; don't edit lines with the word EOF my $terms = $conf{terms} || "IRCNow is a Christian network. Rules: no profanity, no porn, no illegal drugs, no gambling, no slander, no warez, no promoting violence, no spam, illegal cracking, or DDoS. Only one account per person. Don't share passwords. Religious or political content may be moderated. Full terms: https://ircnow.org/terms.php . Do you agree? (yes/no)"; if(defined($conf{die})) { die $conf{die}; } my $wwwpath = "/var/www/htdocs/botnow"; # Web folder path my $ipv6path = "ipv6s"; # ipv6 file path my $netpath = "networks"; # networks file path my $database = "/var/www/botnow/"; # database path my $znclog = "$zncdir/.znc/moddata/adminlog/znc.log"; # znc.log path my $dbpath = "/var/www/botnow/users.db"; unveil("./", "r") or die "Unable to unveil $!"; unveil("$confpath", "r") or die "Unable to unveil $!"; unveil("$netpath", "r") or die "Unable to unveil $!"; unveil("$ipv6path", "rwc") or die "Unable to unveil $!"; unveil("$database", "rwxc") or die "Unable to unveil $!"; unveil("$zonedir", "rwc") or die "Unable to unveil $!"; unveil("$dbpath", "rwc") or die "Unable to unveil $!"; unveil("$dbpath-journal", "rwc") or die "Unable to unveil $!"; #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 mail unveil("/usr/sbin/sendmail", "rx") 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 $!"; #dependencies for doas unveil("/usr/bin/doas", "rx") or die "Unable to unveil $!"; #dependencies for encrypt unveil("/usr/bin/encrypt", "rx") or die "Unable to unveil $!"; #znc.log file unveil("$znclog", "r") or die "Unable to unveil $!"; #dependencies for host unveil("/usr/bin/host", "rx") or die "Unable to unveil $!"; unveil() or die "Unable to lock unveil $!"; #dns and inet for sockets, proc and exec for figlet #rpath for reading file, wpath for writing file, cpath for creating path #flock, fattr for sqlite pledge( qw(stdio rpath wpath cpath inet dns proc exec flock fattr) ) or die "Unable to pledge: $!"; my @networks; my @bots; my @words; my $records; my @months = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ); my @days = qw(Sun Mon Tue Wed Thu Fri Sat Sun); my @chans = split /,/m, $chans; my @teamchans; my @logs; my $dbh; my $fh; if (defined($teamchans)) { @teamchans = split /,/m, $teamchans; } sub readfile { my ($filename) = @_; my (@lines, $fh); open($fh, '<', "$filename") or die "Could not read file '$filename' $!"; chomp(@lines = <$fh>); close $fh; return @lines; } sub writefile { my ($filename, $str) = @_; my (@lines, $fh); open($fh, '>', "$filename") or die "Could not write to $filename"; print $fh $str; close $fh; } # Load list of networks # To add multiple servers for a single network, simply create a new # entry with the same net name; znc ignores addnetwork commands when a network # already exists my @lines = readfile($netpath); foreach my $line (@lines) { if ($line =~ /^#/ or $line =~ /^\s*$/) { # skip comments and whitespace next; } elsif ($line =~ /^\s*([-a-zA-Z0-9]+)\s*([-_.:a-zA-Z0-9]+)\s*(~|\+)?([0-9]+)\s*$/) { my ($name, $server, $port) = ($1, $2, $4); my $trustcerts; if (!defined($3)) { $trustcerts = 0; } elsif ($3 eq "~") { # Use SSL but trust all certs $port = "+".$port; $trustcerts = 1; } else { # Use SSL and verify certs $port = "+".$port; $trustcerts = 0; } push(@networks, {"name" => $name, "server" => $server, "port" => $port, "trustcerts" => $trustcerts }); } else { die "network format invalid: $line\n"; } } # networks must be sorted to avoid multiple connections @networks = sort @networks; # dictionary words for passwords @words = readfile("words"); # Validate ipv6s if it exists, otherwise load addresses from /etc/hostname.if if (!(-s "$ipv6path")) { print "No IPv6 addresses in $ipv6path, loading from $hostnameif...\n"; @lines = readfile($hostnameif); my $ipv6s; foreach my $line (@lines) { if ($line =~ /^\s*inet6 (alias )?([0-9a-f:]{4,}) [0-9]+\s*$/i) { $ipv6s .= "$2\n"; } } writefile($ipv6path, $ipv6s); } @lines = readfile($ipv6path); my $ipv6s; foreach my $line (@lines) { if ($line =~ /^\s*([0-9a-f:]{4,})\s*$/i) { $ipv6s .= "$1\n"; } } writefile($ipv6path, $ipv6s); # create sockets my $sel = IO::Select->new( ); my $lastname = ""; foreach my $network (@networks) { # avoid duplicate connections if ($lastname eq $network->{name}) { next; } $lastname = $network->{name}; my $socket = IO::Socket::INET->new(PeerAddr=>$host, PeerPort=>$port, Proto=>'tcp', Timeout=>'300') || print "Failed to establish connection\n"; $sel->add($socket); my $tmp = {"sock" => $socket}; push(@bots, {%$tmp, %$network}); print $socket "NICK $nick\r\n"; print $socket "USER $nick * * :$nick\r\n"; } while(my @ready = $sel->can_read) { my ($bot, $response); foreach my $socket (@ready) { foreach my $b (@bots) { if($socket == $b->{sock}) { $bot = $b; last; } } if (!defined($response = <$socket>)) { next; } if ($response =~ /^PING :ZNC\r\n$/i) { print $socket "PONG :ZNC\r\n"; if ($bot->{name} =~ /$localnet/i) { updaterecords(); } } elsif ($response =~ /^:irc.znc.in (.*) (.*) :(.*)\r\n$/) { my ($type, $target, $reply) = ($1, $2, $3); parseznc($bot, $type, $target, $reply); } elsif($response =~ /^:([^!]+![^@]+@[^@ ]+) PRIVMSG ([^ ]+) :(.*)\r\n$/i) { my ($hostmask, $target, $reply) = ($1, $2, $3); parsemsg($bot, $hostmask, $target, $reply); } elsif($response =~ /^:([^!]+![^@]+@[^@ ]+) NOTICE ([^ ]+) :(.*)\r\n$/i) { my ($hostmask, $target, $reply) = ($1, $2, $3); parsenotice($bot, $hostmask, $target, $reply); } elsif($response =~ /^:([^!]+![^@]+@[^@ ]+) MODE ([^ ]+) :(.*)\r\n$/i) { my ($hostmask, $target, $reply) = ($1, $2, $3); parsemode($bot, $hostmask, $target, $reply); } elsif($response =~ /^:([^!]+![^@]+@[^@ ]+) JOIN :?(.*)\r\n$/i) { my ($hostmask, $chan) = ($1, $2); parsejoin($bot, $hostmask, $chan); } elsif($response =~ /^:([^!]+![^@]+@[^@ ]+) PART ([^ ]+) :(.*)\r\n$/i) { my ($hostmask, $chan, $msg) = ($1, $2, $3); parsepart($bot, $hostmask, $chan, $msg); } elsif($response =~ /^:([^!]+![^@]+@[^@ ]+) NICK :(.*)\r\n$/i) { my ($hostmask, $nick) = ($1, $2); parsenick($bot, $hostmask, $nick); } elsif ($response =~ /^:([[:graph:]]+) (\d\d\d) $nick.? :?(.*)\r?\n?\r$/i) { my ($server, $code, $reply) = ($1, $2, $3); parseserver($bot, $server, $code, $reply); } else { if ($verbose >= 2) { print "Unexpected: $response\r\n"; } } } } sub parseznc { my ($bot, $type, $target, $reply) = @_; my $socket = $bot->{sock}; if ($type eq "001" && $target =~ /^$nick.?$/ && $reply eq "Welcome to ZNC") { } elsif ($type eq "NOTICE" && $target =~ /^$nick.?$/ && $reply eq "*** To connect now, you can use /quote PASS :, or /quote PASS /: to connect to a specific network.") { } elsif ($type eq "NOTICE" && $target =~ /^$nick.?$/ && $reply eq "*** You need to send your password. Configure your client to send a server password.") { } elsif ($type eq "464" && $target =~ /^$nick.?$/ && $reply eq "Password required") { print $socket "PASS $nick/$bot->{name}:$pass\r\n"; foreach my $chan (@chans) { print $socket "JOIN ".$chan."\r\n"; } if ($bot->{name} =~ /$localnet/i) { print $socket "OPER $nick $pass\r\n"; print $socket "PRIVMSG *status :LoadMod --type=user controlpanel\r\n"; print $socket "PRIVMSG *controlpanel :get Admin $nick\r\n"; print $socket "PRIVMSG *controlpanel :get Nick cloneuser\r\n"; foreach my $chan (@teamchans) { print $socket "JOIN ".$chan."\r\n"; } } } elsif ($type eq "464" && $target =~ /^$nick.?$/ && $reply eq "Invalid Password") { print "ERROR: Wrong Username/Password: $bot->{name}\r\n"; } else { print "Unexpected: type: $type, target: $target, reply: $reply\r\n"; } } sub parseserver { my ($bot, $server, $code, $reply) = @_; my ($sender, $val, $key); my $socket = $bot->{sock}; if ($code =~ /^001$/) { # Server Info if ($verbose >= 3) { print "botnow successfully connected to $bot->{name}\r\n"; } } elsif ($code =~ /^00\d$/) { # Server Info # print "$server $reply\r\n"; } elsif ($code =~ /^2\d\d$/) { # Server Stats # print "$server $reply\r\n"; } elsif ($code == 301 && $reply =~ /^([-_\|`a-zA-Z0-9]+) :([[:graph:]]+)/) { if ($verbose >= 3) { print "$reply\r\n"; } } elsif ($code == 307 && $reply =~ /^([-_\|`a-zA-Z0-9]+) (.*)/) { ($sender, $key) = ($1, "registered"); $val = $2 eq ":is a registered nick" ? "True" : "$2"; my $hostmask = firstval("oldnick", $sender); setkeyval($hostmask, "identified", $val); if ($verbose >= 3) { print "$key: $val\r\n"; } } elsif ($code == 311 && $reply =~ /^([-_\|`a-zA-Z0-9]+) ([^:]+)\s+([^:]+) \* :([^:]*)/) { ($sender, $key, $val) = ($1, "vhost", "$1\!$2\@$3"); my $hostmask = firstval("oldnick", $sender); setkeyval($hostmask, $key, $val); if ($verbose >= 3) { print "$key: $val\r\n"; } } elsif ($code == 312 && $reply =~ /^([-_\|`a-zA-Z0-9]+) ([^:]+) :([^:]+)/) { ($sender, $key, $val) = ($1, "server", $2); my $hostmask = firstval("oldnick", $sender); setkeyval($hostmask, $key, $val); if ($verbose >= 3) { print "$key: $val\r\n"; } } elsif ($code == 313 && $reply =~ /^([-_\|`a-zA-Z0-9]+) :?(.*)/) { ($sender, $key, $val) = ($1, "oper", ($2 eq "is an IRC operator" ? "True" : "$2")); my $hostmask = firstval("oldnick", $sender); setkeyval($hostmask, $key, $val); if ($verbose >= 3) { print "$key: $val\r\n"; } } elsif ($code == 315 && $reply =~ /^([-_\|`a-zA-Z0-9]+) :End of \/?WHOIS list/) { if ($verbose >= 3) { print "End of WHOIS\r\n"; } } elsif ($code == 317 && $reply =~ /^([-_\|`a-zA-Z0-9]+) (\d+) (\d+) :?(.*)/) { ($sender, my $idle, my $epochtime) = ($1, $2, $3); my $hostmask = firstval("oldnick", $sender); setkeyval($hostmask, "idle", $idle); setkeyval($hostmask, "epochtime", $epochtime); if ($verbose >= 3) { print "idle: $idle, epochtime: $epochtime\r\n"; } } elsif ($code == 318 && $reply =~ /^([-_\|`a-zA-Z0-9]+) :End of \/?WHOIS list/) { if ($verbose >= 3) { print "End of WHOIS\r\n"; } } elsif ($code == 319 && $reply =~ /^([-_\|`a-zA-Z0-9]+) :(.*)/) { ($sender, $key, $val) = ($1, "chans", $2); my $hostmask = firstval("oldnick", $sender); setkeyval($hostmask, $key, $val); if ($verbose >= 3) { print "$key: $val\r\n"; } } elsif ($code == 330 && $reply =~ /^([-_\|`a-zA-Z0-9]+) ([-_\|`a-zA-Z0-9]+) :?(.*)/) { ($sender, $key, $val) = ($1, "loggedin", ($3 eq "is logged in as" ? "True" : $2)); my $hostmask = firstval("oldnick", $sender); setkeyval($hostmask, $key, $val); if ($verbose >= 3) { print "$key: $val\r\n"; } } elsif ($code == 338 && $reply =~ /^([-_\|`a-zA-Z0-9]+) ([0-9a-fA-F:.]+) :actually using host/) { ($sender, $key, $val) = ($1, "ip", $2); my $hostmask = firstval("oldnick", $sender); setkeyval($hostmask, $key, $val); if ($verbose >= 3) { print "$key: $val\r\n"; } print "$key: $val\r\n"; #Unexpected: efnet.port80.se 338 jrmu 206.253.167.44 :actually using host } elsif ($code == 378 && $reply =~ /^([-_\|`a-zA-Z0-9]+) :is connecting from ([^ ]+)\s*([0-9a-fA-F:.]+)?/) { ($sender, $key, $val) = ($1, "ip", $3); my $hostmask = firstval("oldnick", $sender); setkeyval($hostmask, $key, $val); if ($verbose >= 3) { print "$key: $val\r\n"; } } elsif ($code == 671 && $reply =~ /^([-_\|`a-zA-Z0-9]+) :is using a secure connection/) { ($sender, $key, $val) = ($1, "ssl", "True"); my $hostmask = firstval("oldnick", $sender); setkeyval($hostmask, $key, $val); if ($verbose >= 3) { print "$key: $val\r\n"; } print "$key: $val\r\n"; } elsif ($code =~ /^332$/) { # Topic # print "$reply\r\n"; } elsif ($code =~ /^333$/) { # Topic # print "$server $reply\r\n"; #karatkievich.freenode.net 333 #ircnow jrmu!znc@206.253.167.44 1579277253 } elsif ($code =~ /^353$/) { # Names # print "$server $code $reply\r\n"; } elsif ($code =~ /^366$/) { # End of names # print "$server $code $reply\r\n"; } elsif ($code =~ /^37\d$/) { # MOTD # print "$server $code $reply\r\n"; } elsif ($code =~ /^381$/) { # IRC Operator Verified # print "IRC Oper Verified\r\n"; } elsif ($code =~ /^401$/) { # IRC Operator Verified # print "IRC Oper Verified\r\n"; } elsif ($code =~ /^422$/) { # MOTD missing # print "$server $code $reply\r\n"; } elsif ($code =~ /^464$/) { # Invalid password for oper foreach my $chan (@teamchans) { sendmsg($bot, $chan, "ERROR: $nick oper password failed; the bot will be unable to view uncloaked IP addresses\r\n"); } } elsif ($code =~ /^477$/) { # Can't join channel foreach my $chan (@teamchans) { sendmsg($bot, $chan, "ERROR: $nick on $server: $reply\r\n"); } } elsif ($code == 716 && $reply =~ /^([-_\|`a-zA-Z0-9]+) :is in \+g mode \(server-side ignore.\)/) { if ($verbose >= 3) { print "$reply\r\n"; } } else { print "Unexpected: $server $code $reply\r\n"; } } sub parsemode { my ($bot, $hostmask, $target, $reply) = @_; if ($verbose >= 3) { print "$hostmask MODE $target $reply\r\n"; } #:guava!guava@guava.guava.ircnow.org MODE guava :+Ci #:ChanServ!services@services.irc.ircnow.org MODE #testing +q jrmu #:jrmu!jrmu@jrmu.staff.ircnow.org MODE #testing +o jrmu } sub parsejoin { my ($bot, $hostmask, $chan) = @_; if ($verbose >= 3) { print "$hostmask JOIN $chan\r\n"; } #:jrmu!jrmu@jrmu.staff.ircnow.org JOIN :#testing } sub parsepart { my ($bot, $hostmask, $chan, $msg) = @_; print "$hostmask PART $chan :$msg\r\n"; #:jrmu!jrmu@jrmu.staff.ircnow.org PART #testing : } sub parsenick { my ($bot, $hostmask, $nick) = @_; print "$hostmask NICK $nick\r\n"; #:Fly0nDaWaLL|dal!psybnc@do.not.h4ck.me NICK :nec|dal } sub sendmsg { my( $bot, $target, $msg )=@_; my $socket = $bot->{sock}; my @lines = split /\r?\n\r?/m, $msg; foreach my $l (@lines) { print $socket "PRIVMSG $target :$l\r\n"; # if not sending to znc if ($bot->{name} =~ /^$localnet$/i and $target !~ /^\*/) { sleep(1); } } } sub mailverify { my( $username, $email, $password, $service, $version )=@_; my $passhash = sha256_hex("$username"); if ($version =~ /mIRC/) { $version = "mIRC"; } my $versionhash = encode_base64($version); my $msg = <<"EOF"; From: $mailname \<$mailfrom\> To: $email Subject: Verify IRCNow Account MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline You created a bouncer! Username: $username Password: $password Server: $hostname Port: $sslport for SSL (secure connection) Port: $plainport for plaintext *IMPORTANT*: Verify your email address: https://www.$hostname/register.php?id=$hash&version=$versionhash You *MUST* click on the link or your account will be deleted. IRCNow EOF open(my $fh, "| /usr/sbin/sendmail -tv -F support -f $mailfrom") or die "Could not send mail $!"; print $fh $msg; close $fh; } sub getrecords { if (!defined($records)) { my @rows = selectall(); if (@rows) { foreach my $row (@rows) { my $hostmask = $row->{vhost}; $records->{$hostmask} = (); foreach $key (keys %$row) { my $val = $row->{$key} || ""; $records->{$hostmask}->{$key} = $val; } } } else { print "Error getting records\n"; } } } sub getkeyval { my ($hostmask, $key) = @_; my $val; if (!defined($records)) { getrecords(); } $val = $records->{$hostmask}->{$key}; if ($verbose >= 3) { print "getkeyval $hostmask: $key => ".($val or "undefined")."\r\n"; } return $val; } sub setkeyval { my ($hostmask, $key, $val) = @_; if (!defined($records)) { getrecords(); } if (!defined($records->{$hostmask})) { insert("vhost", $hostmask); } $records->{$hostmask}->{$key} = $val; # autovivifies if ($verbose >= 3) { print "setkeyval $hostmask: $key => $val\r\n"; } } sub delkey { my ($hostmask, $key) = @_; if (!defined($records)) { getrecords(); } delete($records->{$hostmask}->{$key}); if ($verbose >= 3) { print "delkey $hostmask: $key\r\n"; } } sub delhost { my ($hostmask) = @_; if (!defined($records)) { getrecords(); } deleterow($hostmask); delete($records->{$hostmask}); } sub firstval { my ($key, $val) = @_; if (!defined($records)) { getrecords(); } foreach my $hostmask (keys %$records) { if (exists($records->{$hostmask}->{$key}) && $records->{$hostmask}->{$key} eq $val) { return $hostmask; } } return; } sub createrecord { my ($hostmask) = @_; if (!defined($records)) { getrecords(); } insert("vhost", $hostmask); return 1; } sub updaterecords { if (!defined($records)) { getrecords(); } while (my ($hostmask, $record) = each (%$records)) { foreach my $key (keys %$record) { print "key: $key, val: ".$record->{$key}.", hostmask: $hostmask\n"; update($key, $record->{$key}, $hostmask); } } if ($verbose >= 3) { print "records saved\n"; } return 1; } sub date { my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); my $localtime = sprintf("%04d%02d%02d", $year+1900, $mon+1, $mday); return $localtime; } sub mytime { my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); my $localtime = sprintf("%s %s %d %02d:%02d:%02d", $days[$wday], $months[$mon], $mday, $hour, $min, $sec); return $localtime; } sub www { my( $username, $email, $password, $service )=@_; my $hash = sha256_hex("$username"); my $filename = "$database/www"; my $lines = ""; if (open($fh, '+<', $filename)) { while (my $line = <$fh>) { $lines .= $line; } close $fh; } open($fh, '>', "$filename") or die "Could not write to '$database/www' $!"; print $fh $lines; print $fh "Hash: $hash, Username: $username, Email: $email, Password: $password\n"; close $fh; } sub newpass { my $len = scalar @words; my $pass; for (my $i=0; $i < $passlength; $i++) { my $word = $words[int(rand($len))]; $word =~ s/(\w+)/\u$1/g; $pass .= $word; } return $pass; } sub nickfromhost { my ($hostmask) = @_; if ($hostmask =~ /([^!]+)!/i) { return $1; } return; } # Send $msg to every nick in $staff on the team channel sub sendteam { my( $sender, $msg )=@_; my @nicks = split /,/m, $staff; my $bot; if (!defined($teamchans)) { return; } #prevent duplicates on other networks if ($sender =~ /\|/) { return; } foreach my $b (@bots) { if($b->{name} =~ /$localnet/i) { $bot = $b; last; } } foreach my $chan (@teamchans) { if (defined($staff)) { sendmsg($bot, $chan, join(" ", split /,/m, $staff).": $msg"); } else { sendmsg($bot, $chan, $msg); } } } sub whois { my( $socket, $target )=@_; print $socket "WHOIS $target $target\r\n"; } sub ctcp { my( $socket, $target )=@_; # print $socket "PRIVMSG $target :".chr(01)."CLIENTINFO".chr(01)."\r\n"; # print $socket "PRIVMSG $target :".chr(01)."FINGER".chr(01)."\r\n"; # print $socket "PRIVMSG $target :".chr(01)."SOURCE".chr(01)."\r\n"; print $socket "PRIVMSG $target :".chr(01)."TIME".chr(01)."\r\n"; # print $socket "PRIVMSG $target :".chr(01)."USERINFO".chr(01)."\r\n"; print $socket "PRIVMSG $target :".chr(01)."VERSION".chr(01)."\r\n"; # print $socket "PRIVMSG $target :".chr(01)."PING".chr(01)."\r\n"; } sub parsemsg { my ($bot, $hostmask, $target, $reply) = @_; my $socket = $bot->{sock}; my $sender = nickfromhost($hostmask); my $help = sub { my $msg = <<"EOF"; To request a free bouncer, type !bnc . To reset the password, type !resetpass . To change email, type !set email . EOF sendmsg($bot, $sender, $msg); sendteam($sender, "Help *$sender* on network ".$bot->{name}." $chans"); }; my $captcha = sub { my $num = int(rand(999)); setkeyval($hostmask, "num", $num); my $figlet = `figlet $num`; sendmsg($bot, $sender, $figlet); sendteam($sender, "$sender\'s captcha is $num"); }; if ($hostmask eq '*status!znc@znc.in' && $target =~ /^$nick.?$/) { if ($reply eq "You are currently disconnected from IRC. Use 'connect' to reconnect.") { if ($verbose >= 3) { print "not connected to $bot->{name}\n"; } return; } elsif ($reply =~ /Unable to load module (.*): Module (.*) already loaded./) { if ($verbose >= 3) { print "Module $1 already loaded\n"; } return; } else { print "Unexpected: $hostmask $target $reply\r\n"; } } elsif($hostmask eq '*controlpanel!znc@znc.in') { if ($reply =~ /^Error: User \[cloneuser\] does not exist/) { createclone($bot); sendteam($sender, "Cloneuser created!"); } elsif($reply =~ /^Nick = ([a-zA-Z][-_a-zA-Z0-9]+)$/) { # only one requestnick my $requestnick = $1; my $hostmask = firstval("requestnick", $requestnick); if (!defined($hostmask)) { return; } delkey($hostmask, "requestnick"); sendmsg($bot, nickfromhost($hostmask), "Username already taken. New username:"); } elsif($reply =~ /^Error: User \[(.*)\] does not exist/) { # delete requestnick after granted my $newnick = $1; my $hostmask = firstval("requestnick", $newnick); if (!defined($hostmask)) { return; } setkeyval($hostmask, "newnick", $newnick); delkey($hostmask, "requestnick"); sendmsg($bot, nickfromhost($hostmask), "Email:"); } elsif ($reply =~ /^User (.*) added!$/) { print "User $1 created!\r\n"; } elsif ($reply =~ /^Password has been changed!$/) { print "Password changed\r\n"; } elsif ($reply =~ /^Queued network (.*) of user (.*) for a reconnect.$/) { print "$2 now connecting to $1...\r\n"; } elsif ($reply =~ /^Admin = false/) { sendteam("", "ERROR: $nick is not admin"); die "ERROR: $nick is not admin"; } elsif ($reply =~ /^Admin = true/) { if ($verbose >= 3) { print "$nick is ZNC admin\n"; } } elsif ($reply =~ /(.*) = (.*)/) { my ($key, $val) = ($1, $2); if ($verbose >= 3) { print "$key = $val\r\n"; } } else { print "Unexpected: $hostmask $target $reply\r\n"; } } elsif ($reply =~ /^!(help|request)/i) { if ($chans =~ $target) { sendmsg($bot, $target, "$sender: Please check private message"); } $help->(); } elsif ($reply =~ /^!bnc/i) { if (defined(getkeyval($hostmask, "services"))) { sendmsg($bot, $sender, "Sorry, only one account per person. Please contact staff if you need help."); } else { delhost($hostmask); setkeyval($hostmask, "oldnick", $sender); whois($socket, $sender); if ($chans =~ $target) { sendmsg($bot, $target, "$sender: Please check private message"); } sendmsg($bot, $sender, $terms); sendteam($bot->{name}, "Help *$sender* on ".$bot->{name}); } } elsif ($reply =~ /^!linesmatching ([-_()|0-9A-Za-z:\.?*]{3,})/) { my $pattern = $1; # arbitrary pattern if ($staff !~ /$sender/) { return; } my @lines = linesmatching($pattern); foreach my $l (@lines) { print "$l\n"; } } elsif ($reply =~ /^!usersmatching ([0-9A-Fa-f:\.,]{3,})/) { my $ips = $1; # comma-separated list of IPs if ($staff !~ /$sender/) { return; } sendmsg($bot, $sender, usersmatching($ips)); } elsif ($reply =~ /^!ipsmatching ([0-9A-Za-z_-]{3,})/) { my $usernames = $1; # comma-separated list of usernames if ($staff !~ /$sender/) { return; } sendmsg($bot, $sender, ipsmatching($usernames)); } elsif ($reply =~ /^!host ([-0-9A-Za-z:\.]{3,})/) { my $name = $1; if ($staff !~ /$sender/) { return; } sendmsg($bot, $sender, host($name)); } elsif ($reply =~ /^!setrdns\s+([0-9A-Fa-f:\.]{3,})\s+([-0-9A-Za-z\.]+)/) { my ($ip, $hostname) = ($1, $2); if ($staff !~ /$sender/) { return; } if (setrdns($ip, $hostname)) { sendteam("", "$hostname set to $ip"); } else { sendteam("", "Error: failed to set rDNS"); } } elsif ($reply =~ /^!delrdns\s+([0-9A-Fa-f:\.]{3,})/) { my $ip = $1; if ($staff !~ /$sender/) { return; } my $hostname = "notset"; if (setrdns($ip, $hostname)) { sendteam("", "$ip rDNS deleted"); } else { sendteam("", "Error: failed to set rDNS"); } } elsif ($reply =~ /^!setdns\s+([-0-9A-Za-z\.]+)\s+([0-9A-Fa-f:\.]+)/) { my ($hostname, $ip) = ($1, $2); if ($staff !~ /$sender/) { return; } if (setdns($hostname, $ip)) { sendteam("", "$hostname set to $ip"); } else { sendteam("", "ERROR: failed to set DNS"); } } elsif ($reply =~ /^!deldns\s+([-0-9A-Za-z\.]+)/) { my $hostname = $1; if ($staff !~ /$sender/) { return; } if (deldns($hostname)) { sendteam("", "$hostname deleted"); } else { sendteam("", "ERROR: failed to delete DNS records"); } } elsif ($reply =~ /^!connectdb/) { if ($staff !~ /$sender/) { return; } if (connectdb()) { sendteam("", "connectdb succeeded"); } else { sendteam("", "ERROR: connectdb failed"); } } elsif ($reply =~ /^!updaterecords/) { if ($staff !~ /$sender/) { return; } if (updaterecords()) { sendteam("", "updaterecords succeeded"); } else { sendteam("", "ERROR: updaterecords failed"); } } elsif ($reply =~ /^!insert ([-_0-9A-Za-z]+) ([[:graph:]]+)/) { my ($key, $val) = ($1, $2); if ($staff !~ /$sender/) { return; } my $rows = insert($key, $val); if (!defined($rows)) { sendteam("", "ERROR: insert failed"); } else { sendteam("", "insert: $key => $val"); } } elsif ($reply =~ /^!update ([-_0-9A-Za-z]+) ([[:graph:]]+) ([-_0-9A-Za-z]+)/) { my ($key, $val, $id) = ($1, $2, $3); if ($staff !~ /$sender/) { return; } my $rows = update($key, $val, $id); if (!defined($rows)) { sendteam("", "ERROR: update failed"); } else { sendteam("", "update $rows rows: $key $val"); } } elsif ($reply =~ /^!select ([-=_0-9A-Za-z\s]+) ([-~=_0-9A-Za-z\s]+)/) { my ($key, $val) = ($1, $2); if ($staff !~ /$sender/) { return; } my @rows = selectdb($key, $val); if (@rows) { foreach my $row (@rows) { foreach $key (keys %$row) { my $val = $row->{$key} || ""; print "$key => $val\n"; } } } else { sendteam("", "ERROR: select failed"); } } elsif ($target !~ /^$nick.?/) { # print "$hostmask: $target $reply\r\n"; } elsif (!defined(getkeyval($hostmask, "num"))) { if ($reply =~ /^(y|yes)/i) { setkeyval($hostmask, "terms", "yes"); ctcp($socket, $sender); sendmsg($bot, $sender, "What number is this?"); $captcha->(); } elsif (defined(getkeyval($hostmask, "terms")) && !getkeyval($hostmask, "terms")) { # already asked, so repeat help message $help->(); return; } else { setkeyval($hostmask, "terms", "no"); sendmsg($bot, $sender, "Sorry, command not understood. Type !help or contact staff."); } } elsif (!defined(getkeyval($hostmask, "captcha"))) { if ($reply =~ /^([0-9]+)$/ && $reply == getkeyval($hostmask, "num")) { setkeyval($hostmask, "captcha", $reply); sendmsg($bot, $sender, "New username:"); } else { sendmsg($bot, $sender, "Wrong. What number is this?"); $captcha->(); } } elsif(!defined(getkeyval($hostmask, "newnick"))) { if ($reply =~ /^([a-zA-Z][-a-zA-Z0-9]+)/) { setkeyval($hostmask, "requestnick", $reply); sendmsg($bot, "*controlpanel", "get Nick $reply"); } else { sendmsg($bot, $sender, "Invalid username. New username:"); } } elsif(!defined(getkeyval($hostmask, "email"))) { if ($reply =~ /^(([-_.0-9a-zA-Z]+)\@([-_.0-9a-zA-Z]+))$/) { my $otheracct = firstval("email", $reply); if (defined($otheracct)) { setkeyval($hostmask, "terms", "no"); sendmsg($bot, $sender, "Sorry, only one account per person. Please contact staff if you need help."); return; } my $newnick = getkeyval($hostmask, "newnick"); my $version = getkeyval($hostmask, "VERSION"); setkeyval($hostmask, "email", $reply); my $password = newpass(); my $encrypted = `encrypt $password`; chomp($encrypted); setkeyval($hostmask, "password", $encrypted); my $bindhost = nextdns($newnick); mailverify($newnick, $reply, $password, "bouncer", $version); my $msg = <<"EOF"; Check your email. EOF sendmsg($bot, $sender, $msg); sleep(5); createbnc($bot, $newnick, $password, $bindhost); setkeyval($hostmask, "services", "znc"); www($newnick, $reply, $password, "bouncer"); } else { sendmsg($bot, $sender, "Invalid email. New email:"); } ### Hidden functionality ### } elsif ($reply =~ /^!deluser (.*)$/i) { my $user = $1; if ($staff !~ /$sender/) { return; } print $socket "PRIVMSG *controlpanel :deluser $user\r\n"; } elsif ($reply =~ /^!rehash$/i) { if ($staff !~ /$sender/) { return; } print $socket "PRIVMSG *controlpanel :deluser cloneuser\r\n"; sleep 5; print $socket "PRIVMSG *controlpanel :get Nick cloneuser\r\n"; } elsif ($reply =~ /^!dns\s+([-.0-9a-z]+)\s+([:0-9a-f]+)/i) { my ($fqdn, $ip6) = ($1, $2); if ($staff !~ /$sender/) { return; } dns($fqdn, $ip6); } elsif ($reply =~ /^!nextdns ([-0-9a-zA-Z]+)/i) { my $username = $1; if ($staff !~ /$sender/) { return; } nextdns($username); sendmsg($bot, $sender, "nextdns called"); } elsif ($reply =~ /^!getkeyval ([-_0-9a-zA-Z]+) ([-_0-9a-zA-Z]+)/) { if ($staff !~ /$sender/) { return; } sendmsg($bot, $sender, getkeyval($1, $2)); } elsif ($reply =~ /^!setkeyval ([-_0-9a-zA-Z]+) ([-_0-9a-zA-Z]+) ([-_0-9a-zA-Z]+)/) { if ($staff !~ /$sender/) { return; } sendmsg($bot, $sender, setkeyval($1, $2, $3)); } elsif ($reply =~ /^!delkey ([-_0-9a-zA-Z]+) ([-_0-9a-zA-Z]+)/) { if ($staff !~ /$sender/) { return; } sendmsg($bot, $sender, delkey($1, $2)); } elsif ($reply =~ /^!updaterecords/) { if ($staff !~ /$sender/) { return; } if (updaterecords()) { sendmsg($bot, $sender, "Records saved."); } } else { print "exception $hostmask targe: $target reply: $reply\r\n"; } } sub parsenotice { my ($bot, $hostmask, $target, $reply) = @_; my $socket = $bot->{sock}; my $sender = nickfromhost($hostmask); # CTCP replies if ($hostmask ne '*status!znc@znc.in') { if ($reply =~ /^(PING|VERSION|TIME|USERINFO) (.*)$/i) { my ($key, $val) = ($1, $2); $key = lc $key; setkeyval($hostmask, $key, $val); setkeyval($hostmask, "localtime", mytime()); } } } #sub resetznc { # #AnonIPLimit 10000 #AuthOnlyViaModule false #ConnectDelay 0 #HideVersion true #LoadModule #ServerThrottle #1337 209.141.38.137 #31337 209.141.38.137 #1337 2605:6400:20:5cc:: #31337 2605:6400:20:5cc:: #1337 127.0.0.1 #1338 127.0.0.1 #} # #alias Provides bouncer-side command alias support. #autoreply Reply to queries when you are away #block_motd Block the MOTD from IRC so it's not sent to your client(s). #bouncedcc Bounces DCC transfers through ZNC instead of sending them directly to the user. #clientnotify Notifies you when another IRC client logs into or out of your account. Configurable. #ctcpflood Don't forward CTCP floods to clients #dcc This module allows you to transfer files to and from ZNC #perform Keeps a list of commands to be executed when ZNC connects to IRC. #webadmin Web based administration module. sub createclone { my ($bot) = @_; my $socket = $bot->{sock}; my $password = newpass(); my $msg = <<"EOF"; adduser cloneuser $password set Nick cloneuser cloneuser set Altnick cloneuser cloneuser_ set Ident cloneuser cloneuser set RealName cloneuser cloneuser set MaxNetworks cloneuser 1000 set ChanBufferSize cloneuser 1000 set MaxQueryBuffers cloneuser 1000 set QueryBufferSize cloneuser 1000 set NoTrafficTimeout cloneuser 600 set QuitMsg cloneuser IRCNow and Forever! set RealName cloneuser cloneuser set DenySetBindHost cloneuser true set Timezone cloneuser US/Pacific LoadModule cloneuser controlpanel LoadModule cloneuser chansaver EOF #LoadModule cloneuser buffextras sendmsg($bot, "*controlpanel", $msg); foreach my $n (@networks) { my $net = $n->{name}; my $server = $n->{server}; my $port = $n->{port}; my $trustcerts = $n->{trustcerts}; $msg = <<"EOF"; addnetwork cloneuser $net addserver cloneuser $net $server $port disconnect cloneuser $net EOF if ($trustcerts) { $msg .= "SetNetwork TrustAllCerts cloneuser $net True\r\n"; } foreach my $chan (@chans) { $msg .= "addchan cloneuser $net $chan\r\n"; } sendmsg($bot, "*controlpanel", $msg); } } sub createbnc { my ($bot, $newnick, $password, $bindhost) = @_; my $netname = $bot->{name}; my $msg = <<"EOF"; cloneuser cloneuser $newnick set Nick $newnick $newnick set Altnick $newnick ${newnick}_ set Ident $newnick $newnick set RealName $newnick $newnick set Password $newnick $password set MaxNetworks $newnick 1000 set ChanBufferSize $newnick 1000 set MaxQueryBuffers $newnick 1000 set QueryBufferSize $newnick 1000 set NoTrafficTimeout $newnick 600 set QuitMsg $newnick IRCNow and Forever! set BindHost $newnick $bindhost set DCCBindHost $newnick $bindhost set DenySetBindHost $newnick true reconnect $newnick $netname EOF #set Language $newnick en-US sendmsg($bot, "*controlpanel", $msg); } # returns true upon success, false upon failure sub dns { my ($fqdn, $ip6) = @_; my $username; my $filename = "$zonedir/$hostname"; if ($fqdn =~ /^([a-zA-Z][-a-zA-Z0-9]+)\.$hostname$/) { $username = $1; } else { sendteam("", "Error: not authoritative"); return; } my ($line, $lines); open($fh, '+<', "$filename") or die "Could not read file '$filename' $!"; while(my $line = <$fh>){ # increment the zone's serial number if ($line =~ /([0-9]{10})( [0-9]+ [0-9]+ [0-9]+ [0-9]+ \).*)/) { my $num = $1+1; $line = " $num$2\n"; } $lines .= $line; } close $fh; $lines .= "$username 3600 IN AAAA $ip6\n"; $lines .= "$username 3600 IN A $ip4\n"; open($fh, '>', "$filename.bak") or die "Could not write file '$filename.bak' $!"; print $fh $lines; close $fh; copy "$filename.bak", $filename; my $stdout = `curl -d \"key=$key&hash=$hash&action=rdns&ip=$ip6&rdns=$fqdn\" https://manage.buyvm.net/api/client/command.php`; if ($stdout !~ /success/) { sendteam("", "Error: failed to set rDNS"); return; } if (system("doas -u _nsd nsd-control reload")) { sendteam("", "Error: failed to reload nsd"); return; } sendteam("", "$fqdn set to $ip6"); return 1; } sub nextdns { my ($username) = @_; my $lines; open($fh, '+<', "$ipv6path") or die "Could not read file 'ipv6s' $!"; chomp(my $ipv6 = <$fh>); while(my $line = <$fh>){ $lines .= $line; } close $fh; open($fh, '>', "$ipv6path") or die "Could not write file 'ipv6s' $!"; print $fh $lines; close $fh; if (dns("$username.$hostname", $ipv6)) { return "$username.$hostname"; } return; } sub loadlog { open(my $fh, '<', "$znclog") or die "Could not read file 'znc.log' $!"; chomp(@logs = <$fh>); close $fh; } # return all lines matching a pattern sub linesmatching { my ($pattern) = @_; if (!@logs) { loadlog(); } return grep(/$pattern/, @logs); } # given a comma-separated list of IPs, return all matching users sub usersmatching { my ($ips) = @_; my @ips = split /,/m, $ips; my $pattern = "(".join('|', @ips).")"; if (!@logs) { loadlog(); } my @matches = grep(/$pattern/, @logs); my @nicks; foreach my $match (@matches) { if ($match =~ /^\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[([^]\/]+)(\/[^]]+)?\].*/) { push(@nicks, $1); } } my @sorted = sort @nicks; my @results = do { my %seen; grep { !$seen{$_}++ } @sorted }; # uniq return join(',', @results); } # given a username, return all matching ips sub ipsmatching { my ($usernames) = @_; my @usernames = split /,/m, $usernames; my $pattern = "(".join('|', @usernames).")"; if (!@logs) { loadlog(); } my @matches = grep(/$pattern.*to ZNC/, @logs); my @ips; foreach my $match (@matches) { if ($match =~ /^\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[([^]\/]+)(\/[^]]+)?\] connected to ZNC from (.*)/) { push(@ips, $3); } } my @sorted = sort @ips; my @results = do { my %seen; grep { !$seen{$_}++ } @sorted }; # uniq return join(',', @results); } # given hostname, return IP address; or given IP address, return hostname # if defined, v indicates version sub host { my ($name, $v) = @_; my @matches; my @lines = split /\n/m, `host $name`; if ($name =~ /^[0-9\.]+$/ or $name =~ /^$/) { # IP address foreach my $line (@lines) { if ($line =~ /([\d\.]+).(in-addr|ip6).arpa domain name pointer (.*)/) { push(@matches, $3); } } } else { # hostname foreach my $line (@lines) { if (!defined($v) && $line =~ /$name has (IPv6 )?address ([0-9a-fA-F\.:]+)/) { push(@matches, $2); } elsif ($v == 4 && $line =~ /$name has address ([0-9\.]+)/) { push(@matches, $1); } elsif ($v == 6 && $line =~ /$name has IPv6 address ([0-9a-fA-F\.:]+)/) { push(@matches, $1); } } } return join(',', @matches); } #sub getrdns { # my ($ip) = @_; # my $lookup = `dig -x $ip`; # my $hostname; # if ($lookup =~ /;; ANSWER SECTION:\n.*\s+PTR\s+(.*)/m) { # return $1; # } else { # return "not found"; # } #} # returns true upon success, false upon failure sub setrdns { my ($ip, $hostname) = @_; my $stdout = `curl -d \"key=$key&hash=$hash&action=rdns&ip=$ip&rdns=$hostname\" https://manage.buyvm.net/api/client/command.php`; if ($stdout !~ /success/) { return 0; } return 1; } # returns true upon success, false upon failure sub setdns { my ($domain, $ip) = @_; my $filename = "$zonedir/$hostname"; my $subdomain; if ($domain =~ /^([a-zA-Z][-\.a-zA-Z0-9]+)\.$hostname$/) { $subdomain = $1; } else { return 0; } my ($line, $lines); open($fh, '+<', "$filename") or die "Could not read file '$filename' $!"; while(my $line = <$fh>){ # increment the zone's serial number if ($line =~ /([0-9]{10})( [0-9]+ [0-9]+ [0-9]+ [0-9]+ \).*)/) { my $num = $1+1; $line = " $num$2\n"; } $lines .= $line; } close $fh; if ($ip =~ /^([0-9\.]+)$/) { # if IPv4 $lines .= "$subdomain 3600 IN A $ip\n"; } else { # if IPv6 $lines .= "$subdomain 3600 IN AAAA $ip\n"; } open($fh, '>', "$filename.bak") or die "Could not write file '$filename.bak' $!"; print $fh $lines; close $fh; copy "$filename.bak", $filename; if (system("doas -u _nsd nsd-control reload")) { return 0; } else { return 1; } } sub deldns { my ($domain) = @_; my $filename = "$zonedir/$hostname"; my $subdomain; if ($domain =~ /^([a-zA-Z][-\.a-zA-Z0-9]+)\.$hostname$/) { $subdomain = $1; } else { sendteam("", "ERROR: not authoritative"); return 0; } my ($line, $lines); open($fh, '+<', "$filename") or die "Could not read file '$filename' $!"; while(my $line = <$fh>){ # increment the zone's serial number if ($line =~ /([0-9]{10})( [0-9]+ [0-9]+ [0-9]+ [0-9]+ \).*)/) { my $num = $1+1; $line = " $num$2\n"; } elsif ($line =~ /$subdomain\s*3600\s*IN/) { next; } elsif ($line =~ /$subdomain\s*3600\s*IN/) { next; } $lines .= $line; } close $fh; open($fh, '>', "$filename.bak") or die "Could not write file '$filename.bak' $!"; print $fh $lines; close $fh; copy "$filename.bak", $filename; if (system("doas -u _nsd nsd-control reload")) { return 0; } else { return 1; } } # Connect to database, creating table if necessary # Returns true on success, false on failure sub connectdb { my $dsn = "dbi:SQLite:dbname=$dbpath"; my $user = ""; my $password = ""; $dbh = DBI->connect($dsn, $user, $password, { PrintError => 0, RaiseError => 1, AutoCommit => 1, FetchHashKeyName => 'NAME_lc', }) or die "Couldn't connect to database: " . DBI->errstr; if (!(-s "$dbpath")) { my $sql = <<'END_SQL'; CREATE TABLE users ( id INTEGER PRIMARY KEY, nickname VARCHAR(32), username VARCHAR(100), realname VARCHAR(100), email VARCHAR(100), password VARCHAR(100), vhost VARCHAR(100), ip VARCHAR(100), server VARCHAR(100), version VARCHAR(100), identified INTEGER, oper INTEGER, idle INTEGER, ssl INTEGER, epochtime INTEGER, terms INTEGER, chans VARCHAR(100), date VARCHAR(100), num INTEGER, captcha INTEGER, oldnick VARCHAR(100), newnick VARCHAR(100), services VARCHAR(100), localtime VARCHAR(100), time VARCHAR(100), loggedin VARCHAR(100) ) END_SQL $dbh->do($sql); } return defined($dbh); } # Inserts key, value pair into database # Returns number of rows successfully inserted sub insert { my ($key, $val) = @_; if (!defined($dbh)) { connectdb(); } my $rows = $dbh->do("INSERT INTO users ($key) values (\"$val\")"); return $rows; } # Update key, value pair for record with vhost # Returns number of rows successfully updated sub update { my ($key, $val, $vhost) = @_; if (!defined($dbh)) { connectdb(); } my $rows = $dbh->do("UPDATE users SET $key = ? where vhost = ?", undef, $val, $vhost); return $rows; } # Delete record with vhost # Returns number of rows deleted sub deleterow { my ($vhost) = @_; if (!defined($dbh)) { connectdb(); } my $rows = $dbh->do("DELETE FROM users WHERE vhost = ?", undef, $vhost); return $rows; } # Returns all records in the database sub selectall { if (!defined($dbh)) { connectdb(); } my $sth = $dbh->prepare("SELECT * FROM users"); $sth->execute(); my @results; while (my $row = $sth->fetchrow_hashref) { push(@results, $row); } return @results; } # Returns all records in the database where key equals value sub selectdb { my ($key, $val) = @_; if (!defined($dbh)) { connectdb(); } my $sth = $dbh->prepare("SELECT * FROM users WHERE $key = ?"); $sth->execute($val); my @results; while (my $row = $sth->fetchrow_hashref) { push(@results, $row); } return @results; }