Hacked By AnonymousFox
Current Path : /usr/share/cagefs/ |
|
Current File : //usr/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 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 read_file(filename, exit_on_error=True):
"""
Helper for read file, process errors and make backup before read
:param: filename `str` name of file for read
:param: exit_on_error `bool` use sys.exit on error or raise exception
"""
try:
_file = open(filename, 'r')
content = _file.readlines()
_file.close()
return content
except (OSError, IOError):
logging('Error: failed to read ' + filename, SILENT_FLAG, 1)
if not exit_on_error:
raise
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
CL_ALT_NAME = 'cl.selector'
ETC_CL_ALT_PATH = '/etc/'+CL_ALT_NAME
NATIVE_CONF = ETC_CL_ALT_PATH + '/' + 'native.conf'
is_ea4_enabled_cache = None
def is_ea4_enabled():
"""
Return True if cPanel EasyApache4 (MultiPHP feature) is enabled
"""
global is_ea4_enabled_cache
if is_ea4_enabled_cache is None:
is_ea4_enabled_cache = os.path.lexists('/etc/cpanel/ea4/is_ea4')
return is_ea4_enabled_cache
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
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
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) )
# Returns True if error has occured
def make_userdir(path, perm, uid, gid, parent_path):
# Create directory if it does not exist, and set permissions/owner
fd = create_dir_secure(path, perm, uid, gid, parent_path, logger=logging)
closefd(fd)
return fd is None
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)
files_cache = {}
def read_file_cached(path):
global files_cache
if not (path in files_cache):
files_cache[path] = read_file(path)
return files_cache[path]
# Reads config file and returns paths to alternative binaries (as dictionary)
def get_alt_paths(php_state = None):
CL_ALT_CONF = '/etc/cl.selector/selector.conf'
alt_paths = {}
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:
vers = ar[1]
if (php_state == None) or (vers not in php_state) or (php_state[vers]):
path = ar[3]
alt_paths[path] = 1
return alt_paths
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
alt_dirs = None
# Returns list of dirnames like ['php51', 'php52']
def get_alt_dirs():
global alt_dirs
if alt_dirs == None:
alt_paths = get_alt_paths()
alt_dirs = {}
for path in alt_paths:
# Split string like "/opt/alt/php51/usr/bin/php"
ar = path.split('/')
dirname = ar[3]
if dirname.startswith('php') and isdigits(dirname[len('php'):]):
alt_dirs[dirname] = 1
return list(alt_dirs)
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):
from cagefsctl import get_user_prefix, BASEDIR
# 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)
Hacked By AnonymousFox1.0, Coded By AnonymousFox