Mini Shell
# -*- coding: utf-8 -*-
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
import grp
import json
import logging
import os
import pwd
import socket
import struct
from functools import wraps
from typing import Callable, Optional, Tuple, Any
from clcommon.cpapi import get_main_username_by_uid
from clcommon.lib.cledition import is_cl_solo_edition, is_cl_shared_pro_edition
from xray import gettext as _
from .constants import user_tasks_count, fpm_reload_timeout
from .exceptions import XRayError
from .fpm_utils import FPMReloadController
from .nginx_utils import NginxUserCache
# --------- GLOBALS ---------
logger = logging.getLogger('user_plugin_utils')
_format = '>I'
# --------- FUNCTIONS ---------
def pack_request(_input: Any) -> 'json str':
"""
Pack input for sending
"""
return json.dumps(_input)
def unpack_request(byte_command: bytes) -> Any:
"""
Unpack incoming command
"""
_command = byte_command.decode()
logger.info('Command requested => %s', _command)
return json.loads(_command)
def pack_response(msg: bytes) -> bytes:
"""
Prefix message with a 4-byte length
"""
logger.debug('Packing message of %i length', len(msg))
return struct.pack(_format, len(msg)) + msg
def unpack_response(sock_object: 'socket object') -> bytes:
"""
Read length-prefixed amount of data from socket
"""
chunk = 4096
msg = bytes()
raw_msglen = sock_object.recv(4)
if not raw_msglen:
return sock_object.recv(chunk)
msglen = struct.unpack(_format, raw_msglen)[0]
while len(msg) != msglen:
msg += sock_object.recv(chunk)
return msg
def extract_creds(sock_object: 'socket object') -> Tuple[Any, Any, Any]:
"""
Retrieve credentials from SO_PEERCRED option
"""
_format = '3i'
creds = sock_object.getsockopt(socket.SOL_SOCKET,
socket.SO_PEERCRED,
struct.calcsize(_format))
_pid, _uid, _gid = struct.unpack(_format, creds)
try:
user, group = pwd.getpwuid(_uid).pw_name, grp.getgrgid(_gid).gr_name
except KeyError:
logger.info('Connected by proc %i of %i:%i',
_pid, _uid, _gid)
else:
logger.info('Connected by proc %i of %i:%i (%s:%s)',
_pid, _uid, _gid,
user, group)
return _pid, _uid, _gid
def check_for_root(_uid: int = None) -> bool:
"""
Check for execution as root | command from root
"""
if _uid is None:
_uid = os.geteuid()
return _uid == 0
def get_xray_exec_user() -> Optional[str]:
"""
Retrieve the value of XRAYEXEC_UID env and resolve it to username
"""
proxyuid = os.getenv('XRAYEXEC_UID')
if proxyuid is not None:
_proxyuser = get_main_username_by_uid(int(proxyuid))
logger.info('Got XRAYEXEC_UID: %s (%s), working in USER_MODE',
proxyuid, _proxyuser)
return _proxyuser
def sock_receive(sock_object: 'socket object') -> bytes:
"""
Read all data from socket object
"""
data = b''
while True:
chunk = sock_object.recv(1024)
if not chunk:
logger.debug('All data read, connection ended')
break
data += chunk
return data
def error_response(msg: str) -> 'json str':
"""
Construct an appropriate formatted response in case of error
"""
return json.dumps({'result': msg}, ensure_ascii=False)
def nginx_user_cache() -> Optional[bool]:
"""
Check nginx cache status for current user
"""
proxyuser = get_xray_exec_user()
if proxyuser is not None:
return NginxUserCache(proxyuser).is_enabled
def root_execution_only_check() -> None:
"""
Check if utility is executed as root and throw error in case if no
"""
if not check_for_root():
raise SystemExit(
error_response(_('Only root is allowed to execute this utility')))
# --------- DECORATORS ---------
def user_mode_verification(func: Callable) -> Callable:
"""
Decorator aimed to verify domain owner in X-Ray Manager user mode
Applies to get_domain_info method
"""
def verify(data):
"""
If exists, check XRAYEXEC_UID against domain owner
"""
proxyuser = get_xray_exec_user()
if proxyuser is not None and data.user != proxyuser:
logger.warning('%s does not belong to user %s', data, proxyuser)
raise XRayError(_('%s cannot be found') % str(data))
@wraps(func)
def wrapper(*args, **kwargs):
"""
Wraps func
"""
info = func(*args, **kwargs)
verify(info)
return info
return wrapper
def user_mode_restricted(func: Callable) -> Callable:
"""
Decorator aimed to check if user is not hitting limit of running tasks,
set in X-Ray Manager user mode.
Applies to start and continue methods.
Limiting of user's running tasks is applied to Shared PRO only.
"""
def check(*args):
"""
If XRAYEXEC_UID exists, check if user does not exceed
limit of running tasks
"""
# TODO: [unification] ensure is_cl_shared_pro_edition really needed here
# https://cloudlinux.atlassian.net/browse/XRAY-244 - (seems yes)
if not is_cl_shared_pro_edition(skip_jwt_check=True):
return
proxyuser = get_xray_exec_user()
if proxyuser is not None:
ui_api_cli_instanse = args[0].ui_api_client
resp = ui_api_cli_instanse.get_task_list()
list_of_tasks = resp.get('result')
if list_of_tasks is not None:
running_count = len([item for item in list_of_tasks if
item.get('status') == 'running'])
if running_count >= user_tasks_count:
raise XRayError(
_('Limit of running tasks is {}. '
'You already have {} running task'.format(str(user_tasks_count),
str(user_tasks_count))))
@wraps(func)
def wrapper(*args, **kwargs):
"""
Wraps func
"""
check(*args)
return func(*args, **kwargs)
return wrapper
def with_fpm_reload_restricted(func: Callable) -> Callable:
"""
Decorator aimed to restrict frequent reloads of FPM service
Applies to get_domain_info method
"""
def check(*args, data):
"""
"""
# TODO: [unification] ensure is_cl_solo_edition really needed here
# https://cloudlinux.atlassian.net/browse/XRAY-244 (seems yes)
if is_cl_solo_edition(skip_jwt_check=True):
return
proxyuser = get_xray_exec_user()
if proxyuser is not None and data.panel_fpm:
_fpm_service = args[0].fpm_service_name(data)
if FPMReloadController(_fpm_service).restrict():
raise XRayError(
_('The X-Ray User service is currently busy. Operation is temporarily not permitted. '
'Try again in %s minute') % str(fpm_reload_timeout),
flag='warning')
@wraps(func)
def wrapper(*args, **kwargs):
"""
Wraps func
"""
info = func(*args, **kwargs)
check(*args, data=info)
return info
return wrapper
def username_verification(func: Callable) -> Callable:
def validate(username: str):
"""
If exists, check XRAYEXEC_UID against user passed param
"""
proxyuser = get_xray_exec_user()
if proxyuser is not None and username != proxyuser:
raise XRayError(_('Incorrect user for request'))
@wraps(func)
def wrapper(*args, **kwargs):
response = func(*args, **kwargs)
username = kwargs['username']
validate(username)
return response
return wrapper
def user_mode_advice_verification(func: Callable) -> Callable:
"""
Decorator aimed to verify user in X-Ray Smart Advice user mode
Applies to get_detailed_advice method, which takes part in
advice_details and advice_apply methods
"""
def verify(data: dict) -> None:
"""
If exists, check XRAYEXEC_UID against user in metadata of an advice
"""
proxyuser = get_xray_exec_user()
try:
username = data['metadata']['username']
except KeyError:
raise XRayError(_('Requested advice cannot be verified'))
if proxyuser is not None and username != proxyuser:
raise XRayError(_('Requested advice does not exist'))
@wraps(func)
def wrapper(*args, **kwargs):
"""
Wraps func
"""
advice_info, _ = func(*args, **kwargs)
verify(advice_info)
return advice_info, _
return wrapper
Zerion Mini Shell 1.0