Commit Diff
Diff:
817a3de15f20415ccc796cde724c24f5de6d442c
36349fb877af7495d0a38878f9816c7a2a5c745b
Commit:
36349fb877af7495d0a38878f9816c7a2a5c745b
Tree:
3d6a44d99392e9bac34a841ee705dac1157c941a
Author:
jrmu <jrmu@lecturify.com>
Committer:
jrmu <jrmu@lecturify.com>
Date:
Mon Sep 21 03:47:50 2020 UTC
Message:
Added DNS functions and switched to sqlite for user metadata
blob - e003442ddb936af7f0057dc62b6988e760ea98b2
blob + 72e40c6f8696abc218eced8f9d160cc9cbb334aa
--- README
+++ README
@@ -51,7 +51,8 @@ $ doas make -i
### Changelog ###
+Version 0.04: Switched from flatfiles to sqlite for user metadata
+Version 0.03: Added new DNS commands
Version 0.02: Updated wiki pages, added warnings to common errors, added support
for trustallcerts
-
-Version 0.01 -- First public version of botnow
+Version 0.01: First public version of botnow
blob - 2ba8ad4a955f4fcc3b25df011c8bf05b3bd554de
blob + 0bcb9cf01393cafcfefbe34bed8dedcf6a9572dd
--- botnow.pl
+++ botnow.pl
@@ -8,7 +8,7 @@ my $host = "127.0.0.1";
my $port = "1337";
# Bouncer hostname
-my $hostname = "guava.ircnow.org";
+my $hostname = "example.ircnow.org";
# External IPv4 address, plaintext and ssl port
my $ip4 = "192.168.0.1";
@@ -77,20 +77,27 @@ use OpenBSD::Pledge;
use OpenBSD::Unveil;
use File::Copy qw(copy);
use Digest::SHA qw(sha256_hex);
-use Data::Dumper;
use MIME::Base64;
use Time::HiRes qw ( setitimer ITIMER_VIRTUAL time );
+use DBI;
+use DBD::SQLite;
-my $sysname = "botnow"; # system username
+my $confpath = "botnow.conf"; # Bot conf path
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 $!";
@@ -100,7 +107,6 @@ unveil("/usr/libexec/ld.so", "r") or die "Unable to un
#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("/usr/lib/libc.so.95.1", "r") or die "Unable to unveil $!";
unveil("/bin/sh", "rx") or die "Unable to unveil $!";
#dependencies for doas
@@ -112,9 +118,6 @@ unveil("/usr/bin/encrypt", "rx") or die "Unable to unv
#znc.log file
unveil("$znclog", "r") or die "Unable to unveil $!";
-#dependencies for dig
-#unveil("/usr/bin/dig", "rx") or die "Unable to unveil $!";
-
#dependencies for host
unveil("/usr/bin/host", "rx") or die "Unable to unveil $!";
@@ -122,7 +125,8 @@ 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
-pledge( qw(stdio rpath wpath cpath inet dns proc exec) ) or die "Unable to pledge: $!";
+#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;
@@ -133,20 +137,37 @@ 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, simplify create a new
-# entry for the same net name; znc ignores addnetwork commands when a network
+# 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
-open(my $fh, '<', "networks") or die "Could not read file 'networks' $!";
-while (my $line = <$fh>) {
- my ($name, $server, $trustcerts, $port);
- chomp($line);
+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*$/) {
- ($name, $server, $port) = ($1, $2, $4);
+ my ($name, $server, $port) = ($1, $2, $4);
+ my $trustcerts;
if (!defined($3)) {
$trustcerts = 0;
} elsif ($3 eq "~") { # Use SSL but trust all certs
@@ -161,63 +182,42 @@ while (my $line = <$fh>) {
die "network format invalid: $line\n";
}
}
-close $fh;
+
# networks must be sorted to avoid multiple connections
@networks = sort @networks;
# dictionary words for passwords
-open($fh, '<', "words") or die "Could not read file 'words' $!";
-chomp(@words = <$fh>);
-close $fh;
+@words = readfile("words");
# Validate ipv6s if it exists, otherwise load addresses from /etc/hostname.if
if (!(-s "$ipv6path")) {
- if (open($fh, '+<', $hostnameif)) {
- my $ipv6s;
- while (my $line = <$fh>) {
- if ($line =~ /^\s*inet6 (alias )?([0-9a-f:]{4,}) [0-9]+\s*$/i) {
- $ipv6s .= "$2\n";
- }
+ 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";
}
- close $fh;
- open($fh, '>', "$ipv6path") or die "Could not write to '$ipv6path";
- print $fh $ipv6s;
- close $fh;
- } else {
- die "No IPv6 addresses in $ipv6path and cannot read '$hostnameif' $!";
}
+ writefile($ipv6path, $ipv6s);
}
-if (open($fh, '+<', "$ipv6path")) {
- my $ipv6s;
- while (my $line = <$fh>) {
- if ($line =~ /^\s*([0-9a-f:]{4,})\s*$/i) {
- $ipv6s .= "$1\n";
- }
+@lines = readfile($ipv6path);
+my $ipv6s;
+foreach my $line (@lines) {
+ if ($line =~ /^\s*([0-9a-f:]{4,})\s*$/i) {
+ $ipv6s .= "$1\n";
}
- close $fh;
- open($fh, '>', "$ipv6path") or die "Could not write to '$ipv6path' $!";
- print $fh $ipv6s;
- close $fh;
-} else {
- die "Cannot open '$ipv6path' $!";
}
+writefile($ipv6path, $ipv6s);
-use strict;
-use warnings;
-
-#eval {
-# $SIG{ALRM} = \&saverecords;
-#};
-
# create sockets
my $sel = IO::Select->new( );
my $lastname = "";
foreach my $network (@networks) {
- if ($lastname eq $network->{name}) { # avoid duplicate connections
- next;
- }
+ # 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);
@@ -242,7 +242,7 @@ while(my @ready = $sel->can_read) {
if ($response =~ /^PING :ZNC\r\n$/i) {
print $socket "PONG :ZNC\r\n";
if ($bot->{name} =~ /$localnet/i) {
- saverecords();
+ updaterecords();
}
} elsif ($response =~ /^:irc.znc.in (.*) (.*) :(.*)\r\n$/) {
my ($type, $target, $reply) = ($1, $2, $3);
@@ -300,6 +300,7 @@ sub parseznc {
print "Unexpected: type: $type, target: $target, reply: $reply\r\n";
}
}
+
sub parseserver {
my ($bot, $server, $code, $reply) = @_;
my ($sender, $val, $key);
@@ -310,11 +311,13 @@ sub parseserver {
# 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, $key, $val);
+ 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");
@@ -331,6 +334,8 @@ sub parseserver {
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);
@@ -392,6 +397,8 @@ sub parseserver {
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";
}
@@ -471,30 +478,22 @@ close $fh;
}
sub getrecords {
- my ($filename, $hostmask);
- my $date = date();
- opendir(DIR, "$database") || die "Can't open $database: $!\n";
- # Files follow the format of YYYYMMDD
- my @flist = grep(/^[0-9]+$/, readdir(DIR));
- closedir(DIR);
- if (scalar(@flist) == 0) { # if no records found, create
- $filename = "$database/$date";
- open($fh, '>', "$filename") or die "Could not write file '$filename' $!";
- close $fh;
- } else { # sort numerically descending
- my @sorted = sort {$b <=> $a} @flist;
- $filename = "$database/".$sorted[0];
- }
- open($fh, '+<', $filename) or die "Could not read '$filename' $!";
- while (my $line = <$fh>) {
- if ($line =~ /^hostmask: ([[:graph:]]+)$/) {
- $hostmask = $1;
- $records->{$hostmask} = ();
- } elsif ($line =~ /^([^:]+): (.*)$/) {
- $records->{$hostmask}->{$1} = $2;
+ 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;
+ }
+ }
+ warn Dumper \$records;
+ } else {
+ print "Error getting records\n";
}
}
- close $fh;
}
sub getkeyval {
my ($hostmask, $key) = @_;
@@ -507,26 +506,23 @@ sub getkeyval {
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"; }
- #eval {
- # alarm 10;
- #}
}
sub delkey {
my ($hostmask, $key) = @_;
if (!defined($records)) { getrecords(); }
delete($records->{$hostmask}->{$key});
if ($verbose >= 3) { print "delkey $hostmask: $key\r\n"; }
- #eval {
- # alarm 10;
- #}
}
sub delhost {
my ($hostmask) = @_;
if (!defined($records)) { getrecords(); }
+ deleterow($hostmask);
delete($records->{$hostmask});
- saverecords();
}
sub firstval {
my ($key, $val) = @_;
@@ -538,23 +534,23 @@ sub firstval {
}
return;
}
-sub saverecords {
- my $filename = "$database/".date();
+sub createrecord {
+ my ($hostmask) = @_;
if (!defined($records)) { getrecords(); }
- open($fh, '>', "$filename.bak") or die "Could not write to '$database' $!";
+ insert("vhost", $hostmask);
+ return 1;
+}
+sub updaterecords {
+ if (!defined($records)) { getrecords(); }
while (my ($hostmask, $record) = each (%$records)) {
- print $fh "hostmask: $hostmask\n";
- foreach my $key (sort {lc $a cmp lc $b} keys %$record) {
- print $fh "$key: ".$record->{$key}."\n";
+ foreach my $key (keys %$record) {
+ print "key: $key, val: ".$record->{$key}.", hostmask: $hostmask\n";
+ update($key, $record->{$key}, $hostmask);
}
- print $fh "\n";
}
- close $fh;
- copy "$filename.bak", $filename;
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);
@@ -767,7 +763,7 @@ EOF
if (setdns($hostname, $ip)) {
sendteam("", "$hostname set to $ip");
} else {
- sendteam("", "Error: failed to set DNS");
+ sendteam("", "ERROR: failed to set DNS");
}
} elsif ($reply =~ /^!deldns\s+([-0-9A-Za-z\.]+)/) {
my $hostname = $1;
@@ -775,8 +771,54 @@ EOF
if (deldns($hostname)) {
sendteam("", "$hostname deleted");
} else {
- sendteam("", "Error: failed to delete DNS records");
+ 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"))) {
@@ -880,11 +922,11 @@ EOF
return;
}
sendmsg($bot, $sender, delkey($1, $2));
- } elsif ($reply =~ /^!saverecords/) {
+ } elsif ($reply =~ /^!updaterecords/) {
if ($staff !~ /$sender/) {
return;
}
- if (saverecords()) {
+ if (updaterecords()) {
sendmsg($bot, $sender, "Records saved.");
}
} else {
@@ -901,6 +943,7 @@ sub parsenotice {
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());
}
@@ -1222,4 +1265,104 @@ sub deldns {
} 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;
}
blob - ede77ec7e838ad079c5655994db4afe91da18e6f
blob + c0b9ccf1a8ef041477c6eb3b42eb885af3a0192b
--- makefile
+++ makefile
@@ -21,7 +21,7 @@ botnow: figlet php
chmod ug+rwx ${ZONES}
echo "permit nopass $sysname as _nsd cmd nsd-control" >> /etc/doas.conf
cp register.php ${HTDOCS}/
- cp LICENSE README botnow.pl makefile networks register.php words ${HOMEDIR}/
+ cp LICENSE README botnow.pl Botconf.pm makefile networks register.php words ${HOMEDIR}/
chown -R ${USERNAME}:${USERNAME} ${HOMEDIR}
chmod u+x ${HOMEDIR}/botnow.pl
chown -R ${ZNCUSER}:daemon ${ZNCDIR}
@@ -39,5 +39,7 @@ php:
rcctl restart httpd
sqlite:
+ pkg_add p5-DBI
pkg_add p5-DBD-SQLite
pkg_add sqlite3
+ pkg_add p5-Class-DBI-SQLite-0.11p1