File: //usr/share/l.v.e-manager/panelless-version/lvemanager/LveManager.php
<?php
/**
* 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
*/
if(file_exists(__DIR__ . '/vendor.php')) {
include_once(__DIR__ . '/vendor.php');
}
class LveManager {
/**
* Path to "cloudlinux-cli.py" script.
*
* @var string
*/
public $CLOUDLINUX_CLI = '/usr/bin/sudo /usr/share/l.v.e-manager/utils/cloudlinux-cli.py';
// Default app mode
const APP_MODE = 'PRODUCTION_MODE';
const CONFIG_INI_FILE = '/opt/cpvendor/etc/integration.ini';
protected $userData;
protected $owner = 'user';
protected $bundleName = 'main';
protected $pluginName;
/**
* Required parameters.
*
* @var array
*/
public $userInfo;
public $config;
const OWNER_ADMIN = 'admin';
const OWNER_USER = 'user';
const OWNER_RESELLER = 'reseller';
/**
* Read config file and prepare.
* @return string
*/
function __construct($pluginName = 'lvemanager') {
$budleMapping = Array(
'lvemanager' => 'main',
'resource-usage' => 'resource_usage',
'resource_usage' => 'resource_usage',
'nodejs_selector' => 'nodejs_selector',
'python_selector' => 'python_selector',
'php_selector' => 'php_selector',
'xray' => 'xray',
'wpos' => 'wpos',
);
if (!array_key_exists($pluginName, $budleMapping)) {
$this->_haltWith(400);
return;
}
$this->pluginName = $pluginName;
$this->bundleName = $budleMapping[$pluginName];
$this->loadConfig();
$this->loadUserData();
}
/**
* Halt request with HTTP status code. Production override of
* ``http_response_code(...) + exit()``; tests subclass to record
* the exit instead of terminating the test process.
*
* Status MUST be set before any output: when ``processRequest`` has
* already called ``ob_end_clean()``, an ``echo`` from the caller would
* flush headers with the default 200 and a later ``http_response_code()``
* silently no-ops. Accepting the body here keeps the order correct in
* every call site.
*/
protected function _haltWith($code, $body = null) {
http_response_code($code);
if ($body !== null) {
echo $body;
}
exit();
}
protected function _setHeader($header) {
header($header);
}
protected function _setCookie($name, $value, $options = []) {
setcookie($name, $value, $options);
}
function loadConfig()
{
$iniData = file_get_contents(self::CONFIG_INI_FILE);
$this->config = parse_ini_string($iniData, true)['lvemanager_config'];
if (!$this->config) {
$this->config = Array();
}
}
function loadUserData()
{
$userInfoCli = $this->config['ui_user_info'];
$token = $this->defineAndGetSessionToken();
$this->userData = json_decode(shell_exec($userInfoCli.' '.escapeshellarg($token)));
}
function validateUserData($data) {
if (
!is_object($data)
||
!property_exists($data, 'userName')
||
!is_string($data->userName)
||
!property_exists($data, 'userType')
||
!in_array($data->userType, ['admin', 'reseller', 'user'])
) {
$this->sendErrorResponse('INVALID USER DATA', false, true);
}
}
/**
* Update and return session token if it was provided by vendor.
* @return string
*/
public function defineAndGetSessionToken()
{
if (isset($_GET['token']) && $_GET['token'] !== '') {
// Refuse to plant CLSIDTOKEN from a cross-site GET. SameSite=Strict
// only governs cookie *send*, not Set-Cookie acceptance, so without
// an explicit origin gate any third-party site that links here can
// fix the victim's session. Legitimate panel deep-links arrive
// with same-origin Referer (or no Referer at all for direct nav
// from the panel chrome); cross-site GETs carry a foreign Referer.
if (!$this->_isSameOriginRequest()) {
$this->sendErrorResponse('INVALID SESSION TOKEN', false, true);
return;
}
$token = $_GET['token'];
if (!preg_match('/^[a-zA-Z0-9._-]{1,256}$/', $token)) {
$this->sendErrorResponse('INVALID SESSION TOKEN', false, true);
return;
}
$secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
$this->_setCookie('CLSIDTOKEN', $token, [
'httponly' => true,
'secure' => $secure,
'samesite' => 'Strict',
]);
$uri = strtok($_SERVER['REQUEST_URI'] ?? '/', '?');
// Collapse leading '/' and '\' so an attacker-controlled REQUEST_URI
// (e.g. '//evil.com/') cannot turn this into a protocol-relative
// redirect that browsers resolve to a foreign host.
$uri = '/' . ltrim($uri, "/\\");
$params = $_GET;
unset($params['token']);
if (!empty($params)) {
$uri .= '?' . http_build_query($params);
}
$this->_setHeader('Location: ' . $uri);
$this->_haltWith(302);
return;
}
if (isset($_COOKIE['CLSIDTOKEN'])) {
return $_COOKIE['CLSIDTOKEN'];
}
$this->sendErrorResponse('INVALID SESSION TOKEN', false, true);
}
/**
* Get base URL of site.
* @return string
*/
public function getBaseUrl()
{
return $this->userData->baseUri;
}
/**
* Get plugin version
* @return string
*/
public static function getPluginVersion()
{
return trim(exec('cat /usr/share/l.v.e-manager/version'));
}
/**
* Get name of current authorized user in system.
* @return string
*/
public function getLogin()
{
return $this->userData->userName;
}
/**
* Get name of current authorized user in system.
* @return string
*/
public function getUserId()
{
return $this->userData->userId;
}
/**
* Get URL to vendor resources,
* which were defined in integration config file
* @return string
*/
public function getVendorSrcUrl()
{
return $this->getBaseUrl().'/vendor_src/';
}
/**
* Get current user type: admin|reseller|user
* @return string
*/
public function getUserType() {
return $this->userData->userType;
}
public function getRequestHandler()
{
$pluginMapping = Array(
'lvemanager' => 'send-request.php',
'nodejs_selector' => 'send-request-nodejs.php',
'python_selector' => 'send-request-python.php',
'php_selector' => 'send-request-php-selector.php',
'xray' => 'send-request-xray.php',
'resource_usage' => 'send-request-resource-usage.php',
'wpos' => 'send-request-awp.php',
);
return $pluginMapping[$this->pluginName] ? $pluginMapping[$this->pluginName] : $pluginMapping['lvemanager'];
}
/**
* Check reseller have not access to admin
* and user have not acces to reseller and admin.
*
* @param $owner
* @return down privileges value
*/
public function checkOwner($owner) {
switch ($this->userData->userType) {
case $this::OWNER_RESELLER:
$newOwner = in_array($owner, array($this::OWNER_RESELLER, $this::OWNER_USER)) ? $owner : $this::OWNER_RESELLER;
break;
case $this::OWNER_USER:
$newOwner = $this::OWNER_USER;
break;
default:
$newOwner = $owner;
}
return $newOwner;
}
/**
* Processes of incoming post request.
*
* @param $owner
* @throws pm_Exception
*/
public function processRequest($owner, $plugin_name = '')
{
$this->_checkVulnerabilities();
$this->validateUserData($this->userData);
$this->owner = $this->checkOwner($owner);
if (!isset($_POST['command'])) {
$this->sendErrorResponse('COMMAND NOT SPECIFIED');
}
$data['owner'] = $this->owner;
$data['command'] = $_POST['command'];
if (isset($_POST['method'])) {
$data['method'] = $_POST['method'];
}
if (isset($_POST['params'])) {
$data['params'] = $_POST['params'];
}
if (in_array($this->owner, array(self::OWNER_USER, self::OWNER_RESELLER))) {
$this->userInfo = array(
'username' => $this->getLogin(),
'lve-id' => $this->getUserId(),
);
$data['user_info'] = $this->userInfo;
}
if (isset($_POST['mockJson'])) {
$data['mockJson'] = $_POST['mockJson'];
}
if (isset($_POST['lang'])) {
$data['lang'] = $_POST['lang'];
}
if ($plugin_name !== '') {
$data['plugin_name'] = $plugin_name;
}
$fullCommandStr = sprintf(
"%s --data=%s 2>&1",
$this->CLOUDLINUX_CLI, base64_encode(json_encode($data))
);
putenv('LC_ALL=en_US.UTF-8');
ob_start();
passthru($fullCommandStr);
$responseInJson = ob_get_contents();
ob_end_clean();
$response = json_decode($responseInJson, true);
if (is_null($response) && !empty($responseInJson)) {
$this->sendErrorResponse(
'ERROR.wrong_received_data', false, false,
503, $responseInJson
);
}
if (isset($response['result']) && $response['result'] === 'file') {
$this->serveFile($response['filepath'], $response['filesize'], $data);
} else if (isset($response['result']) && $response['result'] !== 'success' && $response['result'] !== 'rollback') {
$this->sendErrorResponse($responseInJson, true);
} else if (empty($responseInJson)) {
$this->sendErrorResponse('RESPONSE OF COMMAND IS EMPTY');
}
$this->_setHeader('Content-Type: application/json; charset=UTF-8');
echo $responseInJson;
}
/**
* Returns response to SPA with error code, message and break execution of
* script with status code.
*
* @param $message
* @param $isJSON
* @param $logoutSignal
* @param int $statusCode
* @param string $details
*/
public function sendErrorResponse(
$message, $isJSON = false,
$logoutSignal = false, $statusCode = 503,
$details = ''
) {
$this->_setHeader('Content-Type: application/json; charset=UTF-8');
if ($isJSON) {
$body = $message;
} else {
if ($details !== '') {
error_log('LveManager CLI error details: ' . $details);
}
$body = json_encode(array(
'result' => $message,
'logoutSignal' => $logoutSignal,
));
}
$this->_haltWith($statusCode, $body);
}
/**
* Returns response to SPA with file
*
* @param $filepath
*/
public function serveFile($fileName, $fileSize, $requestData) {
$fileSize = (int)$fileSize;
$this->_setHeader("Content-Type: application/x-download");
$this->_setHeader("Content-Length: $fileSize");
$requestData['method'] = 'log_data';
$fullCommandStr = sprintf(
"%s --data=%s 2>&1",
$this->CLOUDLINUX_CLI, base64_encode(json_encode($requestData))
);
$descriptorspec = array(
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("pipe", "w") // stderr is a pipe that the child will write to
);
$pipes = array();
$process = proc_open($fullCommandStr, $descriptorspec, $pipes);
if (is_resource($process)) {
while ($s = fgets($pipes[1])) {
print $s;
flush();
}
}
$this->_haltWith(200);
}
/**
* Checking of vulnerabilities: CSRF and HTTP_REFERER
*/
private function _checkVulnerabilities()
{
$this->_checkCSRFToken();
$this->_checkReferer();
}
/*
* Check CSRF token.
*/
private function _checkCSRFToken()
{
if (!isset($_COOKIE['csrftoken'])
|| $_COOKIE['csrftoken'] !== $_SERVER['HTTP_X_CSRFTOKEN']
) {
$this->sendErrorResponse('BAD FORGERY PROTECTION TOKEN', false, true);
}
}
/**
* Same-origin gate for cookie-planting GETs (see defineAndGetSessionToken).
* Accepts only requests whose Origin or Referer matches the request host,
* or whose Sec-Fetch-Site indicates a same-origin / same-site navigation.
* A request with no Origin, no Referer and no Sec-Fetch-Site (typical of
* direct address-bar navigation by the panel operator) is also accepted —
* a remote attacker triggering the GET via <a>, window.open or form-submit
* causes the browser to attach at least Sec-Fetch-Site or Referer.
*/
private function _isSameOriginRequest()
{
$host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
$scheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http';
$selfOrigin = $scheme . '://' . $host;
if (!empty($_SERVER['HTTP_ORIGIN'])) {
return $_SERVER['HTTP_ORIGIN'] === $selfOrigin;
}
if (!empty($_SERVER['HTTP_REFERER'])) {
return preg_match(
'#^' . preg_quote($selfOrigin, '#') . '(/|$)#',
$_SERVER['HTTP_REFERER']
) === 1;
}
if (!empty($_SERVER['HTTP_SEC_FETCH_SITE'])) {
// Accept only "same-origin" (and "none" for direct address-bar
// navigation). "same-site" is rejected because a sibling
// subdomain on the same registered domain (e.g.
// user1.host.tld next to panel.host.tld) would otherwise be
// accepted as same-site, preserving the session-fixation
// path on multi-tenant panel deployments.
return in_array(
$_SERVER['HTTP_SEC_FETCH_SITE'],
['same-origin', 'none'],
true
);
}
// No Origin, no Referer, no Sec-Fetch-Site. All current browsers
// attach at least Sec-Fetch-Site (including the "none" value for
// direct address-bar navigation), so reaching this branch means
// either a very-legacy client or a non-browser caller; neither
// has a legitimate panel cookie-planting flow. Fail closed.
return false;
}
/**
* Check HTTP_REFERER.
*/
private function _checkReferer()
{
if (!preg_match(
sprintf(
'/^%s:\/\/%s/',
(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on')
? 'https' : 'http',
preg_quote($_SERVER['HTTP_HOST'], '/')
),
$_SERVER['HTTP_REFERER'])
) {
$this->sendErrorResponse('BAD REFERER', false, true);
}
}
public function setCSRFToken()
{
if (!isset($_COOKIE['csrftoken'])) {
$secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
if (PHP_VERSION_ID >= 70300) {
// Modern path: strong CSPRNG + array $options signature with SameSite support.
$this->_setCookie("csrftoken", bin2hex(random_bytes(16)), [
'expires' => time() + 86400,
'path' => '/',
'secure' => $secure,
'samesite' => 'Lax',
]);
} else {
// Plesk-on-CL7 ships PHP 5.4: random_bytes() (PHP 7+) and the setcookie() array
// $options signature (PHP 7.3+) are both unavailable. Fall back to the original
// md5(uniqid(rand(), true)) — cryptographically weak but historically present on
// this EOL stack — and emit SameSite via a raw Set-Cookie header.
$secureFlag = $secure ? '; Secure' : '';
header("Set-Cookie: csrftoken=" . md5(uniqid(rand(), true))
. "; Path=/; Max-Age=86400; SameSite=Lax" . $secureFlag, false);
}
}
}
public function getAppMode()
{
$modeFile = '/usr/share/l.v.e-manager/spa/app_mode.status';
if (file_exists($modeFile)) {
return trim(file_get_contents($modeFile));
}
return self::APP_MODE;
}
/**
* Instert static files
*/
public function insertPanel()
{
$pluginVersion = $this->getPluginVersion();
$requestHandler = $this->getRequestHandler();
$htmlAssetsUri = htmlspecialchars($this->userData->assetsUri, ENT_QUOTES, 'UTF-8');
$htmlBaseUri = htmlspecialchars($this->userData->baseUri, ENT_QUOTES, 'UTF-8');
$htmlLang = htmlspecialchars($this->userData->lang, ENT_QUOTES, 'UTF-8');
$jsUserName = json_encode($this->userData->userName);
$jsUserType = json_encode($this->userData->userType);
$jsLang = json_encode($this->userData->lang);
$jsMainAction = json_encode($this->userData->baseUri . '/' . $requestHandler);
if ($this->pluginName == 'wpos') {
$jsLocalePath = json_encode($this->userData->assetsUri . '/assets/awp-user/i18n/');
return <<<TEMPLATE
<base href="{$htmlBaseUri}">
<link rel="stylesheet" href="{$htmlAssetsUri}/assets/awp-user/common-styles.css" />
<link rel="stylesheet" href="{$htmlAssetsUri}/assets/awp-user/bootstrap.min.css" />
<lvemanager>
Loading...
</lvemanager>
<script type="text/javascript">
var APP_MODE = '{$this->getAppMode()}';
var mainAction = {$jsMainAction};
var panelName = 'Unknown';
var pluginVersion = '{$pluginVersion}';
var userType = {$jsUserType};
var userName = {$jsUserName};
var currentLanguage = {$jsLang};
var localePath = {$jsLocalePath};
</script>
<script src="{$htmlAssetsUri}/assets/awp-user/polyfills.bundle.min.js?v={$pluginVersion}"></script>
<script src="{$htmlAssetsUri}/assets/awp-user/vendor.bundle.min.js?v={$pluginVersion}"></script>
<script src="{$htmlAssetsUri}/assets/awp-user/wpos.bundle.min.js?v={$pluginVersion}"></script>
TEMPLATE;
} else if ($this->pluginName == 'xray') {
$jsLocalePath = json_encode($this->userData->assetsUri . '/assets/xray-user/i18n/');
$jsAssetsStaticPath = json_encode($this->userData->assetsUri . '/assets/');
return <<<TEMPLATE
<base href="{$htmlBaseUri}">
<link rel="stylesheet" href="{$htmlAssetsUri}/assets/xray-user/css/xray-common.css" />
<lvemanager>
Loading...
</lvemanager>
<script type="text/javascript">
var APP_MODE = '{$this->getAppMode()}';
var mainAction = {$jsMainAction};
var panelName = 'Unknown';
var pluginVersion = '{$pluginVersion}';
var userType = {$jsUserType};
var userName = {$jsUserName};
var currentLanguage = {$jsLang};
var assetsStaticPath = {$jsAssetsStaticPath};
var localePath = {$jsLocalePath};
</script>
<script src="{$htmlAssetsUri}/assets/xray-user/xray.bundle.min.js?v={$pluginVersion}"></script>
TEMPLATE;
} else {
$bundleNameEscaped = htmlspecialchars($this->bundleName, ENT_QUOTES, 'UTF-8');
$jsLocalePath = json_encode($this->userData->assetsUri . '/assets/i18n/');
$jsAssetsStaticPath = json_encode($this->userData->assetsUri . '/assets/');
return <<<TEMPLATE
<base href="{$htmlBaseUri}">
<link rel="stylesheet" href="{$htmlAssetsUri}/assets/css/bootstrap.min.css" />
<link rel="stylesheet" href="{$htmlAssetsUri}/assets/css/style.css" />
<link rel="stylesheet" href="{$htmlAssetsUri}/assets/static/common-styles.css" />
<link rel="stylesheet" href="{$htmlAssetsUri}/assets/css/lvemanager.css" />
<lvemanager>
<div class="big_loader_wrapper">
<div class="big_loader">
<img src="{$htmlAssetsUri}/assets/images/loader.svg">
</div>
</div>
</lvemanager>
<script src="{$htmlAssetsUri}/assets/js/jquery.min.js"></script>
<script src="{$htmlAssetsUri}/assets/js/bootstrap.min.js"></script>
<script src="{$htmlAssetsUri}/assets/js/common.js"></script>
<script type="text/javascript">
var APP_MODE = '{$this->getAppMode()}';
var mainAction = {$jsMainAction};
var panelName = 'Unknown';
var pluginVersion = '{$pluginVersion}';
var userType = {$jsUserType};
var userName = {$jsUserName};
var currentLanguage = {$jsLang};
var localePath = {$jsLocalePath};
var assetsStaticPath = {$jsAssetsStaticPath};
</script>
<script src="{$htmlAssetsUri}/assets/js/locales/{$htmlLang}.js"></script>
<script src="{$htmlAssetsUri}/assets/static/polyfills.bundle.min.js?v={$pluginVersion}"></script>
<script src="{$htmlAssetsUri}/assets/static/vendor.bundle.min.js?v={$pluginVersion}"></script>
<script src="{$htmlAssetsUri}/assets/static/{$bundleNameEscaped}.bundle.min.js?v={$pluginVersion}"></script>
TEMPLATE;
}
}
public function availableProducts()
{
$pluginList = Array();
if ($this->userData->userType == 'user')
{
$pluginList[] = Array(
'name' => 'PHP',
'image' => 'image',
'link' => '/open.php?plugin=php_selector'
);
$pluginList[] = Array(
'name' => 'NodeJS',
'image' => 'image',
'link' => '/open.php?plugin=nodejs_selector'
);
$pluginList[] = Array(
'name' => 'Python',
'image' => 'image',
'link' => '/open.php?plugin=python_selector'
);
$pluginList[] = Array(
'name' => 'X-Ray',
'image' => 'image',
'link' => '/open.php?plugin=xray'
);
$pluginList[] = Array(
'name' => 'AccelerateWP',
'image' => 'image',
'link' => '/open.php?plugin=wpos'
);
} else {
$pluginList[] = Array(
'name' => 'LveManager',
'image' => 'image',
'link' => '/open.php?plugin=lvemanager'
);
}
return $pluginList;
}
}