📁 File Manager Pro
v10.0.3 | PHP: 8.1.34
Server: Apache
2026-06-22 06:13:03
📂
/ (Root)
/
opt
/
cloudlinux
/
venv
/
lib
/
python3.11
/
site-packages
/
cl_plus
/
utils
📍 /opt/cloudlinux/venv/lib/python3.11/site-packages/cl_plus/utils
🔄 Refresh
✏️
Editing: web_server_helper.py
Read Only
# coding=utf-8 # # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2020 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENCE.TXT # import logging import glob import os from traceback import format_exc from lxml import etree # NOQA from clcommon.utils import run_command, ExternalProgramFailed, grep, is_litespeed_running from clcommon.cpapi import get_apache_ports_list, get_apache_connections_number, get_apache_max_request_workers class WebServerHelper: """ Helper class for apache/Litespeed collector. """ APACHE_NAME = 'httpd' LITESPEED_NAME = 'litespeed' def __init__(self, _logger: logging.Logger): self._logger = _logger self._web_ports_list = [] self._is_apache = False self._is_litespeed = False self._netstat_bin = '/bin/netstat' self._no_server_message = 'There is no server working at 80 port' self._lsws_config = '/usr/local/lsws/conf/httpd_config.xml' self._is_server_absent_logged = False self.detect_active_server() def get_total_connections(self): """ Get web server total connections (from web server config) :return: tuple (max_req_num, message) max_req_num - Maximum request apache workers number or 0 if error message - OK/Error text """ if self._is_apache: return get_apache_max_request_workers() elif self._is_litespeed: return self._get_ls_total_connections() else: return 0, self._no_server_message def get_current_connections_number(self): """ Retrieves web server's current connections number from system netstat utility :return: tuple (conn_num, message) conn_num - current connections number message - OK/Error text """ # Read apache ports list. Litespeed uses apache's config, so this method works for both servers _httpd_ports_list = get_apache_ports_list() if self._is_litespeed: port_offset, message = self._get_ls_apache_port_offset() if message == "OK": _httpd_ports_list = [port + port_offset for port in _httpd_ports_list] else: return 0, message return self._get_current_connections_number_from_netstat(_httpd_ports_list) def get_connections_number(self): """ Retrieves web server connections number (from apache's mod_status or analog mechanism) :return: tuple (conn_num, message) conn_num - current connections number, 0 if error message - OK/Error text """ if self._is_apache: return get_apache_connections_number() elif self._is_litespeed: return self._get_ls_get_connections_number() else: return 0, self._no_server_message def detect_active_server(self): """ Determine is httpd/Litespeed running :return: True/False running/not running """ main_server_name = self._get_main_web_server_name() if not main_server_name: # Apache/Litespeed not working self._is_apache = False self._is_litespeed = False if not self._is_server_absent_logged: self._logger.info("Apache/Litespeed collector: Apache or Litespeed stopped or absent, collector will not work") self._is_server_absent_logged = True return False self._is_server_absent_logged = False self._web_ports_list = get_apache_ports_list() # Both httpd and litespeed working, select by port if main_server_name == self.LITESPEED_NAME: if not self._is_litespeed: self._logger.info("Apache/Litespeed collector: Using Litespeed") self._is_litespeed = True self._is_apache = False elif main_server_name == self.APACHE_NAME: if not self._is_apache: self._logger.info("Apache/Litespeed collector: Using Apache") self._is_apache = True self._is_litespeed = False return self._is_apache or self._is_litespeed def _get_current_connections_number_from_netstat(self, ports_list): """ Retrieves web server's current connections number from system netstat utility :param ports_list: Port list to search :return: tuple (conn_num, message) conn_num - current connections number message - OK/Error text """ # Build grep line, for example ':80|:443' _grep_ports_line = '|'.join([':{}'.format(val) for val in ports_list]) # /bin/netstat -nt | egrep ':80|:443' | wc -l try: # /bin/netstat -nt cmd_list = [self._netstat_bin, '-nt'] std_out = run_command(cmd_list) std_out_list = std_out.split('\n') out_list = list(grep(_grep_ports_line, match_any_position=True, multiple_search=True, data_from_file=std_out_list)) return len(out_list), 'OK' except ExternalProgramFailed: return 0, format_exc() def _get_ls_total_connections(self): """ Retrieve litespeed total connection number (from config) :return: tuple (max_conn, message) max_conn - Litespeed's total connection number or 0 if error message - OK/Trace """ try: # grep "maxConnections" /usr/local/lsws/conf/httpd_config.xml # XML path httpServerConfig/tuning/maxConnections with open(self._lsws_config) as f: x = etree.parse(f).getroot() element_list = x.xpath("tuning/maxConnections") max_conn = element_list[0].text return int(max_conn), "OK" except (OSError, IOError, IndexError, ValueError, etree.XMLSyntaxError, etree.XMLSchemaParseError, etree.XMLSchemaError): return 0, format_exc() def _get_ls_get_connections_number(self): """ Retrieve litespeed connections number (from LS statistics) :return: tuple (max_conn, message) max_conn - Litespeed's connections number or 0 if error message - OK/Trace """ # NOTE: do NOT add an allowlist on the symlink target directory here. # An earlier iteration restricted real_path to /dev/shm/lsws/status/ # (the layout LiteSpeed >=5.3.6 uses), which silently broke metric # collection on older LSWS or any deployment with a non-default # STATS_DIR — there `.rtreport*` files live as real files directly # in /tmp/lshttpd/. The function is read-only, content is bounded by # the 64 KB cap in _get_connections_num_from_ls_temp_file, and only # an int(TOT_REQS) is returned — there is no data-exfil channel that # would justify the allowlist. # If you ever extend _get_connections_num_from_ls_temp_file to log, # write, or exec the file content, re-add the allowlist (track via # CLPRO-3066 #15 follow-up). try: con_num = 0 # ls -la /tmp/lshttpd/.rtr* ls_status_files_list = glob.glob('/tmp/lshttpd/.rtreport*') # ['/tmp/lshttpd/.rtreport.2', '/tmp/lshttpd/.rtreport'] for ls_filename in ls_status_files_list: # realpath() resolves the symlink (modern LSWS layout — files # under /tmp/lshttpd/ are symlinks into /dev/shm/lsws/status/); # for the legacy layout it is a no-op. Passing the resolved # path to the opener means O_NOFOLLOW sees the real file, not # the symlink, so the open succeeds in both layouts. real_path = os.path.realpath(ls_filename) if not os.path.isfile(real_path): self._logger.warning('Skipping %s, because symlink does not point to real file', ls_filename) continue con_num += self._get_connections_num_from_ls_temp_file(real_path) return con_num, "OK" except (OSError, IOError, ValueError): return 0, format_exc() @staticmethod def _get_connections_num_from_ls_temp_file(ls_temp_filename: str): """ Retrieve connections number from litespeed's plain text answer :param ls_temp_filename: string with litespeed;s answer :return: tuple (conn_num, message) conn_num - current connections number, 0 if error """ fd = os.open(ls_temp_filename, os.O_RDONLY | os.O_NOFOLLOW) with os.fdopen(fd, 'r') as content_file: content = content_file.read(65536) # content example: # VERSION: LiteSpeed Web Server/Enterprise/5.4.9 # UPTIME: 1 day 17:41:40 # BPS_IN: 0, BPS_OUT: 0, SSL_BPS_IN: 0, SSL_BPS_OUT: 0 # MAXCONN: 10000, MAXSSL_CONN: 10000, PLAINCONN: 0, AVAILCONN: 9999, IDLECONN: 0, SSLCONN: 1, AVAILSSL: 9999 # REQ_RATE []: REQ_PROCESSING: 1, REQ_PER_SEC: 0.0, TOT_REQS: 54997, PUB_CACHE_HITS_PER_SEC: 0.0, TOTAL_PUB_CACHE_HITS: 0, PRIVATE_CACHE_HITS_PER_SEC: 0.0, TOTAL_PRIVATE_CACHE_HITS: 0, STATIC_HITS_PER_SEC: 0.0, TOTAL_STATIC_HITS: 50018 # REQ_RATE [_AdminVHost]: REQ_PROCESSING: 1, REQ_PER_SEC: 0.0, TOT_REQS: 4989, PUB_CACHE_HITS_PER_SEC: 0.0, TOTAL_PUB_CACHE_HITS: 0, PRIVATE_CACHE_HITS_PER_SEC: 0.0, TOTAL_PRIVATE_CACHE_HITS: 0, STATIC_HITS_PER_SEC: 0.0, TOTAL_STATIC_HITS: 10 # BLOCKED_IP: # EOF # Extract and return TOT_REQS metric value lines_list = content.split('\n') empty_rrate_lines = list(grep('REQ_RATE []:', fixed_string=True, match_any_position=False, multiple_search=True, data_from_file=lines_list)) for line in empty_rrate_lines: line = line.strip() # line example: # REQ_RATE []: REQ_PROCESSING: 0, REQ_PER_SEC: 0.0, TOT_REQS: 50448, .... l_parts = line.split(',') # Search 'TOT_REQS' parameter for l_part in l_parts: # l_part example: ' REQ_PER_SEC: 0.0' l_part = l_part.strip() if l_part.startswith('TOT_REQS:'): return int(l_part.replace('TOT_REQS:', '').strip()) return 0 def _get_main_web_server_name(self): """ Detects main web server (httpd or litespeed) :return: Name - Main server name: 'litespeed', 'httpd', None """ if is_litespeed_running(): return self.LITESPEED_NAME try: # /sbin/service httpd status # retcode != 0 - litespeed/httpd not running # == 0 - litespeed/httpd running # if 'litespeed' present in stdout - this is litespeed # else - httpd returncode, _, _ = run_command(['/sbin/service', 'httpd', 'status'], return_full_output=True) if returncode != 0: return None return self.APACHE_NAME except ExternalProgramFailed: pass return None def _get_ls_apache_port_offset(self): """ Get Apache port offset for Litespeed There are several cases to handle: - port is defined in config <apachePortOffset>1234</apachePortOffset> => offset = 1234 - parameter is absent in config => offset = 0 - parameter is empty => offset = 0 - errors while reading config => offset = 0 """ offset = 0 try: # grep "apachePortOffset" /usr/local/lsws/conf/httpd_config.xml # XML path httpServerConfig/apachePortOffset with open(self._lsws_config) as f: x = etree.parse(f).getroot() element_list = x.xpath("apachePortOffset") if element_list: offset = element_list[0].text or 0 return int(offset), "OK" except (OSError, IOError, IndexError, ValueError, etree.XMLSyntaxError, etree.XMLSchemaParseError, etree.XMLSchemaError): return 0, format_exc()
💾 Save Changes
❌ Cancel