From 31c92a38cdbcccbb99ac97d7397100e37514e3ba Mon Sep 17 00:00:00 2001 From: Edi Septriyanto Date: Sat, 20 Apr 2024 15:14:08 +0700 Subject: [PATCH] Improve PureFTPd installer --- etc/default/pure-ftpd-common | 26 ++ etc/init.d/pure-ftpd | 7 +- etc/pure-ftpd/conf/AltLog | 1 + etc/pure-ftpd/conf/FSCharset | 1 + etc/pure-ftpd/conf/MinUID | 1 + etc/pure-ftpd/conf/NoAnonymous | 1 + etc/pure-ftpd/conf/PAMAuthentication | 1 + etc/pure-ftpd/conf/PureDB | 1 + etc/pure-ftpd/conf/TLSCipherSuite | 1 + etc/pure-ftpd/conf/UnixAuthentication | 1 + etc/pure-ftpd/pure-ftpd.conf | 60 +++ etc/pure-ftpd/pureftpd-dir-aliases | 10 + sbin/pure-ftpd-wrapper | 518 ++++++++++++++++++++++++++ sbin/pure-uploadscript | Bin 0 -> 22880 bytes 14 files changed, 627 insertions(+), 2 deletions(-) create mode 100644 etc/default/pure-ftpd-common create mode 100644 etc/pure-ftpd/conf/AltLog create mode 100644 etc/pure-ftpd/conf/FSCharset create mode 100644 etc/pure-ftpd/conf/MinUID create mode 100644 etc/pure-ftpd/conf/NoAnonymous create mode 100644 etc/pure-ftpd/conf/PAMAuthentication create mode 100644 etc/pure-ftpd/conf/PureDB create mode 100644 etc/pure-ftpd/conf/TLSCipherSuite create mode 100644 etc/pure-ftpd/conf/UnixAuthentication create mode 100644 etc/pure-ftpd/pure-ftpd.conf create mode 100644 etc/pure-ftpd/pureftpd-dir-aliases create mode 100755 sbin/pure-ftpd-wrapper create mode 100755 sbin/pure-uploadscript diff --git a/etc/default/pure-ftpd-common b/etc/default/pure-ftpd-common new file mode 100644 index 0000000..0781388 --- /dev/null +++ b/etc/default/pure-ftpd-common @@ -0,0 +1,26 @@ +# Configuration for pure-ftpd +# (this file is sourced by /bin/sh, edit accordingly) + +# STANDALONE_OR_INETD +# valid values are "standalone" and "inetd". +# Any change here overrides the setting in debconf. +STANDALONE_OR_INETD=standalone + +# VIRTUALCHROOT: +# whether to use binary with virtualchroot support +# valid values are "true" or "false" +# Any change here overrides the setting in debconf. +VIRTUALCHROOT=false + +# UPLOADSCRIPT: if this is set and the daemon is run in standalone mode, +# pure-uploadscript will also be run to spawn the program given below +# for handling uploads. see /usr/share/doc/pure-ftpd/README.gz or +# pure-uploadscript(8) + +# example: UPLOADSCRIPT=/usr/local/sbin/uploadhandler.pl +UPLOADSCRIPT= + +# if set, pure-uploadscript will spawn running as the +# given uid and gid +UPLOADUID= +UPLOADGID= diff --git a/etc/init.d/pure-ftpd b/etc/init.d/pure-ftpd index a47c3bc..1ff637e 100755 --- a/etc/init.d/pure-ftpd +++ b/etc/init.d/pure-ftpd @@ -60,7 +60,10 @@ test -x $WRAPPER || exit 0 set -e if [ ! -e `dirname $PIDFILE` ];then - mkdir `dirname $PIDFILE` + mkdir `dirname $PIDFILE` + + # label directory correctly on SE Linux systems (#980051) + [ -x /sbin/restorecon ] && /sbin/restorecon `dirname $PIDFILE` fi start_uploadscript() { @@ -82,7 +85,7 @@ case "$1" in start) test "$STANDALONE_OR_INETD" = standalone || exit 0 echo -n "Starting $DESC: " - --start $SSDAEMONLOGOPTS --pidfile "$PIDFILE" \ + start-stop-daemon --start $SSDAEMONLOGOPTS --pidfile "$PIDFILE" \ --exec $WRAPPER -- $SUFFIX start_uploadscript Starting ;; diff --git a/etc/pure-ftpd/conf/AltLog b/etc/pure-ftpd/conf/AltLog new file mode 100644 index 0000000..a13ed72 --- /dev/null +++ b/etc/pure-ftpd/conf/AltLog @@ -0,0 +1 @@ +clf:/var/log/pure-ftpd/transfer.log diff --git a/etc/pure-ftpd/conf/FSCharset b/etc/pure-ftpd/conf/FSCharset new file mode 100644 index 0000000..7edc66b --- /dev/null +++ b/etc/pure-ftpd/conf/FSCharset @@ -0,0 +1 @@ +UTF-8 diff --git a/etc/pure-ftpd/conf/MinUID b/etc/pure-ftpd/conf/MinUID new file mode 100644 index 0000000..83b33d2 --- /dev/null +++ b/etc/pure-ftpd/conf/MinUID @@ -0,0 +1 @@ +1000 diff --git a/etc/pure-ftpd/conf/NoAnonymous b/etc/pure-ftpd/conf/NoAnonymous new file mode 100644 index 0000000..7cfab5b --- /dev/null +++ b/etc/pure-ftpd/conf/NoAnonymous @@ -0,0 +1 @@ +yes diff --git a/etc/pure-ftpd/conf/PAMAuthentication b/etc/pure-ftpd/conf/PAMAuthentication new file mode 100644 index 0000000..7cfab5b --- /dev/null +++ b/etc/pure-ftpd/conf/PAMAuthentication @@ -0,0 +1 @@ +yes diff --git a/etc/pure-ftpd/conf/PureDB b/etc/pure-ftpd/conf/PureDB new file mode 100644 index 0000000..ee48061 --- /dev/null +++ b/etc/pure-ftpd/conf/PureDB @@ -0,0 +1 @@ +/etc/pure-ftpd/pureftpd.pdb diff --git a/etc/pure-ftpd/conf/TLSCipherSuite b/etc/pure-ftpd/conf/TLSCipherSuite new file mode 100644 index 0000000..14a30e4 --- /dev/null +++ b/etc/pure-ftpd/conf/TLSCipherSuite @@ -0,0 +1 @@ +HIGH diff --git a/etc/pure-ftpd/conf/UnixAuthentication b/etc/pure-ftpd/conf/UnixAuthentication new file mode 100644 index 0000000..7ecb56e --- /dev/null +++ b/etc/pure-ftpd/conf/UnixAuthentication @@ -0,0 +1 @@ +no diff --git a/etc/pure-ftpd/pure-ftpd.conf b/etc/pure-ftpd/pure-ftpd.conf new file mode 100644 index 0000000..76f7e4b --- /dev/null +++ b/etc/pure-ftpd/pure-ftpd.conf @@ -0,0 +1,60 @@ +ChrootEveryone yes +BrokenClientsCompatibility no +MaxClientsNumber 50 +Daemonize yes +MaxClientsPerIP 8 +VerboseLog no +DisplayDotFiles yes +AnonymousOnly no +NoAnonymous no +SyslogFacility ftp +DontResolve yes +MaxIdleTime 15 + +# MySQLConfigFile /etc/pureftpd-mysql.conf +# PureDB /etc/pureftpd.pdb +PureDB /etc/pure-ftpd/pureftpd.pdb + +# ExtAuth /var/run/ftpd.sock + +# PAMAuthentication yes +UnixAuthentication yes + +LimitRecursion 10000 8 +AnonymousCanCreateDirs no +MaxLoad 4 + +PassivePortRange 45000 45099 +ForcePassiveIP 52.221.186.193 + +# AntiWarez yes + +# Bind 127.0.0.1,21 + +Umask 133:022 +MinUID 100 +AllowUserFXP no +AllowAnonymousFXP no +ProhibitDotFilesWrite no +ProhibitDotFilesRead no +AutoRename no +AnonymousCantUpload no +# TrustedIP 10.1.1.1 + +# CreateHomeDir yes +# Quota 1000:10 + +# PIDFile /var/run/pure-ftpd.pid +PIDFile /var/run/pure-ftpd/pure-ftpd.pid + +# CallUploadScript yes + +MaxDiskUsage 90 +CustomerProof yes + +IPV4Only no + +TLS 2 +TLSCipherSuite HIGH:MEDIUM:+TLSv1:!SSLv2:!SSLv3 +CertFile /etc/ssl/certs/ssl-cert-snakeoil.pem + diff --git a/etc/pure-ftpd/pureftpd-dir-aliases b/etc/pure-ftpd/pureftpd-dir-aliases new file mode 100644 index 0000000..8f07195 --- /dev/null +++ b/etc/pure-ftpd/pureftpd-dir-aliases @@ -0,0 +1,10 @@ +# Configuration file for directory aliases +# +# To define alias/directory pairs, use alternating lines of alias +# and directory (optional blank lines are allowed) like that: +# +# pictures +# /usr/misc/pictures +# +# sources +# /usr/src diff --git a/sbin/pure-ftpd-wrapper b/sbin/pure-ftpd-wrapper new file mode 100755 index 0000000..21a0b96 --- /dev/null +++ b/sbin/pure-ftpd-wrapper @@ -0,0 +1,518 @@ +#! /usr/bin/perl +# +# Copyright 2002,2003,2004,2005,2007,2009,2010,2011,2014,2019 by Stefan Hornburg (Racke) +# +# 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 2 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, write to the Free +# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307 USA. + +use strict; +use warnings; + +use File::Basename; +use File::Spec; + +use Getopt::Long; + +# Process command line parameters +my $whandler = $SIG{__WARN__}; +$SIG{__WARN__} = sub {print STDERR "$0: @_";}; +my %opts; +unless (GetOptions (\%opts, 'show-options|s')) { + exit 1; +} +$SIG{__WARN__} = $whandler; + +my $daemon = '/usr/sbin/pure-ftpd'; +my @capabilities = @ARGV; + +if ($ARGV[0]) { + $daemon = "$daemon-$ARGV[0]"; +} + +# configuration schema +# +# fields of the array: +# 0. option name +# 1. parser +# 2. priority +# +# SysLogFacility has the highest priority, because we want to +# avoid to log to the wrong location (see pure-ftpd manpage). + +my %conf = ('AllowAnonymousFXP' => ['-W'], + 'AllowDotFiles' => ['-z'], + 'AllowUserFXP' => ['-w'], + 'AltLog' => ['-O %s', \&parse_string], + 'AnonymousBandwidth' => ['-t %s', \&parse_number_1_2], + 'AnonymousCanCreateDirs' => ['-M'], + 'AnonymousCantUpload' => ['-i'], + 'AnonymousOnly', => ['-e'], + 'AnonymousRatio' => ['-q %d:%d', \&parse_number_2], + 'AntiWarez' => ['-s'], + 'AutoRename' => ['-r'], + 'Bind' => ['-S %s', \&parse_string], + 'BrokenClientsCompatibility' => ['-b'], + 'CallUploadScript' => ['-o'], + 'ChrootEveryone' => ['-A'], + 'CreateHomeDir' => ['-j'], + 'CustomerProof' => ['-Z'], + 'Daemonize' => ['-B'], + 'DisplayDotFiles' => ['-D'], + 'DontResolve' => ['-H'], + 'ForcePassiveIP' => ['-P %s', \&parse_string], + 'FortunesFile' => ['-F %s', \&parse_filename], + 'FSCharset' => ['-8 %s', \&parse_skip], + 'ClientCharset' => ['-9 %s', \&parse_skip], + 'IPV4Only' => ['-4'], + 'IPV6Only' => ['-6'], + 'KeepAllFiles' => ['-K'], + 'LimitRecursion' => ['-L %d:%d', \&parse_number_2_unlimited], + 'LogPID' => ['-1'], + 'MaxClientsNumber' => ['-c %d', \&parse_number_1], + 'MaxClientsPerIP' => ['-C %d', \&parse_number_1], + 'MaxDiskUsage' => ['-k %d', \&parse_number_1], + 'MaxIdleTime' => ['-I %d', \&parse_number_1], + 'MaxLoad' => ['-m %d', \&parse_number_1], + 'MinUID' => ['-u %d', \&parse_number_1], + 'NATmode' => ['-N'], + 'NoAnonymous' => ['-E'], + 'NoChmod' => ['-R'], + 'NoRename' => ['-G'], + 'NoTruncate' => ['-0'], + 'PassivePortRange' => ['-p %d:%d', \&parse_number_2], + 'PerUserLimits' => ['-y %d:%d', \&parse_number_2], + 'ProhibitDotFilesRead' => ['-X'], + 'ProhibitDotFilesWrite' => ['-x'], + 'Quota' => ['-n %d:%d', \&parse_number_2], + 'SyslogFacility' => ['-f %s', \&parse_word, 99], + 'TLS' => ['-Y %d', \&parse_number_1], + 'TLSCipherSuite' => ['-J %s', \&parse_string], + 'TrustedGID' => ['-a %d', \&parse_number_1], + 'TrustedIP' => ['-V %s', \&parse_ip], + 'Umask' => ['-U %s:%s', \&parse_umask], + 'UserBandwidth' => ['-T %s', \&parse_number_1_2], + 'UserRatio' => ['-Q %d:%d', \&parse_number_2], + 'VerboseLog' => ['-d'], + ); + +my %authconf = ('ExtAuth' => ['extauth:%s', \&parse_sockname], + 'LDAPConfigFile' => ['ldap:%s', \&parse_filename, 0, + 'ldap'], + 'MySQLConfigFile' => ['mysql:%s', \&parse_filename, 0, + 'mysql'], + 'PGSQLConfigFile' => ['pgsql:%s', \&parse_filename, 0, + 'postgresql'], + 'PAMAuthentication' => ['pam'], + 'PureDB' => ['puredb:%s', \&parse_filename], + 'UnixAuthentication' => ['unix'], + ); + +# examine all configuration files in /etc/pure-ftpd/conf + +my @conffiles; + +opendir (ETCCONF, '/etc/pure-ftpd/conf') + || die "$0: Couldn't examine directory /etc/pure-ftpd/conf: $!\n"; +@conffiles = readdir (ETCCONF); +closedir (ETCCONF); + +# examine authentication files in /etc/pure-ftpd/auth + +my @authfiles; + +opendir (ETCAUTH, '/etc/pure-ftpd/auth') + || die "$0: Couldn't examine directory /etc/pure-ftpd/auth: $!\n"; +@authfiles = sort (grep {-l "/etc/pure-ftpd/auth/$_"} readdir (ETCAUTH)); +closedir (ETCAUTH); + +my ($file, $cref, $name); +my (@options, $option, $ret); + +for my $authname (@authfiles) { + # check if corresponding file exists + next unless $file = readlink("/etc/pure-ftpd/auth/$authname"); + unless (File::Spec->file_name_is_absolute($file)) { + $file = File::Spec->catfile('/etc/pure-ftpd/auth',$file); + } + next unless -f $file; + + # check if configuration directive exists + $name = basename($file); + + # check if we have the right capability for this authentication method + next if $authconf{$authname}->[3] && ! grep {$authconf{$authname}->[3] eq $_} @capabilities; + + if ($ret = parse_file(\%authconf, $file, $name)) { + $ret->[0] = "-l $ret->[0]"; + push (@options, $ret); + } +} + + +for (@conffiles) { + # simply skip files with non-word/non-numeric characters + next unless /^[A-Za-z][A-Za-z0-9]+$/; + + # skip authentication configuration files + next if exists $authconf{$_}; + + $file = "/etc/pure-ftpd/conf/$_"; + if ($ret = parse_file(\%conf, $file, $_)) { + push (@options, $ret); + } +} + +@options = map {split(/ /, $_->[0], 2)} (sort {$b->[1] <=> $a->[1]} @options); + +if (exists $ENV{STANDALONE_OR_INETD} && $ENV{STANDALONE_OR_INETD} eq 'standalone') { + push (@options, '-B'); + print "Running: $daemon ", join (' ', @options), "\n"; +} + +# force PID file to /var/run/pure-ftpd/pure-ftpd.pid +push(@options, '-g', '/var/run/pure-ftpd/pure-ftpd.pid'); + +if ($opts{'show-options'}) { + print join(' ', @options), "\n"; + exit 0; +} + +exec { $daemon } ($daemon, @options) or die "$0: Cannot exec $daemon: $!"; + +sub parse_file { + my ($cref, $file, $option) = @_; + my @lines; + + unless (exists $cref->{$option}) { + die "$0: Invalid configuration file $file: No corresponding directive\n"; + } + + open (FILE, $file) + || die "$0: Couldn't open configuration file $file: $!\n"; + while () { + next unless /\S/; + s/^\s+//; + s/\s+$//; + next if /^\#/; + push (@lines, $_); + } + close (FILE); + + # call parser + for my $line (@lines) { + my $buf = ''; + + if (defined $cref->{$option}->[1]) { + $ret = $cref->{$option}->[1]->(\$buf, $cref->{$option}->[0], $line); + } else { + $ret = parse_yesno(\$buf, $cref->{$option}->[0], $line); + } + + unless ($ret) { + die "$0: Invalid configuration file $file: $buf\n"; + } + + return [$buf, $cref->{$option}->[2] || 0] if length $buf; + } + +} + +sub parse_filename { + my ($buf, $fmt, $val) = @_; + + unless (-f $val) { + $$buf = qq{"$val": No such file}; + return; + } + $$buf = sprintf $fmt, $val; + return 1; +} + +sub parse_ip { + my ($buf, $fmt, $val) = @_; + + if ($val =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ + && $1 < 256 && $2 < 256 && $3 < 256 && $4 < 256) { + $$buf = sprintf $fmt, $val; + return 1; + } + + $$buf = qq{"$val": Invalid IP address}; +} + +sub parse_number_1 { + my ($buf, $fmt, $val) = @_; + + if ($val =~ /\D/) { + $$buf = qq{"$val" not a number}; + return; + } + + $$buf = sprintf $fmt, $val; + return 1; +} + +sub parse_number_1_2 { + my ($buf, $fmt, $val) = @_; + + if ($val =~ /^(\d+)(\s+|:)(\d+)$/) { + $$buf = sprintf $fmt, "$1:$3"; + return 1; + } + + if ($val =~ /^(:?\d+)$/ || $val =~ /^(\d+:)/) { + $$buf = sprintf $fmt, $1; + return 1; + } + + $$buf = qq{"$val" not one or two numbers}; + return; +} + +sub parse_number_2 { + my ($buf, $fmt, $val) = @_; + + if ($val !~ /^(\d+)\s+(\d+)$/) { + $$buf = qq{"$val" not two numbers}; + return; + } + + $$buf = sprintf $fmt, $1, $2; + return 1; +} + +sub parse_number_2_unlimited { + my ($buf, $fmt, $val) = @_; + + if ($val !~ /^(\-?\d+)\s+(\-?\d+)$/) { + $$buf = qq{"$val" not two numbers}; + return; + } + + if ($1 < -1 || $2 < -1) { + $$buf = qq{"$val" smaller than unlimited (-1)}; + return; + } + + $$buf = sprintf $fmt, $1, $2; + return 1; +} + +sub parse_sockname { + my ($buf, $fmt, $val) = @_; + + unless (-S $val) { + $$buf = qq{"$val": No such socket}; + return; + } + $$buf = sprintf $fmt, $val; + return 1; +} + +sub parse_skip { + return 1; +} + +sub parse_string { + my ($buf, $fmt, $val) = @_; + + if ($val =~ /\s/) { + $$buf = qq{"$val" contains whitespace}; + return; + } + + $$buf = sprintf $fmt, $val; + return 1; +} + +sub parse_umask { + my ($buf, $fmt, $val) = @_; + + if ($val !~ /^([0-7]{3,3})\s+([0-7]{3,3})$/) { + $$buf = qq{"$val" not two octal numbers}; + return; + } + + $$buf = sprintf $fmt, $1, $2; + return 1; +} + +sub parse_word { + my ($buf, $fmt, $val) = @_; + + if ($val !~ /^(\w+)$/) { + $$buf = qq{"$val" contains non-word characters}; + return; + } + + $$buf = sprintf $fmt, $1; + return 1; +} + +sub parse_yesno { + my ($buf, $fmt, $val) = @_; + my @y = ('yes', 1, 'on'); + my @n = ('no', 0, 'off'); + + if (grep {$_ eq lc($val)} @y) { + # result is 'yes' + $$buf = $fmt; + return 1; + } + if (grep {$_ eq lc($val)} @n) { + # result is 'no' + $$buf = ''; + return 1; + } + # error + $$buf = qq{"$val" not convertible to true or false}; + return; +} + +__END__ + +=head1 NAME + +pure-ftpd-wrapper - configures and starts Pure-FTPd daemon + +=head1 SYNOPSIS + +pure-ftpd-wrapper + +=head1 DESCRIPTION + +B reads the configuration for the Pure-FTPd daemon +from files in the directory F. Each file in this +directory is related to a command line option. No more than one +line with configuration values is allowed. Empty lines or lines +starting with the comment character C<#> are discarded. + +The Pure-FTPd daemon allows one to use different authentication methods +together. The authentication methods are tried in the order they are +specified on the command line. In order to achieve the same flexibility +with files in the F directory, B +checks all valid symbolic links within the directory F +in alphabetical order. E.g., a link in this directory pointing to +F would enable authentication against +a PureDB database. + +There are no means to configure the I setting, it is hardwired +to /var/run/pure-ftpd/pure-ftpd.pid in this script. + +You can display the Pure-FTPd commandline options with C<-s> or +C<--show-options>: + + pure-ftpd-wrapper --show-options + +=head1 CONFIGURATION + +=head2 Boolean values + +The strings C,C<1>,C enable the corresponding commandline option +(case doesn't matter). To disable the option use C,C<0> or C. + +Configuration files containing boolean values are C, +C, C, C, +C, C, C, C, +C, C, C, +C, C, C, C, +C, C, C, C, C, +C, C, C, C, C, +C, C, C, +C and C. + +=head2 Numerical values + +There are several types of numerical values (one number, two numbers, +one or two numbers, two octal numbers). + +=over 4 + +=item One number + +C, C, C, C, +C, C, C, C. + +=item Two numbers + +C, C, C, +C, C, C. + +=item Two numbers (with unlimited value) + +This allows -1 in addition to positive numbers indicating an unlimited +values. + +C. + +=item One or two numbers + +C, C. + +=item Two octal numbers + +C. + +=back + +=head2 String values + +=over + +=item Arbritrary strings + +C, C, C. + +=item Words + +C. + +=back + +=head2 Character Sets + +C, C. + +These options were removed from PureFTPd in release 1.0.48 and will be ignored by the wrapper. + +=head2 IP values + +C. + +=head2 File values + +These values designate an existing file or socket. + +=over + +=item File + +C, C, C, C, C. + +=item Socket + +C. + +=back + +=head1 AUTHOR + +This manual page was written by Stefan Hornburg (Racke) +for the Debian GNU/Linux system. + + + + + diff --git a/sbin/pure-uploadscript b/sbin/pure-uploadscript new file mode 100755 index 0000000000000000000000000000000000000000..22c2ecab1ac02afb5fc1be90e6552c7ef1cb6894 GIT binary patch literal 22880 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 E0D^rqwEzGB literal 0 HcmV?d00001