diff --git a/index.php b/index.php
index d59146e3..de0b38f4 100644
--- a/index.php
+++ b/index.php
@@ -26,6 +26,7 @@
const AREA = 'login';
require __DIR__ . '/lib/init.php';
+use Froxlor\Api\FroxlorRPC;
use Froxlor\CurrentUser;
use Froxlor\Customer\Customer;
use Froxlor\Database\Database;
@@ -37,10 +38,10 @@ use Froxlor\PhpHelper;
use Froxlor\Settings;
use Froxlor\System\Crypt;
use Froxlor\UI\Panel\UI;
+use Froxlor\UI\Request;
use Froxlor\UI\Response;
use Froxlor\User;
use Froxlor\Validate\Validate;
-use Froxlor\Language;
if ($action == '') {
$action = 'login';
@@ -730,6 +731,58 @@ if ($action == 'resetpwd') {
}
}
+// one-time link login
+if ($action == 'll') {
+ if (!Froxlor::hasUpdates() && !Froxlor::hasDbUpdates()) {
+ $loginname = Request::get('ln');
+ $hash = Request::get('h');
+ if ($loginname && $hash) {
+ $sel_stmt = Database::prepare("
+ SELECT * FROM `" . TABLE_PANEL_LOGINLINKS . "`
+ WHERE `loginname` = :loginname AND `hash` = :hash
+ ");
+ try {
+ $entry = Database::pexecute_first($sel_stmt, ['loginname' => $loginname, 'hash' => $hash]);
+ } catch (Exception $e) {
+ $entry = false;
+ }
+ if ($entry) {
+ // delete entry
+ $del_stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_LOGINLINKS . "` WHERE `loginname` = :loginname AND `hash` = :hash");
+ Database::pexecute($del_stmt, ['loginname' => $loginname, 'hash' => $hash]);
+ if (time() <= $entry['valid_until']) {
+ $valid = true;
+ // validate source ip if specified
+ if (!empty($entry['allowed_from'])) {
+ $valid = false;
+ $ip_list = explode(",", $entry['allowed_from']);
+ if (FroxlorRPC::validateAllowedFrom($ip_list, $_SERVER['REMOTE_ADDR'])) {
+ $valid = true;
+ }
+ }
+ if ($valid) {
+ // login user / select only non-deactivated (in case the user got deactivated after generating the link)
+ $userinfo_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `loginname`= :loginname AND `deactivated` = 0");
+ try {
+ $userinfo = Database::pexecute_first($userinfo_stmt, [
+ "loginname" => $loginname
+ ]);
+ } catch (Exception $e) {
+ $userinfo = false;
+ }
+ if ($userinfo) {
+ $userinfo['userid'] = $userinfo['customerid'];
+ $userinfo['adminsession'] = 0;
+ finishLogin($userinfo);
+ }
+ }
+ }
+ }
+ }
+ }
+ Response::redirectTo('index.php');
+}
+
function finishLogin($userinfo)
{
if (isset($userinfo['userid']) && $userinfo['userid'] != '') {
diff --git a/install/froxlor.sql.php b/install/froxlor.sql.php
index a4cb7c5a..57901d03 100644
--- a/install/froxlor.sql.php
+++ b/install/froxlor.sql.php
@@ -744,7 +744,7 @@ opcache.validate_timestamps'),
('panel', 'logo_overridecustom', '0'),
('panel', 'settings_mode', '0'),
('panel', 'version', '2.0.19'),
- ('panel', 'db_version', '202305230');
+ ('panel', 'db_version', '202305240');
DROP TABLE IF EXISTS `panel_tasks`;
@@ -1051,4 +1051,13 @@ CREATE TABLE `panel_usercolumns` (
KEY adminid (adminid),
KEY customerid (customerid)
) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci;
+
+DROP TABLE IF EXISTS `panel_loginlinks`;
+CREATE TABLE `panel_loginlinks` (
+ `hash` varchar(500) NOT NULL,
+ `loginname` varchar(50) NOT NULL,
+ `valid_until` int(15) NOT NULL,
+ `allowed_from` text NOT NULL,
+ UNIQUE KEY `loginname` (`loginname`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
FROXLORSQL;
diff --git a/install/updates/froxlor/update_2.x.inc.php b/install/updates/froxlor/update_2.x.inc.php
index 50f0f3cb..30eafde4 100644
--- a/install/updates/froxlor/update_2.x.inc.php
+++ b/install/updates/froxlor/update_2.x.inc.php
@@ -498,5 +498,17 @@ if (Froxlor::isDatabaseVersion('202304260')) {
Database::query("ALTER TABLE `" . TABLE_PANEL_DOMAINS . "` DROP COLUMN `ismainbutsubto`;");
Update::lastStepStatus(0);
- Froxlor::updateToDbVersion('202305230');
+ Update::showUpdateStep("Creating new tables and fields");
+ Database::query("DROP TABLE IF EXISTS `panel_loginlinks`;");
+ $sql = "CREATE TABLE `panel_loginlinks` (
+ `hash` varchar(500) NOT NULL,
+ `loginname` varchar(50) NOT NULL,
+ `valid_until` int(15) NOT NULL,
+ `allowed_from` text NOT NULL,
+ UNIQUE KEY `loginname` (`loginname`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;";
+ Database::query($sql);
+ Update::lastStepStatus(0);
+
+ Froxlor::updateToDbVersion('202305240');
}
diff --git a/lib/Froxlor/Api/Commands/Froxlor.php b/lib/Froxlor/Api/Commands/Froxlor.php
index 690b5880..d7cb4920 100644
--- a/lib/Froxlor/Api/Commands/Froxlor.php
+++ b/lib/Froxlor/Api/Commands/Froxlor.php
@@ -37,6 +37,7 @@ use Froxlor\Settings;
use Froxlor\SImExporter;
use Froxlor\System\Cronjob;
use Froxlor\System\Crypt;
+use Froxlor\Validate\Validate;
use PDO;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
@@ -269,6 +270,79 @@ class Froxlor extends ApiCommand
return $this->response(Crypt::generatePassword($length));
}
+ /**
+ * return a one-time login link URL for a given user
+ *
+ * @param int $customerid optional, required if $loginname is not specified, user to create link for
+ * @param string $loginname optional, required if $customerid is not specified, user to create link for
+ * @param int $valid_time optional, value in seconds how long the link will be valid, default is 10 seconds, valid values are numbers from 10 to 120
+ * @param string $allowed_from optional, comma separated list of ip addresses or networks to allow login from via this link
+ *
+ * @access admin
+ * @return string json-encoded array [base => domain, uri => relative link]
+ * @throws Exception
+ */
+ public function generateLoginLink()
+ {
+ if ($this->isAdmin()) {
+ $customer = $this->getCustomerData();
+
+ // cannot create link for deactivated users
+ if ((int)$customer['deactivated'] == 1) {
+ throw new Exception("Cannot generate link for deactivated user", 406);
+ }
+
+ $valid_time = (int)$this->getParam('valid_time', true, 10);
+ $allowed_from = $this->getParam('allowed_from', true, '');
+
+ $valid_time = Validate::validate($valid_time, 'valid time', '/^(1[0-1][0-9]|120|[1-9][0-9])$/', 'invalid_validtime', [10], true);
+
+ // validate allowed_from
+ if (!empty($allowed_from)) {
+ $ip_list = array_map('trim', explode(",", $allowed_from));
+ $_check_list = $ip_list;
+ foreach ($_check_list as $idx => $ip) {
+ if (Validate::validate_ip2($ip, true, 'invalidip', true, true, true) == false) {
+ throw new Exception('Invalid ip address', 406);
+ }
+ // check for cidr
+ if (strpos($ip, '/') !== false) {
+ $ipparts = explode("/", $ip);
+ // shorten IP
+ $ip = inet_ntop(inet_pton($ipparts[0]));
+ // re-add cidr
+ $ip .= '/' . $ipparts[1];
+ } else {
+ // shorten IP
+ $ip = inet_ntop(inet_pton($ip));
+ }
+ $ip_list[$idx] = $ip;
+ }
+ $allowed_from = implode(",", array_unique($ip_list));
+ }
+
+ $hash = hash('sha256', openssl_random_pseudo_bytes(64 * 64));
+
+ $ins_stmt = Database::prepare("
+ INSERT INTO `" . TABLE_PANEL_LOGINLINKS . "`
+ SET `hash` = :hash, `loginname` = :loginname, `valid_until` = :validuntil, `allowed_from` = :allowedfrom
+ ON DUPLICATE KEY UPDATE `hash` = :hash, `valid_until` = :validuntil, `allowed_from` = :allowedfrom
+ ");
+ Database::pexecute($ins_stmt, [
+ 'hash' => $hash,
+ 'loginname' => $customer['loginname'],
+ 'validuntil' => time() + $valid_time,
+ 'allowedfrom' => $allowed_from
+ ]);
+
+ return $this->response([
+ 'base' => 'https://' . Settings::Get('system.hostname') . '/' . (Settings::Get('system.froxlordirectlyviahostname') != 1 ? basename(\Froxlor\Froxlor::getInstallDir()) . '/' : ''),
+ 'uri' => 'index.php?action=ll&ln=' . $customer['loginname'] . '&h=' . $hash
+ ]);
+ }
+ throw new Exception("Not allowed to execute given command.", 403);
+ }
+
/**
* can be used to remotely run the integritiy checks froxlor implements
*
diff --git a/lib/Froxlor/Api/FroxlorRPC.php b/lib/Froxlor/Api/FroxlorRPC.php
index 4a85872b..ed5398dd 100644
--- a/lib/Froxlor/Api/FroxlorRPC.php
+++ b/lib/Froxlor/Api/FroxlorRPC.php
@@ -112,11 +112,11 @@ class FroxlorRPC
*
* @return bool
*/
- private static function validateAllowedFrom(array $allowed_from, string $remote_addr): bool
+ public static function validateAllowedFrom(array $allowed_from, string $remote_addr): bool
{
// shorten IP for comparison
$remote_addr = inet_ntop(inet_pton($remote_addr));
- // check for diret matches
+ // check for direct matches
if (in_array($remote_addr, $allowed_from)) {
return true;
}
diff --git a/lib/Froxlor/Cli/MasterCron.php b/lib/Froxlor/Cli/MasterCron.php
index 72d43f70..ec85d230 100644
--- a/lib/Froxlor/Cli/MasterCron.php
+++ b/lib/Froxlor/Cli/MasterCron.php
@@ -166,6 +166,9 @@ final class MasterCron extends CliCommand
FroxlorLogger::getInstanceOf()->setCronLog(0);
}
+ // clean up possible old login-links
+ Database::query("DELETE FROM `" . TABLE_PANEL_LOGINLINKS . "` WHERE `valid_until` < UNIX_TIMESTAMP()");
+
return $result;
}
diff --git a/lib/Froxlor/Froxlor.php b/lib/Froxlor/Froxlor.php
index df17100a..39f4aab5 100644
--- a/lib/Froxlor/Froxlor.php
+++ b/lib/Froxlor/Froxlor.php
@@ -34,7 +34,7 @@ final class Froxlor
const VERSION = '2.0.19';
// Database version (YYYYMMDDC where C is a daily counter)
- const DBVERSION = '202305230';
+ const DBVERSION = '202305240';
// Distribution branding-tag (used for Debian etc.)
const BRANDING = '';
diff --git a/lib/tables.inc.php b/lib/tables.inc.php
index 43c52b08..26214333 100644
--- a/lib/tables.inc.php
+++ b/lib/tables.inc.php
@@ -56,3 +56,4 @@ const TABLE_PANEL_FPMDAEMONS = 'panel_fpmdaemons';
const TABLE_PANEL_PLANS = 'panel_plans';
const TABLE_API_KEYS = 'api_keys';
const TABLE_PANEL_USERCOLUMNS = 'panel_usercolumns';
+const TABLE_PANEL_LOGINLINKS = 'panel_loginlinks';
diff --git a/lng/de.lng.php b/lng/de.lng.php
index b899a717..6cf9fcf9 100644
--- a/lng/de.lng.php
+++ b/lng/de.lng.php
@@ -926,6 +926,7 @@ return [
'2fa_wrongcode' => 'Der angegebene Code ist nicht korrekt',
'gnupgextensionnotavailable' => 'Die PHP GnuPG Extension ist nicht verfügbar. PGP Schlüssel können nicht validiert werden.',
'invalidpgppublickey' => 'Der angegebene PGP Public Key ist ungültig',
+ 'invalid_validtime' => 'Wert der valid_time in Sekunden muss zwischen 10 und 120 liegen.',
],
'extras' => [
'description' => 'Hier können Sie zusätzliche Extras einrichten, wie zum Beispiel einen Verzeichnisschutz.
Die Änderungen sind erst nach einer kurzen Zeit wirksam.',
diff --git a/lng/en.lng.php b/lng/en.lng.php
index d4cb30fa..9169ec37 100644
--- a/lng/en.lng.php
+++ b/lng/en.lng.php
@@ -995,6 +995,7 @@ return [
'2fa_wrongcode' => 'The code entered is not valid',
'gnupgextensionnotavailable' => 'The PHP GnuPG extension is not available. Unable to validate PGP Public Key',
'invalidpgppublickey' => 'The PGP Public Key is not valid',
+ 'invalid_validtime' => 'Valid time in seconds can only be between 10 and 120',
],
'extras' => [
'description' => 'Here you can add some extras, for example directory protection.
The system will need some time to apply the new settings after every change.',