Compare commits

...

13 Commits

Author SHA1 Message Date
Michael Kaufmann
c236d9eaab set version to 2.0.20 for upcoming release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-06-02 20:13:36 +02:00
Michael Kaufmann
688994e40c idna encode umlaut-emailaddresses when adding email-forwarder
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-29 20:52:57 +02:00
Michael Kaufmann
9facaee809 re-enable fcgid/php-fpm activation-validate-check
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-28 15:49:06 +02:00
Michael Kaufmann
a7dd5f4685 show 0 value of resource-fields if value is empty, fixes #1149
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-28 10:46:28 +02:00
Michael Kaufmann
da810ea953 secure filename of local-archive in webupdate
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-25 09:51:55 +02:00
Michael Kaufmann
51b6e067e8 idna encode umlaut-emailaddresses when adding/editing email-account; use correct password-suggestion-layout in change-email-account formfield
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-25 08:26:39 +02:00
Michael Kaufmann
34cf6698bc remove superfluous try_files in nginx config if php-backend (non-fastcgi) is used
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-15 20:14:26 +02:00
Michael Kaufmann
4642160724 add same loginfail restrictions for entering 2fa code as for user/pwd login
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-12 10:36:27 +02:00
Nicolas Thumann
78a259ef3b Fix IPv6 address in cookie domain (#1137)
* Implement getCookieHost to extract cookie host from HTTP_HOST
2023-05-10 08:26:08 +02:00
Nicolas Thumann
68cf4ab69a Fix typo in English privileged_passwd (#1136) 2023-05-09 18:52:43 +02:00
Michael Kaufmann
d5661d492d set version to 2.0.19 for bugfix release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-07 11:07:31 +02:00
Michael Kaufmann
6900898ae1 typo in updater
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-07 11:03:21 +02:00
Michael Kaufmann
d90fb7fa68 fix mysql-pdo check on installation, set version to 2.0.18 for bugfix release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-07 10:54:47 +02:00
18 changed files with 149 additions and 46 deletions

View File

@@ -28,7 +28,7 @@ require __DIR__ . '/lib/init.php';
use Froxlor\Froxlor;
use Froxlor\FroxlorLogger;
use Froxlor\Http\HttpClient;
use Froxlor\FileDir;
use Froxlor\Install\AutoUpdate;
use Froxlor\Settings;
use Froxlor\UI\Panel\UI;
@@ -132,7 +132,7 @@ elseif ($page == 'getdownload') {
elseif ($page == 'extract') {
if (isset($_POST['send']) && $_POST['send'] == 'send') {
$toExtract = isset($_POST['archive']) ? $_POST['archive'] : null;
$localArchive = Froxlor::getInstallDir() . '/updates/' . $toExtract;
$localArchive = FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/updates/' . $toExtract);
$log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "Extracting " . $localArchive . " to " . Froxlor::getInstallDir());
$result = AutoUpdate::extractZip($localArchive);
if ($result > 0) {
@@ -146,7 +146,7 @@ elseif ($page == 'extract') {
Response::redirectTo('admin_updates.php');
} else {
$toExtract = isset($_GET['archive']) ? $_GET['archive'] : null;
$localArchive = Froxlor::getInstallDir() . '/updates/' . $toExtract;
$localArchive = FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/updates/' . $toExtract);
}
if (!file_exists($localArchive)) {

View File

@@ -53,9 +53,15 @@ if ($action == '2fa_entercode') {
Response::redirectTo('index.php');
exit();
}
$smessage = isset($_GET['showmessage']) ? (int)$_GET['showmessage'] : 0;
$message = "";
if ($smessage > 0) {
$message = lng('error.2fa_wrongcode');
}
// show template to enter code
UI::view('login/enter2fa.html.twig', [
'pagetitle' => lng('login.2fa')
'pagetitle' => lng('login.2fa'),
'message' => $message
]);
} elseif ($action == '2fa_verify') {
// verify code from 2fa code-enter form
@@ -68,25 +74,25 @@ if ($action == '2fa_entercode') {
// verify entered code
$tfa = new FroxlorTwoFactorAuth('Froxlor ' . Settings::Get('system.hostname'));
$result = ($_SESSION['secret_2fa'] == 'email' ? true : $tfa->verifyCode($_SESSION['secret_2fa'], $code, 3));
// get user-data
$table = $_SESSION['uidtable_2fa'];
$field = $_SESSION['uidfield_2fa'];
$uid = $_SESSION['uid_2fa'];
$isadmin = $_SESSION['unfo_2fa'];
// either the code is valid when using authenticator-app, or we will select userdata by id and entered code
// which is temporarily stored for the customer when using email-2fa
if ($result) {
// get user-data
$table = $_SESSION['uidtable_2fa'];
$field = $_SESSION['uidfield_2fa'];
$uid = $_SESSION['uid_2fa'];
$isadmin = $_SESSION['unfo_2fa'];
$sel_param = [
'uid' => $uid
];
if ($_SESSION['secret_2fa'] == 'email') {
// verify code by selecting user by id and the temp. stored code,
// so only if it's the correct code, we get the user-data
$sel_stmt = Database::prepare("SELECT * FROM $table WHERE `" . $field . "` = :uid AND `data_2fa` = :code");
$sel_stmt = Database::prepare("SELECT * FROM " . $table . " WHERE `" . $field . "` = :uid AND `data_2fa` = :code");
$sel_param['code'] = $code;
} else {
// Authenticator-verification has already happened at this point, so just get the user-data
$sel_stmt = Database::prepare("SELECT * FROM $table WHERE `" . $field . "` = :uid");
$sel_stmt = Database::prepare("SELECT * FROM " . $table . " WHERE `" . $field . "` = :uid");
}
$userinfo = Database::pexecute_first($sel_stmt, $sel_param);
// whoops, no (valid) user? Start again
@@ -108,15 +114,50 @@ if ($action == '2fa_entercode') {
// when using email-2fa, remove the one-time-code
if ($userinfo['type_2fa'] == '1') {
$del_stmt = Database::prepare("UPDATE $table SET `data_2fa` = '' WHERE `" . $field . "` = :uid");
$del_stmt = Database::prepare("UPDATE " . $table . " SET `data_2fa` = '' WHERE `" . $field . "` = :uid");
$userinfo = Database::pexecute_first($del_stmt, [
'uid' => $uid
]);
}
exit();
}
// wrong 2fa code - treat like "wrong password"
$stmt = Database::prepare("
UPDATE " . $table . "
SET `lastlogin_fail`= :lastlogin_fail, `loginfail_count`=`loginfail_count`+1
WHERE `" . $field . "`= :uid
");
Database::pexecute($stmt, [
"lastlogin_fail" => time(),
"uid" => $uid
]);
// get data for processing further
$stmt = Database::prepare("
SELECT `loginname`, `loginfail_count`, `lastlogin_fail` FROM " . $table . "
WHERE `" . $field . "`= :uid
");
$fail_user = Database::pexecute_first($stmt, [
"uid" => $uid
]);
if ($fail_user['loginfail_count'] >= Settings::Get('login.maxloginattempts') && $fail_user['lastlogin_fail'] > (time() - Settings::Get('login.deactivatetime'))) {
// Log failed login
$rstlog = FroxlorLogger::getInstanceOf([
'loginname' => $_SERVER['REMOTE_ADDR']
]);
$rstlog->logAction(FroxlorLogger::LOGIN_ACTION, LOG_WARNING, "User '" . $fail_user['loginname'] . "' entered wrong 2fa code too often.");
unset($fail_user);
Response::redirectTo('index.php', [
'showmessage' => '3'
]);
exit();
}
unset($fail_user);
// back to form
Response::redirectTo('index.php', [
'showmessage' => '2'
'action' => '2fa_entercode',
'showmessage' => '1'
]);
exit();
} elseif ($action == 'login') {

View File

@@ -697,7 +697,7 @@ opcache.validate_timestamps'),
('system', 'distribution', ''),
('system', 'update_channel', 'stable'),
('system', 'updatecheck_data', ''),
('system', 'update_notify_last', '2.0.17'),
('system', 'update_notify_last', '2.0.20'),
('system', 'traffictool', 'goaccess'),
('system', 'req_limit_per_interval', 60),
('system', 'req_limit_interval', 60),
@@ -744,7 +744,7 @@ opcache.validate_timestamps'),
('panel', 'logo_overridetheme', '0'),
('panel', 'logo_overridecustom', '0'),
('panel', 'settings_mode', '0'),
('panel', 'version', '2.0.17'),
('panel', 'version', '2.0.20'),
('panel', 'db_version', '202304260');

View File

@@ -482,3 +482,18 @@ if (Froxlor::isFroxlorVersion('2.0.16')) {
Update::showUpdateStep("Updating from 2.0.16 to 2.0.17", false);
Froxlor::updateToVersion('2.0.17');
}
if (Froxlor::isFroxlorVersion('2.0.17')) {
Update::showUpdateStep("Updating from 2.0.17 to 2.0.18", false);
Froxlor::updateToVersion('2.0.18');
}
if (Froxlor::isFroxlorVersion('2.0.18')) {
Update::showUpdateStep("Updating from 2.0.18 to 2.0.19", false);
Froxlor::updateToVersion('2.0.19');
}
if (Froxlor::isFroxlorVersion('2.0.19')) {
Update::showUpdateStep("Updating from 2.0.19 to 2.0.20", false);
Froxlor::updateToVersion('2.0.20');
}

View File

@@ -99,6 +99,11 @@ class EmailAccounts extends ApiCommand implements ResourceEntity
Response::standardError('notallowedtouseaccounts', '', true);
}
if (!empty($emailaddr)) {
$idna_convert = new IdnaWrapper();
$emailaddr = $idna_convert->encode($emailaddr);
}
// get email address
$result = $this->apiCall('Emails.get', [
'id' => $id,
@@ -357,6 +362,11 @@ class EmailAccounts extends ApiCommand implements ResourceEntity
$ea_optional = $id > 0;
$emailaddr = $this->getParam('emailaddr', $ea_optional, '');
if (!empty($emailaddr)) {
$idna_convert = new IdnaWrapper();
$emailaddr = $idna_convert->encode($emailaddr);
}
// validation
$result = $this->apiCall('Emails.get', [
'id' => $id,

View File

@@ -77,6 +77,11 @@ class EmailForwarders extends ApiCommand implements ResourceEntity
$idna_convert = new IdnaWrapper();
$destination = $idna_convert->encode($destination);
if (!empty($emailaddr)) {
$idna_convert = new IdnaWrapper();
$emailaddr = $idna_convert->encode($emailaddr);
}
$result = $this->apiCall('Emails.get', [
'id' => $id,
'emailaddr' => $emailaddr

View File

@@ -1171,7 +1171,6 @@ class Nginx extends HttpConfigBase
$phpopts .= "\t\tinclude " . Settings::Get('nginx.fastcgiparams') . ";\n";
$phpopts .= "\t\tfastcgi_param SCRIPT_FILENAME \$request_filename;\n";
$phpopts .= "\t\tfastcgi_param PATH_INFO \$fastcgi_path_info;\n";
$phpopts .= "\t\ttry_files \$fastcgi_script_name =404;\n";
$phpopts .= "\t\tfastcgi_pass " . Settings::Get('system.nginx_php_backend') . ";\n";
$phpopts .= "\t\tfastcgi_index index.php;\n";
if ($domain['ssl'] == '1' && $ssl_vhost) {

View File

@@ -31,7 +31,7 @@ final class Froxlor
{
// Main version variable
const VERSION = '2.0.17';
const VERSION = '2.0.20';
// Database version (YYYYMMDDC where C is a daily counter)
const DBVERSION = '202304260';

View File

@@ -42,7 +42,7 @@ class Install
public $phpVersion;
public $formfield;
public string $requiredVersion = '7.4.0';
public array $requiredExtensions = ['session', 'ctype', 'mysql', 'xml', 'filter', 'posix', 'mbstring', 'curl', 'gmp', 'json', 'gd'];
public array $requiredExtensions = ['session', 'ctype', 'xml', 'filter', 'posix', 'mbstring', 'curl', 'gmp', 'json', 'gd'];
public array $suggestedExtensions = ['bcmath', 'zip'];
public array $suggestions = [];
public array $criticals = [];

View File

@@ -87,16 +87,38 @@ class UI
return $isHttps && (strcasecmp('on', $isHttps) == 0 || strcasecmp('https', $isHttps) == 0);
}
/**
* Extract the cookie host from HTTP_HOST, stripping the port.
*/
public static function getCookieHost(): ?string
{
if (empty($_SERVER['HTTP_HOST']))
return null;
$colonPosition = strrpos($_SERVER['HTTP_HOST'], ':');
// There's no port in the host
if ($colonPosition === false)
return $_SERVER['HTTP_HOST'];
$closingSquareBracketPosition = strrpos($_SERVER['HTTP_HOST'], ']');
// The host is an IPv4 address or hostname with port
if ($closingSquareBracketPosition === false)
return substr($_SERVER['HTTP_HOST'], 0, $colonPosition);
// The host is an IPv6 address with port
return substr($_SERVER['HTTP_HOST'], 0, $closingSquareBracketPosition + 1);
}
/**
* send various security related headers
*/
public static function sendHeaders()
{
$cookie_host = empty($_SERVER['HTTP_HOST']) ? null : explode (':', $_SERVER['HTTP_HOST'])[0];
session_set_cookie_params([
'lifetime' => self::$install_mode ? 7200 : 600, // will be renewed based on settings in lib/init.php
'path' => '/',
'domain' => $cookie_host,
'domain' => self::getCookieHost(),
'secure' => self::requestIsHttps(),
'httponly' => true,
'samesite' => 'Strict'

View File

@@ -52,16 +52,16 @@ class Check
];
$check_array = [
'system_mod_fcgid_enabled' => [
'other_post_field' => 'system_phpfpm_enabled',
'system_mod_fcgid' => [
'other_post_field' => 'phpfpm_enabled',
'other_enabled' => 'phpfpm.enabled',
'other_enabled_lng' => 'phpfpmstillenabled',
'deactivate' => [
'phpfpm.enabled_ownvhost' => 0
]
],
'system_phpfpm_enabled' => [
'other_post_field' => 'system_mod_fcgid_enabled',
'phpfpm_enabled' => [
'other_post_field' => 'system_mod_fcgid',
'other_enabled' => 'system.mod_fcgid',
'other_enabled_lng' => 'fcgidstillenabled',
'deactivate' => [

View File

@@ -133,7 +133,7 @@ return [
'customers' => [
'label' => lng('admin.customers'),
'type' => 'textul',
'value' => $result['customers'],
'value' => empty($result['customers']) ? '0' : $result['customers'],
'maxlength' => 9,
'mandatory' => true
],
@@ -146,7 +146,7 @@ return [
'domains' => [
'label' => lng('admin.domains'),
'type' => 'textul',
'value' => $result['domains'],
'value' => empty($result['domains']) ? '0' : $result['domains'],
'maxlength' => 9,
'mandatory' => true
],
@@ -159,49 +159,49 @@ return [
'diskspace' => [
'label' => lng('customer.diskspace') . ' (' . lng('customer.mib') . ')',
'type' => 'textul',
'value' => $result['diskspace'],
'value' => empty($result['diskspace']) ? '0' : $result['diskspace'],
'maxlength' => 6,
'mandatory' => true
],
'traffic' => [
'label' => lng('customer.traffic') . ' (' . lng('customer.gib') . ')',
'type' => 'textul',
'value' => $result['traffic'],
'value' => empty($result['traffic']) ? '0' : $result['traffic'],
'maxlength' => 4,
'mandatory' => true
],
'subdomains' => [
'label' => lng('customer.subdomains'),
'type' => 'textul',
'value' => $result['subdomains'],
'value' => empty($result['subdomains']) ? '0' : $result['subdomains'],
'maxlength' => 9,
'mandatory' => true
],
'emails' => [
'label' => lng('customer.emails'),
'type' => 'textul',
'value' => $result['emails'],
'value' => empty($result['emails']) ? '0' : $result['emails'],
'maxlength' => 9,
'mandatory' => true
],
'email_accounts' => [
'label' => lng('customer.accounts'),
'type' => 'textul',
'value' => $result['email_accounts'],
'value' => empty($result['email_accounts']) ? '0' : $result['email_accounts'],
'maxlength' => 9,
'mandatory' => true
],
'email_forwarders' => [
'label' => lng('customer.forwarders'),
'type' => 'textul',
'value' => $result['email_forwarders'],
'value' => empty($result['email_forwarders']) ? '0' : $result['email_forwarders'],
'maxlength' => 9,
'mandatory' => true
],
'email_quota' => [
'label' => lng('customer.email_quota') . ' (' . lng('customer.mib') . ')',
'type' => 'textul',
'value' => $result['email_quota'],
'value' => empty($result['email_quota']) ? '0' : $result['email_quota'],
'maxlength' => 9,
'visible' => Settings::Get('system.mail_quota_enabled') == '1',
'mandatory' => true
@@ -209,13 +209,13 @@ return [
'ftps' => [
'label' => lng('customer.ftps'),
'type' => 'textul',
'value' => $result['ftps'],
'value' => empty($result['ftps']) ? '0' : $result['ftps'],
'maxlength' => 9
],
'mysqls' => [
'label' => lng('customer.mysqls'),
'type' => 'textul',
'value' => $result['mysqls'],
'value' => empty($result['mysqls']) ? '0' : $result['mysqls'],
'maxlength' => 9,
'mandatory' => true
]

View File

@@ -43,13 +43,16 @@ return [
'email_password' => [
'label' => lng('login.password'),
'type' => 'password',
'autocomplete' => 'off'
],
'email_password_suggestion' => [
'label' => lng('customer.generated_pwd'),
'type' => 'text',
'visible' => (Settings::Get('panel.password_regex') == ''),
'value' => Crypt::generatePassword()
'autocomplete' => 'off',
'next_to' => [
'email_password_suggestion' => [
'next_to_prefix' => lng('customer.generated_pwd') . ':',
'type' => 'text',
'visible' => (Settings::Get('panel.password_regex') == ''),
'value' => Crypt::generatePassword(),
'readonly' => true
]
]
]
]
]

View File

@@ -46,7 +46,7 @@ return [
'autocomplete' => 'off',
'mandatory' => true,
'next_to' => [
'admin_password_suggestion' => [
'email_password_suggestion' => [
'next_to_prefix' => lng('customer.generated_pwd') . ':',
'type' => 'text',
'visible' => (Settings::Get('panel.password_regex') == ''),

View File

@@ -331,11 +331,10 @@ if (CurrentUser::hasSession()) {
}
}
// update cookie lifetime
$cookie_host = empty($_SERVER['HTTP_HOST']) ? null : explode (':', $_SERVER['HTTP_HOST'])[0];
$cookie_params = [
'expires' => time() + Settings::Get('session.sessiontimeout'),
'path' => '/',
'domain' => $cookie_host,
'domain' => UI::getCookieHost(),
'secure' => UI::requestIsHttps(),
'httponly' => true,
'samesite' => 'Strict'

View File

@@ -926,6 +926,7 @@ return [
'domaincannotbeedited' => 'Keine Berechtigung, um die Domain %s zu bearbeiten',
'invalidcronjobintervalvalue' => 'Cronjob Intervall muss einer der folgenden Werte sein: %s',
'phpgdextensionnotavailable' => 'Die PHP GD Extension ist nicht verfügbar. Bild-Daten können nicht validiert werden.',
'2fa_wrongcode' => 'Der angegebene Code ist nicht korrekt',
],
'extras' => [
'description' => 'Hier können Sie zusätzliche Extras einrichten, wie zum Beispiel einen Verzeichnisschutz.<br />Die Änderungen sind erst nach einer kurzen Zeit wirksam.',

View File

@@ -995,6 +995,7 @@ return [
'domaincannotbeedited' => 'You are not permitted to edit the domain %s',
'invalidcronjobintervalvalue' => 'Cronjob interval must be one of: %s',
'phpgdextensionnotavailable' => 'The PHP GD extension is not available. Unable to validate image-data',
'2fa_wrongcode' => 'The code entered is not valid',
],
'extras' => [
'description' => 'Here you can add some extras, for example directory protection.<br />The system will need some time to apply the new settings after every change.',
@@ -1193,7 +1194,7 @@ Yours sincerely, your administrator',
'database_edit' => 'Edit database',
'size' => 'Size',
'privileged_user' => 'Privileged database user',
'privileged_passwd' => 'Password for priviliged user',
'privileged_passwd' => 'Password for privileged user',
'unprivileged_passwd' => 'Password for unprivileged user',
'mysql_ssl_ca_file' => 'SSL server certificate',
'mysql_ssl_verify_server_certificate' => 'Verify SSL server certificate'

View File

@@ -10,6 +10,13 @@
<div class="card-body">
<h5 class="card-title">{{ pagetitle }}</h5>
{% if message is not empty %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">{{ lng('error.error') }}</h4>
<p>{{ message|raw }}</p>
</div>
{% endif %}
<div class="mb-3">
<label for="2fa_code" class="col-form-label">{{ lng('login.2facode') }}</label>
<input class="form-control" type="text" name="2fa_code" id="2fa_code" value="" autocomplete="off" autofocus required/>