/
opt
/
cloudlinux
/
venv
/
lib64
/
python3.11
/
site-packages
/
xray
/
manager
/
Upload Filee
HOME
# -*- coding: utf-8 -*- # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENSE.TXT """ This module contains classes implementing X-Ray Manager behaviour for DirectAdmin """ import os import re import subprocess import urllib.parse from collections import ChainMap from glob import glob import chardet from xray import gettext as _ from xray.internal import phpinfo_utils from .base import BaseManager from ..internal.exceptions import XRayManagerError, XRayMissingDomain, XRayManagerExit from ..internal.types import DomainInfo from ..internal.user_plugin_utils import ( user_mode_verification, with_fpm_reload_restricted ) class DirectAdminManager(BaseManager): """ Class implementing an X-Ray manager behaviour for DirectAdmin """ da_options_conf = '/usr/local/directadmin/custombuild/options.conf' da_domain_pattern = '/usr/local/directadmin/data/users/*/domains/*.conf' da_subdomain_pattern = '/usr/local/directadmin/data/users/*/domains/*.subdomains' da_alias_pattern = '/usr/local/directadmin/data/users/*/domains/*.pointers' da_docroot_override_pattern = '/usr/local/directadmin/data/users/*/domains/*.subdomains.docroot.override' VERSIONS_DA = { 'php54': '/usr/local/php54/lib/php.conf.d', 'php55': '/usr/local/php55/lib/php.conf.d', 'php56': '/usr/local/php56/lib/php.conf.d', 'php70': '/usr/local/php70/lib/php.conf.d', 'php71': '/usr/local/php71/lib/php.conf.d', 'php72': '/usr/local/php72/lib/php.conf.d', 'php73': '/usr/local/php73/lib/php.conf.d', 'php74': '/usr/local/php74/lib/php.conf.d', 'php80': '/usr/local/php80/lib/php.conf.d', 'php81': '/usr/local/php81/lib/php.conf.d', 'php82': '/usr/local/php82/lib/php.conf.d', 'php83': '/usr/local/php83/lib/php.conf.d' } def supported_versions(self) -> ChainMap: """ Get supported PHP versions :return: a chained map with basic supported versions and DirectAdmin supported versions """ return ChainMap(self.VERSIONS, self.VERSIONS_DA) def file_readlines(self, filename: str) -> list: """ Read lines from file :param filename: a name of file to read :return: list of stripped lines """ def get_file_encoding(): """ Retrieve file encoding """ with open(filename, 'rb') as f: result = chardet.detect(f.read()) return result['encoding'] try: with open(filename, encoding=get_file_encoding()) as f: return [line.strip() for line in f.readlines()] except OSError as e: self.logger.error('Failed to read [DA conf] file', extra={'fname': filename, 'err': str(e)}) raise XRayManagerExit(_('Failed to read file %s') % filename) from e @property def php_options(self) -> dict: """ Retrieve DirectAdmin PHP settings :return: dict of format {'1': {ver, fpm}, '2': {ver, fpm}...} where '1', '2' etc is an ordinal number of a handler as it is defined in options.conf """ parsed_options = dict() opts = self.file_readlines(self.da_options_conf) def inner_filter(seq, marker): """ Filter PHP release|mode items in seq by marker :param seq: initial sequence :param marker: should be contained in seq item :return: all items from seq containing marker """ return [l for l in seq if marker in l and 'php' in l and not l.startswith('#')] for index, o in enumerate(zip(inner_filter(opts, 'release'), inner_filter(opts, 'mode')), start=1): release, mode = o if 'no' not in release: parsed_options[str(index)] = { 'ver': f"php{''.join(release.split('=')[-1].split('.'))}", 'fpm': 'fpm' in mode, 'handler': mode.split('=')[-1] } return parsed_options @property def main_domains(self) -> dict: """ Retrieve main domains configuration files """ domains = dict() for dom_conf in glob(self.da_domain_pattern): name = os.path.basename(dom_conf).split('.conf')[0] domains[name] = dom_conf return domains @property def subdomains(self) -> dict: """ Retrieve subdomains configuration files """ subdomains = dict() for sub_conf in glob(self.da_subdomain_pattern): for subdom in self.file_readlines(sub_conf): sub_parent = f"{os.path.basename(sub_conf).split('.subdomains')[0]}" sub_name = f"{subdom}.{sub_parent}" subdomains[ sub_name] = f"{sub_conf.split('.subdomains')[0]}.conf" return subdomains @property def aliases(self) -> dict: """ Retrieve aliases configuration files """ aliases = dict() for alias_conf in glob(self.da_alias_pattern): parent_domain_name = alias_conf.split('.pointers')[0] for alias in self.file_readlines(alias_conf): alias_info = alias.split('=') alias_name = alias_info[0] _type = alias_info[-1] if _type == 'pointer': # pointers are not considered as domains, # because they just perform a redirect to parent domain continue aliases[alias_name] = f"{parent_domain_name}.conf" try: for sub in self.file_readlines( f"{parent_domain_name}.subdomains"): aliases[ f"{sub}.{alias_name}"] = f"{parent_domain_name}.conf" except XRayManagerError: # there could be no subdomains pass return aliases @property def subdomains_php_settings(self) -> dict: """ Retrieve subdomains_docroot_override configuration files """ sub_php_set = dict() for sub_doc_override in glob(self.da_docroot_override_pattern): for subdomline in self.file_readlines(sub_doc_override): subdompart, data = urllib.parse.unquote( subdomline).split('=', maxsplit=1) php_select_value = re.search(r'(?<=php1_select=)\d(?=&)', data) if php_select_value is not None: domname = f"{os.path.basename(sub_doc_override).split('.subdomains.docroot.override')[0]}" subdomname = f"{subdompart}.{domname}" sub_php_set[subdomname] = php_select_value.group() return sub_php_set @property def all_sites(self) -> dict: """ Retrieve all domains and subdomains, existing on DA server, including aliases in the form of dict {domain_name: domain_config} :return: {domain_name: domain_config} including subdomains """ da_sites = dict() for bunch in self.main_domains, self.subdomains, self.aliases: da_sites.update(bunch) return da_sites @user_mode_verification @with_fpm_reload_restricted def get_domain_info(self, domain_name: str) -> DomainInfo: """ Retrieve information about given domain from control panel environment: PHP version, user of domain, fpm status :param domain_name: name of domain :return: a DomainInfo object """ try: domain_conf = self.all_sites[domain_name] except KeyError: self.logger.warning( 'Domain does not exist on the server or is a pointer (no task allowed for pointers)', extra={'domain_name': domain_name}) raise XRayMissingDomain(domain_name, message=_("Domain '%(domain_name)s' does not exist on this server " "or is a pointer (no task allowed for pointers)")) data = self.file_readlines(domain_conf) def find_item(item: str) -> str: """ Get config value of item (e.g. item=value) :param item: key to get value of :return: value of item """ found = [line.strip() for line in data if item in line] try: return found[0].split('=')[-1] except IndexError: return '1' opts = self.php_options # Trying to get the subdomain handler first, # get main domain handler if nothing is set for subdomain php_selected = self.subdomains_php_settings.get( domain_name) or find_item('php1_select') if self.phpinfo_mode: config = phpinfo_utils.get_php_configuration( find_item('username'), domain=domain_name) domain_info = DomainInfo( name=domain_name, panel_php_version=config.get_full_php_version('php'), php_ini_scan_dir=config.absolute_ini_scan_dir, # indicates that there is no need to apply selector # and try to resolve php version, the one given in # php_version is final one is_selector_applied=True, user=find_item('username'), panel_fpm=config.is_php_fpm, handler=php_selected ) else: domain_info = DomainInfo(name=domain_name, panel_php_version=opts[php_selected]['ver'], user=find_item('username'), panel_fpm=opts[php_selected]['fpm'], handler=php_selected) self.logger.info( 'Retrieved domain info: domain %s owned by %s uses php version %s', domain_name, domain_info.user, domain_info.handler) return domain_info def panel_specific_selector_enabled(self, domain_info: DomainInfo) -> bool: """ Check if selector is enabled specifically for DirectAdmin Required to be implemented by child classes :param domain_info: a DomainInfo object :return: True if yes, False otherwise """ compatible_handlers = ('suphp', 'lsphp', 'fastcgi') current_handler = self.php_options[domain_info.handler]['handler'] return domain_info.handler == '1' and current_handler in compatible_handlers def fpm_service_name(self, dom_info: DomainInfo) -> str: """ Get DirectAdmin FPM service name :param dom_info: a DomainInfo object :return: FPM service name """ return f'php-fpm{dom_info.panel_php_version[-2:]}' def php_procs_reload(self, domain_info: DomainInfo) -> None: """ Copy xray.so for current version, create ini_location directory Reload FPM service or kill all *php* processes of user :param domain_info: a DomainInfo object """ try: subprocess.run(['/usr/share/alt-php-xray/da_cp_xray', domain_info.panel_php_version[-2:]], capture_output=True, text=True) except self.subprocess_errors as e: self.logger.error('Failed to copy xray.so', extra={'err': str(e), 'info': domain_info}) if self.php_options[domain_info.handler]['handler'] == 'mod_php': try: subprocess.run(['/usr/sbin/service', 'httpd', 'restart'], capture_output=True, text=True) self.logger.info('httpd restarted') except self.subprocess_errors as e: self.logger.error('Failed to restart httpd', extra={'err': str(e), 'info': domain_info}) super().php_procs_reload(domain_info)