#!/usr/bin/perl
use strict; use warnings; use utf8;
use Getopt::Long;
use File::Slurp;
use JSON;
use FindBin;
my $self;

sub sys {
    my $cmd = shift;
    my %opts = @_;
    $opts{die} = 1 if !defined $opts{die};

    #print "++ $cmd\n";
    system $cmd;
    if ($? == -1) {
        die "-- $cmd\nfailed to execute: $!\n";
    } elsif ($? & 127) {
        my $signal = $? & 127;
        die "-- $cmd\nchild died with signal $signal\n";
    }
    my $ex = $? >> 8;
    if ($ex > 0 and $opts{die}) {
        print STDERR "-- $cmd\nexit code $ex\n";
        print STDERR "Continue? [y/N]\n";
        my $ans = <STDIN>;
        exit 1 if $ans !~ /^y/i;
    }
    return $ex;
}

sub check_exists {
    my $error = 0;
    for my $x (qw(siteroot apachecf)) {
        if (-e $self->{$x}) {
            print STDERR "$x already exists: $self->{$x}\n";
            $error = 1;
        }
    }
    my $id_ex = sys "id $self->{user} >/dev/null 2>&1", die => 0;
    if ($id_ex == 0) {
        print STDERR "User '$self->{user}' already exists.\n";
        $error = 1;
    }
    exit 1 if $error;
}

sub check_dns {
    my $ipa = qx(ip a);
    my $badname = '';
    for my $d (@{$self->{domains}}) {
        my $host = qx(host $d $self->{resolver});
        my $found = 0;
        while ($host =~ /has.*address (\S+)/g) {
            my $addr = $1;
            $found = 1;
            print "Resolving $d -> $addr";
            if ($ipa !~ /\Q$addr\E/) {
                print "    NOT ON THIS SERVER\n";
                $badname .= " $d";
            } else {
                print "\n";
            }
        }
        if (!$found) {
            print "Resolving $d -> NOT FOUND\n";
            $badname .= " $d";
        }
    }
    if ($badname ne '') {
        die "These domain names do not resolv to an address on this server:\n".
            " $badname\n";
    }
}

sub generate_random {
    for my $x (qw(dbpasswd ftppasswd adminurl)) {
        my $passwd = qx(pwgen 12 1);
        chomp $passwd;
        if (length($passwd) != 12) {
            die "pwgen generated invalid password: $passwd\n";
        }
        $self->{$x} = $passwd;
        print "Generating random: $x = $passwd\n";
    }
}

sub filesystem {
    sys "mkdir $self->{siteroot}";
    if ($self->{quota} ne 'off' and $self->{quota} > 0) {
        my $kb = $self->{quota} * 1024;
        sys "setquota $self->{user} $kb $kb 0 0 /www";
    }
}

sub user {
    sys "adduser --disabled-password --home $self->{siteroot} --no-create-home --firstuid 10000 --gecos '' --shell /bin/false $self->{user}";
}

sub siteroot_setup {
    my @dirs = qw(public tmp);
    if ($self->{use_site_log}) {
        push @dirs, 'log';
    }
    for my $d (@dirs) {
        my $path = "$self->{siteroot}/$d";
        next if -d $path;
        sys "mkdir $path";
    }

    my $indexfn = "$self->{siteroot}/public/index.html";
    if (!-e $indexfn) {
        write_file($indexfn,
            "$self->{domains}->[0] site temporarily unavailable.\n");
    }
    my $robotsfn = "$self->{siteroot}/public/robots.txt";
    if (!-e $robotsfn) {
        write_file($robotsfn,
            "User-agent: *\nDisallow: /\n");
    }
    if ($self->{use_owner}) {
        write_file("$self->{siteroot}/.INFO",
            sprintf("%s\nowner=%s\n", scalar localtime time, $self->{site_owner}));
    }
    sys "www-reset-acl $self->{site}";
}

