Files
Froxlor/lib/Froxlor/Cli/ConfigServices.php
Michael Kaufmann 5f05478c76 improve/update proftpd configuration template; fixes #1148
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-07-24 10:38:44 +02:00

514 lines
18 KiB
PHP

<?php
/**
* This file is part of the Froxlor project.
* Copyright (c) 2010 the Froxlor Team (see authors).
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* 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. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you can also view it online at
* https://files.froxlor.org/misc/COPYING.txt
*
* @copyright the authors
* @author Froxlor team <team@froxlor.org>
* @license https://files.froxlor.org/misc/COPYING.txt GPLv2
*/
namespace Froxlor\Cli;
use Froxlor\Config\ConfigParser;
use Froxlor\Database\Database;
use Froxlor\FileDir;
use Froxlor\Froxlor;
use Froxlor\PhpHelper;
use Froxlor\Settings;
use Froxlor\SImExporter;
use Froxlor\System\Crypt;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
final class ConfigServices extends CliCommand
{
private $yes_to_all_supported = [
'bookworm',
'bionic',
'bullseye',
'buster',
'focal',
'jammy',
];
protected function configure()
{
$this->setName('froxlor:config-services');
$this->setDescription('Configure system services');
$this->addOption('create', 'c', InputOption::VALUE_NONE, 'Create a services list configuration for the --apply option.')
->addOption('apply', 'a', InputOption::VALUE_REQUIRED, 'Configure your services by given configuration file/string. To create one run the command with the --create option.')
->addOption('list', 'l', InputOption::VALUE_NONE, 'Output the services that are going to be configured using a given config file (--apply option). No services will be configured.')
->addOption('daemon', 'd', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'When used with --apply you can specify one or multiple daemons. These will be the only services that get configured.')
->addOption('import-settings', 'i', InputOption::VALUE_REQUIRED, 'Import settings from another froxlor installation. This can be done standalone or in addition to --apply.')
->addOption('yes-to-all', 'A', InputOption::VALUE_NONE, 'Install packages without asking questions (Debian/Ubuntu only currently)');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$result = self::SUCCESS;
$result = $this->validateRequirements($input, $output);
require Froxlor::getInstallDir() . '/lib/functions.php';
if ($result == self::SUCCESS && $input->getOption('import-settings') == false && $input->getOption('create') == false && $input->getOption('apply') == false) {
$output->writeln('<error>No option given to do something, exiting.</>');
return self::INVALID;
}
// import settings if given
if ($result == self::SUCCESS && $input->getOption('import-settings')) {
$result = $this->importSettings($input, $output);
}
if ($result == self::SUCCESS && $input->getOption('yes-to-all')) {
if (in_array(Settings::Get('system.distribution'), $this->yes_to_all_supported)) {
putenv("DEBIAN_FRONTEND=noninteractive");
exec("echo 'APT::Get::Assume-Yes \"true\";' > /tmp/_tmp_apt.conf");
putenv("APT_CONFIG=/tmp/_tmp_apt.conf");
} else {
$output->writeln('<comment>--yes-to-all ignored, not configured for supported distribution</>');
}
}
if ($result == self::SUCCESS) {
$io = new SymfonyStyle($input, $output);
if ($input->getOption('create')) {
$result = $this->createConfig($input, $output, $io);
} elseif ($input->getOption('apply')) {
$result = $this->applyConfig($input, $output, $io);
} elseif ($input->getOption('list') || $input->getOption('daemon')) {
$output->writeln('<error>Options --list and --daemon only work together with --apply.</>');
$result = self::INVALID;
}
}
if ($input->getOption('yes-to-all') && in_array(Settings::Get('system.distribution'), $this->yes_to_all_supported)) {
putenv("DEBIAN_FRONTEND");
unlink("/tmp/_tmp_apt.conf");
putenv("APT_CONFIG");
}
return $result;
}
private function importSettings(InputInterface $input, OutputInterface $output)
{
$importFile = $input->getOption('import-settings');
if (strtoupper(substr($importFile, 0, 4)) == 'HTTP') {
$output->writeln("Settings file seems to be an URL, trying to download");
$target = "/tmp/froxlor-import-settings-" . time() . ".json";
if (@file_exists($target)) {
@unlink($target);
}
$this->downloadFile($importFile, $target);
$importFile = $target;
}
if (!is_file($importFile)) {
$output->writeln('<error>Given settings file is not a file</>');
return self::INVALID;
} elseif (!file_exists($importFile)) {
$output->writeln('<error>Given settings file cannot be found (' . $importFile . ')</>');
return self::INVALID;
} elseif (!is_readable($importFile)) {
$output->writeln('<error>Given settings file cannot be read (' . $importFile . ')</>');
return self::INVALID;
}
$imp_content = file_get_contents($importFile);
SImExporter::import($imp_content);
$output->writeln("<info>Successfully imported settings from '" . $input->getOption('import-settings') . "'</info>");
return self::SUCCESS;
}
private function downloadFile($src, $dest)
{
set_time_limit(0);
// This is the file where we save the information
$fp = fopen($dest, 'w+');
// Here is the file we are downloading, replace spaces with %20
$ch = curl_init(str_replace(" ", "%20", $src));
curl_setopt($ch, CURLOPT_TIMEOUT, 50);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// write curl response to file
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// get curl response
curl_exec($ch);
curl_close($ch);
fclose($fp);
}
private function createConfig(InputInterface $input, OutputInterface $output, SymfonyStyle $io)
{
$_daemons_config = [
'distro' => ""
];
$config_dir = Froxlor::getInstallDir() . '/lib/configfiles/';
// show list of available distro's
$distros = glob($config_dir . '*.xml');
// tmp array
$distributions_select_data = [];
//set default os.
$os_dist = ['ID' => 'bullseye'];
$os_version = ['0' => '11'];
$os_default = $os_dist['ID'];
//read os-release
if (file_exists('/etc/os-release')) {
$os_dist = parse_ini_file('/etc/os-release', false);
if (is_array($os_dist) && array_key_exists('ID', $os_dist) && array_key_exists('VERSION_ID', $os_dist)) {
$os_version = explode('.', $os_dist['VERSION_ID'])[0];
}
}
// read in all the distros
foreach ($distros as $_distribution) {
// get configparser object
$dist = new ConfigParser($_distribution);
// get distro-info
$dist_display = $dist->getCompleteDistroName();
// store in tmp array
$distributions_select_data[$dist_display] = str_replace(".xml", "", strtolower(basename($_distribution)));
//guess if this is the current distro.
$ver = explode('.', $dist->distributionVersion)[0];
if (strtolower($os_dist['ID']) == strtolower($dist->distributionName) && $os_version == $ver) {
$os_default = str_replace(".xml", "", strtolower(basename($_distribution)));
}
}
// sort by distribution name
ksort($distributions_select_data);
// list all distributions
$table_rows = [];
$valid_dists = [];
foreach ($distributions_select_data as $name => $filename) {
$table_rows[] = [$filename, $name];
$valid_dists[] = $filename;
}
$io->table(
['ID', 'Distribution'],
$table_rows
);
$_daemons_config['distro'] = $io->choice('Choose distribution', $valid_dists, $os_default);
// go through all services and let user check whether to include it or not
$configfiles = new ConfigParser($config_dir . '/' . $_daemons_config['distro'] . ".xml");
$services = $configfiles->getServices();
foreach ($services as $si => $service) {
$output->writeln("--- " . strtoupper($si) . " ---");
$_daemons_config[$si] = "";
$daemons = $service->getDaemons();
$default_daemon = "";
$table_rows = [];
$valid_options = [];
if ($si != 'system') {
$table_rows[] = ['x', 'No'];
$valid_options[] = 'x';
}
foreach ($daemons as $di => $dd) {
$title = $dd->title;
if ($dd->default) {
$default_daemon = $di;
$title .= " (default)";
}
$table_rows[] = [$di, $title];
$valid_options[] = $di;
}
$io->table(
['Value', 'Name'],
$table_rows
);
$daemons['x'] = 'x';
if ($si == 'system') {
$_daemons_config[$si] = [];
// for the system/other services we need a multiple choice possibility
$output->writeln("<comment>Select every service you need. Enter empty value when done</>");
$sysservice = "";
do {
$sysservice = $io->ask('Choose service');
if (!empty($sysservice)) {
$_daemons_config[$si][] = $sysservice;
}
} while (!empty($sysservice));
// add 'cron' as fixed part (doesn't hurt if it exists)
if (!in_array('cron', $_daemons_config[$si])) {
$_daemons_config[$si][] = 'cron';
}
} else {
// for all others -> only one value
$_daemons_config[$si] = $io->choice('Choose service', $valid_options, $default_daemon);
}
}
$daemons_config = json_encode($_daemons_config);
$output_file = $io->ask("Choose output-filename", "/tmp/froxlor-config-" . date('Ymd') . ".json");
file_put_contents($output_file, $daemons_config);
$output->writeln("<info>Successfully generated service-configfile '" . $output_file . "'</>");
$output->writeln([
"",
"<info>You can now apply this config running:</>",
"php " . Froxlor::getInstallDir() . "bin/froxlor-cli froxlor:config-services --apply=" . $output_file,
""
]);
$proceed = $io->confirm("Do you want to apply the config now?", false);
if ($proceed) {
passthru("php " . Froxlor::getInstallDir() . "bin/froxlor-cli froxlor:config-services --apply=" . $output_file);
}
return self::SUCCESS;
}
private function applyConfig(InputInterface $input, OutputInterface $output, SymfonyStyle $io)
{
$applyFile = $input->getOption('apply');
// check if plain JSON
$decoded_config = json_decode($applyFile, true);
$skipFileCheck = false;
if (json_last_error() == JSON_ERROR_NONE) {
$skipFileCheck = true;
}
if (!$skipFileCheck) {
if (strtoupper(substr($applyFile, 0, 4)) == 'HTTP') {
$output->writeln("Config file seems to be an URL, trying to download");
$target = "/tmp/froxlor-config-" . time() . ".json";
if (@file_exists($target)) {
@unlink($target);
}
$this->downloadFile($applyFile, $target);
$applyFile = $target;
}
if (!is_file($applyFile)) {
$output->writeln('<error>Given config file is not a file</>');
return self::INVALID;
} elseif (!file_exists($applyFile)) {
$output->writeln('<error>Given config file cannot be found (' . $applyFile . ')</>');
return self::INVALID;
} elseif (!is_readable($applyFile)) {
$output->writeln('<error>Given config file cannot be read (' . $applyFile . ')</>');
return self::INVALID;
}
$config = file_get_contents($applyFile);
$decoded_config = json_decode($config, true);
}
if ($input->getOption('list') != false) {
$table_rows = [];
foreach ($decoded_config as $service => $daemon) {
if (is_array($daemon) && count($daemon) > 0) {
foreach ($daemon as $sysdaemon) {
$table_rows[] = [$service, $sysdaemon];
}
} else {
if ($daemon == 'x') {
$daemon = '--- skipped ---';
}
$table_rows[] = [$service, $daemon];
}
}
$io->table(
['Service', 'Selected daemon'],
$table_rows
);
return self::SUCCESS;
}
$only_daemon = [];
if ($input->getOption('daemon') != false) {
$only_daemon = $input->getOption('daemon');
}
if (!empty($decoded_config)) {
$config_dir = Froxlor::getInstallDir() . 'lib/configfiles/';
$configfiles = new ConfigParser($config_dir . '/' . $decoded_config['distro'] . ".xml");
$services = $configfiles->getServices();
$replace_arr = $this->getReplacerArray();
// be sure the fallback certificate specified in the settings exists
$certFile = Settings::Get('system.ssl_cert_file');
$keyFile = Settings::Get('system.ssl_key_file');
if (empty($certFile) || empty($keyFile) || !file_exists($certFile) || !file_exists($keyFile)) {
$output->writeln('<comment>Creating missing certificate ' . $certFile . '</>');
Crypt::createSelfSignedCertificate();
}
foreach ($services as $si => $service) {
$output->writeln("--- Configuring: " . strtoupper($si) . " ---");
if (!isset($decoded_config[$si]) || $decoded_config[$si] == 'x') {
$output->writeln('<comment>Skipping ' . strtoupper($si) . ' configuration as desired</>');
continue;
}
$daemons = $service->getDaemons();
foreach ($daemons as $di => $dd) {
// check for desired service
if (($si != 'system' && $decoded_config[$si] != $di) || (is_array($decoded_config[$si]) && !in_array($di, $decoded_config[$si]))) {
continue;
}
$output->writeln("Configuring '" . $di . "'");
if (!empty($only_daemon) && !in_array($di, $only_daemon)) {
$output->writeln('<comment>Skipping ' . $di . ' configuration as desired</>');
continue;
}
// run all cmds
$confarr = $dd->getConfig();
foreach ($confarr as $action) {
switch ($action['type']) {
case "install":
$output->writeln("Installing required packages");
$result = null;
passthru(strtr($action['content'], $replace_arr), $result);
if (strlen($result) > 1) {
echo $result;
}
break;
case "command":
exec(strtr($action['content'], $replace_arr));
break;
case "file":
if (array_key_exists('content', $action)) {
$output->writeln('<comment>Creating file "' . $action['name'] . '"</>');
file_put_contents($action['name'], trim(strtr($action['content'], $replace_arr)));
} elseif (array_key_exists('subcommands', $action)) {
foreach ($action['subcommands'] as $fileaction) {
if (array_key_exists('execute', $fileaction) && $fileaction['execute'] == "pre") {
exec(strtr($fileaction['content'], $replace_arr));
} elseif (array_key_exists('execute', $fileaction) && $fileaction['execute'] == "post") {
exec(strtr($fileaction['content'], $replace_arr));
} elseif ($fileaction['type'] == 'file') {
$output->writeln('<comment>Creating file "' . $fileaction['name'] . '"</>');
file_put_contents($fileaction['name'], trim(strtr($fileaction['content'], $replace_arr)));
}
}
}
break;
}
}
}
}
// set is_configured flag
Settings::Set('panel.is_configured', '1', true);
// run cronjob at the end to ensure configs are all up to date
exec('php ' . Froxlor::getInstallDir() . 'bin/froxlor-cli froxlor:cron --force');
// and done
$output->writeln('<info>All services have been configured</>');
return self::SUCCESS;
} else {
$output->writeln('<error>Unable to decode given JSON file</>');
return self::INVALID;
}
}
private function getReplacerArray()
{
$customer_tmpdir = '/tmp/';
if (Settings::Get('system.mod_fcgid') == '1' && Settings::Get('system.mod_fcgid_tmpdir') != '') {
$customer_tmpdir = Settings::Get('system.mod_fcgid_tmpdir');
} elseif (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.tmpdir') != '') {
$customer_tmpdir = Settings::Get('phpfpm.tmpdir');
}
// try to convert namserver hosts to ip's
$ns_ips = "";
$known_ns_ips = [];
if (Settings::Get('system.nameservers') != '') {
$nameservers = explode(',', Settings::Get('system.nameservers'));
foreach ($nameservers as $nameserver) {
$nameserver = trim($nameserver);
// DNS servers might be multi homed; allow transfer from all ip
// addresses of the DNS server
$nameserver_ips = PhpHelper::gethostbynamel6($nameserver);
// append dot to hostname
if (substr($nameserver, -1, 1) != '.') {
$nameserver .= '.';
}
// ignore invalid responses
if (!is_array($nameserver_ips)) {
// act like PhpHelper::gethostbynamel6() and return unmodified hostname on error
$nameserver_ips = [
$nameserver
];
} else {
$known_ns_ips = array_merge($known_ns_ips, $nameserver_ips);
}
if (!empty($ns_ips)) {
$ns_ips .= ',';
}
$ns_ips .= implode(",", $nameserver_ips);
}
}
// AXFR server
if (Settings::Get('system.axfrservers') != '') {
$axfrservers = explode(',', Settings::Get('system.axfrservers'));
foreach ($axfrservers as $axfrserver) {
if (!in_array(trim($axfrserver), $known_ns_ips)) {
if (!empty($ns_ips)) {
$ns_ips .= ',';
}
$ns_ips .= trim($axfrserver);
}
}
}
Database::needSqlData();
$sql = Database::getSqlData();
$replace_arr = [
'<SQL_UNPRIVILEGED_USER>' => $sql['user'],
'<SQL_UNPRIVILEGED_PASSWORD>' => $sql['passwd'],
'<SQL_DB>' => $sql['db'],
'<SQL_HOST>' => $sql['host'],
'<SQL_SOCKET>' => isset($sql['socket']) ? $sql['socket'] : null,
'<SERVERNAME>' => Settings::Get('system.hostname'),
'<SERVERIP>' => Settings::Get('system.ipaddress'),
'<NAMESERVERS>' => Settings::Get('system.nameservers'),
'<NAMESERVERS_IP>' => $ns_ips,
'<VIRTUAL_MAILBOX_BASE>' => Settings::Get('system.vmail_homedir'),
'<VIRTUAL_UID_MAPS>' => Settings::Get('system.vmail_uid'),
'<VIRTUAL_GID_MAPS>' => Settings::Get('system.vmail_gid'),
'<SSLPROTOCOLS>' => (Settings::Get('system.use_ssl') == '1') ? 'imaps pop3s' : '',
'<CUSTOMER_TMP>' => FileDir::makeCorrectDir($customer_tmpdir),
'<BASE_PATH>' => Froxlor::getInstallDir(),
'<BIND_CONFIG_PATH>' => FileDir::makeCorrectDir(Settings::Get('system.bindconf_directory')),
'<WEBSERVER_RELOAD_CMD>' => Settings::Get('system.apachereload_command'),
'<CUSTOMER_LOGS>' => FileDir::makeCorrectDir(Settings::Get('system.logfiles_directory')),
'<FPM_IPCDIR>' => FileDir::makeCorrectDir(Settings::Get('phpfpm.fastcgi_ipcdir')),
'<WEBSERVER_GROUP>' => Settings::Get('system.httpgroup'),
'<SSL_CERT_FILE>' => Settings::Get('system.ssl_cert_file'),
'<SSL_KEY_FILE>' => Settings::Get('system.ssl_key_file'),
];
return $replace_arr;
}
}