diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d0fb30 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.local.json diff --git a/config.dist.json b/config.dist.json new file mode 100644 index 0000000..66e4288 --- /dev/null +++ b/config.dist.json @@ -0,0 +1,18 @@ +# www-admtools default configuration +# you should override settings in `config.local.json' + +#"dbrootpw": "XXX", +#"dbrootpw_file": "/etc/mysql/XXX", +"logfile": "/var/log/www-create-site.log", + +"use_owner": 0, +"use_site_log": 0, +"quota": "off", +"use_admin": 0, +"use_ftpd": 0, + +"use_ocsp_stapling": 1, +"itk_assignuser": 0, +"php_fpm": "off", + +# vim: set ft=config tabstop=2 shiftwidth=2 expandtab smarttab: diff --git a/crontab.example b/crontab.example new file mode 100644 index 0000000..72f38b7 --- /dev/null +++ b/crontab.example @@ -0,0 +1,4 @@ +# www-admtools + +0-59/5 * * * * root /opt/www-admtools/www-update-diskusage + diff --git a/www-create-site b/www-create-site index f296f70..e2b7d58 100755 --- a/www-create-site +++ b/www-create-site @@ -1,9 +1,9 @@ #!/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; +use JSON; +use FindBin; my $self; sub sys { @@ -74,7 +74,7 @@ sub check_dns { } sub generate_random { - for my $x (qw(dbpasswd)) { + for my $x (qw(dbpasswd ftppasswd adminurl)) { my $passwd = qx(pwgen 12 1); chomp $passwd; if (length($passwd) != 12) { @@ -87,6 +87,10 @@ sub generate_random { sub filesystem { sys "mkdir $self->{siteroot}"; + if ($self->{quota} > 0) { + my $kb = $self->{quota} * 1024; + sys "setquota $self->{user} $kb $kb 0 0 /www"; + } } sub user { @@ -94,7 +98,11 @@ sub user { } sub siteroot_setup { - for my $d (qw(public tmp)) { + 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"; @@ -110,6 +118,10 @@ sub siteroot_setup { 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}"; } @@ -164,55 +176,77 @@ sub letsencrypt { } 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); + 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 .= "\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". + " RewriteRule ^/(.*) https://%{HTTP_HOST}/\$1 [R,L]\n". + "\n". + "\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"; + if ($self->{use_ocsp_stapling}) { + $c.=" SSLUseStapling on\n". + " SSLStaplingReturnResponderErrors off\n". + " SSLStaplingFakeTryLater off\n". + " SSLStaplingStandardCacheTimeout 86400\n". + " SSLStaplingResponderTimeout 2\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". + " {siteroot}/public>\n". + " Options -Indexes -FollowSymlinks +SymLinksIfOwnerMatch\n". + " AllowOverride all\n". + " Require all granted\n". + " \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 .= "\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}"; @@ -229,44 +263,104 @@ sub find_defcert { 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 = { - site => "", + 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 => read_file('/etc/mysql/jelszo') =~ s/\s//gr, + 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 => '7.4', + use_ocsp_stapling => 1, + php_fpm => 'off', + quota => 'off', logfile => '/var/log/www-create-site.log', }; -my $usage = <{itk_assignuser}) - --php-fpm VER create PHP FPM configuration (default VER: $self->{php_fpm}) -EOT + +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]ocsp-stapling set SSLUseStapling on (default: ". + ($self->{use_ocsp_stapling} ? 'yes' : 'no') .")\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; }, + "ocsp-stapling" => \$self->{use_ocsp_stapling}, + "no-ocsp-stapling" => sub { $self->{use_ocsp_stapling} = 0; }, "php-fpm=s" => \$self->{php_fpm}, - 'help|h' => sub { print $usage; exit 0 } - ) or die $usage; + "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"; + 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"; @@ -284,6 +378,7 @@ $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"; @@ -293,6 +388,12 @@ if ($self->{defcert}) { $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}) { @@ -308,16 +409,26 @@ if (!$self->{defcert} and !$self->{no_dnscheck}) { letsencrypt; } apache; +proftpd; 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 +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); diff --git a/www-delete-site b/www-delete-site index 41fa34b..b1716cd 100755 --- a/www-delete-site +++ b/www-delete-site @@ -9,9 +9,10 @@ rm /etc/apache2/sites-enabled/$site.conf apachectl graceful www-phpfpm -s $site -d deluser www-$site +sed -i -e '/^$site:/d' /etc/proftpd/ftpd.passwd 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 +-> remove from letsencrypt domains.txt, revoke certificate EOT # vim: set tabstop=4 shiftwidth=4 expandtab smarttab: diff --git a/www-phpfpm b/www-phpfpm index 3af5db8..8001202 100755 --- a/www-phpfpm +++ b/www-phpfpm @@ -1,5 +1,4 @@ #!/usr/bin/perl -# 2021-08-02 use strict; use warnings; use utf8; use Getopt::Long; use File::Slurp; diff --git a/www-reset-acl b/www-reset-acl index 0f96d26..95ee5b9 100755 --- a/www-reset-acl +++ b/www-reset-acl @@ -1,5 +1,4 @@ #!/bin/bash -# 2021-08-02 www-data r-x d=$1 if [ -z "$d" ]; then echo "usage: $0 " diff --git a/www-update-diskusage b/www-update-diskusage new file mode 100755 index 0000000..d801346 --- /dev/null +++ b/www-update-diskusage @@ -0,0 +1,21 @@ +#!/bin/bash +if ! mountpoint -q /www; then + echo "/www not a mountpoint" >&2 + exit 1 +fi +cd /www + +for d in *; do + if [[ $d == lost+found ]]; then continue; fi + if [[ $d == aquota.user ]]; then continue; fi + if [[ $d == _admin ]]; then continue; fi + if mountpoint -q $d; then + (date; df -mP $d) > $d/disk_usage.txt + else + Q=$(quota -u www-$d 2>/dev/null) + if [[ $? == 0 ]]; then + (date; echo "$Q") > $d/disk_usage.txt + fi + fi +done +# vim: set tabstop=4 shiftwidth=4 expandtab smarttab: