package ONO::ToolBox::Auth::ToolBox;
################################################################################
# COPYRIGHT / LICENSE #
################################################################################
#
# This file is part of the ONO Software Project.
#
# Copyright (C) 2000-2025 Jos KIRPS [ www.kirps.com | jos_AT_kirps_DOT_com ]
# and The Joopita Project [ www.joopita.org | contact_AT_joopita_DOT_com ]
#
# This file, as well as other parts of the ONO Software Project or related
# elements, are FREE SOFTWARE available under the ARTISTIC LICENSE 2.0.
#
# 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.
#
# For the full license, see /ono/osr/license/LICENSE.txt, or write to
# jos_AT_kirps_DOT_com or contact_AT_joopita_DOT_com.
#
################################################################################
# END OF COPYRIGHT / LICENSE, HERE COMES THE CODE ... #
################################################################################
use strict;
use ONO::IO;
use ONO::DB;
use ONO::Lib::Code::RandomID;
use ONO::Lib::Web::Cookie;
use ONO::Lib::Web::MaliciousIP;
use ONO::Lib::DateTime::ToolBox;
use ONO::Lib::Data::Crypt;
use ONO::ToolBox::Auth;
use ONO::ToolBox::Logfile;
use ONO::ToolBox::SendMail;
use ONO::ToolBox::ONO;
###############################################################################
# AUTH USING PASSWORD
###############################################################################
sub password_check {
#: Check the root user passsword.
my (
$self,
$db,
$community,
$username,
$password,
) = @_;
my $success;
if ($username eq "root") {
foreach my $user (ONO::IO->list("etc/shadow")) {
my @sp = split(/:/,$user);
if ($sp[0] eq $username && ONO::Lib::Data::Crypt->pwdchk($password,$sp[1])) {
$success++;
}
}
}
return $success;
}
###############################################################################
# LOGIN
###############################################################################
sub login {
my (
$self,
$db,
$community,
$vars_ref,
) = @_;
#: This is the login feature used by the ONO Admin and ONO Desk front end
#: applications.
my %vars = %$vars_ref;
if (!ONO::Lib::Web::MaliciousIP->detect) {
$vars{'login_blocked'} = &bad_login_block("",$vars{'username'},$vars{'lang'});
my $domain = $ENV{'SERVER_NAME'};
my %site = ONO::ToolBox::ONO->get();
if ($site{'domain'}) {
$domain = $site{'domain'};
}
if ($vars{'username'} && !$vars{'login_blocked'}) {
if ($vars{'username'} eq "root") {
foreach my $user (ONO::IO->list("etc/shadow")) {
my @sp = split(/:/,$user);
if ($sp[0] eq $vars{'username'}) {
if (ONO::Lib::Data::Crypt->pwdchk($vars{'password'},$sp[1])) {
# login successful
foreach my $pass (ONO::IO->list("etc/passwd")) {
my @pd = split(/:/,$pass);
if ($pd[0] eq $vars{'username'}) {
$vars{'sid'} = ONO::Lib::Code::RandomID->make(64);
}
}
} else {
# failed login attempt
ONO::ToolBox::SendMail->sendmail(
"<noreply\@$domain>",
"mail:tracker",
"[$domain] FAILED ONO ROOT LOGIN ATTEMPT [1]",
"Username: root\n\nIP: $ENV{'REMOTE_ADDR'}",
);
}
}
}
} else {
my $exists;
foreach my $line (ONO::DB->select($db,"ono_community_users","username = '$vars{'username'}'")) {
my @data = ONO::DB->readcols($line);
$exists++;
if (ONO::Lib::Data::Crypt->pwdchk($vars{'password'},$data[4])) {
# login successful
$vars{'sid'} = ONO::Lib::Code::RandomID->make(64);
} else {
# failed login attempt
if (ONO::IO->exists("etc/security/onologin.conf")) {
my %security = ONO::IO->confread("etc/security/onologin.conf","L");
# we'll report ALL failed attempts for now (admin vs user not implemented yet)
if ($security{'AdminReportLoginAttempts'} || $security{'UserReportLoginAttempts'}) {
ONO::ToolBox::SendMail->sendmail(
"<noreply\@$domain>",
"mail:tracker",
"[$domain] FAILED ONO LOGIN ATTEMPT [2]",
"Username: $vars{'username'}\n\nIP: $ENV{'REMOTE_ADDR'}",
);
}
}
}
}
if (!$exists && ONO::IO->exists("etc/security/onologin.conf")) {
my %security = ONO::IO->confread("etc/security/onologin.conf","L");
# we'll report ALL failed attempts for now (admin vs user not implemented yet)
if ($security{'AdminReportLoginAttempts'} || $security{'UserReportLoginAttempts'}) {
ONO::ToolBox::SendMail->sendmail(
"<noreply\@$domain>",
"mail:tracker",
"[$domain] FAILED ONO LOGIN ATTEMPT [3]",
"Username: $vars{'username'} (no such user in our database)\n\nIP: $ENV{'REMOTE_ADDR'}",
);
}
}
}
}
if ($vars{'sid'}) {
$vars{'sid'} = "$vars{'username'}-$vars{'sid'}";
my $MOBILE = ONO::ToolBox::Auth->mobile_identifier;
ONO::IO->mkpath("var/ono/sessions");
ONO::IO->store("var/ono/sessions/$vars{'username'}$MOBILE.txt",$vars{'sid'});
my $cookie_time;
if ($vars{'keep_session'}) {
$cookie_time = "90d";
}
$vars{'cookie_sid'} = ONO::Lib::Web::Cookie->make("ono_session",$vars{'sid'},$cookie_time);
$vars{'*'} .= ";sid=$vars{'sid'}";
} else {
&bad_login_attempt("",$vars{'username'});
}
}
return \%vars;
}
###############################################################################
# LOGOUT
###############################################################################
sub logout {
my (
$self,
$db,
$community,
$vars_ref,
) = @_;
#: This is the logout feature used by the ONO Admin and ONO Desk front end
#: applications.
my %vars = %$vars_ref;
ONO::IO->rm("var/ono/sessions/$vars{'username'}.txt");
$vars{'sid'} = "";
return \%vars;
}
###############################################################################
# LOGIN ATTEMPTS
###############################################################################
sub bad_login_block {
my $username = $_[1];
#: React to bad user login attempts, it order to block evil robots.
#:
#: Username fails are quite strict - 3 in a row using the same ip (100 seconds),
#: 5 using different IPs, or 5 within 17 minutes
#:
#: IP login fails are less aggressive, as there could be many users sharing a single ip rangs
#: (companies, institutions, universities, schools, ...)
my (
$sec,$min,$hour,
$mday,$mon,$year,
$wday,$yday,$timestamp
) = ONO::Lib::DateTime::ToolBox->get;
my $timestamp100 = substr($timestamp,0,8); # 100 secs - about 1.7 mins
my $timestamp1000 = substr($timestamp,0,7); # 1000 secs - about 17 mins
my ($block,$block_user,$block_ip,$block_userip,$min17) = (0,0,0,0,0);
if ($username) {
if (ONO::IO->size("var/tmp/loginattempts/$timestamp100/user-$username.txt") > 5) {
$block_user++;
}
if (ONO::IO->size("var/tmp/loginattempts/$timestamp100/userip-$username-$ENV{'REMOTE_ADDR'}.txt") > 3) {
$block_userip++;
}
if (ONO::IO->size("var/tmp/loginattempts/$timestamp1000/user-$username.txt") > 5) {
$block_user++;
$min17++;
}
if (ONO::IO->size("var/tmp/loginattempts/$timestamp100/userip-$username-$ENV{'REMOTE_ADDR'}.txt") > 5) {
$block_userip++;
$min17++;
}
}
if (ONO::IO->size("var/tmp/loginattempts/$timestamp100/ip-$ENV{'REMOTE_ADDR'}.txt") > 50) {
$block_ip++;
}
if (ONO::IO->size("var/tmp/loginattempts/$timestamp1000/ip-$ENV{'REMOTE_ADDR'}.txt") > 100) {
$block_ip++;
$min17++;
}
my $too_many = "Too many bad login attempts";
my $please_wait = "please wait a few minutes";
my $please_wait2 = "try again in 15 minutes";
my $err_user = "using your username";
my $err_ip = "from your IP address";
if ($_[2] eq "de" || $_[2] eq "lu") {
$too_many = "Zu viele Login Versuche";
$please_wait = "bitte ein paar Minuten warten";
$please_wait2 = "bitte 15 Minuten warten";
$err_user = "mit deinem Benutzernamen";
$err_ip = "von deiner IP Addresse";
}
if ($_[2] eq "fr") {
$too_many = "Trop d'essais échoués";
$please_wait = "veuillez réessayer dans quelques minutes";
$please_wait2 = "veuillez réessayer dans 15 minutes";
$err_user = "avec votre nom d'utilisateur";
$err_ip = "avec votre adresse IP";
}
if ($min17) {
$please_wait = $please_wait2;
}
if ($block_user) {
$block = qq~<div>$too_many $err_user - $please_wait!</div>~;
}
if ($block_ip) {
$block = qq~<div>$too_many $err_ip - $please_wait!</div>~;
}
if ($block_userip) {
$block = qq~<div>$too_many - $please_wait!</div>~;
}
# write to log, send to tracker
if ($block) {
ONO::ToolBox::Logfile->log("var/log/badlogins/$year$mon$mday.log",$username,"User blocked (User/IP: $block_userip, User: $block_user, IP: $block_ip)");
# send a mail not more than every 15 mins
if (ONO::IO->load("var/tmp/loginattempts/lastsendmail.txt") < ($timestamp - 960)) {
my $MAIL = "Users have been blocked because of too many bad user login attempts!\n\n";
foreach my $line (reverse ONO::IO->list("var/log/badlogins/$year$mon$mday.log")) {
$line =~ s~^(.*?);~~;
$MAIL .= $line;
}
ONO::ToolBox::SendMail->sendmail(
"mail:noreply",
"mail:tracker",
"[$ENV{'HTTP_HOST'}] WARNING - User logins have been BLOCKED",
$MAIL,
);
ONO::ToolBox::Logfile->log("var/log/badlogins/$year$mon$mday.log",$username,"Notification e-mail has been sent to mail:tracker");
ONO::IO->store("var/tmp/loginattempts/lastsendmail.txt",$timestamp);
}
}
if (ONO::IO->devstation) {
$block = 0;
}
return $block;
}
sub bad_login_attempt {
#: Detect bad user login attempts, it order to block evil robots.
#:
#: Failed logins are being logged to files by username and IP, the
#: file size indicates the number of failed login attempts.
my $username = $_[1];
my (
$sec,$min,$hour,
$mday,$mon,$year,
$wday,$yday,$timestamp
) = ONO::Lib::DateTime::ToolBox->get;
my $timestamp100 = substr($timestamp,0,8); # 100 secs - about 1.7 mins
my $timestamp1000 = substr($timestamp,0,7); # 1000 secs - about 17 mins
# create dirs
ONO::IO->mkdir("var/tmp/loginattempts");
ONO::IO->mkdir("var/tmp/loginattempts/$timestamp100");
ONO::IO->mkdir("var/tmp/loginattempts/$timestamp1000");
# garbage collector
foreach my $dir (ONO::IO->dir("var/tmp/loginattempts")) {
if ((length $dir == 8 && $dir < $timestamp100) || (length $dir == 7 && $dir < $timestamp1000)) {
ONO::IO->rmdir("var/tmp/loginattempts/$dir");
}
}
# write the data
ONO::IO->append("var/tmp/loginattempts/$timestamp100/user-$username.txt","X");
ONO::IO->append("var/tmp/loginattempts/$timestamp100/ip-$ENV{'REMOTE_ADDR'}.txt","X");
ONO::IO->append("var/tmp/loginattempts/$timestamp100/userip-$username-$ENV{'REMOTE_ADDR'}.txt","X");
ONO::IO->append("var/tmp/loginattempts/$timestamp1000/user-$username.txt","X");
ONO::IO->append("var/tmp/loginattempts/$timestamp1000/ip-$ENV{'REMOTE_ADDR'}.txt","X");
ONO::IO->append("var/tmp/loginattempts/$timestamp1000/userip-$username-$ENV{'REMOTE_ADDR'}.txt","X");
# write to log
ONO::ToolBox::Logfile->log("var/log/badlogins/$year$mon$mday.log",$username,"Failed login on $ENV{'REQUEST_URI'}","");
# we'll return the data, although this will probably not be needed...
return ($username,$ENV{'REMOTE_ADDR'},$timestamp100,$timestamp1000);
}
###############################################################################
# end of script
###############################################################################
1;
__END__