11 use Digest::SHA qw(sha256_hex);
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 my $pfconfpath = "/etc/pf.conf";
31 my $relaydconfpath = "/etc/relayd.conf";
34 main::cbind("pub", "-", "shell", \&mshell);
35 main::cbind("msg", "-", "shell", \&mshell);
38 #dependencies for figlet
39 unveil("/usr/local/bin/figlet", "rx") or die "Unable to unveil $!";
40 unveil("/usr/lib/libc.so.95.1", "r") or die "Unable to unveil $!";
41 unveil("/usr/libexec/ld.so", "r") or die "Unable to unveil $!";
42 #dependencies for shell account
43 unveil($passpath, "r") or die "Unable to unveil $!";
44 unveil($httpdconfpath, "rwxc") or die "Unable to unveil $!";
45 unveil($acmeconfpath, "rwxc") or die "Unable to unveil $!";
46 unveil($pfconfpath, "rwxc") or die "Unable to unveil $!";
47 unveil($relaydconfpath, "rwxc") or die "Unable to unveil $!";
48 unveil("/usr/sbin/chown", "rx") or die "Unable to unveil $!";
49 unveil("/bin/chmod", "rx") or die "Unable to unveil $!";
50 unveil("/usr/sbin/groupadd", "rx") or die "Unable to unveil $!";
51 unveil("/usr/sbin/useradd", "rx") or die "Unable to unveil $!";
52 unveil("/usr/sbin/groupdel", "rx") or die "Unable to unveil $!";
53 unveil("/usr/sbin/userdel", "rx") or die "Unable to unveil $!";
54 unveil("/bin/mkdir", "rx") or die "Unable to unveil $!";
55 unveil("/bin/ln", "rx") or die "Unable to unveil $!";
56 unveil("/usr/sbin/acme-client", "rx") or die "Unable to unveil $!";
57 unveil("/bin/rm", "rx") or die "Unable to unveil $!";
58 unveil("/bin/mv", "rx") or die "Unable to unveil $!";
59 unveil("/home/", "rwxc") or die "Unable to unveil $!";
62 # !shell <username> <email>
63 # !shell captcha <captcha>
65 my ($bot, $nick, $host, $hand, @args) = @_;
68 ($chan, $text) = ($args[0], $args[1]);
69 } else { $text = $args[0]; }
70 my $hostmask = "$nick!$host";
71 if (defined($chan) && $chans =~ /$chan/) {
72 main::putserv($bot, "PRIVMSG $chan :$nick: Please check private message");
75 main::putserv($bot, "PRIVMSG $nick :Type !help for new instructions");
76 foreach my $chan (@teamchans) {
77 main::putservlocalnet($bot, "PRIVMSG $chan :Help shell *$nick* on ".$bot->{name});
80 } elsif (main::isstaff($bot, $nick) && $text =~ /^delete\s+([[:ascii:]]+)/) {
82 if (SQLite::deleterows("shell", "username", $username)) {
84 deleteshell($username);
85 foreach my $chan (@teamchans) {
86 main::putserv($bot, "PRIVMSG $chan :$username deleted");
91 ### TODO: Check duplicate emails ###
92 my @rows = SQLite::selectrows("irc", "nick", $nick);
93 foreach my $row (@rows) {
94 my $password = SQLite::get("shell", "ircid", $row->{id}, "password");
95 if (defined($password)) {
96 main::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help.");
100 if ($text =~ /^lastseen\s+([[:alnum:]]+)/) {
102 if ($text =~ /^captcha\s+([[:alnum:]]+)/) {
104 my $ircid = SQLite::id("irc", "nick", $nick, $expires);
105 if (!defined($ircid)) { die "undefined ircid"; }
106 my $captcha = SQLite::get("shell", "ircid", $ircid, "captcha");
107 if ($text ne $captcha) {
108 main::putserv($bot, "PRIVMSG $nick :Wrong captcha. To get a new captcha, type !shell <username> <email>");
111 my $pass = Hash::newpass();
112 chomp(my $encrypted = `encrypt $pass`);
113 my $username = SQLite::get("shell", "ircid", $ircid, "username");
114 my $email = SQLite::get("shell", "ircid", $ircid, "email");
115 my $version = SQLite::get("shell", "ircid", $ircid, "version");
116 my $bindhost = "$username.$hostname";
117 SQLite::set("shell", "ircid", $ircid, "password", $encrypted);
118 if (DNS::nextdns($username)) {
120 createshell($username, $pass, $bindhost);
121 mailshell($username, $email, $pass, "shell", $version);
122 main::putserv($bot, "PRIVMSG $nick :Check your email!");
124 #www($newnick, $reply, $password, "bouncer");
126 foreach my $chan (@teamchans) {
127 main::putserv($bot, "PRIVMSG $chan :Assigning bindhost $bindhost failed");
131 } elsif ($text =~ /^([[:alnum:]]+)\s+([[:ascii:]]+)/) {
132 my ($username, $email) = ($1, $2);
133 my @users = col($passpath, 1, ":");
134 my @matches = grep(/^$username$/i, @users);
135 if (scalar(@matches) > 0) {
136 main::putserv($bot, "PRIVMSG $nick :Sorry, username taken. Please choose another username, or contact staff for help.");
139 # my $captcha = join'', map +(0..9,'a'..'z','A'..'Z')[rand(10+26*2)], 1..4;
140 my $captcha = int(rand(999));
141 my $ircid = int(rand(2147483647));
142 SQLite::set("irc", "id", $ircid, "localtime", time());
143 SQLite::set("irc", "id", $ircid, "date", main::date());
144 SQLite::set("irc", "id", $ircid, "hostmask", $hostmask);
145 SQLite::set("irc", "id", $ircid, "nick", $nick);
146 SQLite::set("shell", "ircid", $ircid, "username", $username);
147 SQLite::set("shell", "ircid", $ircid, "email", $email);
148 SQLite::set("shell", "ircid", $ircid, "captcha", $captcha);
149 main::whois($bot->{sock}, $nick);
150 main::ctcp($bot->{sock}, $nick);
151 main::putserv($bot, "PRIVMSG $nick :".`figlet $captcha`);
152 main::putserv($bot, "PRIVMSG $nick :$captchaURL".encode_base64($captcha));
153 main::putserv($bot, "PRIVMSG $nick :Type !shell captcha <text>");
154 foreach my $chan (@teamchans) {
155 main::putservlocalnet($bot, "PRIVMSG $chan :$nick\'s captcha on $bot->{name} is $captcha");
158 main::putserv($bot, "PRIVMSG $nick :Invalid username or email. Type !shell <username> <email> to try again.");
159 foreach my $chan (@teamchans) {
160 main::putserv($bot, "PRIVMSG $chan :Help *$nick* on ".$bot->{name});
165 my( $username, $email, $password, $service, $version )=@_;
166 my $passhash = sha256_hex("$username");
167 my $versionhash = encode_base64($version);
169 You created a shell account!
175 Your Ports: $startPort to $endPort
177 To customize your vhost, connect to ask in #ircnow
179 *IMPORTANT*: Verify your email address:
181 https://www.$hostname/register.php?id=$passhash&version=$versionhash
183 You *MUST* click on the link within 24 hours or your account will be deleted.
187 Mail::mail($mailfrom, $email, $mailname, "Verify IRCNow Account", $body);
192 # my ($bot, $nick, $host, $hand, $text) = @_;
193 # if ($staff !~ /$nick/) { return; }
194 # if ($text =~ /^ips?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
195 # my $ips = $1; # space-separated list of IPs
196 # main::putserv($bot, "PRIVMSG $nick :".regexlist($ips));
197 # } elsif ($text =~ /^users?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
198 # my $users = $1; # space-separated list of usernames
199 # main::putserv($bot, "PRIVMSG $nick :".regexlist($users));
200 # } elsif ($text =~ /^[-_()|0-9A-Za-z:,\.?*\s]{3,}$/) {
201 # my @lines = regex($text);
202 # foreach my $l (@lines) { print "$l\n"; }
206 # my ($bot, $nick, $host, $hand, $text) = @_;
207 # if ($staff !~ /$nick/) { return; }
208 # if ($text =~ /^network\s+del\s+([[:graph:]]+)\s+(#[[:graph:]]+)$/) {
209 # my ($user, $chan) = ($1, $2);
210 # foreach my $n (@main::networks) {
211 # main::putserv($bot, "PRIVMSG *controlpanel :delchan $user $n->{name} $chan");
217 # open(my $fh, '<', "$authlog") or die "Could not read file 'authlog' $!";
218 # chomp(@logs = <$fh>);
222 # return all lines matching a pattern
224 # my ($pattern) = @_;
225 # if (!@logs) { loadlog(); }
226 # return grep(/$pattern/, @logs);
229 # given a list of IPs, return matching users
230 # or given a list of users, return matching IPs
233 # my @items = split /[,\s]+/m, $items;
234 # my $pattern = "(".join('|', @items).")";
235 # if (!@logs) { loadlog(); }
236 # my @matches = grep(/$pattern/, @logs);
238 # foreach my $match (@matches) {
239 # if ($match =~ /^\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[([^]\/]+)(\/[^]]+)?\] connected to ZNC from (.*)/) {
240 # my ($user, $ip) = ($1, $3);
241 # if ($items =~ /[.:]/) { # items are IP addresses
242 # push(@results, $user);
243 # } else { # items are users
244 # push(@results, $ip);
248 # my @sorted = sort @results;
249 # @results = do { my %seen; grep { !$seen{$_}++ } @sorted }; # uniq
250 # return join(' ', @results);
254 my ($username, $password, $bindhost) = @_;
255 system "doas groupadd $username";
256 system "doas adduser -batch $username $username $username `encrypt $password`";
257 system "doas chmod 700 /home/$username /home/$username/.ssh";
258 system "doas chmod 600 /home/$username/{.Xdefaults,.cshrc,.cvsrc,.login,.mailrc,.profile}";
259 system "doas mkdir /var/www/htdocs/$username";
260 system "doas ln -s /var/www/htdocs/$username /home/$username/htdocs";
261 system "doas chown -R $username:www /var/www/htdocs/$username /home/$username/htdocs";
262 system "doas chmod -R o-rx /var/www/htdocs/$username /home/$username/htdocs";
263 system "doas chmod -R g+rwx /var/www/htdocs/$username /home/$username/htdocs";
264 my $lusername = lc $username;
266 server "$lusername.$hostname" {
268 location "/.well-known/acme-challenge/*" {
273 fastcgi socket "/run/php-fpm.sock"
275 root "/htdocs/$username"
278 main::appendfile($httpdconfpath, $block);
280 domain "$lusername.$hostname" {
281 domain key "/etc/ssl/private/$lusername.$hostname.key"
282 domain full chain certificate "/etc/ssl/$lusername.$hostname.crt"
283 sign with letsencrypt
286 main::appendfile($acmeconfpath, $block);
287 configurepf($username);
288 system "doas rcctl reload httpd";
289 system "doas acme-client -F $lusername.$hostname";
290 system "doas ln -s /etc/ssl/$lusername.$hostname.crt /etc/ssl/$lusername.$hostname.fullchain.pem";
291 system "doas pfctl -f /etc/pf.conf";
292 configurerelayd($username);
294 ~ * * * * acme-client $lusername.$hostname && rcctl reload relayd
296 system "echo $block | doas crontab -";
302 my ($username, $bindhost) = @_;
303 my $lusername = lc $username;
304 system "doas groupdel $username";
305 system "doas userdel $username";
306 system "doas rm -f /etc/ssl/$lusername.$hostname.crt /etc/ssl/$lusername.$hostname.fullchain.pem /etc/ssl/private/$lusername.$hostname.key";
307 my $httpdconf = main::readstr($httpdconfpath);
309 server "$lusername.$hostname" {
311 location "/.well-known/acme-challenge/*" {
316 fastcgi socket "/run/php-fpm.sock"
318 root "/htdocs/$username"
321 $block =~ s/{/\\{/gm;
322 $block =~ s/}/\\}/gm;
323 $block =~ s/\./\\./gm;
324 $block =~ s/\*/\\*/gm;
325 $httpdconf =~ s{$block}{}gm;
327 main::writefile($httpdconfpath, $httpdconf);
329 my $acmeconf = main::readstr($acmeconfpath);
331 domain "$lusername.$hostname" {
332 domain key "/etc/ssl/private/$lusername.$hostname.key"
333 domain full chain certificate "/etc/ssl/$lusername.$hostname.fullchain.pem"
334 sign with letsencrypt
337 $block =~ s/{/\\{/gm;
338 $block =~ s/}/\\}/gm;
339 $block =~ s/\./\\./gm;
340 $block =~ s/\*/\\*/gm;
341 $acmeconf =~ s{$block}{}gm;
342 main::writefile($acmeconfpath, $acmeconf);
347 # Return column $i from $filename as an array with file separator $FS
349 my ($filename, $i, $FS) = @_;
350 my @rows = main::readarray($filename);
352 foreach my $row (@rows) {
353 if ($row =~ /^(.*?)$FS/) {
361 my $username = shift;
362 my @read = split('\n', main::readstr($pfconfpath) );
364 my $previousline = "";
366 foreach my $line(@read)
368 my $currline = $line;
369 if( $currline ne "# end user ports") {
370 $previousline = $currline;
372 #pass in proto {tcp udp} to port {31361:31370} user {JL}
373 if( $previousline =~ /(\d*):(\d*)/ ) {
374 my $startport = ( $1 + 10 );
375 my $endport = ( $2 + 10 );
376 my $insert = "pass in proto {tcp udp} to port {$startport:$endport} user {$username}";
377 push(@pfcontent, $insert);
378 $startPort = $startport;
382 push(@pfcontent, $currline)
384 main::writefile("$pfconfpath", join("\n",@pfcontent))
387 sub configurerelayd {
389 my $block = "tls { keypair $username.$hostname }";
390 my $relaydconf = main::readstr($relaydconfpath);
392 if ($relaydconf =~ /^.*tls\s+{\s+keypair\s+[.0-9a-zA-Z]+\s*}/m) {
393 $newconf = "$`$&\n\t$block$'";
395 main::writefile($relaydconfpath, $newconf);
398 #unveil("./newacct", "rx") or die "Unable to unveil $!";
399 1; # MUST BE LAST STATEMENT IN FILE