Merge remote-tracking branch 'origin/main' into customeremail-overview
This commit is contained in:
@@ -559,7 +559,7 @@ class Domains extends ApiCommand implements ResourceEntity
|
||||
|
||||
// validate dns if lets encrypt is enabled to check whether we can use it at all
|
||||
if ($letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') {
|
||||
$domain_ips = PhpHelper::gethostbynamel6($domain);
|
||||
$domain_ips = PhpHelper::gethostbynamel6($domain, true, Settings::Get('system.le_domain_dnscheck_resolver'));
|
||||
$selected_ips = $this->getIpsFromIdArray($ssl_ipandports);
|
||||
if ($domain_ips == false || count(array_intersect($selected_ips, $domain_ips)) <= 0) {
|
||||
Response::standardError('invaliddnsforletsencrypt', '', true);
|
||||
@@ -1523,7 +1523,7 @@ class Domains extends ApiCommand implements ResourceEntity
|
||||
|
||||
// validate dns if lets encrypt is enabled to check whether we can use it at all
|
||||
if ($letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') {
|
||||
$domain_ips = PhpHelper::gethostbynamel6($result['domain']);
|
||||
$domain_ips = PhpHelper::gethostbynamel6($result['domain'], true, Settings::Get('system.le_domain_dnscheck_resolver'));
|
||||
$selected_ips = $this->getIpsFromIdArray($ssl_ipandports);
|
||||
if ($domain_ips == false || count(array_intersect($selected_ips, $domain_ips)) <= 0) {
|
||||
Response::standardError('invaliddnsforletsencrypt', '', true);
|
||||
|
||||
@@ -262,7 +262,7 @@ class SubDomains extends ApiCommand implements ResourceEntity
|
||||
// validate dns if lets encrypt is enabled to check whether we can use it at all
|
||||
if ($letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') {
|
||||
$our_ips = Domain::getIpsOfDomain($domain_check['id']);
|
||||
$domain_ips = PhpHelper::gethostbynamel6($completedomain);
|
||||
$domain_ips = PhpHelper::gethostbynamel6($completedomain, true, Settings::Get('system.le_domain_dnscheck_resolver'));
|
||||
if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) {
|
||||
Response::standardError('invaliddnsforletsencrypt', '', true);
|
||||
}
|
||||
@@ -738,7 +738,7 @@ class SubDomains extends ApiCommand implements ResourceEntity
|
||||
// validate dns if lets encrypt is enabled to check whether we can use it at all
|
||||
if ($result['letsencrypt'] != $letsencrypt && $letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') {
|
||||
$our_ips = Domain::getIpsOfDomain($result['parentdomainid']);
|
||||
$domain_ips = PhpHelper::gethostbynamel6($result['domain']);
|
||||
$domain_ips = PhpHelper::gethostbynamel6($result['domain'], true, Settings::Get('system.le_domain_dnscheck_resolver'));
|
||||
if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) {
|
||||
Response::standardError('invaliddnsforletsencrypt', '', true);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ final class ValidateAcmeWebroot extends CliCommand
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('froxlor:validate-acme-webroot');
|
||||
$this->setDescription('Validates the Le_Webroot value is correct for froxlor managed domains with Let\s Encrypt certificate.');
|
||||
$this->setDescription('Validates the Le_Webroot value is correct for froxlor managed domains with Let\'s Encrypt certificate.');
|
||||
$this->addOption('yes-to-all', 'A', InputOption::VALUE_NONE, 'Do not ask for confirmation, update files if necessary');
|
||||
}
|
||||
|
||||
@@ -56,6 +56,11 @@ final class ValidateAcmeWebroot extends CliCommand
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
if ((int) Settings::Get('system.leenabled') == 0) {
|
||||
$io->info("Let's Encrypt not activated in froxlor settings.");
|
||||
$result = self::INVALID;
|
||||
}
|
||||
|
||||
if ($result == self::SUCCESS) {
|
||||
$yestoall = $input->getOption('yes-to-all') !== false;
|
||||
$helper = $this->getHelper('question');
|
||||
@@ -64,9 +69,37 @@ final class ValidateAcmeWebroot extends CliCommand
|
||||
$sel_stmt = Database::prepare("SELECT id, domain FROM panel_domains WHERE `letsencrypt` = '1' AND aliasdomain IS NULL ORDER BY id ASC");
|
||||
Database::pexecute($sel_stmt);
|
||||
$domains = $sel_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
// check for froxlor-vhost
|
||||
if (Settings::Get('system.le_froxlor_enabled') == '1') {
|
||||
$domains[] = [
|
||||
'id' => 0,
|
||||
'domain' => Settings::Get('system.hostname')
|
||||
];
|
||||
}
|
||||
$upd_stmt = Database::prepare("UPDATE domain_ssl_settings SET expirationdate=NULL WHERE `domainid` = :did");
|
||||
$acmesh_dir = dirname(Settings::Get('system.acmeshpath'));
|
||||
$acmesh_challenge_dir = Settings::Get('system.letsencryptchallengepath');
|
||||
$acmesh_challenge_dir = rtrim(FileDir::makeCorrectDir(Settings::Get('system.letsencryptchallengepath')), "/");
|
||||
$recommended = rtrim(FileDir::makeCorrectDir(Froxlor::getInstallDir()), "/");
|
||||
|
||||
if ($acmesh_challenge_dir != $recommended) {
|
||||
$io->warning([
|
||||
"ACME challenge docroot from settings differs from the current installation directory.",
|
||||
"Settings: '" . $acmesh_challenge_dir . "'",
|
||||
"Default/recommended value: '" . $recommended . "'",
|
||||
]);
|
||||
$question = new ConfirmationQuestion('Fix ACME challenge docroot setting? [yes] ', true, '/^(y|j)/i');
|
||||
if ($yestoall || $helper->ask($input, $output, $question)) {
|
||||
Settings::Set('system.letsencryptchallengepath', $recommended);
|
||||
$former_value = $acmesh_challenge_dir;
|
||||
$acmesh_challenge_dir = $recommended;
|
||||
// need to update the corresponding acme-alias config-file
|
||||
$acme_alias_file = Settings::Get('system.letsencryptacmeconf');
|
||||
$sed_params = "s@".$former_value."@" . $acmesh_challenge_dir . "@";
|
||||
FileDir::safe_exec('sed -i -e "' . $sed_params . '" ' . escapeshellarg($acme_alias_file));
|
||||
$count_changes++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($domains as $domain_arr) {
|
||||
$domain = $domain_arr['domain'];
|
||||
$acme_domain_conf = FileDir::makeCorrectFile($acmesh_dir . '/' . $domain . '/' . $domain . '.conf');
|
||||
@@ -113,6 +146,7 @@ final class ValidateAcmeWebroot extends CliCommand
|
||||
}
|
||||
if ($count_changes > 0) {
|
||||
if (Froxlor::hasUpdates() || Froxlor::hasDbUpdates()) {
|
||||
$io->info("Changes detected but froxlor has been updated. Inserting task to rebuild vhosts after update.");
|
||||
Cronjob::inserttask(TaskId::REBUILD_VHOST);
|
||||
} else {
|
||||
$question = new ConfirmationQuestion('Changes detected. Force cronjob to refresh certificates? [yes] ', true, '/^(y|j)/i');
|
||||
@@ -120,6 +154,8 @@ final class ValidateAcmeWebroot extends CliCommand
|
||||
passthru(FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/bin/froxlor-cli') . ' froxlor:cron -f -d');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$io->success("No changes necessary.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -521,7 +521,7 @@ EOC;
|
||||
foreach ($loop_domains as $idx => $domain) {
|
||||
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Validating DNS of " . $domain);
|
||||
// ips according to NS
|
||||
$domain_ips = PhpHelper::gethostbynamel6($domain);
|
||||
$domain_ips = PhpHelper::gethostbynamel6($domain, true, Settings::Get('system.le_domain_dnscheck_resolver'));
|
||||
if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) {
|
||||
// no common ips...
|
||||
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $domain . " due to no system known IP address via DNS check");
|
||||
@@ -557,7 +557,7 @@ EOC;
|
||||
if (Settings::Get('system.letsencryptreuseold') != '1') {
|
||||
$acmesh_cmd .= " --always-force-new-domain-key";
|
||||
}
|
||||
if (Settings::Get('system.letsencryptca') == 'letsencrypt_test') {
|
||||
if (substr(Settings::Get('system.letsencryptca'), -5) == '_test') {
|
||||
$acmesh_cmd .= " --staging";
|
||||
}
|
||||
if ($force) {
|
||||
|
||||
@@ -31,10 +31,10 @@ final class Froxlor
|
||||
{
|
||||
|
||||
// Main version variable
|
||||
const VERSION = '2.0.7';
|
||||
const VERSION = '2.0.9';
|
||||
|
||||
// Database version (YYYYMMDDC where C is a daily counter)
|
||||
const DBVERSION = '202212060';
|
||||
const DBVERSION = '202301180';
|
||||
|
||||
// Distribution branding-tag (used for Debian etc.)
|
||||
const BRANDING = '';
|
||||
|
||||
@@ -100,11 +100,17 @@ class FroxlorLogger
|
||||
self::$ml->pushHandler(new SyslogHandler('froxlor', LOG_USER, Logger::DEBUG));
|
||||
break;
|
||||
case 'file':
|
||||
$logger_logfile = Settings::Get('logger.logfile');
|
||||
$logger_logfile = FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/logs/' . Settings::Get('logger.logfile'));
|
||||
// is_writable needs an existing file to check if it's actually writable
|
||||
@touch($logger_logfile);
|
||||
if (empty($logger_logfile) || !is_writable($logger_logfile)) {
|
||||
Settings::Set('logger.logfile', '/tmp/froxlor.log');
|
||||
Settings::Set('logger.logfile', 'froxlor.log');
|
||||
$logger_logfile = FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/logs/froxlor.log');
|
||||
@touch($logger_logfile);
|
||||
if (empty($logger_logfile) || !is_writable($logger_logfile)) {
|
||||
// not writable in our own directory? Skip
|
||||
break;
|
||||
}
|
||||
}
|
||||
self::$ml->pushHandler(new StreamHandler($logger_logfile, Logger::DEBUG));
|
||||
break;
|
||||
|
||||
@@ -101,7 +101,7 @@ class Preconfig
|
||||
$agree = [
|
||||
'title' => 'Check',
|
||||
'fields' => [
|
||||
'update_changesagreed' => ['type' => 'checkbox', 'value' => 1, 'label' => '<strong>I have read the update notifications above and I am aware of the changes made to my system.</strong>'],
|
||||
'update_changesagreed' => ['mandatory' => true, 'type' => 'checkrequired', 'value' => 1, 'label' => '<strong>I have read the update notifications above and I am aware of the changes made to my system.</strong>'],
|
||||
'update_preconfig' => ['type' => 'hidden', 'value' => 1]
|
||||
]
|
||||
];
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace Froxlor;
|
||||
|
||||
use Exception;
|
||||
use Froxlor\UI\Panel\UI;
|
||||
use Net_DNS2_Exception;
|
||||
use Net_DNS2_Resolver;
|
||||
use Throwable;
|
||||
use voku\helper\AntiXSS;
|
||||
|
||||
@@ -244,45 +246,60 @@ class PhpHelper
|
||||
* ipv6 aware gethostbynamel function
|
||||
*
|
||||
* @param string $host
|
||||
* @param boolean $try_a
|
||||
* default true
|
||||
* @param boolean $try_a default true
|
||||
* @param string|null $nameserver set additional resolver nameserver to use (e.g. 1.1.1.1)
|
||||
* @return boolean|array
|
||||
*/
|
||||
public static function gethostbynamel6($host, $try_a = true)
|
||||
public static function gethostbynamel6(string $host, bool $try_a = true, string $nameserver = null)
|
||||
{
|
||||
$dns6 = @dns_get_record($host, DNS_AAAA);
|
||||
if (!is_array($dns6)) {
|
||||
// no record or failed to check
|
||||
$dns6 = [];
|
||||
}
|
||||
if ($try_a == true) {
|
||||
$dns4 = @dns_get_record($host, DNS_A);
|
||||
if (!is_array($dns4)) {
|
||||
// no record or failed to check
|
||||
$dns4 = [];
|
||||
}
|
||||
$dns = array_merge($dns4, $dns6);
|
||||
} else {
|
||||
$dns = $dns6;
|
||||
}
|
||||
$ips = [];
|
||||
foreach ($dns as $record) {
|
||||
if ($record["type"] == "A") {
|
||||
// always use compressed ipv6 format
|
||||
$ip = inet_ntop(inet_pton($record["ip"]));
|
||||
$ips[] = $ip;
|
||||
|
||||
try {
|
||||
// set the default nameservers to use, use the system default if none are provided
|
||||
$resolver = new Net_DNS2_Resolver($nameserver ? ['nameservers' => [$nameserver]] : []);
|
||||
|
||||
// get all ip addresses from the A record and normalize them
|
||||
if ($try_a) {
|
||||
try {
|
||||
$answer = $resolver->query($host, 'A')->answer;
|
||||
foreach ($answer as $rr) {
|
||||
$ips[] = inet_ntop(inet_pton($rr->address));
|
||||
}
|
||||
} catch (Net_DNS2_Exception $e) {
|
||||
// we can't do anything here, just continue
|
||||
}
|
||||
}
|
||||
if ($record["type"] == "AAAA") {
|
||||
// always use compressed ipv6 format
|
||||
$ip = inet_ntop(inet_pton($record["ipv6"]));
|
||||
$ips[] = $ip;
|
||||
|
||||
// get all ip addresses from the AAAA record and normalize them
|
||||
try {
|
||||
$answer = $resolver->query($host, 'AAAA')->answer;
|
||||
foreach ($answer as $rr) {
|
||||
$ips[] = inet_ntop(inet_pton($rr->address));
|
||||
}
|
||||
} catch (Net_DNS2_Exception $e) {
|
||||
// we can't do anything here, just continue
|
||||
}
|
||||
} catch (Net_DNS2_Exception $e) {
|
||||
// fallback to php's dns_get_record if Net_DNS2 has no resolver available, but this may cause
|
||||
// problems if the system's dns is not configured correctly; for example, the acme pre-check
|
||||
// will fail because some providers put a local ip in /etc/hosts
|
||||
|
||||
// get all ip addresses from the A record and normalize them
|
||||
if ($try_a) {
|
||||
$answer = @dns_get_record($host, DNS_A);
|
||||
foreach ($answer as $rr) {
|
||||
$ips[] = inet_ntop(inet_pton($rr['ip']));
|
||||
}
|
||||
}
|
||||
|
||||
// get all ip addresses from the AAAA record and normalize them
|
||||
$answer = @dns_get_record($host, DNS_AAAA);
|
||||
foreach ($answer as $rr) {
|
||||
$ips[] = inet_ntop(inet_pton($rr['ipv6']));
|
||||
}
|
||||
}
|
||||
if (count($ips) < 1) {
|
||||
return false;
|
||||
} else {
|
||||
return $ips;
|
||||
}
|
||||
|
||||
return count($ips) > 0 ? $ips : false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -87,6 +87,10 @@ class FroxlorTwig extends AbstractExtension
|
||||
new TwigFunction('linker', [
|
||||
$this,
|
||||
'getLink'
|
||||
]),
|
||||
new TwigFunction('mix', [
|
||||
$this,
|
||||
'getMix'
|
||||
])
|
||||
];
|
||||
}
|
||||
@@ -158,4 +162,9 @@ class FroxlorTwig extends AbstractExtension
|
||||
{
|
||||
return 'froxlortwig';
|
||||
}
|
||||
|
||||
public function getMix($mix = '')
|
||||
{
|
||||
return mix($mix);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,11 @@
|
||||
* change the options below to either true or false
|
||||
*/
|
||||
return [
|
||||
'enable_webupdate' => false
|
||||
/**
|
||||
* enable/disable the possibility to update froxlor from within the web-interface,
|
||||
* recommended value for debian/ubuntu package users is false to rely on apt and not have version mixup.
|
||||
* This is also useful for providers that manage the servers but give admin access to froxlor to handle
|
||||
* updates the way the providers does it (e.g. automation, etc.)
|
||||
*/
|
||||
'enable_webupdate' => false,
|
||||
];
|
||||
|
||||
@@ -3458,11 +3458,7 @@ ssl_key = <<SSL_KEY_FILE>
|
||||
# auth_ssl_username_from_cert=yes.
|
||||
#ssl_cert_username_field = commonName
|
||||
|
||||
# SSL DH parameters
|
||||
# Generate new params with `openssl dhparam -out /etc/dovecot/dh.pem 4096`
|
||||
# Or migrate from old ssl-parameters.dat file with the command dovecot
|
||||
# gives on startup when ssl_dh is unset.
|
||||
ssl_dh = </usr/share/dovecot/dh.pem
|
||||
ssl_dh_parameters_length = 2048
|
||||
|
||||
# SSL protocols to use
|
||||
#ssl_protocols = !SSLv3
|
||||
|
||||
@@ -30,6 +30,7 @@ return [
|
||||
'title' => lng('admin.domain_add'),
|
||||
'image' => 'fa-solid fa-globe',
|
||||
'self_overview' => ['section' => 'domains', 'page' => 'domains'],
|
||||
'id' => 'domain_add',
|
||||
'sections' => [
|
||||
'section_a' => [
|
||||
'title' => lng('domains.domainsettings'),
|
||||
|
||||
@@ -30,6 +30,7 @@ return [
|
||||
'title' => lng('admin.domain_edit'),
|
||||
'image' => 'fa-solid fa-globe',
|
||||
'self_overview' => ['section' => 'domains', 'page' => 'domains'],
|
||||
'id' => 'domain_edit',
|
||||
'sections' => [
|
||||
'section_a' => [
|
||||
'title' => lng('domains.domainsettings'),
|
||||
|
||||
@@ -26,6 +26,14 @@
|
||||
use Froxlor\Language;
|
||||
use Froxlor\UI\Request;
|
||||
|
||||
/**
|
||||
* Render a template with the given data.
|
||||
* Mostly used if we have no template-engine (twig).
|
||||
*
|
||||
* @param $template
|
||||
* @param $attributes
|
||||
* @return array|false|string|string[]
|
||||
*/
|
||||
function view($template, $attributes)
|
||||
{
|
||||
$view = file_get_contents(dirname(__DIR__) . '/templates/' . $template);
|
||||
@@ -33,11 +41,26 @@ function view($template, $attributes)
|
||||
return str_replace(array_keys($attributes), array_values($attributes), $view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current translation for a given string.
|
||||
*
|
||||
* @param string $identifier
|
||||
* @param array $arguments
|
||||
* @return array|string
|
||||
*/
|
||||
function lng(string $identifier, array $arguments = [])
|
||||
{
|
||||
return Language::getTranslation($identifier, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a request variable.
|
||||
*
|
||||
* @param string $identifier
|
||||
* @param string|null $default
|
||||
* @param string|null $session
|
||||
* @return mixed|string|null
|
||||
*/
|
||||
function old(string $identifier, string $default = null, string $session = null)
|
||||
{
|
||||
if ($session && isset($_SESSION[$session])) {
|
||||
@@ -45,3 +68,26 @@ function old(string $identifier, string $default = null, string $session = null)
|
||||
}
|
||||
return Request::any($identifier, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading the mix manifest file from given theme.
|
||||
* This file contains the hashed filenames of the assets.
|
||||
* It must be always placed in the theme assets folder.
|
||||
*
|
||||
* @param $filename
|
||||
* @return mixed|string
|
||||
*/
|
||||
function mix($filename)
|
||||
{
|
||||
if (preg_match('/templates\/(.+)\/assets\/(.+)\/(.+)/', $filename, $matches)) {
|
||||
$mixManifest = dirname(__DIR__) . '/templates/' . $matches[1] . '/assets/mix-manifest.json';
|
||||
if (file_exists($mixManifest)) {
|
||||
$manifest = json_decode(file_get_contents($mixManifest), true);
|
||||
$key = '/' . $matches[2] . '/' . $matches[3];
|
||||
if ($manifest && !empty($manifest[$key])) {
|
||||
$filename = 'templates/' . $matches[1] . '/assets' . $manifest[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $filename;
|
||||
}
|
||||
|
||||
@@ -277,14 +277,14 @@ if (is_array($_themeoptions) && array_key_exists('js', $_themeoptions['variants'
|
||||
if (is_array($_themeoptions['variants'][$themevariant]['js'])) {
|
||||
foreach ($_themeoptions['variants'][$themevariant]['js'] as $jsfile) {
|
||||
if (file_exists('templates/' . $theme . '/assets/js/' . $jsfile)) {
|
||||
$js .= '<script type="text/javascript" src="templates/' . $theme . '/assets/js/' . $jsfile . '"></script>' . "\n";
|
||||
$js .= '<script type="text/javascript" src="' . mix('templates/' . $theme . '/assets/js/' . $jsfile) . '"></script>' . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_array($_themeoptions['variants'][$themevariant]['css'])) {
|
||||
foreach ($_themeoptions['variants'][$themevariant]['css'] as $cssfile) {
|
||||
if (file_exists('templates/' . $theme . '/assets/css/' . $cssfile)) {
|
||||
$css .= '<link href="templates/' . $theme . '/assets/css/' . $cssfile . '" rel="stylesheet" type="text/css" />' . "\n";
|
||||
$css .= '<link href="' . mix('templates/' . $theme . '/assets/css/' . $cssfile) . '" rel="stylesheet" type="text/css" />' . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +158,7 @@ return [
|
||||
'docs' => [
|
||||
'label' => lng('admin.documentation'),
|
||||
'icon' => 'fa-solid fa-circle-info',
|
||||
'show_element' => (!Settings::IsInList('panel.customer_hide_options', 'misc.documentation')),
|
||||
'elements' => [
|
||||
[
|
||||
'url' => 'https://docs.froxlor.org/v2/user-guide/',
|
||||
|
||||
Reference in New Issue
Block a user