????
Current Path : /usr/local/ssl/share/cagefs/ |
Current File : //usr/local/ssl/share/cagefs/cagefslib.py |
# -*- coding: utf-8 -*- # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENSE.TXT from __future__ import print_function from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import locale from typing import AnyStr, Dict, List, Optional from future import standard_library standard_library.install_aliases() from builtins import * import errno import re import sys import shutil import glob import subprocess import inspect import pickle import configparser import time import filecmp import syslog import traceback import io import datetime import stat import os import functools # provides functions for secure filesystem and I/O operations from secureio import read_file_secure, write_file_secure, set_user_perm, open_file_not_symlink, set_root_perm from secureio import create_dir_secure, closefd, set_owner_dir_secure, set_perm_dir_secure from secureio import root_flag, print_error, get_groups, clpwd, SILENT_FLAG, logging, get_perm # Provides functions to detect different control panels import cldetectlib from clcagefslib.const import CL_ALT_NAME, ETC_CL_ALT_PATH, BASEDIR from clcagefslib.fs import get_linksafe_gid, get_user_prefix from clcagefslib.io import make_userdir, read_file, read_file_cached from clcagefslib.selector.configure import is_ea4_enabled from clcagefslib.selector.paths import get_alt_dirs from clcommon.clfunc import byteify, unicodeify from clcommon import ClPwd, clcaptain from clcommon.const import Feature from clcommon.cpapi import is_panel_feature_supported import phpinivalidator from signals_handlers import sigterm_check from clcommon.utils import ( ExternalProgramFailed, is_socket_file, mod_makedirs, ) from cldetectlib import get_boolean_param, CL_CONFIG_FILE from logs import logger class CageFSException(Exception): def __init__(self, *args, **kwargs): Exception.__init__(self, *args, **kwargs) # This variables are set by cagefsctl module ETC_VERSION_NAME = ".etc.version" ETC_VERSION = "/" + ETC_VERSION_NAME # CageFS global settings & values CAGEFS_INI = '/etc/cagefs/cagefs.ini' PHP_CONF = '/etc/cl.selector/php.conf' ETC_TEMPLATE_DIR = "/usr/share/cagefs" ETC_TEMPLATE_NEW_DIR = "/usr/share/cagefs/etc.new" VAR_RUN_CAGEFS = '/var/run/cagefs' VERBOSE_FLAG = 0 # Validate PHP options or not validate_alt_php_ini = False PHP_OPTIONS_LOGFILE = "/var/log/cagefs-php-opt-check.log" php_log_opt = None GLOBAL_PLESK_CFG = '/etc/psa/psa.conf' FALLBACK_PLESK_VHOSTS_D = '/var/www/vhosts' SYSTEMD_JOURNAL_SOCKET = '/run/systemd/journal/dev-log' LOG_SOCKET = '/usr/share/cagefs-skeleton/dev/log' DEV_LOG_SOCKET = '/dev/log' SYSCONFIG_SYSLOG = "/etc/sysconfig/syslog" CAGEFS_SOCKET = " -a " + LOG_SOCKET RSYSLOG_CONF = '/etc/rsyslog.conf' CHROOT_CONF = '/etc/rsyslog.d/cagefs-syslog-socket.conf' CHROOT_OLD_CONF = '/etc/rsyslog.d/schroot.conf' def touch(fname): """ /bin/touch analog - update timestamp of a file if it exists or create a file otherwise :param fname: file path :type fname: string """ try: os.utime(fname, None) except OSError: open(fname, 'a').close() def _add_syslog_socket_for_syslog_pkg() -> None: """ Add syslog socket into CageFS, add it to syslog config and restart syslog service """ def _insert(original, new, pos): """ Inserts new inside original at pos. """ return original[:pos] + new + original[pos:] lines = read_file(SYSCONFIG_SYSLOG) for i, _ in enumerate(lines): if lines[i].startswith("SYSLOGD_OPTIONS"): # CageFS socket is not added yet ? if lines[i].find(CAGEFS_SOCKET) == -1: if lines[i][-2] == '"' or lines[i][-2] == "'": tmp = _insert(lines[i], CAGEFS_SOCKET, -2) lines[i] = tmp else: tmp = _insert(lines[i], CAGEFS_SOCKET, -1) lines[i] = tmp break write_file(SYSCONFIG_SYSLOG, lines, make_backup=True) ExecuteSimple('/sbin/service syslog restart &> /dev/null') def _add_syslog_socket_for_rsyslog_pkg() -> None: """ Add syslog socket into CageFS, add it to rsyslog config and restart rsyslog service """ chroot_conf_content = f'$AddUnixListenSocket {LOG_SOCKET}\n' lines = read_file(RSYSLOG_CONF) for i, _ in enumerate(lines): pos = lines[i].find('$ModLoad imuxsock') if pos not in (-1, 0): # enable module `imuxsock` if it was disabled lines[i] = lines[i][pos:] break # if exists old rsyslog config file then unlink it if os.path.isfile(CHROOT_OLD_CONF): with open(CHROOT_OLD_CONF, "r") as f: old_content = f.read() if old_content == chroot_conf_content: os.unlink(CHROOT_OLD_CONF) write_file( RSYSLOG_CONF, lines, make_backup=True, ) write_file( CHROOT_CONF, [chroot_conf_content], make_backup=True, ) ExecuteSimple('/sbin/service rsyslog restart &> /dev/null') def add_syslog_socket() -> None: """ Add cagefs skeleton syslog socket to syslog config file. Create .conf file for rsyslog Restart syslog/rsyslog service """ if is_new_syslog_socket_used(): if is_old_syslog_socket_in_cage(): # We should disable using of older version of socket in CageFS # if server has systemd-journal package which uses socket by path # SYSTEMD_JOURNAL_SOCKET. # The newer version of socket is mounted in function # cagefsctl._mount_systemd_journal_socket which is used at moment # mounting of CageFS skeleton # see for details: CAG-1062: Mount socket of systemd-journal into CageFS remove_syslog_socket() # touch /usr/share/cagefs/need.remount for future remounting already # mounted users because we want to provide changes to them touch('/usr/share/cagefs/need.remount') elif os.path.isfile(SYSCONFIG_SYSLOG): _add_syslog_socket_for_syslog_pkg() elif os.path.isfile(RSYSLOG_CONF): _add_syslog_socket_for_rsyslog_pkg() def remove_syslog_socket(): """ Remove syslog socket info for cagefs from system syslog configs Restart syslog/rsyslog service """ if os.path.isfile(SYSCONFIG_SYSLOG): lines = read_file(SYSCONFIG_SYSLOG) for i, _ in enumerate(lines): if lines[i].startswith("SYSLOGD_OPTIONS"): tmp = lines[i].replace(CAGEFS_SOCKET, "") lines[i] = tmp break write_file( SYSCONFIG_SYSLOG, lines, make_backup=True, ) ExecuteSimple('/sbin/service syslog restart &> /dev/null') if os.path.isfile(CHROOT_CONF): try: os.unlink(CHROOT_CONF) except OSError as e: print_error('removing', CHROOT_CONF, ':', str(e)) ExecuteSimple('/sbin/service rsyslog restart &> /dev/null') def is_new_syslog_socket_used() -> bool: """ File `/dev/log` is symlink to socket `/run/systemd/journal/dev-log` if server uses the newer version of syslog socket """ return os.path.islink(DEV_LOG_SOCKET) and \ os.path.realpath(DEV_LOG_SOCKET) == SYSTEMD_JOURNAL_SOCKET and \ is_socket_file(SYSTEMD_JOURNAL_SOCKET) def is_old_syslog_socket_in_cage() -> bool: """ Return True if CageFS has into self an old syslog socket """ return is_socket_file(LOG_SOCKET) def getItem(txt1, txt2, op): try: i1 = int(txt1) except ValueError: i1 = -1 try: i2 = int(txt2) except ValueError: i2 = -1 if i1 == -1 or i2 == -1: if op == 0: return txt1 > txt2 else: return txt1 < txt2 else: if op == 0: return i1 > i2 else: return i1 < i2 # Compare version of types xx.xx.xxx... and yyy.yy.yy.y.. # if xxx and yyy is numbers, than comapre as numbers # else - compare as strings def verCompare(base, test): base = base.split(".") test = test.split(".") if (len(base) > len(test)): ln = len(test) else: ln = len(base) for i in range(ln): if getItem(base[i], test[i], 0): return 1 if getItem(base[i], test[i], 1): return -1 if len(base) == len(test): return 0 elif len(base) > len(test): return 1 else: return -1 def unlink(path): try: os.unlink(path) except OSError: pass # Write message to php options log file def php_options_log_write(msg, unknown_options_list, invalid_values_options_list, invalid_options_list): global php_log_opt root_flag_saved = root_flag if not root_flag: uid, gid = get_perm() set_root_perm() try: if php_log_opt is None: umask_saved = os.umask(0o77) # log_file is opened in "line buffered" mode php_log_opt = open(PHP_OPTIONS_LOGFILE, 'a', 1) os.umask(umask_saved) php_log_opt.write(datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S") + ": " + msg + "\n") if unknown_options_list: php_log_opt.write(" - The following options have been disabled as unknown:\n") for option in unknown_options_list: php_log_opt.write(" * " + option + "\n") if invalid_values_options_list: php_log_opt.write(" - The following options have been disabled as have incorrect values:\n") for option in invalid_values_options_list: php_log_opt.write(" * " + option + "\n") if invalid_options_list: php_log_opt.write(" - The following options have been disabled as invalid (have no values):\n") for option in invalid_options_list: php_log_opt.write(" * " + option + "\n") except (OSError, IOError) as e: print_error("writing to ", PHP_OPTIONS_LOGFILE, str(e)) sys.exit(1) if not root_flag_saved: set_user_perm(uid, gid) def print_exception(level = syslog.LOG_ERR, includetraceback = False): exctype, exception, exctraceback = sys.exc_info() excclass = str(exception.__class__) message = str(exception) if not includetraceback: msg = "%s: %s" % (excclass, message) msg = msg.replace('Errno', 'Err code') syslog.syslog(level, msg) print(msg, file=sys.stderr) else: try: import StringIO except ImportError: # for python 3 excfd = io.StringIO() else: # for python 2 excfd = StringIO.StringIO() traceback.print_exception(exctype, exception, exctraceback, None, excfd) for line in excfd.getvalue().split("\n"): syslog.syslog(level, line) print(line, file=sys.stderr) def _read_vhosts_dir(): try: with open(GLOBAL_PLESK_CFG, 'rt') as f: data = f.read() except Exception: return None # e.g. case when Plesk is not installed match = re.search(r'^HTTPD_VHOSTS_D[ \t]+(\S+)$', data, re.MULTILINE) if not match: return None return match.groups()[0].rstrip("/") # CAG-673 PLESK_VHOSTS_D = _read_vhosts_dir() or FALLBACK_PLESK_VHOSTS_D # Black list of files and directories (that should not exist in skeleton) black_list = [] def is_in_black_list(_file): rfile = strip_path(_file) rfile = addslash(rfile) for path in black_list: path = addslash(path) if rfile.startswith(path): return True return False def lineno(): """Returns the current line number in program""" return inspect.currentframe().f_back.f_lineno def stripslash(_dir): if _dir != '': if (_dir[-1] == '/'): return _dir[:-1] return _dir def addslash(_dir): if _dir == '': return '/' if (_dir[-1] != '/'): return '%s/' % (_dir,) return _dir # Turns on/off additional checks and debug output # This variable is set by cagefsctl module debug_option = False # Path to skeleton set by cagefsctl module SKELETON = '' # Pathes to white and safe lists used by cagefs-fuse # This variables are set by cagefsctl module FUSE_WHITE_LIST = '' FUSE_SAFE_LIST = '' # Save (overwrite) safe list for cagefs-fuse def save_etc_safe_list(safe_list): sigterm_check() umask_saved = os.umask(0o22) _file = open(FUSE_SAFE_LIST, 'w') for filename in safe_list: _file.write('%s\n' % filename) _file.close() os.umask(umask_saved) os.chmod(FUSE_SAFE_LIST, 0o600) def strip_path(path: str) -> str: """ Remove leading path to skeleton from the specified path. """ path = path.removeprefix(SKELETON) return path or '/' # White list of files in system's /etc directory white_list = {} # Function adds directory tree rooted in src to list def add_tree_to_list(src, _list, follow_symlinks=False, cut_path=None, add_path=None): for name in os.listdir(src): srcname = os.path.join(src, name) path = srcname if cut_path != None: path = path[len(cut_path):] if add_path != None: path = add_path + path _list[path] = 1 if os.path.isdir(srcname) and (follow_symlinks or (not os.path.islink(srcname))): add_tree_to_list(srcname, _list, cut_path=cut_path, add_path=add_path) def get_real_path(path): return os.path.join(os.path.realpath(os.path.dirname(path)), os.path.basename(path)) def copy2etc(path: str) -> None: if not path.startswith('/etc/'): return if move_to_alternatives(path, etc=True): return if path.startswith('/etc/cl.php.d/') or path.startswith('/etc/cl.selector/'): return if path in ['/etc/passwd', '/etc/group', '/etc/shadow', '/etc/cl.php.d', '/etc/cl.selector']: return if not os.path.exists(path): return destination = ETC_TEMPLATE_NEW_DIR + path if os.path.isfile(path) or (os.path.islink(path) and is_path_read_only_mounted(os.path.realpath(path))): copy_file(path, destination) else: copytree(path, destination, True) # Returns True if path refers to object in /etc directory # and adds to white_list directory tree rooted in path # Also copies path to the template of /etc directory def add_to_white_list(path): global white_list path = stripslash(path) # Strip path to skeleton from path path = strip_path(path) # path = get_real_path(path) copy2etc(path) if path.startswith('/etc/'): if (path not in white_list) and os.path.exists(path): white_list[path] = 1 if os.path.isdir(path): add_tree_to_list(path, white_list, True) return True return False # Function copies paths (list of paths) to the template of /etc directory def copy_to_etc(paths): for path in paths: path = stripslash(path) # Strip path to skeleton from path path = strip_path(path) copy2etc(path) path2 = get_real_path(path) if path2 != path: copy2etc(path2) path3 = os.path.realpath(path) if path3 != path and path3 != path2: copy2etc(path3) if os.path.islink(path): linkto = os.readlink(path) copy2etc(linkto) # lines of mp-file (set by cagefsctl module) mounts = [] def path_includes_mount_point_comparator(path, mount): return mount.startswith(path) def path_is_mounted_comparator(path, mount): return path.startswith(mount) def mounts_are_found_comparator(path, mount): return path_includes_mount_point_comparator(path, mount) or path_is_mounted_comparator(path, mount) # path == path to directory or file in skeleton def mounts_are_found(path, comparator, mounts_list=None): if mounts_list is None: mounts_list = mounts # Strip path to skeleton from path path = strip_path(path) # path = os.path.realpath(path) path = addslash(path) if path.startswith('/etc/') or path.startswith('/var/log/'): return True for line in mounts_list: if line != '' and line[0] == '/': line = line.rstrip() line = addslash(line) if comparator(path, line): return True return False # path == path to directory or file in skeleton def path_is_mounted(path): return mounts_are_found(path, path_is_mounted_comparator) # path == path to directory or file in skeleton def path_includes_mount_point(path): return mounts_are_found(path, path_includes_mount_point_comparator) def is_path_read_only_mounted(path: str) -> bool: from cagefsctl import MountpointConfig read_only_mounts = MountpointConfig().read_only_mounts return mounts_are_found(path, path_is_mounted_comparator, mounts_list=read_only_mounts) # List of libraries for each binary file in cagefs-skeleton directory # key = binary file # value = list of libraries libs_list = {} def add_libs_to_list(binary, libs): global libs_list libs_list[binary] = libs def get_libs_from_list(binary): try: return libs_list[binary] except KeyError: return None def del_libs_from_list(binary): try: del libs_list[binary] except KeyError: pass # Save libs_list to file def save_libs(filename): sigterm_check() try: umask_saved = os.umask(0o77) _file = open(filename, "wb") pickle.dump(byteify(libs_list), _file, protocol=2) _file.close() os.umask(umask_saved) os.chmod(filename, 0o600) except Exception as err: print_error("while saving", filename, "-", err) # Load libs_list from file def load_libs(filename): global libs_list if os.path.isfile(filename): try: _file = open(filename, "rb") libs_list = unicodeify(pickle.load(_file, encoding=locale.getpreferredencoding())) _file.close() except Exception as err: print_error("loading", filename, "-", err) # List of files and directories in cagefs-skeleton directory files_list = {} def path_is_in_list(path): path = stripslash(path) # Strip path to skeleton from path path = strip_path(path) path = get_real_path(path) return path in files_list def check_error(path, linenum): if (not debug_option) or path_is_mounted(path): return # Strip path to skeleton from path path = strip_path(path) if os.path.lexists(path): if not path_is_in_list(path): print_error('Error in line', linenum, ': ', path, 'is not in list') # Adds to list directory tree rooted in path def add_to_list(path, add_tree = True): global files_list path = stripslash(path) if debug_option: rpath = get_real_path(path) if path != rpath: print_error('Error in line', lineno(), ': ', path, '!=', rpath) if os.path.lexists(path): # Strip path to skeleton from path path = strip_path(path) # if not path_is_mounted(path): if path not in files_list: files_list[path] = 1 if add_tree and os.path.isdir(path) and (not os.path.islink(path)): add_tree_to_list(path, files_list, False) else: if debug_option: print_error('line', lineno(), 'path does not exist:', path) stat_cache = {} def cached_lstat(path, use_cache=True): global stat_cache if not use_cache: return os.lstat(path) try: res = stat_cache[path] except KeyError: stat_cache[path] = res = os.lstat(path) return res def clear_stat_cache(path): try: del stat_cache[path] except KeyError: pass # Returns True if files are "equal", False if not def is_same_metadata(fileA, fileB, sbA=None, sbB=None, use_cache=True, relative_symlinks=False): if (sbA==None): sbA = cached_lstat(fileA, use_cache=use_cache) if (sbB==None): sbB = cached_lstat(fileB, use_cache=use_cache) if (stat.S_ISLNK(sbA[stat.ST_MODE]) != stat.S_ISLNK(sbB[stat.ST_MODE])): return False if (stat.S_ISLNK(sbA[stat.ST_MODE])): realfileA = os.readlink(fileA) realfileB = os.readlink(fileB) if relative_symlinks: relative_path = get_relative_path(realfileA, fileB) return realfileB == relative_path return realfileA == realfileB modeA = sbA[stat.ST_MODE] modeB = sbB[stat.ST_MODE] # clear SUID & SGID before comparing modeA = (modeA & ~stat.S_ISUID) & ~stat.S_ISGID modeB = (modeB & ~stat.S_ISUID) & ~stat.S_ISGID # Compare permissions and file type bits if modeA != modeB: return False # Comapre mtime, size, owner, group if (sbA[stat.ST_MTIME] != sbB[stat.ST_MTIME]) or (sbA[stat.ST_SIZE] != sbB[stat.ST_SIZE])\ or (sbA[stat.ST_UID] != sbB[stat.ST_UID]) or (sbA[stat.ST_GID] != sbB[stat.ST_GID]): return False return True def is_update_needed(original, injail, origstatbuf=None, injailstatbuf=None, use_cache=True, relative_symlinks=False): """ Returns: True if update of "injail" file is needed False if update is NOT needed (file in jail has same metadata) """ try : return not is_same_metadata(original, injail, sbA=origstatbuf, sbB=injailstatbuf, use_cache=use_cache, relative_symlinks=relative_symlinks) except OSError as e: # file does not exist in cagefs-skeleton - update is needed return (e.errno == 2) class StaticallyLinkedError(Exception): pass def _parse_lib_path(line: str, executable: str) -> Optional[str]: if "no version information available" in line: return splitted = line.split() if not splitted: print_error('failed to parse ldd output', line[:-1]) return if (splitted[0:2] == ['statically', 'linked']) \ or (splitted[0:1] + splitted[2:4] == ['not', 'dynamic', 'executable']): raise StaticallyLinkedError() if splitted[0] in ('linux-gate.so.1', 'linux-vdso.so.1') \ or (len(splitted) == 4 and splitted[2:4] == ['not', 'found']): return if len(splitted) >= 3: # the line containing dynamic linker may vary, # but we want it to be added to CageFS anyway dynamic_linker = '/lib64/ld-linux-x86-64.so.2' lib_path = dynamic_linker if dynamic_linker in splitted else splitted[2] elif len(splitted) >= 1 and splitted[0][0] == '/': lib_path = splitted[0] else: print_error('failed to parse ldd output', line[:-1]) return if not os.path.exists(lib_path): print_error('ldd returns non existing library', lib_path, 'for', executable) return return lib_path def get_ldd_libs(executable: str) -> List[str]: """ Returns list of libraries for the executable """ import struct retval = [] try: # check if executable is binary f = open(executable, 'rb') # read 4 byte signature signature = struct.unpack('<I', f.read(4))[0] f.close() except: return retval # binary executables have signature 464C457Fh, # e.g. ELF (in little-endian format) if signature != 0x464C457F: return retval ldd_path = '/usr/bin/ldd' # ldd now prints warnings in stdout, they should be omitted # This array stores all error patterns for skip # Sample of line with such pattern: # <lib>: <other_lib>: no version information available (required by <lib>) # This warning doesn't produce any crucial errors, but can break parse # Note (gponomarenko): ldd can't load libraries from fs mounted with noexec option (e.g. /tmp) # When it tries then there is an error in stdout: # "error while loading shared libraries: <lib.so>: failed to map segment from shared object" p = subprocess.Popen([ldd_path, executable], shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, text=True) for line in p.stdout.readlines(): try: lib_path = _parse_lib_path(line, executable) except StaticallyLinkedError: break if lib_path is not None: retval.append(lib_path) return retval # os.path.realpath() seems to do the same: # NO: it cannot handle symlink resolving WITH a chroot def resolve_realpath(path, chroot='', include_file=0): if (path=='/'): return '/' spath = split_path(path) basename = '' if (not include_file): basename = spath[-1] spath = spath[:-1] ret = '/' doscounter=0# a symlink loop may otherwise hang this script #print 'path',path,'spath',spath for entry in spath: ret = os.path.join(ret,entry) #print 'lstat',ret sb = cached_lstat(ret) if (stat.S_ISLNK(sb.st_mode)): doscounter+=1 realpath = os.readlink(ret) if (realpath[0]=='/'): ret = os.path.normpath(chroot+realpath) else: tmp = os.path.normpath(os.path.join(os.path.dirname(ret),realpath)) if (len(chroot)>0 and tmp[:len(chroot)]!=chroot): print_error('symlink ', tmp, ' points outside jail, ABORT') raise Exception("Symlink points outside jail") ret = tmp return os.path.join(ret,basename) # similar to shutil.copymode(src, dst) but we do not copy any SUID bits # the caller should catch any exceptions! def copy_time_and_permissions(src, dst, be_verbose=0, allow_suid=0, copy_ownership=0): sbuf = os.stat(src) # in python 2.1 the return value is a tuple, not an object, st_mode is field 0 # mode = stat.S_IMODE(sbuf.st_mode) mode = stat.S_IMODE(sbuf[stat.ST_MODE]) if (not allow_suid): if (mode & (stat.S_ISUID | stat.S_ISGID)): logging('removing setuid and setgid permissions from '+dst, SILENT_FLAG, be_verbose) mode = (mode & ~stat.S_ISUID) & ~stat.S_ISGID os.utime(dst, (sbuf[stat.ST_ATIME], sbuf[stat.ST_MTIME])) if (copy_ownership): os.chown(dst, sbuf[stat.ST_UID], sbuf[stat.ST_GID]) os.chmod(dst, mode) def split_path(path): spath = path.split('/') res = [] for item in spath: if item: res.append(item) return res def join_path(spath): if (len(spath)==0): return '/' ret = '' for entry in spath: ret += '/'+entry return ret handled_dir = {} def gen_path_key(path, copy_permissions, copy_ownership): return path + "_"+str(copy_permissions) + "_" + str(copy_ownership) def create_parent_path(chroot,path,be_verbose=0, copy_permissions=1, allow_suid=0, copy_ownership=0): sigterm_check() # Do not create path if it is in black list if is_in_black_list(path): return chroot+path # check if we already processed that directory global handled_dir key = gen_path_key(path, copy_permissions, copy_ownership) if key in handled_dir: return handled_dir[key] # the first part of the function checks the already existing paths in the jail # and follows any symlinks relative to the jail spath = split_path(path) existpath = chroot i=0 while (i<len(spath)): sigterm_check() origpath = join_path(spath[0:i+1]) origkey = gen_path_key(origpath, copy_permissions, copy_ownership) if origkey not in handled_dir: tmp1 = os.path.join(existpath,spath[i]) if not os.path.exists(tmp1): break tmp = resolve_realpath(tmp1,chroot,1) if not os.path.exists(tmp): break existpath = tmp if copy_permissions and not path_is_mounted(existpath): try: copy_time_and_permissions(origpath, existpath, be_verbose, allow_suid, copy_ownership) except OSError as e: print_error('failed to copy time/permissions/owner from', origpath, 'to', existpath, ':', e.strerror) handled_dir[origkey]=existpath else: existpath = handled_dir[origkey] i+=1 # the second part of the function creates the missing parts in the jail # according to the original directory names, including any symlinks while (i<len(spath)): sigterm_check() origpath = join_path(spath[0:i+1]) jailpath = os.path.join(existpath,spath[i]) try: sb = cached_lstat(origpath) except OSError as e: print_error('failed to lstat('+origpath+'):', e.strerror) return None if (stat.S_ISDIR(sb.st_mode)): try: injailsb = cached_lstat(jailpath) if not stat.S_ISDIR(injailsb.st_mode): clear_stat_cache(jailpath) os.unlink(jailpath) except OSError: pass logging('Create directory '+jailpath,SILENT_FLAG,be_verbose) try: os.mkdir(jailpath, 0o755) add_to_list(jailpath, False) except OSError as e: logging('Warning: failed to create directory ' + jailpath + ' -- ' + e.strerror, SILENT_FLAG, be_verbose) if (copy_permissions): try: copy_time_and_permissions(origpath, jailpath, be_verbose, allow_suid, copy_ownership) except OSError as e: print_error('failed to copy time/permissions/owner from', origpath, 'to', jailpath, ':', e.strerror) elif (stat.S_ISLNK(sb.st_mode)): realfile = update_symlink_in_skeleton(origpath, jailpath) add_to_list(jailpath, False) if (realfile[0]=='/'): jailpath = create_parent_path(chroot, realfile, be_verbose, copy_permissions, allow_suid, copy_ownership) check_error(jailpath, lineno()) else: tmp = os.path.normpath(os.path.join(os.path.dirname(jailpath), realfile)) if (len(chroot)>0 and tmp[:len(chroot)]!=chroot): print_error('symlink '+tmp+' points outside jail, ABORT') raise Exception("Symlink points outside jail") realfile = tmp[len(chroot):] jailpath = create_parent_path(chroot, realfile, be_verbose, copy_permissions, allow_suid, copy_ownership) check_error(jailpath, lineno()) existpath = jailpath i+=1 add_to_list(existpath, False) # save directory as processed so we don't test it twice. handled_dir[key]=existpath return existpath def copy_with_permissions(src, dst, be_verbose=0, try_hardlink=1, retain_owner=0): """copies/links the file and the permissions, except any setuid or setgid bits""" if is_in_black_list(dst): return try: injailsb = cached_lstat(dst) if stat.S_ISDIR(injailsb.st_mode): clear_stat_cache(dst) shutil.rmtree(dst) except (IOError, OSError, shutil.Error): pass do_normal_copy = 1 if (try_hardlink==1): try: os.link(src,dst) do_normal_copy = 0 add_to_list(dst) except: print_error('Linking '+src+' to '+dst+' failed, will revert to copying') pass if (do_normal_copy == 1): try: shutil.copyfile(src,dst) add_to_list(dst, add_tree = False) copy_time_and_permissions(src, dst, be_verbose, allow_suid=0, copy_ownership=retain_owner) except (IOError, OSError, shutil.Error) as e: print_error('ERROR: copying file and permissions ', src, ' to ', dst, ': ', e.strerror) def copy_device(chroot, path, be_verbose=1, retain_owner=1): if path_is_mounted(path): return try: sb = os.lstat(path) except OSError: logging('Device ' + path+ ' does NOT exist in real system',SILENT_FLAG,be_verbose) return create_parent_path(chroot, os.path.dirname(path), be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=1) chrootpath = resolve_realpath(chroot+path,chroot) if (stat.S_ISCHR(sb.st_mode)): mode = 'c' elif (stat.S_ISBLK(sb.st_mode)): mode = 'b' elif (stat.S_ISLNK(sb.st_mode)): try: realfile = os.readlink(path) logging('Creating symlink '+chrootpath+' to '+realfile,SILENT_FLAG,be_verbose) remove_file_or_dir(chrootpath, check_mounts = True) os.symlink(realfile,chrootpath) except OSError: logging('Failed to create symlink '+chrootpath+' to '+realfile,SILENT_FLAG, 1) if realfile[0] != '/': realfile = os.path.normpath(os.path.join(os.path.dirname(path), realfile)) if not realfile.startswith('/proc/') and os.path.exists(realfile): copy_device(chroot, realfile, be_verbose, retain_owner) return else: return # major = st_rdev divided by 256 (8bit reserved for the minor number) # minor = remainder of st_rdev divided by 256 major, minor = divmod(sb.st_rdev, 256) try: if not os.path.lexists(chrootpath): logging('Creating device '+chroot+path,SILENT_FLAG,be_verbose) os.spawnlp(os.P_WAIT, 'mknod','mknod', chrootpath, str(mode), str(major), str(minor)) else: logging('Device '+chrootpath+' does exist already',SILENT_FLAG,be_verbose) copy_time_and_permissions(path, chrootpath, allow_suid=0, copy_ownership=retain_owner) except OSError: logging('Failed to create device '+chrootpath,SILENT_FLAG, 1) def copy_dir_recursive(chroot,_dir,force_overwrite=0, be_verbose=0, check_libs=1, try_hardlink=1, retain_owner=0, handledfiles=[], update=0): """copies a directory and the permissions recursive, except any setuid or setgid bits""" sigterm_check() if is_in_black_list(_dir): return handledfiles files2 = () for entry in os.listdir(_dir): sigterm_check() tmp = os.path.join(_dir, entry) try: sbuf = cached_lstat(tmp) if (stat.S_ISDIR(sbuf.st_mode)): epath = create_parent_path(chroot, tmp, be_verbose=be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=retain_owner) check_error(epath, lineno()) # add_to_list(epath, False) handledfiles = copy_dir_recursive(chroot,tmp,force_overwrite, be_verbose, check_libs, try_hardlink, retain_owner, handledfiles, update=update) else: files2 += os.path.join(_dir, entry), except OSError as e: print_error('failed to investigate source file',tmp,':',e.strerror) handledfiles = copy_binaries_and_libs(chroot,files2,force_overwrite, be_verbose, check_libs, try_hardlink, retain_owner, handledfiles, update=update) if debug_option: for item in files2: check_error(item, lineno()) return handledfiles def libs_check_is_needed(_file, mode): return (_file.find('/lib') != -1 or _file.find('.so') != -1\ or (mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH))) def write_file(filename, lines, add_eol=False, make_backup=False): """ Helper for write lines to file :param: filename `str` filename for write :param: lines `list` list with content lines :param: add_eol `bool` if True than add \n to end each line """ sigterm_check() try: if make_backup: try: backup_name = "{}.bak".format(filename) shutil.copyfile(filename, backup_name) os.chmod(backup_name, stat.S_IMODE(os.lstat(filename).st_mode)) except (IOError, OSError, shutil.Error): pass with open(filename, "w") as f: splitter = "" if not add_eol else "\n" f.write(splitter.join(lines)) except (OSError, IOError): logging('Error: failed to write ' + filename, SILENT_FLAG, 1) sys.exit(1) def isdigit(n): return n in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] # Function returns True if string contains digits only def isdigits(s): if not s: return False for char in s: if not isdigit(char): return False return True def get_version(line, sign): length = len(sign) pos = line.find(sign) if pos != -1: end = line[pos+length:] pos2 = 0 while pos2 < len(end): if not isdigit(end[pos2]): break pos2 += 1 ver = end[:pos2] return int(ver) return 0 # command = path to wrapper inside skeleton def update_wrapper(program, alias, command): # copy content of the program script = program[:] # replace "ALIAS" with alias for i in range(len(script)): tmp = script[i].replace("ALIAS", alias) script[i] = tmp if os.path.isfile(command): try: os.unlink(command) except (OSError, IOError): logging('Error: failed to delete ' + command, SILENT_FLAG, 1) sys.exit(1) umask_saved = os.umask(0o22) write_file(command, script) os.umask(umask_saved) # Returns True if wrapper is not installed or its version is older than required # command = path to wrapper inside skeleton def wrapper_not_installed(command, version, sign): if not os.path.isfile(command): # wrapper is NOT installed return True GREP = "/bin/grep" try: # run the "grep" command and suppress it's output p = subprocess.Popen([GREP, "-m", "1", sign, command],\ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) (out, _) = p.communicate() # check return code of the child if p.returncode == 2: # trouble logging('Error while executing ' + GREP + " -m 1 " + sign + " " + command, SILENT_FLAG, 1) # assume that wrapper is NOT installed return True elif p.returncode != 0: # signature is NOT found - wrapper is NOT installed yet return True except OSError: logging('Error: failed to run ' + GREP + " -m 1 " + sign + " " + command, SILENT_FLAG, 1) # assume that wrapper is NOT installed return True if out != None: ver = get_version(out, sign) if ver >= version: # wrapper is installed already return False # wrapper is NOT installed return True # Path to wrapper scripts (sources) PROXY_PATH = "/usr/share/cagefs/safeprograms/" # Signature of proxy script SIGNATURE = "#CageFS proxyexec wrapper - ver " # pairs "Path to wrapper" : "Alias" wrappers = {} # pairs "Path to wrapper" : "Name of wrapper" wrappers_names = {} def get_proxy_version(lines: List[str]) -> int: """ Detect wrapper version from the file lines. If unable to detect, return -1. """ for line in lines: version = get_version(line, SIGNATURE) if version != 0: return version return -1 def install_wrapper(_file): # Strip path to skeleton from path to file _file = strip_path(_file) if not os.path.isfile(_file): return # Read lines of proxy script proxy = read_file(PROXY_PATH + wrappers_names[_file]) # Determine version of proxy script proxy_ver = get_proxy_version(proxy) # Installing if version equals -1 as it's most likely user's custom wrapper if proxy_ver == -1 or wrapper_not_installed(SKELETON+_file, proxy_ver, SIGNATURE): update_wrapper(proxy, wrappers[_file], SKELETON+_file) try: # os.chmod(command, 0755) copy_time_and_permissions(_file, SKELETON+_file, be_verbose=0, allow_suid=0, copy_ownership=1) except (OSError, IOError): logging('Error: failed to set permissions/owner to ' + SKELETON+_file, SILENT_FLAG, 1) sys.exit(1) # Returns True if file is wrapper def update_proxy_wrapper(_file): sigterm_check() # Strip path to skeleton from path to file _file = strip_path(_file) if _file in wrappers: add_to_list(_file, add_tree = False) install_wrapper(_file) return True return False def remove_file_or_dir(path, check_mounts = False): try: os.unlink(path) except (OSError, IOError): pass if os.path.isdir(path): if (not check_mounts) or (not path_includes_mount_point(path)): shutil.rmtree(path, True) else: logging('Error: failed to remove directory ' + path + 'because it includes mount points', SILENT_FLAG, 1) # Paths to binaries (with appropriate alias) that can be replaced by alternatives # alias -> path orig_binaries = \ { 'php' : '/usr/bin/php-cgi', 'php-cli' : '/usr/bin/php', 'php.ini' : '/etc/php.ini', 'lsphp' : '/usr/local/bin/lsphp', 'php-fpm' : '/usr/local/sbin/php-fpm' } # original binaries are moved to this directory (in skeleton) ALT_DEST_PATH = '/usr/selector' # original php.ini is moved to this directory (in skeleton) ALT_DEST_ETC_PATH = '/usr/selector.etc' # User's "selector" directory that contains symlinks to alternatives NATIVE_CONF = ETC_CL_ALT_PATH + '/' + 'native.conf' def is_mandatory(alias): """ Returns True if php file for appropriate alias is mandatory for proper work of PHP Selector (i.e the file should exist and should be replaced with symlink successfully) :param alias: alias for php file :type alias: string """ if is_ea4_enabled(): return False return alias in ('php', 'php-cli', 'php.ini') config_loaded = False def read_native_conf(): global orig_binaries, config_loaded if not config_loaded: if os.path.isfile(NATIVE_CONF): f = open(NATIVE_CONF, 'r') for line in f: if not line.startswith('#'): line = line.strip() ar = line.split('=', 1) if len(ar) == 2: alias = ar[0].strip() path = ar[1].strip() if alias in orig_binaries: orig_binaries[alias] = path f.close() config_loaded = True def is_etc_in_native_conf(): read_native_conf() for path in orig_binaries.values(): path2 = os.path.realpath(path) if path2.startswith('/etc/'): return True return False def get_usr_selector_path(alias): if alias == 'php.ini': return ALT_DEST_ETC_PATH+'/'+alias return ALT_DEST_PATH+'/'+alias def kill_php(file_name): if file_name != '' and (not file_name.endswith('.ini')): Execute(['/usr/bin/killall', '-q', file_name], check_return_code=False) def create_php_stub(alias): """ Create stub (empty file) for php file. Return True when error has occured """ if alias == 'php.ini': selector_dir = SKELETON + ALT_DEST_ETC_PATH else: selector_dir = SKELETON + ALT_DEST_PATH stub_path = os.path.join(selector_dir, alias) if not os.path.lexists(stub_path): try: if not os.path.isdir(selector_dir): mod_makedirs(selector_dir, 0o755) umask_saved = os.umask(0o22) open(stub_path, 'w').close() os.umask(umask_saved) except (OSError, IOError) as e: print_error("Failed to write:", stub_path, ':', str(e)) return True if (linksafe_gid := get_linksafe_gid()) is not None: os.chown(stub_path, -1, linksafe_gid) return False def move_to_alternatives(path, etc = False): """ Move php file to /usr/selector* directory inside cagefs-skeleton and create symlink to it Return True if php binary has been moved successfully, False otherwise :param path: path to original php file :type path: string :param etc: True = /etc directory is being processed, False otherwise :type etc: bool """ sigterm_check() if etc: if not path.startswith('/etc/'): return False spath = path else: spath = strip_path(path) read_native_conf() for alias in orig_binaries: sigterm_check() orig_path = os.path.realpath(orig_binaries[alias]) if (spath == orig_path) and os.path.isfile(orig_path) and (not os.path.islink(orig_path)): if alias in ('php', 'php-cli', 'lsphp', 'php.ini'): if is_ea4_enabled(): create_php_stub(alias) # do not move php file when EA4 is used return False filename = alias DEST_PATH = get_usr_selector_path(alias) LINK_TO = ETC_CL_ALT_PATH+'/'+filename dest_file = SKELETON + DEST_PATH dest_dir = os.path.dirname(dest_file) if etc: orig_file = ETC_TEMPLATE_NEW_DIR + orig_path else: orig_file = SKELETON + orig_path for parent_path in (dest_dir, os.path.dirname(orig_file)): if not os.path.isdir(parent_path): try: mod_makedirs(parent_path, 0o755) except OSError as e: msg = f'Error: failed to create directory {parent_path} : {str(e).replace("Errno", "Err code")}' logger.error(msg, exc_info=e) logging(msg, SILENT_FLAG, 1) return False if is_update_needed(orig_path, dest_file, use_cache=False): if copy_file(orig_path, dest_file, create_parent_dir = False): file_name = os.path.basename(orig_path) kill_php(file_name) if copy_file(orig_path, dest_file, create_parent_dir = False): logging('Error copying '+orig_path+' to '+dest_file, SILENT_FLAG, 1) return False if (linksafe_gid := get_linksafe_gid()) is not None: os.chown(dest_file, -1, linksafe_gid) try: if os.path.islink(orig_file): if os.readlink(orig_file) != LINK_TO: os.unlink(orig_file) os.symlink(LINK_TO, orig_file) else: remove_file_or_dir(orig_file) os.symlink(LINK_TO, orig_file) except OSError as e: msg = f'Error: failed to create symlink {orig_file} : {str(e).replace("Errno", "Err code")}' logger.error(msg, exc_info=e) logging(msg, SILENT_FLAG, 1) return False if not etc: add_to_list(orig_file, add_tree = False) return True return False def is_path_in_exclusions(path): if path == '/': return True path = stripslash(path) return path in { '/bin', '/boot', '/dev', '/etc', '/lib', '/lost+found', '/mnt', '/proc', '/root', '/sbin', '/sys', '/tmp', '/usr', '/var', '/home', # Keep both dirs just to be sure for cases when two dirs exists # simultaneously, which is itself wrong configuration, but we don't # want to fail even in such case '/var/www/vhosts', PLESK_VHOSTS_D, } PLESK_ORIG_WRAPPER_FILENAME = "/var/www/cgi-bin/cgi_wrapper/cgi_wrapper" def __copy_wrapper(A, B, C=None): logging( 'Copying ' + A + ' to ' + B, SILENT_FLAG, 1) shutil.copyfile(A, B) if C: copy_time_and_permissions(C, B) else: copy_time_and_permissions(A, B) def install_plesk_wrapper(): sigterm_check() try: if cldetectlib.is_plesk(): # copy Plesk wrappers CLOUDLINUX_WRAPPER = "/var/www/cgi-bin/cgi_wrapper/cloudlinux_wrapper" CLOUDLINUX_WRAPPER_PACKAGE = "/usr/share/cagefs-plugins/plesk-cagefs/cloudlinux_wrapper" WRAPPERS = ( (PLESK_ORIG_WRAPPER_FILENAME, SKELETON+"/var/www/cgi-bin/cgi_wrapper/cgi_wrapper.orig.cagefs", PLESK_ORIG_WRAPPER_FILENAME), ("/usr/share/cagefs-plugins/plesk-cagefs/cgi_wrapper", SKELETON+PLESK_ORIG_WRAPPER_FILENAME, PLESK_ORIG_WRAPPER_FILENAME), (CLOUDLINUX_WRAPPER_PACKAGE, SKELETON+CLOUDLINUX_WRAPPER, PLESK_ORIG_WRAPPER_FILENAME), (CLOUDLINUX_WRAPPER_PACKAGE, CLOUDLINUX_WRAPPER, PLESK_ORIG_WRAPPER_FILENAME) ) # create parent directory (/usr/share/cagefs-skeleton/var/www/cgi-bin/cgi_wrapper) dirpath = os.path.dirname(SKELETON+PLESK_ORIG_WRAPPER_FILENAME) if not os.path.lexists(dirpath): mod_makedirs(dirpath, 0o755) # Copy wrappers to cagefs skeleton for src, dst, perm in WRAPPERS: __copy_wrapper(src, dst, perm) except (OSError, IOError) as e: print_error('failed to install Plesk wrapper: ' + str(e)) # there is a very tricky situation for this function: # suppose /srv/jail/opt/bin is a symlink to /usr/bin # try to lstat(/srv/jail/opt/bin/foo) and you get the result for /usr/bin/foo # so use resolve_realpath to find you want lstat(/srv/jail/usr/bin/foo) # def copy_binaries_and_libs(chroot, binarieslist, force_overwrite=0, be_verbose=0, check_libs=1, try_hardlink=1, retain_owner=0, try_glob_matching=0, handledfiles=[], update=0): """copies a list of executables and their libraries to the chroot""" sigterm_check() if (chroot[-1] == '/'): chroot = chroot[:-1] for _file in binarieslist: sigterm_check() if _file in handledfiles: continue # Build white list of files in etc directory and do nothing else if add_to_white_list(_file): if os.path.isfile(_file) or os.path.islink(_file): handledfiles.append(_file) continue # Do nothing if path is mounted to skeleton from real system if path_is_mounted(_file): if os.path.isfile(_file) or os.path.islink(_file): handledfiles.append(_file) continue try: sb = cached_lstat(_file) except OSError as e: if (e.errno == 2): if (try_glob_matching == 1): ret = glob.glob(_file) if (len(ret)>0): handledfiles = copy_binaries_and_libs(chroot, ret, force_overwrite, be_verbose, check_libs, try_hardlink=try_hardlink, retain_owner=retain_owner, try_glob_matching=0, handledfiles=handledfiles, update=update) if debug_option: for item in ret: check_error(item, lineno()) else: logging('Source file(s) '+_file+' do not exist',SILENT_FLAG,be_verbose) else: logging('Source file(s) '+_file+' do not exist',SILENT_FLAG,be_verbose) else: print_error('failed to investigate source file',_file,':',e.strerror) continue # source file exists, resolve the chroot realfile create_parent_path(chroot,os.path.dirname(_file), be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=retain_owner) try: chrootrfile = resolve_realpath(os.path.normpath(chroot+'/'+_file),chroot) except OSError: continue # Check again after resolve_realpath... # Do nothing if path is mounted to skeleton from real system if path_is_mounted(chrootrfile): if os.path.isfile(_file) or os.path.islink(_file): handledfiles.append(_file) continue # Install wrapper and do nothing else if update_proxy_wrapper(chrootrfile): handledfiles.append(_file) continue if _file == PLESK_ORIG_WRAPPER_FILENAME: install_plesk_wrapper() handledfiles.append(_file) continue # Replace php binary with symlink (move php binary to /usr/selector) if move_to_alternatives(chrootrfile): php_libs = get_ldd_libs(_file) handledfiles = copy_binaries_and_libs(chroot, php_libs, force_overwrite, be_verbose, check_libs = 0, try_hardlink = try_hardlink, handledfiles = handledfiles, update = update) handledfiles.append(_file) continue # Do nothing if path is blacklisted if is_in_black_list(chrootrfile): if os.path.isfile(_file) or os.path.islink(_file): handledfiles.append(_file) continue try: chrootsb = cached_lstat(chrootrfile) chrootfile_exists = 1 add_to_list(chrootrfile) except OSError as e: if (e.errno == 2): chrootfile_exists = 0 else: print_error('failed to investigate destination file ',chroot,_file,':',e.strerror) if ((force_overwrite == 0) and (update == 0) and chrootfile_exists and not stat.S_ISDIR(chrootsb.st_mode)): logging(''+chrootrfile+' already exists, will not touch it',SILENT_FLAG,be_verbose) check_error(chrootrfile, lineno()) else: if (chrootfile_exists): if (force_overwrite): if (stat.S_ISREG(chrootsb.st_mode) or stat.S_ISLNK(chrootsb.st_mode)): logging('Destination file '+chrootrfile+' exists, will delete to force update',SILENT_FLAG,be_verbose) try: os.unlink(chrootrfile) except OSError as e: print_error('ERROR: failed to delete',chrootrfile,':',e.strerror) elif (stat.S_ISDIR(chrootsb.st_mode)): logging('Destination dir '+chrootrfile+' exists',SILENT_FLAG,be_verbose) elif (update): if stat.S_ISREG(chrootsb.st_mode) or stat.S_ISLNK(chrootsb.st_mode): if (is_update_needed(_file, chrootrfile, sb, chrootsb, relative_symlinks=True)): logging('Destination file '+chrootrfile+' needs update',SILENT_FLAG,be_verbose) try: os.unlink(chrootrfile) except OSError as e: print_error('failed to delete',chrootrfile,':',e.strerror) else: logging('Destination file '+chrootrfile+' does NOT need update',SILENT_FLAG,be_verbose) add_to_list(chrootrfile, add_tree = False) check_error(chrootrfile, lineno()) handledfiles.append(_file) if stat.S_ISLNK(chrootsb.st_mode): try: realfile = os.readlink(_file) except (OSError, IOError): realfile = None if realfile != None and (not is_path_in_exclusions(realfile)): if (realfile[0] != '/'): realfile = os.path.normpath(os.path.join(os.path.dirname(_file),realfile)) handledfiles = copy_binaries_and_libs(chroot, [realfile], force_overwrite, be_verbose, check_libs=check_libs, try_hardlink=try_hardlink, retain_owner=retain_owner, handledfiles=handledfiles, update=update) check_error(realfile, lineno()) # in python 2.1 the return value is a tuple, not an object, st_mode is field 0 # mode = stat.S_IMODE(sbuf.st_mode) mode = stat.S_IMODE(sb[stat.ST_MODE]) if (check_libs and libs_check_is_needed(_file, mode)): libs = get_libs_from_list(_file) # file is NOT found in list of libs (key is NOT found in dict)? if libs == None: libs = get_ldd_libs(_file) add_libs_to_list(_file, libs) handledfiles = copy_binaries_and_libs(chroot, libs, force_overwrite, be_verbose, check_libs=0, try_hardlink=try_hardlink, handledfiles=handledfiles, update=update) if debug_option: for item in libs: check_error(item, lineno()) continue elif (stat.S_ISDIR(chrootsb.st_mode)): logging('Destination dir '+chrootrfile+' exists',SILENT_FLAG,be_verbose) else: if (stat.S_ISDIR(chrootsb.st_mode)): pass # for a directory we also should inspect all the contents, so we do not # skip to the next item of the loop else: logging('Destination file '+chrootrfile+' exists',SILENT_FLAG,be_verbose) continue # endif (chrootfile_exists) create_parent_path(chroot,os.path.dirname(_file), be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=retain_owner) if (stat.S_ISLNK(sb.st_mode)): realfile = update_symlink_in_skeleton(_file, chrootrfile) add_to_list(chrootrfile, add_tree = False) handledfiles.append(_file) if not is_path_in_exclusions(realfile): if (realfile[0] != '/'): realfile = os.path.normpath(os.path.join(os.path.dirname(_file),realfile)) handledfiles = copy_binaries_and_libs(chroot, [realfile], force_overwrite, be_verbose, check_libs=check_libs, try_hardlink=try_hardlink, retain_owner=retain_owner, handledfiles=handledfiles, update=update) check_error(realfile, lineno()) elif (stat.S_ISDIR(sb.st_mode)): epath = create_parent_path(chroot, _file, be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=retain_owner) epath = stripslash(epath) if debug_option and (epath != chrootrfile): print_error('line', lineno(), ': ', epath, '!=', chrootrfile) # add_to_list(chrootrfile) handledfiles = copy_dir_recursive(chroot,_file,force_overwrite, be_verbose, check_libs=check_libs, try_hardlink=try_hardlink, retain_owner=retain_owner, handledfiles=handledfiles, update=update) check_error(chrootrfile, lineno()) elif (stat.S_ISREG(sb.st_mode)): if (try_hardlink): logging ('Trying to link '+_file+' to '+chrootrfile ,SILENT_FLAG,1) else: logging ('Copying '+_file+' to '+chrootrfile,SILENT_FLAG,1) copy_with_permissions(_file,chrootrfile,be_verbose, try_hardlink=try_hardlink, retain_owner=retain_owner) handledfiles.append(_file) check_error(chrootrfile, lineno()) elif (stat.S_ISCHR(sb.st_mode) or stat.S_ISBLK(sb.st_mode)): copy_device(chroot, _file, be_verbose, retain_owner) # in python 2.1 the return value is a tuple, not an object, st_mode is field 0 # mode = stat.S_IMODE(sbuf.st_mode) mode = stat.S_IMODE(sb[stat.ST_MODE]) if (check_libs and libs_check_is_needed(_file, mode)): if stat.S_ISLNK(sb.st_mode) or stat.S_ISREG(sb.st_mode): libs = get_ldd_libs(_file) add_libs_to_list(_file, libs) handledfiles = copy_binaries_and_libs(chroot, libs, force_overwrite, be_verbose, check_libs=0, try_hardlink=try_hardlink, handledfiles=handledfiles, update=update) if debug_option: for item in libs: check_error(item, lineno()) return handledfiles def config_get_option_as_list(cfgparser, sectionname, optionname): """retrieves a comma separated option from the configparser and splits it into a list, returning an empty list if it does not exist""" retval = [] if (cfgparser.has_option(sectionname,optionname)): inputstr = cfgparser.get(sectionname,optionname) for tmp in inputstr.split(','): item = tmp.strip() if item != '': retval += [item] return retval def init_passwd_and_group(dest_dir, users_list, groups_list, be_verbose=0): if (dest_dir[-1] == '/'): dest_dir = dest_dir[:-1] if not os.path.isdir(dest_dir): try: mod_makedirs(dest_dir, 0o755) except OSError: print_error('creating', dest_dir) sys.exit(1) os.chmod(dest_dir, 0o755) users = [] users.extend(users_list) groups = [] groups.extend(groups_list) if (sys.platform[4:7] == 'bsd'): open(dest_dir+'/passwd','a').close() open(dest_dir+'/spwd.db','a').close() open(dest_dir+'/pwd.db','a').close() open(dest_dir+'/master.passwd','a').close() else: if (not os.path.isfile(dest_dir+'/passwd')): fd2 = open(dest_dir+'/passwd','w') else: # the passwds file exists, check if any of the users exist already fd2 = open(dest_dir+'/passwd','r+') line = fd2.readline() while (len(line)>0): pwstruct = line.split(':') if (len(pwstruct) >=3): if ((pwstruct[0] in users) or (pwstruct[2] in users)): logging('user '+pwstruct[0]+' exists in '+dest_dir+'/passwd',SILENT_FLAG,be_verbose) try: users.remove(pwstruct[0]) except ValueError: pass try: users.remove(pwstruct[2]) except ValueError: pass line = fd2.readline() fd2.seek(0,2) if (len(users) > 0): fd = open('/etc/passwd','r') line = fd.readline() while (len(line)>0): pwstruct = line.split(':') if (len(pwstruct) >=3): if ((pwstruct[0] in users) or (pwstruct[2] in users)): fd2.write(line) logging('writing user '+pwstruct[0]+' to '+dest_dir+'/passwd',SILENT_FLAG,be_verbose) if (not pwstruct[3] in groups): groups += [pwstruct[3]] line = fd.readline() fd.close() fd2.close() # Copy permissions from real system to template file copy_time_and_permissions('/etc/passwd', dest_dir+'/passwd', be_verbose=0, allow_suid=0, copy_ownership=1) # do the same sequence for the group files if (not os.path.isfile(dest_dir+'/group')): fd2 = open(dest_dir+'/group','w') else: fd2 = open(dest_dir+'/group','r+') line = fd2.readline() while (len(line)>0): groupstruct = line.split(':') if (len(groupstruct) >=2): if ((groupstruct[0] in groups) or (groupstruct[2] in groups)): logging('group '+groupstruct[0]+' exists in '+dest_dir+'/group',SILENT_FLAG,be_verbose) try: groups.remove(groupstruct[0]) except ValueError: pass try: groups.remove(groupstruct[2]) except ValueError: pass line = fd2.readline() fd2.seek(0,2) if (len(groups) > 0): fd = open('/etc/group','r') line = fd.readline() while (len(line)>0): groupstruct = line.split(':') if (len(groupstruct) >=2): if ((groupstruct[0] in groups) or (groupstruct[2] in groups)): fd2.write(line) logging('writing group '+groupstruct[0]+' to '+dest_dir+'/group',SILENT_FLAG,be_verbose) line = fd.readline() fd.close() fd2.close() # Copy permissions from real system to template file copy_time_and_permissions('/etc/group', dest_dir+'/group', be_verbose=0, allow_suid=0, copy_ownership=1) def init_safe_users_and_groups(dest_dir, users_list ,groups_list, be_verbose=0): if (dest_dir[-1] == '/'): dest_dir = dest_dir[:-1] umask_saved = os.umask(0o22) if not os.path.isdir(dest_dir): try: mod_makedirs(dest_dir, 0o751) except OSError: print_error('creating', dest_dir) sys.exit(1) os.chmod(dest_dir, 0o751) users = [] users.extend(users_list) groups = [] groups.extend(groups_list) if (not os.path.isfile(dest_dir+'/safe.users')): fd2 = open(dest_dir+'/safe.users','w') else: # the file exists, check if any of the users exist already fd2 = open(dest_dir+'/safe.users','r+') line = fd2.readline() line = line.rstrip() while (len(line)>0): if line in users: logging('user '+line+' exists in '+dest_dir+'/safe.users',SILENT_FLAG,be_verbose) try: users.remove(line) except ValueError: pass line = fd2.readline() line = line.rstrip() fd2.seek(0,2) if (len(users) > 0): fd = open('/etc/passwd','r') line = fd.readline() while (len(line)>0): pwstruct = line.split(':') if (len(pwstruct) >=3): if ((pwstruct[0] in users) or (pwstruct[2] in users)): fd2.write(pwstruct[0]+"\n") logging('writing user '+pwstruct[0]+' to '+dest_dir+'/safe.users',SILENT_FLAG,be_verbose) line = fd.readline() fd.close() fd2.close() try: os.chmod(dest_dir+'/safe.users', 0o600) except (OSError, IOError): logging("Error: failed to set permissions to "+dest_dir+'/safe.users',SILENT_FLAG, 1) # do the same sequence for the group files if (not os.path.isfile(dest_dir+'/safe.groups')): fd2 = open(dest_dir+'/safe.groups','w') else: fd2 = open(dest_dir+'/safe.groups','r+') line = fd2.readline() line = line.rstrip() while (len(line)>0): if line in groups: logging('group '+line+' exists in '+dest_dir+'/safe.groups',SILENT_FLAG,be_verbose) try: groups.remove(line) except ValueError: pass line = fd2.readline() line = line.rstrip() fd2.seek(0,2) if (len(groups) > 0): fd = open('/etc/group','r') line = fd.readline() while (len(line)>0): groupstruct = line.split(':') if (len(groupstruct) >=2): if ((groupstruct[0] in groups) or (groupstruct[2] in groups)): fd2.write(groupstruct[0]+"\n") logging('writing group '+groupstruct[0]+' to '+dest_dir+'/safe.groups',SILENT_FLAG,be_verbose) line = fd.readline() fd.close() fd2.close() try: os.chmod(dest_dir+'/safe.groups', 0o600) except (OSError, IOError): logging("Error: failed to set permissions to "+dest_dir+'/safe.groups',SILENT_FLAG, 1) os.umask(umask_saved) def init_shadow(dest_dir, users_list, be_verbose=0): if (dest_dir[-1] == '/'): dest_dir = dest_dir[:-1] if not os.path.isdir(dest_dir): try: mod_makedirs(dest_dir, 0o755) except OSError: print_error('Error while creating', dest_dir) sys.exit(1) os.chmod(dest_dir, 0o755) users = [] users.extend(users_list) if (not os.path.isfile(dest_dir+'/shadow')): fd2 = open(dest_dir+'/shadow','w') else: # the shadow file exists, check if any of the users exist already fd2 = open(dest_dir+'/shadow','r+') line = fd2.readline() while (len(line)>0): pwstruct = line.split(':') if (len(pwstruct) >=1): if pwstruct[0] in users: logging('user '+pwstruct[0]+' exists in '+dest_dir+'/shadow',SILENT_FLAG,be_verbose) try: users.remove(pwstruct[0]) except ValueError: pass line = fd2.readline() fd2.seek(0,2) if (len(users) > 0): fd = open('/etc/shadow','r') line = fd.readline() while (len(line)>0): pwstruct = line.split(':') if (len(pwstruct) >=1): if pwstruct[0] in users: fd2.write(line) logging('writing user '+pwstruct[0]+' to '+dest_dir+'/shadow',SILENT_FLAG,be_verbose) line = fd.readline() fd.close() fd2.close() # Copy permissions from real system to template file copy_time_and_permissions('/etc/shadow', dest_dir+'/shadow', be_verbose=0, allow_suid=0, copy_ownership=1) def add_user_to_shadow(dest_dir, user, be_verbose = 0): dest_dir = stripslash(dest_dir) if not os.path.isfile(dest_dir+'/shadow'): logging("Error: "+dest_dir+'/shadow does not exist', SILENT_FLAG, 1) return if os.path.islink(dest_dir+'/shadow'): logging("Error: "+dest_dir+'/shadow is a symlink', SILENT_FLAG, 1) return fd = open('/etc/shadow', 'r') dest = open(dest_dir+'/shadow', 'a') line = fd.readline() while len(line) > 0: pwstruct = line.split(':', 1) if len(pwstruct) >= 1: if pwstruct[0] == user: dest.write(line) logging('Writing user '+pwstruct[0]+' to '+dest_dir+'/shadow', SILENT_FLAG, be_verbose) break line = fd.readline() fd.close() dest.close() # Returns common end of two lists (or strings) def get_common_end(s1, s2): if len(s1) == 0 or len(s2) == 0: return None min_len = min(len(s1), len(s2)) pos = -1 while pos >= -min_len: if s1[pos] != s2[pos]: break pos -= 1 if pos == -1: return None return s1[pos+1:] def copy_path(src: str, dst: str) -> int: """ Copy a path from a source to a destination. If there are shared ending directories between source and destination paths, iterates over the common ending directories, creating each corresponding directory in a destination path. Copies timestamp and permissions from source subdirectories. For example, if src = '/root/dir1/dir2' and dst = '/usr/share/cagefs-skeleton/dir1/dir2', running this function will result in creating directories 'dir1' and 'dir2' within the '/usr/share/cagefs-skeleton' path. """ src = os.path.normpath(src) dst = os.path.normpath(dst) src = stripslash(src) dst = stripslash(dst) if (src == '') or (dst == '') or (src[0] != '/') or (dst[0] != '/'): logging("Error: invalid paths src = "+src+" dst = "+dst, SILENT_FLAG, 1) # error return 1 common = get_common_end(split_path(src), split_path(dst)) if common is None: try: mod_makedirs(dst, 0o755) except (IOError, OSError): # logging("Error: failed to create "+dst, SILENT_FLAG, 1) # error # return 1 pass # success return 0 common_str = join_path(common) dst_path = dst[:-len(common_str)] src_path = src[:-len(common_str)] for _dir in common: dst_path = dst_path+'/'+_dir src_path = src_path+'/'+_dir try: mod_makedirs(dst_path, 0o755) except (IOError, OSError): # logging("Error: failed to create path "+dst_path, SILENT_FLAG, 1) # return 1 pass try: copy_time_and_permissions(src_path, dst_path, be_verbose=0, allow_suid=0, copy_ownership=1) except (IOError, OSError) as e: logging('ERROR: while copying permissions '+src_path+' to '+dst_path+': '+e.strerror, SILENT_FLAG, 1) return 1 # success return 0 def oslstat(path: AnyStr) -> os.stat_result | None: """ Securely get status of a file or a file descriptor. Returns None if unable to retrieve the status. """ try: return os.lstat(path) except (IOError, OSError): return None def copytree(src: str, dst: str, symlinks: bool = True, overwrite: bool = True, skip_src_dirs: list[str] | None = None, update: bool = False, skip_dst_files: list[str] | None = None) -> int: """ Recursively copy an entire directory tree. This function acts like shutil.copytree, but works if destination directory already exists and does not fail if symlink exists. Copies timestamp and permissions from source subdirectories. """ if skip_src_dirs is None: skip_src_dirs = [] if skip_dst_files is None: skip_dst_files = [] if src in skip_src_dirs: return 0 names = os.listdir(src) # If destination is a directory, do nothing, # otherwise (file or symlink), remove it try: dstbuf = os.lstat(dst) if stat.S_ISDIR(dstbuf.st_mode): dst_exists = True else: if dst in skip_dst_files: return 0 try: os.unlink(dst) except (IOError, OSError) as e: logging('ERROR: failed to delete file '+dst+' : '+str(e), SILENT_FLAG, 1) return 1 dst_exists = False except (IOError, OSError): dst_exists = False error = 0 # If destination does not exist (or removed on the previous step) # create required subdirectories if not dst_exists: if copy_path(src, dst) == 1: error = 1 # Copy contents of the source directory to the destination one for name in names: srcname = os.path.join(src, name) dstname = os.path.join(dst, name) try: srcbuf = cached_lstat(srcname) dstbuf = oslstat(dstname) dstname_exists = dstbuf is not None if symlinks and stat.S_ISLNK(srcbuf.st_mode): if (not overwrite) and dstname_exists: continue if dstname_exists and (dstname in skip_dst_files): continue linkto = os.readlink(srcname) if dstname_exists: if stat.S_ISDIR(dstbuf.st_mode): shutil.rmtree(dstname, True) else: os.unlink(dstname) os.symlink(linkto, dstname) elif stat.S_ISDIR(srcbuf.st_mode): if copytree(srcname, dstname, symlinks, overwrite, skip_src_dirs, update) == 1: error = 1 else: if (not overwrite) and dstname_exists: continue if dstname_exists and (dstname in skip_dst_files): continue if dstname_exists and update and (not is_update_needed(srcname, dstname, srcbuf, dstbuf)): continue if dstname_exists: if stat.S_ISLNK(dstbuf.st_mode): os.unlink(dstname) elif stat.S_ISDIR(dstbuf.st_mode): shutil.rmtree(dstname, True) shutil.copyfile(srcname, dstname) copy_time_and_permissions(srcname, dstname, be_verbose=0, allow_suid=0, copy_ownership=1) except (IOError, OSError, shutil.Error) as err: logging('ERROR: while copying '+srcname+' to '+dstname+': '+str(err), SILENT_FLAG, 1) error = 1 try: copy_time_and_permissions(src, dst, be_verbose=0, allow_suid=0, copy_ownership=1) except (IOError, OSError) as err: logging('ERROR: while copying permissions '+src+' to '+dst+': '+str(err), SILENT_FLAG, 1) error = 1 return error def copy_file(srcname: str, dstname: str, create_parent_dir: bool = True, update: bool = False) -> int: """ Copy a source file to a specified destination. The algorithm is as follows: - if the source is a directory - fail; - if the source is a symlink, remove the current destination and create a symlink in its place that points to the same location as the source symlink; - otherwise - remove current destination, and copy the source file copying its time and permissions as well. """ sigterm_check() try: srcbuf = cached_lstat(srcname) if stat.S_ISDIR(srcbuf.st_mode): # error return 1 dstbuf = oslstat(dstname) dstname_exists = dstbuf is not None if not dstname_exists: parent_dir = os.path.dirname(dstname) if parent_dir != '/' and create_parent_dir: copy_path(os.path.dirname(srcname), parent_dir) if stat.S_ISLNK(srcbuf.st_mode): linkto = os.readlink(srcname) if dstname_exists: if stat.S_ISDIR(dstbuf.st_mode): shutil.rmtree(dstname, True) else: os.unlink(dstname) os.symlink(linkto, dstname) else: if dstname_exists and update and (not is_update_needed(srcname, dstname, srcbuf, dstbuf)): return 0 if dstname_exists: if stat.S_ISDIR(dstbuf.st_mode): shutil.rmtree(dstname, True) else: os.unlink(dstname) shutil.copyfile(srcname, dstname) copy_time_and_permissions(srcname, dstname, be_verbose=0, allow_suid=0, copy_ownership=1) except (IOError, OSError, shutil.Error) as e: logging('ERROR: while copying '+srcname+' to '+dstname+': '+e.strerror, SILENT_FLAG, 1) return 1 # Success return 0 def test_numitem_exist(item,num,filename): try: fd = open(filename,'r') except: return 0 line = fd.readline() while (len(line)>0): pwstruct = line.split(':') if (len(pwstruct) > num and pwstruct[num] == item): fd.close() return 1 line = fd.readline() return 0 def test_user_exist(user, passwdfile): return test_numitem_exist(user,0,passwdfile) def test_group_exist(group, groupfile): return test_numitem_exist(group,0,groupfile) def get_all_users_with_uid(uid): # get all users from /etc/passwd return clpwd.get_names(uid) def ExecuteSimple(command): proc = subprocess.Popen(command, shell=True, executable='/bin/bash', stdout=subprocess.PIPE, text=True, bufsize=-1) return proc.communicate()[0] def Execute(command, check_return_code = True, merge_stderr = False, exit_on_error = True): try: if merge_stderr: p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) else: # run the command and suppress it's output p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) (out, _) = p.communicate() if check_return_code: # check return code of the child if p.returncode != 0: logging('Error while executing ' + ' '.join(command), SILENT_FLAG, 1) except OSError: logging('Error: failed to run ' + ' '.join(command), SILENT_FLAG, 1) if exit_on_error: sys.exit(1) raise return out def get_version_str(line, sign): length = len(sign) pos = line.find(sign) if pos != -1: end = line[pos+length:] pos2 = 0 while pos2 < len(end): if not isdigit(end[pos2]): break pos2 += 1 ver = end[:pos2] return ver return '' # Returns value of specified option from postgresql.conf def get_postgres_config(option): value = '' PSQL_CONF = "/var/lib/pgsql/data/postgresql.conf" if os.path.isfile(PSQL_CONF): psql_conf = read_file(PSQL_CONF) for i in range(len(psql_conf)): line = psql_conf[i] if line and line[0] != '#' and line.find(option) != -1: v = line.split('=', 1) opt_name = v[0].strip() if opt_name == option: val = v[1].strip() pos = val.find("#") if pos != -1: val = val[:pos] val = val.strip() val = val.strip("'") val = val.strip('"') value = val break return value # Returns port of postgresql server (as string) def get_postgres_port(): OPTS = '/var/lib/pgsql/data/postmaster.opts' if os.path.isfile(OPTS): lines = read_file(OPTS) for line in lines: v = line.split() for i in range(len(v)): s = v[i] s = s.strip("'") s = s.strip('"') if s == '-p': try: port = v[i+1] except: print_error('Error while parsing', OPTS) sys.exit(1) port = port.strip("'") port = port.strip('"') return port return get_postgres_config('port') def detect_postgres(): PGSQL_SOCKET_CFG = "/usr/share/cagefs/pgsql.socket.name" default_pg_port = '5432' port = get_postgres_port() if port == '': print('Warning: Port of PostgreSQL server is not detected, using default: {}'.format(default_pg_port)) port = default_pg_port socket_name = '/tmp/.s.PGSQL.'+port # Not default port ? if port != default_pg_port: write_file(PGSQL_SOCKET_CFG, [socket_name+'\n']) else: if os.path.isfile(PGSQL_SOCKET_CFG): try: os.unlink(PGSQL_SOCKET_CFG) except (OSError, IOError): # try to write default socket name to config file write_file(PGSQL_SOCKET_CFG, [socket_name+'\n']) # ETC_MPFILE should be read to cagefslib.mounts before call of this function def print_suids(src): if strip_path(src) == '/proc': return mounted = path_is_mounted(src) for name in os.listdir(src): srcname = os.path.join(src, name) try: mode = os.lstat(srcname).st_mode except OSError: print_error('lstat() failed for path', srcname) continue if stat.S_ISLNK(mode): continue if stat.S_ISDIR(mode): print_suids(srcname) elif (mode & (stat.S_ISUID | stat.S_ISGID)): if mounted: print('Mounted to skeleton:', srcname) else: print('Copied to skeleton:', srcname) def get_users_from_passwd(path): users = {} if os.path.isfile(path): pf = open(path, 'r') for line in pf: user = line.split(':')[0] users[user] = 1 pf.close() return users def remove_unwanted_users_from_groups(users = None, path = ETC_TEMPLATE_NEW_DIR): sigterm_check() group_file = path + '/etc/group' if not os.path.isfile(group_file): return lines = read_file(group_file) if users == None: users = get_users_from_passwd(path+'/etc/passwd') file_changed = False for i in range(len(lines)): splitted = lines[i].split(':') if (len(splitted) == 4) and (splitted[3] not in ('', '\n')): group_users = splitted[3][:-1].split(',') new_group_users = [] changed = False for user in group_users: if user in users: new_group_users.append(user) else: changed = True if changed: tmp = splitted[0]+':x:'+splitted[2]+':' tmp2 = ','.join(new_group_users) tmp += tmp2+'\n' lines[i] = tmp file_changed = True if file_changed: write_file(group_file, lines) try: shutil.copystat('/etc/group', group_file) except (OSError, IOError, shutil.Error): pass def read_symlinks(cl_alt_dir): links = {} if os.path.isdir(cl_alt_dir): for _file in os.listdir(cl_alt_dir): path = os.path.join(cl_alt_dir, _file) if os.path.islink(path): link_to = os.readlink(path) links[path] = link_to return links # Returns True if error has occured def write_symlinks(links): error = False for path in links: try: link_to = links[path] if os.path.islink(path): if link_to == os.readlink(path): continue elif os.path.isdir(path): continue try: os.unlink(path) except OSError as e: if e.errno == errno.ENOENT: # No such file error logger.info(f'Path {path} does not exist') else: logger.error(f'Error: Unable to remove path {path}', exc_info=e) os.symlink(link_to, path) except OSError as e: msg = f'Error: failed to create symlink {path} : {str(e).replace("Errno", "Err code")}' logger.error(msg, exc_info=e) logging(msg, SILENT_FLAG, 1) error = True return error def is_writable(sbuf, uid, gid): groups = get_groups(uid, gid) mode = sbuf.st_mode # owner has write access ? if sbuf.st_uid == uid: return (mode & stat.S_IWUSR) return ( ((mode & stat.S_IWGRP) and (sbuf.st_gid in groups)) or (mode & stat.S_IWOTH) ) def make_dir(path, perm, allow_symlink = False, update_perm = True): """ Create directory if it does not exist. Check for symlink (race conditions are not handled). Returns True if error has occured :param path: path to directory :type path: string :param perm: Linux permissions :type perm: int :param allow_symlink: True = allow path to be symlink, False = delete symlink and create directory :type allow_symlink: bool :param update_perm: True = set permissions when path exists :type update_perm: bool """ path_exists = True try: sbuf = os.lstat(path) except OSError: path_exists = False if path_exists: if stat.S_ISDIR(sbuf.st_mode) or (allow_symlink and os.path.isdir(path)): if update_perm: return set_perm(path, perm) return False else: try: os.unlink(path) except (OSError, IOError): if os.path.lexists(path): logging('Error: failed to remove ' + path, SILENT_FLAG, 1) return True try: mod_makedirs(path, perm) except OSError as e: if not os.path.isdir(path): msg = f'Error: failed to create directory {path} : {str(e).replace("Errno", "Err code")}' logger.error(msg, exc_info=e) logging(msg, SILENT_FLAG, 1) return True return False # Returns True if error has occured def set_perm(path, perm): try: os.chmod(path, perm) return False except (OSError, IOError): logging('Error: failed to set permissions to ' + path, SILENT_FLAG, 1) return True # Returns True if error has occured def set_owner(path, uid, gid): try: os.chown(path, uid, gid) return False except (OSError, IOError): logging('Error: failed to set ownership to ' + path, SILENT_FLAG, 1) return True # basepath == '/home/user/.cagefs' def fix_owner_of_personal_mounts(basepath, real_homepath, uid, gid, personal_mounts): for mount in personal_mounts: names = split_path(mount) path = '' for name in names: path = os.path.join(path, name) dirpath = os.path.join(basepath, path) fd = set_owner_dir_secure(dirpath, uid, gid, real_homepath) if fd is None: # Error has occured or directory does not exist # So, do not process subdirectories break closefd(fd) # is_user_enabled = True: users are enabled # is_user_enabled = False: users are disabled def update_status(users, is_user_enabled, fix_owner = False): if not (fix_owner or cldetectlib.is_ispmanager()): return import cagefs_ispmanager_lib personal_mounts = [] if fix_owner: from cagefsctl import MountpointConfig mp_config = MountpointConfig(skip_errors=True, skip_cpanel_check=True) personal_mounts = mp_config.personal_mounts for user in users: try: pw = clpwd.get_pw_by_name(user) except ClPwd.NoSuchUserException: continue real_homepath = os.path.realpath(pw.pw_dir) path = os.path.join(pw.pw_dir, '.cagefs') status_flag = os.path.join(path, '.cagefs.enabled') # Set owner fd = set_owner_dir_secure(path, pw.pw_uid, pw.pw_gid, real_homepath) if fd is None: # Error has occured continue # Set permissions fd = set_perm_dir_secure(path, 0o771, real_homepath, fd=fd) if fd is None: # Error has occured continue closefd(fd) set_user_perm(pw.pw_uid, pw.pw_gid) # .cagefs.enabled files are not needed, we remove them unconditionally remove_file_or_dir(status_flag) set_root_perm() # update ISP manager user wrappers cagefs_ispmanager_lib.ispmanager_create_user_wrapper_detect_php_ver(pw, is_user_enabled, True) if fix_owner: fix_owner_of_personal_mounts(path, real_homepath, pw.pw_uid, pw.pw_gid, personal_mounts) def get_php_info(): php_path = orig_binaries['php-cli'] php_ini_path = orig_binaries['php.ini'] return Execute([php_path, '-c', php_ini_path, '-i']) def get_list_of_php_modules(): php_path = orig_binaries['php-cli'] php_ini_path = orig_binaries['php.ini'] try: if is_ea4_enabled(): result = Execute([php_path, '-m'], merge_stderr=True, exit_on_error=False) else: result = Execute([php_path, '-c', php_ini_path, '-qm'], merge_stderr=True, exit_on_error=False) except OSError: result = '' return result php_modules = None # Returns list of php extension modules (.so) def get_php_modules(): global php_modules if php_modules == None: read_native_conf() lines = get_list_of_php_modules().split('\n') php_modules = {} for line in lines: if line and (not line.startswith('[')): module_name = line.replace(' ', '_').lower() php_modules[module_name] = 1 return list(php_modules) alt_versions = None # Reads config file and returns versions of alternative php binaries (as dictionary) def get_alt_versions(): global alt_versions if alt_versions is None: CL_ALT_CONF = '/etc/cl.selector/selector.conf' alt_versions = {} if os.path.isfile(CL_ALT_CONF): lines = read_file_cached(CL_ALT_CONF) for line in lines: line = line.rstrip() if line != "": ar = line.split() if len(ar) == 4 and ar[0] == 'php': vers = ar[1] # Key - short version (5.4), value - full version (5.4.39) alt_versions[vers] = ar[2] return alt_versions # version -> alias -> path alt_conf = None # Reads config file and returns path to alternative php binary def get_alt_conf(vers, alias = 'php', get_aliases = False): global alt_conf if alt_conf == None: CL_ALT_CONF = '/etc/cl.selector/selector.conf' alt_conf = {} if os.path.isfile(CL_ALT_CONF): lines = read_file_cached(CL_ALT_CONF) for line in lines: line = line.rstrip() if line != "": ar = line.split() if len(ar) == 4: cur_alias = ar[0] cur_vers = ar[1] cur_path = ar[3] if cur_vers in alt_conf: temp = alt_conf[cur_vers] else: temp = {} temp[cur_alias] = cur_path alt_conf[cur_vers] = temp if vers in alt_conf: if get_aliases: # Return all available aliases for specified version return list(alt_conf[vers].keys()) elif alias in alt_conf[vers]: # Return path for version and alias specified return alt_conf[vers][alias] return None def get_alt_aliases(vers): if vers == 'native': return list(orig_binaries) else: aliases = get_alt_conf(vers, get_aliases = True) if aliases == None: return [] return aliases def get_alt_php_libs(): FIND = '/usr/bin/find' altpaths = [] binpaths = [] for altdir in get_alt_dirs(): path = '/opt/alt/' + altdir binpath = path + '/usr/bin' sbinpath = path + '/usr/sbin' if os.path.isdir(path): altpaths.append(path) else: continue if os.path.isdir(binpath): binpaths.append(binpath) if os.path.isdir(sbinpath): binpaths.append(sbinpath) paths = [] if altpaths: paths.extend(Execute([FIND] + altpaths + ['-name', '*.so']).split('\n')) if binpaths: paths.extend(Execute([FIND] + binpaths + ['-type', 'f']).split('\n')) libs = set() for path in paths: if path: for lib in get_ldd_libs(path): libs.add(lib) return list(libs) # Detect user's PHP version def get_php_version_for_user(username): # read link /var/cagefs/[prefix]/[username]/etc/cl.selector/php # --> /opt/alt/php54/usr/bin/php-cgi # or # --> /usr/selector/php path = BASEDIR + '/' + get_user_prefix(username) + '/' + username + '/etc/cl.selector/' user_php_file = path + 'php' if not os.path.islink(user_php_file): user_php_file = path + 'lsphp' if not os.path.islink(user_php_file): return None link_to = os.readlink(user_php_file) if link_to.startswith('/usr/selector/'): return 'native' # PHP ver is not native, determine version from link_to if link_to.startswith('/opt/alt/php'): php_ver = link_to.replace('/opt/alt/php', '') php_ver = php_ver[:php_ver.find('/')] return php_ver # PHP version not determined return None # Name and location of user's php.d directory, where symlinks to .ini files are stored CL_PHP_DIR_NAME = 'cl.php.d' ETC_CL_PHP_PATH = '/etc/'+CL_PHP_DIR_NAME # Backup files that contain user's settings for Cloudlinux Alternatives CL_ALT_BACKUP_DIR = '.cl.selector' CL_ALT_DEFAULTS = 'defaults.cfg' # php_vers 'php5.4' # php_modules = { 'php5.3' : ['dom', 'xmlreader'], 'php5.4' : ['dom', 'xsl'] } # dirpath is something like '/var/cagefs/prefix/user/etc/cl.php.d/alt-php54' # dirname is something like 'php54' def enable_extensions_symlinks(php_vers, php_modules, dirpath, dirname): for mod in php_modules[php_vers]: link_name = mod + '.ini' link_path = os.path.join(dirpath, link_name) link_to = os.path.join('/opt/alt', dirname, 'etc', 'php.d.all', link_name) if not os.path.islink(link_path): remove_file_or_dir(link_path) try: os.symlink(link_to, link_path) except OSError as e: msg = f'Error: failed to create symlink {link_path} : {str(e).replace("Errno", "Err code")}' logger.error(msg, exc_info=e) logging(msg, SILENT_FLAG, 1) deps_cache = {} def get_dependencies(alt_dir): """ Return dependencies of php modules (extensions), determined by parsing of ini files in specified directory :param alt_dir: path to directory where ini files are (something like '/opt/alt/php54/etc/php.d.all') :type alt_dir: string :return: something like { 'mailparse' : ['mbstring'], 'xsl' : ['dom'], 'xmlreader' : ['dom'] } :rtype: dict """ global deps_cache if alt_dir not in deps_cache: deps = {} if os.path.isdir(alt_dir): for ini_file in os.listdir(alt_dir): ini_path = alt_dir + '/' + ini_file if ini_path.endswith('.ini') and os.path.isfile(ini_path): extname = ini_file[:-len('.ini')] deps[extname] = [] ini_file = read_file_cached(ini_path) for line in ini_file: line = line.rstrip() if line.startswith('extension') or line.startswith('zend_extension'): if line.endswith('"'): line = line.replace('"', '') elif line.endswith("'"): line = line.replace("'", '') ar = line.split('=', 1) if (len(ar) == 2) and ar[1].endswith('.so'): ext = os.path.basename(ar[1].lstrip()) ext = ext[:-len('.so')] if extname != ext: deps[extname].append(ext) deps_cache[alt_dir] = deps return deps_cache[alt_dir] # Build load order of modules using recursive algorithm def get_load_order(load_order, deps, mod): if mod in deps: for dep in deps[mod]: get_load_order(load_order, deps, dep) if mod not in load_order: load_order.append(mod) # Build load order of modules NOT using recursive algorithm def get_load_order_not_recursive(load_order, deps, mod): if mod in deps: for dep in deps[mod]: if dep not in load_order and dep in deps: load_order.append(dep) if mod not in load_order: load_order.append(mod) def build_load_order(php_vers, php_modules, ini_path, deps = None, quiet = False): """ :param php_vers: something like 'php5.4' :type php_vers: string :param php_modules: { 'php5.3' : ['dom', 'xmlreader'], 'php5.4' : ['dom', 'xsl'] } :type php_modules: dict :param ini_path: path to directory where ini files are (something like '/opt/alt/php54/etc/php.d.all') :type ini_path: string """ if deps is None: deps = get_dependencies(ini_path) for func in (get_load_order, get_load_order_not_recursive): load_order = [] try: # ioncube loader should be first in the load order if 'ioncube_loader' in php_modules[php_vers]: func(load_order, deps, 'ioncube_loader') elif 'ioncube_loader_4' in php_modules[php_vers]: func(load_order, deps, 'ioncube_loader_4') for mod in php_modules[php_vers]: if mod not in ('ioncube_loader', 'ioncube_loader_4'): func(load_order, deps, mod) break except RuntimeError: if not quiet: logging('Error: cyclic dependencies of PHP modules detected. Depth of dependencies will be limited to 1', SILENT_FLAG, 1) return load_order php_ini_validator = None bad_try_init_phpinivalidator_trigger = False def read_custom_php_settings(homepath, filename, uid, gid, php_vers=None, user_name=None, alt_php_ini_file=None): global php_ini_validator global bad_try_init_phpinivalidator_trigger global validate_alt_php_ini if homepath is None: return None # Read backup of custom php settings in user's home directory backup_path = os.path.join(homepath, CL_ALT_BACKUP_DIR, filename) if not os.path.isfile(backup_path): return None php_ini_lines = read_file_secure(backup_path, uid, gid, exit_on_error = False) if validate_alt_php_ini: # Do PHP options validation if not bad_try_init_phpinivalidator_trigger and php_ini_validator is None: try: php_ini_validator = phpinivalidator.PHPINIvalidator(phpconf_path=PHP_CONF) except (OSError, IOError): bad_try_init_phpinivalidator_trigger = True if bad_try_init_phpinivalidator_trigger: return php_ini_lines if not php_vers: php_vers = phpinivalidator.get_php_ver(backup_path) alt_vers = get_alt_versions() output_lines_list = php_ini_validator.validate(input_phpini_lines=php_ini_lines, php_ver=alt_vers[php_vers]) if php_ini_validator.unknown_options or php_ini_validator.invalid_values_options or php_ini_validator.invalid_options: if user_name: log_message = "User: " + user_name else: log_message = "User: Unknown" log_message += "; PHP version: " + php_vers + "\n Backup file: " + backup_path if alt_php_ini_file: log_message += "\n Destination file: " + alt_php_ini_file else: log_message += "\n Destination file: Unknown" php_options_log_write(log_message, php_ini_validator.unknown_options, php_ini_validator.invalid_values_options, php_ini_validator.invalid_options) else: # Pass PHP options validation output_lines_list = php_ini_lines return output_lines_list def enable_extensions(php_vers, php_modules, dirpath, dirname, uid, gid, homepath=None, user_name=None): """ Enable specified extensions for specific php version and user :param php_vers: php version, something like 'php5.4' :type php_vers: string :param php_modules: extesions enabled for different php version for the user specified like { 'php5.3' : ['dom', 'xmlreader'], 'php5.4' : ['dom', 'xsl'] } :type php_modules: dict :param dirpath: path where generated alt_php.ini file is written to (something like '/var/cagefs/prefix/user/etc/cl.php.d/alt-php54') :type dirpath: string :param dirname: name of directory for specified php version inside /opt/alt directory (something like 'php54') :type dirname: string :param uid: uid of user :type uid: int :param gid: gid of user :type gid: int :param homepath: path to home directory of user (something like '/home/user') :type homepath: string :param user_name: name of user :type user_name: string """ ini_path = os.path.join('/opt/alt', dirname, 'etc', 'php.d.all') # Build load order of php modules # example:['bcmath', 'dom', 'gd', 'imap', 'json', 'mcrypt'] load_order = build_load_order(php_vers, php_modules, ini_path) # Build content of alt_php.ini file alt_php_ini = [] for module in load_order: alt_php_ini.append(';---'+module+'---\n') # module.ini path # example: /opt/alt/php54/etc/php.d.all/imap.ini module_ini_path = ini_path + '/' + module + '.ini' # settings per module.ini # example: ['; Enable imap extension module\n', 'extension=imap.so\n'] module_ini = read_file_cached(module_ini_path) for line in module_ini: line = line.rstrip() # Do not add empty lines and comments to alt_php.ini file if line and not line.startswith(';'): line += '\n' if line not in alt_php_ini: alt_php_ini.append(line) alt_php_ini.append('\n') # Write generated alt_php.ini file user_ini_path = os.path.join(dirpath, 'alt_php.ini') try: # custom users php settings from /home/user/.cl.selector/alt_phpX.X.cfg # example: [';---fileinfo---\n', ';extension=fileinfo.so\n', ';\n', # ';---phar---\n', ';extension=phar.so\n', ';\n'] custom_php_settings = read_custom_php_settings(homepath, 'alt_'+dirname+'.cfg', uid, gid, php_vers=php_vers[3:], user_name=user_name, alt_php_ini_file=user_ini_path) except (OSError, IOError): custom_php_settings = None if custom_php_settings is not None: alt_php_ini.extend(custom_php_settings) alt_php_ini.append('\n') write_file_secure(alt_php_ini, user_ini_path, uid, gid) # userpath is something like '/var/cagefs/prefix/user/etc/cl.php.d' (directory should exist already) # def_vers = destination php version # cl_alt_def_modules = default set of php modules from global defaults # php_modules = set of php modules from user's backup # vers_changed = True if def_vers != def_vers_old (selected php version has been changed for the user) # def_vers_old = previous (old) version of php # force = True: reset selected php modules to global defaults (ignore user's backup) # rebuild = True: rebuild (regenerate) alt_php.ini files # user_name = None - user name for logging def select_default_php_modules(userpath, homepath, uid, gid, def_vers, cl_alt_def_modules, php_modules, vers_changed, def_vers_old, force, rebuild, user_name=None): alt_vers = get_alt_versions() if def_vers == None: def_vers = 'native' if def_vers_old == None: def_vers_old = 'native' if php_modules == None: php_modules = {} if cl_alt_def_modules == None: cl_alt_def_modules = {} real_userpath = os.path.realpath(userpath) modules_changed = False for vers in alt_vers: php_vers = 'php' + vers dirname = 'php' + vers.replace('.', '') altdir = 'alt-' + dirname dirpath = os.path.join(userpath, altdir) user_ini_path = os.path.join(dirpath, 'alt_php.ini') if (not os.path.lexists(user_ini_path)) or force or rebuild: if make_userdir(dirpath, 0o755, uid, gid, real_userpath): continue if (php_vers not in php_modules) or force: if php_vers in cl_alt_def_modules: php_modules[php_vers] = cl_alt_def_modules[php_vers] else: modules = get_php_modules() php_modules[php_vers] = modules modules_changed = True # Create symlinks for php modules # enable_extensions_symlinks(php_vers, php_modules, dirpath, dirname) # Create alt_php.ini file for php modules enable_extensions(php_vers, php_modules, dirpath, dirname, uid, gid, homepath=homepath, user_name=user_name) # else: # Actions commented out are not secure, because user is owner of the parent (/var/cagefs/prefix/user/etc/cl.php.d) directory # make_dir(dirpath, 0755) # set_owner(dirpath, uid, gid) # set_perm(user_ini_path, 0644) # set_owner(user_ini_path, uid, gid) if vers_changed: new_vers = def_vers else: new_vers = def_vers_old if vers_changed or modules_changed: # Save backup write_cl_alt_to_backup(homepath, new_vers, php_modules, uid, gid) def read_cl_alt_backup_as_user(homepath, uid, gid): backup_path = os.path.join(homepath, CL_ALT_BACKUP_DIR, CL_ALT_DEFAULTS) set_user_perm(uid, gid) result = read_cl_alt_backup(backup_path) set_root_perm() return result def read_cl_alt_backup(backup_path): try: backup_file = open_file_not_symlink(backup_path) except (OSError, IOError): return None, None, None, None cfg = configparser.ConfigParser(interpolation=None, strict=False) try: cfg.readfp(backup_file) except configparser.Error: # ignore invalid (corrupted) backup file return None, None, None, None try: def_vers = config_get_option_as_list(cfg, 'versions', 'php')[0] except IndexError: return None, None, None, None modules = {} php_state = {} other = {} for section in cfg.sections(): dirname = section.replace('.', '') if dirname.startswith('php') and isdigits(dirname[len('php'):]): modules[section] = config_get_option_as_list(cfg, section, 'modules') if cfg.has_option(section, 'state') and (cfg.get(section, 'state').strip().lower().startswith('disable')): php_state[section[len('php'):]] = False elif section not in ('versions',): options = {} for option in cfg.options(section): options[option] = cfg.get(section, option) other[section] = options if ('phpnative' in other) and ('state' in other['phpnative']) and (other['phpnative']['state'].strip().lower().startswith('disable')): php_state['native'] = False return def_vers, modules, php_state, other cl_alt_def_vers = None cl_alt_def_modules = None cl_alt_def_php_state = None cl_alt_def_other = None def read_cl_alt_defaults(): global cl_alt_def_vers, cl_alt_def_modules, cl_alt_def_php_state, cl_alt_def_other if cl_alt_def_vers != None and cl_alt_def_modules != None and cl_alt_def_php_state != None and cl_alt_def_other != None: return cl_alt_def_vers, cl_alt_def_modules, cl_alt_def_php_state, cl_alt_def_other cl_alt_def_vers, cl_alt_def_modules, cl_alt_def_php_state, cl_alt_def_other = read_cl_alt_backup(os.path.join(ETC_CL_ALT_PATH, CL_ALT_DEFAULTS)) return cl_alt_def_vers, cl_alt_def_modules, cl_alt_def_php_state, cl_alt_def_other # homepath = '/home/user' # def_vers = '5.1' # modules = {'php5.1': ['json', 'curl'], 'php5.2': ['calendar', 'ctypes']} def write_cl_alt_to_backup(homepath, def_vers, modules, uid, gid, state = None, other = None): if homepath == None: # write global defaults backup_path = os.path.join(ETC_CL_ALT_PATH, CL_ALT_DEFAULTS) drop_perm = False else: # write user's backup file backup_dir = os.path.join(homepath, CL_ALT_BACKUP_DIR) real_homepath = os.path.realpath(homepath) if make_userdir(backup_dir, 0o755, uid, gid, real_homepath): return backup_path = os.path.join(homepath, CL_ALT_BACKUP_DIR, CL_ALT_DEFAULTS) # Drop privileges drop_perm = True # Generate file content backup = [] backup.append('[versions]\n') backup.append('php='+def_vers+'\n') if modules == None: modules = {} for section in modules: backup.append('\n['+section+']\n') backup.append('modules='+','.join(modules[section])+'\n') if state != None: vers = section[len('php'):] if (vers in state) and (not state[vers]): backup.append('state=disabled\n') if other != None: for section in other: backup.append('\n['+section+']\n') for option in other[section]: backup.append(option+'='+other[section][option]+'\n') # Write content to the file write_file_secure(backup, backup_path, uid, gid, drop_perm) def get_mounted_dirs(all_cagefs_mounts=False, without_nosuid=False, rw_mounts_only=False, all_mounts=False): """ Return list of mounts points :param all_cagefs_mounts: return CageFS mounts points only :param without_nosuid: return mount points without 'nosuid' attribute :param rw_mounts_only: return rw mount points only (i.e. mounts without 'ro' attribute) :param all_mounts: return all mount points """ mounts_list = [] with open("/proc/mounts", "r") as mounts: for line in mounts: if all_mounts or line.find('cagefs-etcfs') == -1 and line.find('cagefs-varfs') == -1: p = line.split() mountpoint = p[1] opts = p[3].split(',') if without_nosuid and 'nosuid' in opts: continue if rw_mounts_only and 'ro' in opts: continue if all_mounts or mountpoint.find(SKELETON+'/') != -1 \ or (all_cagefs_mounts and (mountpoint.find('/var/cagefs/') != -1 or mountpoint.find('/.cagefs/') != -1)): mounts_list.append(mountpoint[mountpoint.find('/'):]) return mounts_list def get_homeN_dirs(use_glob=False): """ Returns set of base home directories like {"/home0", "/home1", .., "/home9"} including "/home" """ pattern = re.compile(r'(/home\d?)/') dirs = set() if use_glob: for path in glob.glob('/home*'): m = pattern.match(addslash(path)) if m and os.path.isdir(path): dirs.add(m.group(1)) else: pw = clpwd.get_user_dict() for user in pw: line = pw[user].pw_dir m = pattern.match(line) if m: dirs.add(m.group(1)) return dirs listdir_cache = {} def cached_listdir(path): global listdir_cache path = stripslash(path) if path not in listdir_cache: try: sbuf = cached_lstat(path) if stat.S_ISDIR(sbuf.st_mode): res = listdir_cache[path] = os.listdir(path) else: res = listdir_cache[path] = [] except (OSError, IOError): res = listdir_cache[path] = [] else: res = listdir_cache[path] return res CUSTOM_ETC = '/etc/cagefs/custom.etc/' def get_custom_etc_list(): return cached_listdir(CUSTOM_ETC) def get_additional_etc_files_for_user(username: str, user_etc_path: str) -> Dict[str, int]: """ Get additional files for a user to be placed within their '/etc' directory. This includes retrieving files added by the 'custom.etc' directory mechanism and mount points defined in the 'cagefs.mp' file. Args: username: The user's name user_etc_path: The user etc path, like '/var/cagefs/<prefix>/<username>/etc' """ return { **get_custom_etc_files_for_user(username, user_etc_path), **get_etc_dirs_from_mounts_for_user(username, user_etc_path), } def get_custom_etc_files_for_user(username: str, user_etc_path: str) -> Dict[str, int]: """ Get a list of additional files for a user, which have been added to the user's '/etc' directory by utilizing the 'custom.etc' directory mechanism. Args: username: The user's name user_etc_path: The user etc path, like '/var/cagefs/<prefix>/<username>/etc' """ etc_list = {} if username in get_custom_etc_list(): path = CUSTOM_ETC + username try: sbuf = cached_lstat(path) except (OSError, IOError): return etc_list if stat.S_ISDIR(sbuf.st_mode): add_tree_to_list(path, etc_list, cut_path = path, add_path = user_etc_path) return etc_list def get_etc_dirs_from_mounts_for_user(username: str, user_etc_path: str) -> Dict[str, int]: """ Get a list of additional directories for a user, which have been added to the user's '/etc' by defining additional mount points within the 'cagefs.mp' file. Process only the mount points splitted by username or UID, as only these are mounted to the user's '/var/cagefs/.../etc', which is subsequently mounted to the skeleton's '/etc'. Args: username: The user's name user_etc_path: The user etc path, like '/var/cagefs/<prefix>/<username>/etc' """ import cagefsctl etc_list = {} mp_config = cagefsctl.MountpointConfig(skip_errors=True, skip_cpanel_check=True) splitted_by_username_mounts = mp_config.splitted_by_username_mounts splitted_by_uid_mounts = mp_config.splitted_by_uid_mounts _process_etc_mounts(splitted_by_username_mounts, username, user_etc_path, etc_list) try: user_uid = str(clpwd.get_uid(username)) except clpwd.NoSuchUserException: return etc_list _process_etc_mounts(splitted_by_uid_mounts, user_uid, user_etc_path, etc_list) return etc_list def _process_etc_mounts(mounts: List[str], user_identifier: str, user_etc_path: str, etc_list: Dict[str, int]) -> None: """ Process mount points and construct a list of '/etc' ones and their respective user subdirectories. Retrieve a list of contents within the mount point, and if it contains the user's identifier (UID for mount points splitted by UIDs, or username for mount points splitted by usernames), add all the subdirectories to the resulting list. """ for path in mounts: if not path.startswith('/etc/') or user_identifier not in cached_listdir(path): continue path = path.replace('/etc/', '', 1) parts = path.split('/') parts.append(user_identifier) current_path = user_etc_path for part in parts: current_path = os.path.join(current_path, part) etc_list[current_path] = 1 # user_etc_path = something like '/var/cagefs/prefix/user/etc' def update_custom_etc_files_for_user(user, user_etc_path): if user in get_custom_etc_list(): _dir = CUSTOM_ETC + user try: sbuf = cached_lstat(_dir) except (OSError, IOError): return if stat.S_ISDIR(sbuf.st_mode): for filename in os.listdir(_dir): if filename not in [ CL_ALT_NAME, CL_PHP_DIR_NAME, ETC_VERSION_NAME ]: src = _dir + '/' + filename dest = user_etc_path +'/' + filename try: sbuf = cached_lstat(src) except (OSError, IOError) as e: logging('Error: lstat() failed file ' + src + ' : ' + str(e), SILENT_FLAG, 1) continue if stat.S_ISDIR(sbuf.st_mode): copytree(src, dest, update = True) else: copy_file(src, dest, update = True) CUSTOM_ETC_LOG = '/usr/share/cagefs/custom.etc/' def custom_etc_present(): # Directory is not empty ? if cached_listdir(CUSTOM_ETC_LOG): return True return False # list_of_files = list of paths without path to etc directory (like ['/hosts']) def save_custom_etc_log(user, list_of_files): try: _ = cached_lstat(CUSTOM_ETC_LOG) except (OSError, IOError): umask_saved = os.umask(0) os.mkdir(CUSTOM_ETC_LOG, 0o700) os.umask(umask_saved) if list_of_files: write_file(CUSTOM_ETC_LOG+user, list_of_files, add_eol = True) else: try: os.unlink(CUSTOM_ETC_LOG+user) except (OSError, IOError): pass # Returns list of custom etc files (or directories) that has been removed from /etc/cagefs/custom.etc/user directory # list_of_files = list of paths without path to etc directory (like ['/hosts']) def get_custom_etc_files_to_delete(user, list_of_files): res = [] if user in cached_listdir(CUSTOM_ETC_LOG): old_list = read_file(CUSTOM_ETC_LOG+user) for path in old_list: path = path.rstrip() if path not in list_of_files: if not (path.startswith('/'+CL_ALT_NAME) or path.startswith('/'+CL_PHP_DIR_NAME) or path.startswith(ETC_VERSION)): res.append(path) res.sort() return res def cut_path(_list, path): plen = len(path) res = set() for p in _list: res.add(p[plen:]) return res def is_path_secure(path): try: sbuf = os.lstat(path) except OSError: return False if not stat.S_ISREG(sbuf.st_mode): return False return not ( (sbuf.st_uid != 0) or (sbuf.st_gid != 0) or (sbuf.st_mode & stat.S_IWOTH) ) cagefs_ini_cfg = None def read_cagefs_ini(): global cagefs_ini_cfg if cagefs_ini_cfg is not None: return if (not os.path.isfile(CAGEFS_INI)) or (not is_path_secure(CAGEFS_INI)): cagefs_ini_cfg = None return cagefs_ini_cfg = configparser.ConfigParser(interpolation=None, strict=False) try: cagefs_ini_cfg.read(CAGEFS_INI) except configparser.Error: cagefs_ini_cfg = None def get_update_period(): seconds_in_24h = 60*60*24 read_cagefs_ini() if cagefs_ini_cfg is None: return seconds_in_24h res = config_get_option_as_list(cagefs_ini_cfg, 'common', 'update_period_days') try: days = int(res[0]) if days < 0: days = 1 except (ValueError, IndexError): days = 1 return seconds_in_24h * days def set_cagefs_ini_option(section, option, value): global cagefs_ini_cfg read_cagefs_ini() if cagefs_ini_cfg is None: cagefs_ini_cfg = configparser.ConfigParser(interpolation=None, strict=False) if not cagefs_ini_cfg.has_section(section): cagefs_ini_cfg.add_section(section) cagefs_ini_cfg.set(section, option, value) ini_file = open(CAGEFS_INI, 'w') cagefs_ini_cfg.write(ini_file) ini_file.close() set_owner(CAGEFS_INI, 0, 0) set_perm(CAGEFS_INI, 0o600) def set_update_period(days): set_cagefs_ini_option('common', 'update_period_days', str(days)) # Reads parameters for tmpwatch from config file def get_tmpwatch_params(): is_ubuntu = os.path.isfile('/opt/alt/tmpreaper/usr/sbin/tmpreaper') TMPWATCH = '/opt/alt/tmpreaper/usr/sbin/tmpreaper 720' if is_ubuntu else '/usr/sbin/tmpwatch -umclq 720' read_cagefs_ini() if cagefs_ini_cfg is None: return TMPWATCH if cagefs_ini_cfg.has_option('common', 'tmpwatch'): return cagefs_ini_cfg.get('common', 'tmpwatch') return TMPWATCH def set_tmpwatch_params(params_str): set_cagefs_ini_option('common', 'tmpwatch', params_str) def get_tmpwatch_dirs(): read_cagefs_ini() if cagefs_ini_cfg is None: return [] return config_get_option_as_list(cagefs_ini_cfg, 'common', 'tmpwatch_dirs') LAST_UPDATE_TIME = '/usr/share/cagefs/last_update_time.txt' def save_last_update_time(): write_file(LAST_UPDATE_TIME, [str(int(time.time()))], add_eol = True) os.chmod(LAST_UPDATE_TIME, 0o644) def read_last_update_time(): if os.path.isfile(LAST_UPDATE_TIME): content = read_file(LAST_UPDATE_TIME) try: return int(content[0].strip()) except (ValueError, IndexError): pass return 0 def update_of_cagefs_skeleton_is_needed(): update_period = get_update_period() if update_period == 0: return True last_update = read_last_update_time() current_time = int(time.time()) return current_time >= (last_update + update_period) # Compare directories or files # Returns True if they are equal, False otherwise # This function does NOT follow symlinks # It compares values of symlinks via readlink() # Unless shallow is given and is false, files with identical os.stat() # signatures are taken to be equal (st_atime is ignored in comparison). def are_dirs_equal(dir1, dir2, shallow = True): sbuf1 = oslstat(dir1) if not sbuf1: return False sbuf2 = oslstat(dir2) if not sbuf1: return False if stat.S_ISDIR(sbuf1.st_mode): if not stat.S_ISDIR(sbuf2.st_mode): return False listdir1 = os.listdir(dir1) listdir1.sort() listdir2 = os.listdir(dir2) listdir2.sort() if listdir1 != listdir2: return False for name in listdir1: path1 = dir1 + '/' + name path2 = dir2 + '/' + name if not are_dirs_equal(path1, path2, shallow): return False return True elif stat.S_ISDIR(sbuf2.st_mode): return False if shallow or stat.S_ISLNK(sbuf1.st_mode) or stat.S_ISLNK(sbuf2.st_mode): # Compare values of symlinks or metadata of files return is_same_metadata(dir1, dir2, sbA = sbuf1, sbB = sbuf2) else: # Compare content of files return filecmp.cmp(dir1, dir2, shallow = False) def clean_dir_from_old_session_files(dir_path, max_lifetime): """ Clean directories from old files :param dir_path: Dir path to clean :param max_lifetime: Max lifetime for clean :return: None """ sessions = glob.glob(os.path.join(dir_path, "sess_[a-z0-9]*")) cur_time = time.time() for sess in sessions: try: s = os.stat(sess) ctime = s.st_ctime if cur_time - ctime > max_lifetime: os.unlink(sess) except OSError: pass def get_opts_from_php_ini(path, default_time, default_path='/tmp'): """ Read php.ini and returns session.save_path and session.gc_maxlifitime options :param str path: Path to ini file :param int default_time: Return that time when can not get value from config :param str default_path: Return that path when can not get value from config :return: Tuple (session.save_path, session.gc_maxlifitime) :rtype: (str, int) """ try: with open(path, "r") as config: for line in config.readlines(): l = line.strip() if l.startswith(';'): continue elif l.startswith("session.save_path") and "=" in l: default_path = (l.split("=")[1]).strip() elif l.startswith("session.gc_maxlifetime") and "=" in l: default_time = int(l.split("=")[1]) except (IndexError, ValueError, IOError): pass return default_path.strip("\"'"), default_time def get_relative_path(original, dest): """ Convert symlink value (path) from absolute to relative :param original: path to original file :param dest: path where symlink will be created """ if original.startswith('/'): return os.path.relpath(strip_path(original), strip_path(os.path.dirname(dest))) return original def relative_symlink(original, dest): """ Create relative symlink instead of absolute :param original: path to original file :param dest: path where symlink will be created """ relative_path = get_relative_path(original, dest) os.symlink(relative_path, dest) def update_symlink_in_skeleton(origpath, jailpath): """ Create symlink or update if changed. Return value of original symlink (destination it points to) :param origpath: path to symlink in real file system :param jailpath: path to symlink in cagefs-skeleton """ realfile = os.readlink(origpath) try: relative_path = get_relative_path(realfile, jailpath) if os.path.islink(jailpath): if os.readlink(jailpath) != relative_path: os.unlink(jailpath) else: remove_file_or_dir(jailpath, check_mounts=True) if not os.path.islink(jailpath): logging(f'Creating symlink {jailpath} to {relative_path}', SILENT_FLAG, 1) os.symlink(relative_path, jailpath) except OSError as e: logging(f'Failed to create symlink {jailpath} to {relative_path}: {str(e)}', SILENT_FLAG, 1) return realfile def create_utmp_in_skeleton(): """ Create symlink /usr/share/cagefs-skeleton/var/run/utmp -> /var/run/cagefs/utmp needed for emulation of /var/run/utmp inside CageFS For details see CAG-706 """ if not os.path.isdir(SKELETON): return utmp_cagefs = VAR_RUN_CAGEFS + '/utmp' skel_cagefs_dir = SKELETON + VAR_RUN_CAGEFS skel_utmp = SKELETON + '/var/run/utmp' try: if not os.path.isdir(skel_cagefs_dir): mod_makedirs(skel_cagefs_dir, 0o755) except OSError as e: logging('Error: failed to create directory ' + skel_cagefs_dir + ' : ' + str(e), SILENT_FLAG, 1) try: if not os.path.islink(skel_utmp): os.symlink(utmp_cagefs, skel_utmp) except OSError as e: logging('Error: failed to create symlink ' + skel_utmp + ' -> ' + utmp_cagefs + ' : ' + str(e), SILENT_FLAG, 1) def create_utmp_for_user(user, exit_on_error=True): """ Create user's personal /home/user/.cagefs/var/run/cagefs/utmp file For details see CAG-706 :param user: user name :type user: string :param exit_on_error: True == execute sys.exit(1) when error has occured :type exit_on_error: bool """ try: pw = clpwd.get_pw_by_name(user) except ClPwd.NoSuchUserException: return utmp_dir = pw.pw_dir + '/.cagefs' + VAR_RUN_CAGEFS utmp_file = utmp_dir + '/utmp' if not os.path.lexists(utmp_file): set_user_perm(pw.pw_uid, pw.pw_gid) try: if not os.path.isdir(utmp_dir): clcaptain.mkdir(utmp_dir, 0o700, recursive=True) clcaptain.write(utmp_file, '') except (OSError, IOError, ExternalProgramFailed): print_exception() if exit_on_error: sys.exit(1) set_root_perm() def is_clean_user_php_sessions_enabled(): """ Check clean_php_sessions parameter in config file By default sessions cleanup is enabled """ if not get_boolean_param(CL_CONFIG_FILE, 'clean_user_php_sessions', default_val=True): return False return True @functools.lru_cache(maxsize=None) def is_running_without_lve(): return not is_panel_feature_supported(Feature.LVE)