sub mysql {
    if (-e "/var/lib/mysql/$self->{dbname}") {
        print "Database already exists, skipping: $self->{dbname}\n";
        $self->{dbpasswd} = '-- previous database password --';
        return;
    }
    if (!open MYSQL, '|-', "mysql -u root -p$self->{dbrootpw}") {
        die "failed to run mysql\n";
    }
    print MYSQL <<EOT;
    create database $self->{dbname} charset = utf8mb4;
grant all privileges on $self->{dbname}.* to $self->{dbuser}\@localhost identified by '$self->{dbpasswd}';
flush privileges;
EOT
    if (!close MYSQL) {
        if ($!) {
            die "error closing mysql pipe: $!\n";
        } else {
            die "mysql exit code: $?\n";
        }
    }
}

sub letsencrypt {
    my $domlist = read_file($self->{letsencrypt_dom});
    my $first = $self->{domains}->[0];
    if ($domlist =~ /^\Q$first\E\s+(.*)$/m) {
        my $san = $1;
        my $san_notfound = '';
        for (my $i = 1; $i < scalar @{$self->{domains}}; $i++) {
            my $d = $self->{domains}->[$i];
            if ($san !~ /\b\Q$d\E\b/) {
                $san_notfound .= " $d";
            }
        }
        if ($san_notfound ne '') {
            die "found domain in letsencrypt domain list:\n".
                "$first $san\n\n".
                "missing SAN domains:$san_notfound\n".
                "I can't handle this.\n";
        }
        print "Letsencrypt certificates already in domains.txt.\n";
    } else {
        append_file($self->{letsencrypt_dom},
            join(' ', @{$self->{domains}}) ."\n"
            );
        sys "/etc/dehydrated/dehydrated -c";
    }
}

sub apache {
    my $lt = scalar localtime time;
    my $logbase = "\${APACHE_LOG_DIR}/$self->{site}";
    if ($self->{use_site_log}) {
        $logbase = "$self->{siteroot}/log/apache";
    }
    my $c = "# generated: $lt\n";
    if ($self->{use_owner}) {
        $c.="# owner: $self->{site_owner}\n";
    }
    $c .=   "<VirtualHost *:80>\n".
            "    ServerName $self->{domains}->[0]\n";
    for (my $i = 1; $i < scalar @{$self->{domains}}; $i++) {
        $c.="    ServerAlias $self->{domains}->[$i]\n\n";
    }
    $c .=   "    ErrorLog $logbase.notls.err.log\n".
            "    CustomLog $logbase.notls.log detailed\n".
            "    RewriteEngine On\n".
            "    RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge/\n".
            "    RewriteRule ^/(.*) https://%{HTTP_HOST}/\$1 [R,L]\n".
            "</VirtualHost>\n".
            "<VirtualHost *:443>\n";
    if ($self->{itk_assignuser}) {
        $c.="    AssignUserId $self->{user} $self->{group}\n\n";
    }
    $c .=   "    SSLEngine on\n".
            "    SSLCertificateFile $self->{tlscrt}\n".
            "    SSLCertificateKeyFile $self->{tlskey}\n";
    $c .=   "    ServerName $self->{domains}->[0]\n";
    for (my $i = 1; $i < scalar @{$self->{domains}}; $i++) {
        $c.="    ServerAlias $self->{domains}->[$i]\n\n";
    }
    $c .=   "    ErrorLog $logbase.err.log\n".
            "    CustomLog $logbase.log detailed\n".
            "    DocumentRoot $self->{siteroot}/public\n".
            "    <Directory $self->{siteroot}/public>\n".
            "        Options -Indexes -FollowSymlinks +SymLinksIfOwnerMatch\n".
            "        AllowOverride all\n".
            "        Require all granted\n".
            "    </Directory>\n".
            "    Header always set Strict-Transport-Security \"max-age=31536000;\"\n";
    if ($self->{use_admin}) {
        $c.="    Alias /.$self->{adminurl}/ /var/www/_admin/\n";
    }
    if ($self->{php_fpm} and $self->{php_fpm} =~ /^\d/) {
        $c.="    #-[[-www-phpfpm\n".
            "    #-]]-www-phpfpm\n";
    }
    $c .=   "</VirtualHost>\n".
            "# vim: set tabstop=4 shiftwidth=4 expandtab smarttab:\n";
    write_file($self->{apachecf}, $c);
    sys "apachectl graceful";
}

