ONO::ToolBox::Auth::ToolBox

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__