#! /usr/bin/perl -T
# gpigeonctl: user, cookies and GPG key management for gpigeon.cgi
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# Copyright (c) 2020-2021, Miquel Lionel
use warnings;
use strict;
use Email::Valid;
use Term::ReadKey;
use Crypt::Argon2 qw(argon2id_pass);
use DBI;
delete @ENV{qw(IFS PATH CDPATH BASH_ENV)};
$ENV{'PATH'} = q{bin_path_goes_here};
my $dbh_path = q{db_path_goes_here};
my $cookiesdir = q{cookies_dir_goes_here};
my $GNUPGHOME = q{gpgdir_goes_here};
my $web_user = q{web_user_goes_here};
my $web_dir = q{web_dir_goes_here};
my ($escaddr, $ynchoice) = undef;
my $opt = $ARGV[0];
my $version = 0.1;
my $dbh = DBI->connect("DBI:SQLite:dbname=$dbh_path", undef, undef,
{ RaiseError => 1,
AutoCommit => 1,
})
or die $DBI::errstr;
sub DbGetLine {
my ($dbh, $query) = @_;
my $prep = $dbh->prepare( $query );
my $exec = $prep->execute() or die $DBI::errstr;
if ($exec < 0){
print $DBI::errstr;
}
while (my @rows = $prep->fetchrow_array()) {
my $row = $rows[0];
return $row;
}
}
sub ListUsers {
my ($dbh, $query) = @_;
my $prep = $dbh->prepare( $query );
my $exec = $prep->execute() or die $DBI::errstr;
if ($exec < 0){
print $DBI::errstr;
}
while (my @rows = $prep->fetchrow_array()) {
print "$row[0]\t$rows[1]";
}
}
sub RecursiveChown {
my ($junk, $junk2, $uid, $gid) = getpwnam($web_user);
if ($_ =~ qr|^([-+@\w./]+)$|){ # pattern taken from File::Find
chown $uid, $gid, "$1";
}
}
sub DeleteCookies {
if ($_ =~ /^([\w]+)\.txt$/){
unlink "$1.txt";
}
}
sub EscapeArobase {
my $esc = shift;
if ($esc =~ /^([-\@\w.]+)$/) {
$esc = $1; # $data now untainted
$esc =~ s/@/\\@/;
return $esc;
} else {
die "\n"; # log this somewhere
}
}
sub PrintHelp{
print 'Copyright (c) 2020-2021, Miquel Lionel ',"\n\n";
print 'usage: gpigeonctl [init] [adduser] [deluser] [cleancookies] [cleanlinks] [version]', "\n";
exit 0;
}
sub SetMail {
print "Mail address: ";
my $addr = ;
if (not Email::Valid->address($addr)){
die "\nNot a valid email address.";
}
chomp $addr;
return $addr;
}
sub SetNick {
my $addr = shift;
print "\nNickname (optional): ";
my $nick = ;
chomp $nick;
if (length($nick) eq 0){
$nick = $addr;
return $nick;
}
elsif (defined $nick and not $nick =~ /^([\w]+)$/){
die "\nYour nickname must have only alphanumeric characters.\n";
}
return $nick;
}
sub SetPasswd {
ReadMode 2;
print "\nPassword: ";
my $pass = ;
if (not length($pass) > 10){
ReadMode 1;
die "\nFor your safety, you should have a password at least 10 characters long.\n";
}
ReadMode 1;
chomp $pass;
my $salt = `openssl rand 16`;
my $hash = argon2id_pass($pass, $salt, 3, '32M', 1, 32);
}
sub TransferGPGPubKey {
my ($addr, $GNUPGHOME) = @_;
my $escaddr = EscapeArobase($addr);
my $gpgid = '0x'.`gpg --with-colons -k $escaddr | grep "pub:u" | cut -d':' -f5`;
chomp $gpgid;
if (not $gpgid =~ /^([\w]+)$/ and not length($gpgid) eq 18){
die "\nYour GPG 0xlong key id is not a correct one. It seems that no public key was tied to the provided e-mail address.\n";
}
else{
$gpgid = $1;
print "\nGPG ID: $gpgid\n";
return $gpgid;
}
}
# i should use a module for this lol
if (defined $opt){
if($opt eq 'init'){
if ( -e $dbh_path){
print "The database already exist at $dbh_path.\n";
print "Overwrite ? [y/n] ";
$ynchoice = ;
chomp $ynchoice;
if ($ynchoice eq 'o' or $ynchoice eq 'y'){
unlink $dbh_path;
print "Done.\n";
}
}
if (-d $GNUPGHOME){
use File::Path qw/rmtree/;
print "GPG home directory already exists at $GNUPGHOME. Delete it ? [y/n] ";
$ynchoice = ;
chomp $ynchoice;
if ($ynchoice eq 'o' or $ynchoice eq 'y'){
rmtree($GNUPGHOME);
print "Done.\n";
}
}
my $addr = SetMail();
my $nick = SetNick($addr);
my $hash = SetPasswd();
my $gpgid = TransferGPGPubKey($addr,$GNUPGHOME);
my ($junk, $junk2, $uid, $gid) = getpwnam($web_user);
use File::Path qw/make_path/;
make_path($GNUPGHOME);
chmod(0700,$GNUPGHOME);
open my $out, '>', "$GNUPGHOME/gpg.conf" or die $!;
print $out "use-agent\n";
print $out "charset utf-8\n";
print $out "no-escape-from-lines\n";
print $out "trust-model always\n";
print $out "personal-digest-preferences SHA512 SHA384 SHA256 SHA224\n";
print $out "default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 BZIP2 ZLIB ZIP Uncompressed";
close $out;
use GPG;
`gpg -a --export $gpgid > key.asc`;
`gpg --homedir $GNUPGHOME --import key.asc`;
my $gpg = new GPG(gnupg_path => "/usr/bin", homedir => "$GNUPGHOME");
my $enc_msg = $gpg->encrypt("test", $gpgid)
or die "\nOops, it seems gpg won't encrypt the test message. Here's why :\n",$gpg->error();
my $dbh = DBI->connect("DBI:SQLite:dbname=$dbh_path", undef, undef,
{
RaiseError => 1,
AutoCommit => 1,
}) or die $DBI::errstr;
$dbh->do('create table users (userid integer primary key, mail text NOT NULL UNIQUE, name text UNIQUE, pass text NOT NULL, gpgfp text NOT NULL UNIQUE, isadmin integer NOT NULL)') or die $DBI::errstr;
$dbh->do('create index idx_usersid on users(userid)') or die $DBI::errstr;
$dbh->do(qq{INSERT INTO users VALUES( ?, '$addr', '$nick', '$hash', '$gpgid', 1)}) or die $DBI::errstr;
$dbh->disconnect;
unlink 'key.asc';
find(\&recursivechown, $cookiesdir);
find(\&recursivechown, $GNUPGHOME);
chown $uid, $gid, $dbh_path;
print "\nThe database has been initialized.\n";
exit 0;
}
if ($opt eq 'adduser'){
if (not -e $dbh_path){
print "It seems that the database doesn't exist. Type `gpigeonctl init' in a terminal to create it.\n";
exit 1;
}
my $addr = SetMail();
my $nick = SetNick($addr);
my $hash = SetPasswd();
my $gpgid = TransferGPGPubKey($addr,$GNUPGHOME);
my $dbh = DBI->connect("DBI:SQLite:dbname=$dbh_path", undef, undef,
{ RaiseError => 1,
AutoCommit => 1,
})
or die $DBI::errstr;
$dbh->do(qq{INSERT INTO users VALUES( ?, '$addr', '$nick', '$hash', '$gpgid', 1)}) or die $DBI::errstr;
$dbh->disconnect;
print "\nUser $addr added succesfully\n";
exit 0;
}
if ($opt eq 'deluser'){
use File::Path qw/rmtree/;
my $addr = SetMail();
my $esc = EscapeArobase($addr);
my $uid = DbGetLine($dbh, "SELECT userid FROM users WHERE mail='$esc'") or die "$!";
$dbh->do(qq{DELETE FROM users where mail='$addr'}) or die $DBI::errstr;
$dbh->disconnect;
if (defined $uid and $uid > 0){
rmtree("$cookiesdir/$uid", "$web_dir/l/$uid",
{ verbose => 1,
safe => 1
});
#GPG module doesn't delete key
`GNUPGHOME="$GNUPGHOME" gpg --yes --batch --delete-key $esc`;
}
print "\nUser $addr deleted succesfully\n";
exit 0;
}
if ($opt eq 'cleancookies'){
print "This will clean the entire cookie directory at $cookiesdir.\n";
print "Proceed ? [y/n]";
$ynchoice = ;
chomp $ynchoice;
if ($ynchoice eq 'o' or $ynchoice eq 'y'){
rmtree("$cookiesdir",{safe=>1,keep_root=>1});
print "All cookies have been cleaned. Tell your users to clear their caches and reconnect.\n";
}
exit 0;
}
if ($opt eq 'cleanlinks'){
$ynchoice = ;
chomp $ynchoice;
if ($ynchoice eq 'o' or $ynchoice eq 'y'){
rmtree("$web_dir/cgi-bin/l",{safe=>1,keep_root=>1});
print "All generated links have been deleted.\n";
}
exit 0;
}
if ($opt eq 'invite'){
my $verb = shift;
if ($verb eq 'gen'){
my $preconf_mail = undef;
my $mailfield = q{};
my $for_x = undef;
print "Set an email address beforehand ? [y/n] ";
$ynchoice = ;
chomp $ynchoice;
if ($ynchoice eq 'o' or $ynchoice eq 'y'){
$preconf_mail = SetMail();
$mailfield = qq{};
$for_x = "for $preconf_mail";
}
use File::Path qw/mkpath/;
mkpath($invites_dir) unless -d $invites_dir;
my $randengine = String::Random->new;
my $randfn = $randengine->randregex('\w{64}') . '.cgi';
my $invite_path = "$invites_dir/$randfn";
open my $in, '<', $invites_tmpl or die "Can't open template for invites : $!";
open my $out, '>', $invite_path or die "Can't write to invite path: $!";
while (<$in>) {
s/{mailfield_goes_here}/{$mailfield}/g;
print $out $_;
}
close $in or die "$!";
chmod(0755, $invite_path) or die "$!";
close $out or die "$!";
print "\nSuccess ! The link was generated to $invite_path $for_x.";
}
}
if ($opt eq 'version'){
print "$version\n";
exit 0;
}
if ($opt eq 'list'){
my $verb = shift;
if (defined $verb){
if ($verb eq 'users'){
my $dbh = DBI->connect("DBI:SQLite:dbname=$dbh_path", undef, undef,
{
RaiseError => 1,
AutoCommit => 1,
}) or die $DBI::errstr;
ListUsers($dbh);
}
}
else{
print "Valid 'list' actions are:\n\tusers\n" ;
}
}
PrintHelp();
}
else {
PrintHelp();
}