sub proftpd {
    return if !$self->{use_ftpd};
    my $uid = qx(id -u $self->{user});
    chomp $uid;
    if ($uid < 10000 or $uid > 11000) {
        die "got invalid uid for user '$self->{user}': $uid\n";
    }
    my $gid = qx(id -g $self->{user});
    chomp $gid;
    sys "echo $self->{ftppasswd} | ftpasswd --stdin --passwd --file /etc/proftpd/ftpd.passwd --name $self->{ftpuser} --home $self->{siteroot} --shell /bin/false --uid $uid --gid $gid";
}

sub php_fpm {
    if ($self->{php_fpm} and $self->{php_fpm} =~ /^\d/) {
        sys "www-phpfpm -s $self->{site} -p $self->{php_fpm}";
    }
}

sub find_defcert {
    for my $d (read_file('/etc/dehydrated/domains.txt')) {
        chomp $d;
        if ($d =~ /^[^#]/) {
            return $d;
        }
    }
    return qx{hostname -f};
}

sub read_config {
    my ($fn) = @_;
    my $contents;
    eval {
        $contents = read_file($fn);
    };
    return if $@;
    my $cf;
    eval {
        my $json = JSON->new->utf8->relaxed;
        $cf = $json->decode("{ $contents }");
    };
    if ($@) {
        print STDERR "warning: configuration $fn has JSON errors: $@\n";
        return;
    }
    for my $k (keys %$cf) {
        $self->{$k} = $cf->{$k};
    }
}

$self = {
    config_files => [
        "$FindBin::RealBin/config.dist.json",
        "$FindBin::RealBin/config.local.json"],
    use_site_log => 0,
    use_owner => 0,
    use_admin => 0,
    use_ftpd => 0,
    site => '',
    domains => [],
    dbpasswd => '',
    dbrootpw_file => undef,
    dbrootpw => undef,
    letsencrypt_dom => "/etc/dehydrated/domains.txt",
    resolver => '8.8.8.8',
    defcert => 0,
    defcert_name => find_defcert,
    no_dnscheck => 0,
    itk_assignuser => 0,
    php_fpm => 'off',
    quota => 'off',
    logfile => '/var/log/www-create-site.log',
    };

sub usage {
    my $t = "usage: $0 OPTIONS..\n";
    if ($self->{use_owner}) {
        $t.=" -o|--owner NAME        new website's owner (mandatory)\n";
    }
    $t .=   " -s|--site NAME         new website's short name ([a-z][a-z0-9]*)\n".
            " -d|--domain 'D [D..]'  list of domain names for website\n".
            " --mysqlrootpw PASSWD   mysql root password (default: read from file)\n".
            " --defcert              skip letsencrypt, use server's cert: $self->{defcert_name}\n".
            " --no-dns-check         skip dns check (with implicit --defcert)\n".
            " --[no]itk-assignuser   use mpm-itk AssignUserId (default: ".
                                    ($self->{itk_assignuser} ? 'yes' : 'no') .")\n".
            " --php-fpm {off|VER}    create PHP FPM configuration (default: $self->{php_fpm})\n".
            " --[no]adminurl         create site specific admin URL (default: ".
                                    ($self->{use_admin} ? 'yes' : 'no') .")\n".
            " --[no]ftpd             create proftpd user (default: ".
                                    ($self->{use_ftpd} ? 'yes' : 'no') .")\n".
            " --quota {off|SIZE}     quota size in MB (default: $self->{quota})\n";
    return $t;
}

for my $cfn (@{$self->{config_files}}) {
    read_config($cfn);
}

GetOptions(
    "owner|o=s" => \$self->{site_owner},
    "site|s=s" => \$self->{site},
    "domain|d=s@" => \$self->{domains},
    "mysqlrootpw=s" => \$self->{dbrootpw},
    "defcert" => \$self->{defcert},
    "no-dns-check" => \$self->{no_dnscheck},
    "itk-assignuser" => \$self->{itk_assignuser},
    "no-itk-assignuser" => sub { $self->{itk_assignuser} = 0; },
    "php-fpm=s" => \$self->{php_fpm},
    "adminurl" => \$self->{use_admin},
    "noadminurl" => sub { $self->{use_admin} = 0; },
    "ftpd" => \$self->{use_ftpd},
    "noftpd" => sub { $self->{use_ftpd} = 0; },
    "quota=s" => \$self->{quota},
    'help|h' => sub { print usage(); exit 0 }
    ) or die usage();

if (!$self->{site}) {
    die "no site name given\n". usage();
}
if ($self->{use_owner} and !$self->{site_owner}) {
    die "no site owner given\n";
}
if ($self->{site} !~ /^[a-z][a-z0-9]*$/) {
    die "site contains invalid characters: $self->{site}\n";
}
my $domain_text = join(' ', @{$self->{domains}});
$domain_text =~ s/(^\s*|\s*$)//g;
$self->{domains} = [split /\s+/, $domain_text];
if (scalar @{$self->{domains}} < 1) {
    die "no domain name given\n";
}

$self->{siteroot} = "/www/$self->{site}";
$self->{apachecf} = "/etc/apache2/sites-enabled/$self->{site}.conf";
$self->{user} = "www-$self->{site}";
$self->{group} = $self->{user};
$self->{dbname} = $self->{site};
$self->{dbuser} = $self->{site};
$self->{ftpuser} = $self->{site};
#
$self->{tlscrt} = "/etc/dehydrated/certs/$self->{domains}->[0]/fullchain.pem";
$self->{tlskey} = "/etc/dehydrated/certs/$self->{domains}->[0]/privkey.pem";

if ($self->{defcert}) {
    $self->{tlscrt} = "/etc/dehydrated/certs/$self->{defcert_name}/fullchain.pem";
    $self->{tlskey} = "/etc/dehydrated/certs/$self->{defcert_name}/privkey.pem";
}

if (!defined $self->{dbrootpw}) {
    if (!defined $self->{dbrootpw_file}) {
        die "no mysql root password, or password file given\n";
    }
    $self->{dbrootpw} = read_file($self->{dbrootpw_file}) =~ s/\s//gr;
}

check_exists;
if (!$self->{no_dnscheck}) {
    check_dns;
}
generate_random;

user;
filesystem;
siteroot_setup;
mysql;
if (!$self->{defcert} and !$self->{no_dnscheck}) {
    letsencrypt;
}
apache;
proftpd;
php_fpm;

my $domains = join(' ', @{$self->{domains}});
my $sitedata = 
    "Siteroot: $self->{siteroot}\n".
    "Domain: $domains\n".
    "MySQL db: $self->{dbname}\n".
    "MySQL user: $self->{dbuser}\n".
    "MySQL passwd: $self->{dbpasswd}\n";
if ($self->{use_admin}) {
    $sitedata .=
        "MySQL Adminer: https://$self->{domains}->[0]/.$self->{adminurl}/adminer.php\n";
}
if ($self->{use_ftpd}) {
    $sitedata .=
        "FTP: $self->{domains}->[0] (port 21, plain + TLS)\n".
        "FTP user: $self->{ftpuser}\n".
        "FTP passwd: $self->{ftppasswd}\n";
}

print "\n$sitedata";
umask(0066);
append_file($self->{logfile},
    "\n",
    scalar localtime time, "\n",
    $sitedata);

# vim: set tabstop=4 shiftwidth=4 expandtab smarttab:
