www-admtools/www-create-site
2021-08-26 00:34:48 +02:00

330 lines
9.1 KiB
Perl
Executable File

#!/usr/bin/perl
# 2021-08-02 <tom@bitfit.hu> php-fpm + sok egyeb fejlesztes
use strict; use warnings; use utf8;
use Getopt::Long;
use File::Slurp;
use Data::Dumper;
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)) {
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}";
}
sub user {
sys "adduser --disabled-password --home $self->{siteroot} --no-create-home --firstuid 10000 --gecos '' --shell /bin/false $self->{user}";
}
sub siteroot_setup {
for my $d (qw(public tmp)) {
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");
}
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 $alias = '';
for (my $i = 1; $i < scalar @{$self->{domains}}; $i++) {
$alias .= " ServerAlias $self->{domains}->[$i]\n";
}
chomp $alias;
my $itk = '';
if ($self->{itk_assignuser}) {
$itk = " AssignUserId $self->{user} $self->{group}\n";
}
my $lt = scalar localtime time;
my $cf = <<EOT;
# generated: $lt
<VirtualHost *:80>
ServerName $self->{domains}->[0]
$alias
ErrorLog \${APACHE_LOG_DIR}/$self->{site}.notls.err.log
CustomLog \${APACHE_LOG_DIR}/$self->{site}.notls.log detailed
RewriteEngine On
RewriteRule ^/(.*) https://%{HTTP_HOST}/\$1 [R,L]
</VirtualHost>
<VirtualHost *:443>
$itk SSLEngine on
SSLCertificateFile $self->{tlscrt}
SSLCertificateKeyFile $self->{tlskey}
SSLUseStapling on
SSLStaplingReturnResponderErrors off
SSLStaplingFakeTryLater off
SSLStaplingStandardCacheTimeout 86400
SSLStaplingResponderTimeout 2
ServerName $self->{domains}->[0]
$alias
ErrorLog \${APACHE_LOG_DIR}/$self->{site}.err.log
CustomLog \${APACHE_LOG_DIR}/$self->{site}.log detailed
DocumentRoot $self->{siteroot}/public
<Directory $self->{siteroot}/public>
Options -Indexes -FollowSymlinks +SymLinksIfOwnerMatch
AllowOverride all
Require all granted
</Directory>
Header always set Strict-Transport-Security "max-age=31536000;"
#-[[-www-phpfpm
#-]]-www-phpfpm
</VirtualHost>
# vim: set tabstop=4 shiftwidth=4 expandtab smarttab:
EOT
write_file($self->{apachecf}, $cf);
sys "apachectl graceful";
}
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};
}
$self = {
site => "",
domains => [],
dbpasswd => "",
dbrootpw => read_file('/etc/mysql/jelszo') =~ s/\s//gr,
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 => '7.4',
logfile => '/var/log/www-create-site.log',
};
my $usage = <<EOT;
usage: $0 OPTIONS..
-s|--site NAME new website's short name ([a-z][a-z0-9]*)
-d|--domain 'D [D..]' list of domain names for website
--mysqlrootpw PASSWD mysql root password (default is set)
--defcert use server's own certificate (skip letsencrypt)
--no-dns-check skip dns check (with implicit --defcert)
--itk-assignuser use mpm-itk AssignUserId (default: $self->{itk_assignuser})
--php-fpm VER create PHP FPM configuration (default VER: $self->{php_fpm})
EOT
GetOptions(
"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},
"php-fpm=s" => \$self->{php_fpm},
'help|h' => sub { print $usage; exit 0 }
) or die $usage;
if (!$self->{site}) {
die "no site name given\n$usage";
}
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->{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";
}
check_exists;
if (!$self->{no_dnscheck}) {
check_dns;
}
generate_random;
user;
filesystem;
siteroot_setup;
mysql;
if (!$self->{defcert} and !$self->{no_dnscheck}) {
letsencrypt;
}
apache;
php_fpm;
my $domains = join(' ', @{$self->{domains}});
my $sitedata = <<EOT;
Siteroot: $self->{siteroot}
Domain: $domains
MySQL db: $self->{dbname}
MySQL user: $self->{dbuser}
MySQL passwd: $self->{dbpasswd}
EOT
print "\n$sitedata";
umask(0066);
append_file($self->{logfile},
"\n",
scalar localtime time, "\n",
$sitedata);
# vim: set tabstop=4 shiftwidth=4 expandtab smarttab: