diff --git a/README.md b/README.md index a4373e1..1201eb1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,50 @@ pureftpd-auth ============= -ortiche pure_authd authenticator script written in perl \ No newline at end of file +Ortiche pure_authd authenticator script written in perl. Based on ziokernel code. + +Dependencies +--- + +apt-get install pure-ftpd \ + libcrypt-eksblowfish-perl \ + libclass-mix-perl \ + libparams-classify-perl \ + liblog-log4perl-perl \ + dh-make-perl + +dh-make-perl --build --cpan Crypt::Passwd::XS +dpkg -i libcrypt-passwd-xs-perl_0.601-1_amd64.deb + +Config +--- + +echo "/usr/local/ortiche/pureftpd-auth.pl" > /etc/pure-ftpd/conf/ExtAuth +echo "yes" > /etc/pure-ftpd/conf/CreateHomeDir +rm -fr /etc/pure-ftpd/auth/ +ln -s /etc/pure-ftpd/conf/ExtAuth /etc/pure-ftpd/auth/ExtAuth + +Auth +--- +mkdir /usr/local/ortiche/auth +vi /usr/local/ortiche/auth/pureftpd.pl + + +#!/usr/bin/perl + +$DB_DB="ftp_db"; +$DB_HOST="localhost"; +$DB_USER="ftp_user_ro"; +$DB_PASS="your_password"; +$DB_TABLE="ftp_table"; + + +Running +--- +/usr/sbin/pure-ftpd -d -A -B -4 -Z -p 50000:50601 -H -Y 1 -E -k 98 -C 5 -c 30 -lextauth:/var/run/pure-ftpd/extauth.sock + +/usr/sbin/pure-authd -s /var/run/pure-ftpd/extauth.sock -r /usr/local/ortiche/pureftpd-auth.pl & + + + + diff --git a/pureftpd-auth.pl b/pureftpd-auth.pl new file mode 100755 index 0000000..fede40e --- /dev/null +++ b/pureftpd-auth.pl @@ -0,0 +1,197 @@ +#!/usr/bin/perl + +### v.2.0.2 by zio 06/4/2012 +### v.3.0.0 by gine 11/10/2015 +### v.3.0.1 by gine 22/10/2015 + + +### uses +# +use DBI; +#use Crypt::Random; +#use Crypt::Passwd::XS qw(unix_sha256_crypt); +#use Crypt::Passwd::XS qw(unix_md5_crypt); +use Crypt::Passwd::XS; ### The blowfish crypt() scheme is currently unsupported by Crypt::Passwd::XS::crypt() , +use Crypt::Eksblowfish::Bcrypt qw(bcrypt); ### so additional Crypt::Eksblowfish::Bcrypt::bcrypt() is being used +use Digest::SHA qw(sha256_hex); +use Log::Log4perl qw(get_logger :levels); + +### conf +# +$SCRIPT_DIR="/usr/local/ortiche"; +$AUTH_DIR="$SCRIPT_DIR/auth"; +require "$AUTH_DIR/pureftpd.pl"; + +$AUTH_LOG="/var/log/pure-ftpd/auth.log"; ### log kept for debug, to remove in future because of privacy !!! + +$SHA256_DEFAULT_ROUNDS = 5000; + +### variables read from environment ( pure-authd ) +# +$REQ_USER=$ENV{AUTHD_ACCOUNT}; ### TODO: sanitize input ? +$REQ_PASS=$ENV{AUTHD_PASSWORD}; # +$SERVER_IP=$ENV{AUTHD_LOCAL_IP}; +#AUTHD_LOCAL_PORT +#AUTHD_REMOTE_IP +#AUTHD_ENCRYPTED + +@numbers = split(/\./, $SERVER_IP); +$ip_number = pack("C4", @numbers); +($server_name) = (gethostbyaddr($ip_number, 2))[0]; +if (not $server_name) +{ + $server_name=$SERVER_IP; +} + +### variables shared with subs +# +my $DB_conn; +my $auth_logger; + +### always returns with sub auth_return() ... +# +sub auth_return +{ + my ( $RET_AUTH, $RET_DIR ) = @_; + + $auth_logger->info("auth_ok:", $RET_AUTH, " , reason: auth terminated, user: ", $REQ_USER); + + if ( $RET_AUTH == 1 ) ### add extra checks on fields... + { + print "auth_ok:1\n"; +# print "uid:", $RET_UID, "\n"; #### TODO: fix DB and user creation scripts instead!!! +# print "gid:", $RET_GID, "\n"; ## + print "uid:33\n"; ## + print "gid:33\n"; ## + print "dir:", $RET_DIR, "\n"; + print "end\n"; + exit 0; + } + else + { + print "auth_ok:", $RET_AUTH, "\n"; + print "end\n"; + exit 1; + } +} + + +### logging subs +# +sub auth_log_init +{ +# log4perl levels: +# DEBUG INFO WARN ERROR FATAL + Log::Log4perl->easy_init({ + file => ">> $AUTH_LOG", + level => $INFO, + layout => "[3.0.1] [%d{yy/MM/dd-HH:mm}] [%p] (%F{2} line %L) %m%n", + },{ + file => "STDERR", + level => $DEBUG, + layout => "[3.0.1] [%d{yy/MM/dd-HH:mm}] [%p] (%F{2} line %L) %m%n", + }); + $auth_logger = Log::Log4perl->get_logger(); +} + + +### digests, salts and crypt() subs +# +sub check_crypt { + my ( $plain_password, $hashed_password ) = @_; + my $encrypted; + if ( $hashed_password =~ /(\$2a\$[0-9]{2}\$[A-Za-z0-9.\/]{22})/ ) { + $encrypted = Crypt::Eksblowfish::Bcrypt::bcrypt($plain_password, $hashed_password); ### Blowfish scheme + } else { + $encrypted = Crypt::Passwd::XS::crypt($plain_password, $hashed_password); ### MD5, SHA256 schemes + } + + return ( $encrypted eq $hashed_password ); +} + +sub sha256_salt { +# my $salt = en_base64(Crypt::Random::makerandom_octet(Length=>16)); + my @letters = ('A' .. 'Z', 'a' .. 'z', '0' .. '9', '/', '.'); + my $salt; + for (1 .. 16) { $salt .= $letters[rand@letters]; } + return '$5$'.'rounds='.$SHA256_DEFAULT_ROUNDS.'$'.$salt; +} + +#sub migra_update_pw { +# my ( $req_user, $req_pass ) = @_; +# my $p_sha256 = sha256_hex($req_pass); +# my $p_sha256_crypt_salt = Crypt::Passwd::XS::crypt($req_pass, sha256_salt()); +# my $UPD_QUERY = "UPDATE $DB_TABLE SET password = NULL, password_sha256 = '$p_sha256', password_crypt = '$p_sha256_crypt_salt' WHERE username = '$req_user'"; +# $auth_logger->info("updated password for hrd style: ".$REQ_USER); +# return $DB_conn->do($UPD_QUERY); +#} + +#deprecated +sub migra_pw { + my ( $req_user, $req_pass ) = @_; + my $p_sha256_crypt_salt = Crypt::Passwd::XS::crypt($req_pass, sha256_salt()); + $auth_logger->info("going to run password migration, user: ".$REQ_USER); + my $UPD_QUERY = "UPDATE $DB_TABLE SET password = NULL, password_crypt = '$p_sha256_crypt_salt' WHERE username = '$req_user'"; + return $DB_conn->do($UPD_QUERY); +} + +sub update_hrd_pw { + my ( $req_user, $req_pass ) = @_; + my $p_sha256 = sha256_hex($req_pass); + my $UPD_QUERY = "UPDATE $DB_TABLE SET password_sha256 = '$p_sha256' WHERE username = '$req_user'"; + $auth_logger->info("updated password for hrd style, user: ".$REQ_USER); + return $DB_conn->do($UPD_QUERY); +} + +### end of subs + +### begins setting up log +# +auth_log_init(); +$auth_logger->info("reason: auth begun , user: ", $REQ_USER); +$server_name='web00.ortiche.net'; +#$auth_logger->debug("server ip: '$SERVER_IP'\n"); +#$auth_logger->debug("server name: '$server_name'\n"); + +if ( $REQ_USER eq "" ) +{ + $auth_logger->fatal("reason: empty user"); + auth_return(-1, ""); +} + +$DB_conn = DBI->connect("DBI:mysql:database=$DB_DB;host=$DB_HOST", $DB_USER, $DB_PASS) + || $auth_logger->fatal("fatal: connect() to DB") + && auth_return(-1, ""); + +### runs query and checks +# +my $QUERY_pureftpd = $DB_conn->prepare("SELECT path, password_sha256, password_crypt FROM $DB_TABLE JOIN directories USING (dir_id) JOIN hosts_urls USING (url_id) JOIN hosts USING (host_id) WHERE username='$REQ_USER' AND ftp = 'Y' AND hostname = '$server_name'") + || $auth_logger->fatal("prepare() query, reason: no user? no ftp=Y? wrong \$server_name ?, user: ".$REQ_USER) + && auth_return(-1, ""); + +$QUERY_pureftpd->execute() + || $auth_logger->fatal("execute() query, reason: no user? no ftp=Y? wrong \$server_name ?, user: ".$REQ_USER) + && auth_return(-1, ""); + +my ( $READ_path, $READ_hrd_sha256, $READ_new_crypt ) = $QUERY_pureftpd->fetchrow_array(); + +### TODO: place here separated checks on ftp = 'Y' AND hostname = '$server_name' ; remove clause from above SELECT +### + +if ( $READ_path eq "" ) { $auth_logger->warn("warning: empty field PATH"); } +if ( $READ_hrd_sha256 eq "" ) { $auth_logger->warn("warning: empty field HRD_SHA256"); } +if ( $READ_new_crypt eq "" ) { $auth_logger->warn("warning: empty field NEW_CRYPT"); } + +if ( $QUERY_pureftpd->rows == 0 ) { + $auth_logger->error("user not found, reason: no user? no ftp=Y ? wrong \$server_name ?, user: ".$REQ_USER); + auth_return(0, ""); +} else { + $auth_logger->info("User: ".$REQ_USER); + if (check_crypt($REQ_PASS, $READ_new_crypt)) { + $auth_logger->info("auth_ok:1, GOOD pwd match!, user: ".$REQ_USER); + auth_return(1, $READ_path); + } else { + $auth_logger->info("auth_ok:-1, reason: wrong password from check_crypt(), user: ".$REQ_USER); + auth_return(-1, ""); + } +}