From decf8e09a61259c963e8535cac303a17745dfbef Mon Sep 17 00:00:00 2001 From: ROTTLER Tamas Date: Thu, 26 Aug 2021 00:34:48 +0200 Subject: [PATCH] thrid --- Makefile | 6 + www-create-site | 329 ++++++++++++++++++++++++++++++++++++++++++++++++ www-delete-site | 17 +++ www-phpfpm | 216 +++++++++++++++++++++++++++++++ www-reset-acl | 45 +++++++ 5 files changed, 613 insertions(+) create mode 100644 Makefile create mode 100755 www-create-site create mode 100755 www-delete-site create mode 100755 www-phpfpm create mode 100755 www-reset-acl diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..638b8ee --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +all: +install: + ln -srf www-create-site /usr/local/sbin + ln -srf www-delete-site /usr/local/sbin + ln -srf www-reset-acl /usr/local/sbin + ln -srf www-phpfpm /usr/local/sbin diff --git a/www-create-site b/www-create-site new file mode 100755 index 0000000..f296f70 --- /dev/null +++ b/www-create-site @@ -0,0 +1,329 @@ +#!/usr/bin/perl +# 2021-08-02 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 = ; + 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 <{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 = < + 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] + + +$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 + {siteroot}/public> + Options -Indexes -FollowSymlinks +SymLinksIfOwnerMatch + AllowOverride all + Require all granted + + Header always set Strict-Transport-Security "max-age=31536000;" + #-[[-www-phpfpm + #-]]-www-phpfpm + +# 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 = <{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 = <{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: diff --git a/www-delete-site b/www-delete-site new file mode 100755 index 0000000..41fa34b --- /dev/null +++ b/www-delete-site @@ -0,0 +1,17 @@ +#!/usr/bin/perl +use strict; use warnings; use utf8; +my $site = $ARGV[0] || 'SITE'; +$site =~ s/\/$//; +print "No site given. using SITE as placeholder\n" if $site eq 'SITE'; +print < Do this: +rm /etc/apache2/sites-enabled/$site.conf +apachectl graceful +www-phpfpm -s $site -d +deluser www-$site +echo 'DROP DATABASE $site;' | mysql -u root -p`cat /etc/mysql/jelszo` +echo 'DROP USER $site\@localhost;' | mysql -u root -p`cat /etc/mysql/jelszo` +rm -r /www/$site +- letsencrypt domains.txt-bol kivenni, revoke certificate +EOT +# vim: set tabstop=4 shiftwidth=4 expandtab smarttab: diff --git a/www-phpfpm b/www-phpfpm new file mode 100755 index 0000000..3af5db8 --- /dev/null +++ b/www-phpfpm @@ -0,0 +1,216 @@ +#!/usr/bin/perl +# 2021-08-02 +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 = ; + exit 1 if $ans !~ /^y/i; + } + return $ex; +} + +sub fpmconf { + my $dir = "$self->{phpconfdir}/fpm/pool.d"; + my $fn = "$dir/$self->{site}.conf"; + if (!-d $dir) { + die "error: directory `$dir' doesn't exist\n"; + } + if (-e $fn and !$self->{force_overwrite}) { + print "$fn already exists; use --force-overwrite to overwrite\n"; + return; + } + my $conf = <{site}] +user = $self->{user} +group = $self->{group} +listen = /run/php/php$self->{php}-fpm-$self->{site}.sock +listen.owner = www-data +listen.group = www-data +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 + +php_admin_value[open_basedir] = $self->{siteroot} +php_admin_value[sys_temp_dir] = $self->{siteroot}/tmp +php_admin_value[upload_tmp_dir] = $self->{siteroot}/tmp +php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,dl,setenv +EOT + + write_file($fn, $conf); + print "$fn written, reloading fpm service.\n"; + sys "systemctl reload php$self->{php}-fpm.service"; +} + +sub fpmconf_find { + my %conf; + for my $ver (glob("$self->{phpconfbase}/[0-9]*")) { + $ver =~ s/^.*\///; + next if $ver !~ /^\d[\d.]+$/; + my $fn = "$self->{phpconfbase}/$ver/fpm/pool.d/$self->{site}.conf"; + if (-e $fn) { + $conf{$ver} = $fn; + } + } + return %conf; +} + +sub fpmconf_list { + my %conf = fpmconf_find; + for my $ver (sort keys %conf) { + print "$ver -> $conf{$ver}\n"; + } +} + +sub fpmconf_remove_other { + my %conf = fpmconf_find; + for my $ver (sort keys %conf) { + next if $ver eq $self->{php}; + if ($self->{keep_other}) { + print "keeping php $ver pool conf: $conf{$ver}\n"; + next; + } + if (!unlink($conf{$ver})) { + print "error: could not delete $conf{$ver}\n"; + next; + } + print "$conf{$ver} removed, reloading php$ver-fpm\n"; + sys "systemctl reload php$ver-fpm.service"; + } +} + +sub apacheconf { + my @oldconf = read_file($self->{apachecf}); + my @newconf; + + my @phpblock = ( + qq{}, + qq{ SetHandler "proxy:unix:/run/php/php$self->{php}-fpm-$self->{site}.sock|fcgi://localhost"}, + qq{} + ); + + my $found = 0; + for (my $i = 0; $i <= $#oldconf; $i++) { + my $line = $oldconf[$i]; + push @newconf, $line; + if ($line !~ /^(\s*)#-\[\[-www-phpfpm/) { + next; + } + my $indent = $1; + while ($oldconf[$i] !~ /^\s*#-]]-www-phpfpm/) { + $i++; + if ($i >= $#oldconf) { + die "error: block without end found in: $self->{apachecf}\n"; + } + } + for my $bl (@phpblock) { + push @newconf, "$indent$bl\n"; + } + push @newconf, $oldconf[$i]; + $found = 1; + } + if (!$found) { + die "no #-[[-www-phpfpm block found in: $self->{apachecf}\n"; + } + write_file($self->{apachecf}, @newconf); + print "$self->{apachecf} written, reloading apache\n"; + sys "systemctl reload apache2.service"; +} + +$self = { + site => "", + php => undef, + phpconfbase => '/etc/php', + phpconfdir => undef, + force_overwrite => 0, + keep_other => 0, + list => 0, + delete => 0, + }; +my $usage = < \$self->{site}, + "php|p=s" => \$self->{php}, + "force-overwrite|f" => \$self->{force_overwrite}, + "keep-other|k" => \$self->{keep_other}, + "list|l" => \$self->{list}, + "delete|d" => \$self->{delete}, + '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"; +} + +if ($self->{list}) { + fpmconf_list; + exit; +} +if ($self->{delete}) { + if ($self->{keep_other} or $self->{force_overwrite} or $self->{list} or $self->{php}) { + die "--delete incompatible with other options\n"; + } + $self->{php} = '..invalid..'; + fpmconf_remove_other; + exit; +} + +if (!$self->{php}) { + die "no PHP version given\n$usage"; +} +if ($self->{php} !~ /^\d[\d\.]+$/) { + die "invalid PHP version given: $self->{php}\n$usage"; +} +$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->{phpconfdir} = "$self->{phpconfbase}/$self->{php}"; + +if (!-d $self->{siteroot}) { + die "site doesn't exist: directory `$self->{siteroot}' not found\n"; +} +if (!-d $self->{phpconfdir}) { + die "php configuration directory doesn't exist: $self->{phpconfdir}\n"; +} + +fpmconf; +apacheconf; +fpmconf_remove_other; + +# vim: set tabstop=4 shiftwidth=4 expandtab smarttab: diff --git a/www-reset-acl b/www-reset-acl new file mode 100755 index 0000000..0f96d26 --- /dev/null +++ b/www-reset-acl @@ -0,0 +1,45 @@ +#!/bin/bash +# 2021-08-02 www-data r-x +d=$1 +if [ -z "$d" ]; then + echo "usage: $0 " + exit 1 +fi +d=$(echo $d | sed -e 's/\/$//') + +cd /www +if [ ! -d $d ]; then + echo "directory does not exist: $d" + exit 1 +fi + +user="www-$d" +id $user >/dev/null 2>&1 +if [ $? -gt 0 ]; then + echo "corresponding web user does not exist: $user" + exit 1 +fi + +chown -R $user:$user $d + +setfacl -R -b -k $d +setfacl -R -d -m m:rwx $d +setfacl -R -d -m u::rwx $d +setfacl -R -d -m g::--- $d +setfacl -R -d -m o::--- $d +setfacl -R -d -m u:$user:rwx $d +setfacl -R -d -m g:wwwadmin:rwx $d +setfacl -R -d -m g:wwwsftp:rwx $d +setfacl -R -d -m g:www-data:r-x $d +# +setfacl -R -m m:rw- $d +setfacl -R -m u::rwx $d +setfacl -R -m g::--- $d +setfacl -R -m o::--- $d +setfacl -R -m u:$user:rwx $d +setfacl -R -m g:wwwadmin:rwx $d +setfacl -R -m g:wwwsftp:rwx $d +setfacl -R -m g:www-data:r-x $d +find $d -type d -print0 | xargs -0 setfacl -m m:rwx + +# vim: set tabstop=4 shiftwidth=4 expandtab smarttab: