From 173a172cf834aca748fb8739447d5f1e92a01fad Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 19 Mar 2024 17:31:32 +0100 Subject: [PATCH] Resolve #485 --- plugins/change-password-cpanel/driver.php | 121 ++++++++ plugins/change-password-cpanel/index.php | 17 ++ plugins/change-password-cpanel/jsonapi.php | 339 +++++++++++++++++++++ 3 files changed, 477 insertions(+) create mode 100644 plugins/change-password-cpanel/driver.php create mode 100644 plugins/change-password-cpanel/index.php create mode 100644 plugins/change-password-cpanel/jsonapi.php diff --git a/plugins/change-password-cpanel/driver.php b/plugins/change-password-cpanel/driver.php new file mode 100644 index 000000000..61d8d222a --- /dev/null +++ b/plugins/change-password-cpanel/driver.php @@ -0,0 +1,121 @@ +oConfig = $oConfig; + $this->oLogger = $oLogger; + } + + public static function isSupported() : bool + { + return !empty($_ENV['CPANEL']) && \is_readable('/usr/local/cpanel/php/cpanel.php'); + } + + public static function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('cpanel_host')->SetLabel('cPanel Host') + ->SetDefaultValue('127.0.0.1'), + \RainLoop\Plugins\Property::NewInstance('cpanel_port')->SetLabel('cPanel Port') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) + ->SetDefaultValue(2087), + \RainLoop\Plugins\Property::NewInstance('cpanel_ssl')->SetLabel('Use SSL') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(false), + \RainLoop\Plugins\Property::NewInstance('cpanel_user')->SetLabel('cPanel User') + ->SetDefaultValue(''), + \RainLoop\Plugins\Property::NewInstance('cpanel_pass')->SetLabel('cPanel Password') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) + ->SetDefaultValue(''), + \RainLoop\Plugins\Property::NewInstance('cpanel_allowed_emails')->SetLabel('Allowed emails') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@domain1.net user2@domain1.net *@domain2.net') + ->SetDefaultValue('*') + ); + } + + public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool + { + if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'cpanel_allowed_emails', ''))) { + return false; + } + + $this->oLogger->Write('CPANEL: Try to change password for '.$oAccount->Email()); + + if (!\class_exists('cPanel\\jsonapi')) { + require_once __DIR__ . '/jsonapi.php'; + } + + $sHost = $this->oConfig->Get('plugin', 'cpanel_host', '127.0.0.1'); + $iPort = $this->oConfig->Get('plugin', 'cpanel_port', 2087); + $sUser = $this->oConfig->Get('plugin', 'cpanel_user', ''); + $sPassword = $this->oConfig->Get('plugin', 'cpanel_pass', ''); + + if (empty($sHost) || 1 > $iPort || !\strlen($sUser) || !\strlen($sPassword)) { + $this->oLogger->Write('CPANEL: Incorrent configuration data', \MailSo\Log\Enumerations\Type::ERROR); + return false; + } + + $sEmail = $oAccount->Email(); + $sEmailUser = \MailSo\Base\Utils::getEmailAddressLocalPart($sEmail); + $sEmailDomain = \MailSo\Base\Utils::getEmailAddressDomain($sEmail); + + $sHost = \str_replace('{user:domain}', $sEmailDomain, $sHost); + $sUser = \str_replace('{user:email}', $sEmail, $sUser); + $sUser = \str_replace('{user:login}', $sEmailUser, $sUser); + $sPassword = \str_replace('{user:password}', (string) $oPrevPassword, $sPassword); + + $bResult = false; + try + { + $oJSONApi = new \cPanel\jsonapi($sHost); + $oJSONApi->set_port($iPort); + $oJSONApi->set_protocol($this->oConfig->Get('plugin', 'cpanel_ssl', false) ? 'https' : 'http'); + $oJSONApi->set_debug(false); +// $oJSONApi->set_http_client('fopen'); +// $oJSONApi->set_http_client('curl'); + $oJSONApi->password_auth($sUser, $sPassword); + + $aArgs = array( + 'email' => $sEmailUser, + 'domain' => $sEmailDomain, + 'password' => $sNewPassword + ); + + $sResult = $oJSONApi->api2_query($sUser, 'Email', 'passwdpop', $aArgs); + if ($sResult) { + $this->oLogger->Write('CPANEL: '.$sResult, \MailSo\Log\Enumerations\Type::INFO); + + $aResult = \json_decode($sResult, true); + $bResult = isset($aResult['cpanelresult']['data'][0]['result']) && + !!$aResult['cpanelresult']['data'][0]['result']; + } + + if (!$bResult) { + $this->oLogger->Write('CPANEL: '.$sResult, \MailSo\Log\Enumerations\Type::ERROR); + } + } + catch (\Exception $oException) + { + $this->oLogger->WriteException($oException); + } + + return $bResult; + } +} diff --git a/plugins/change-password-cpanel/index.php b/plugins/change-password-cpanel/index.php new file mode 100644 index 000000000..a3ee6138d --- /dev/null +++ b/plugins/change-password-cpanel/index.php @@ -0,0 +1,17 @@ +user = $user; + } + + if ($password != null) { + $this->set_password($password); + } + + $this->host = $host; + + // Detemine what the default http client should be. + if ( \function_exists('curl_setopt') ) { + $this->http_client = "curl"; + } elseif ( \ini_get('allow_url_fopen') ) { + $this->http_client = "fopen"; + } else { + throw new \Exception('allow_url_fopen and curl are neither available in this PHP configuration'); + } + + } + + public function set_debug( bool $debug = true ) + { + $this->debug = $debug; + } + + public function set_host( string $host ) + { + $this->host = $host; + } + + public function set_port( int $port ) + { + if ($port < 1 || $port > 65535) { + throw new \Exception('non integer or negative integer passed to set_port'); + } + + // Account for ports that are non-ssl + if ($port == '2086' || $port == '2082' || $port == '80' || $port == '2095') { + $this->set_protocol('http'); + } + + $this->port = $port; + } + + public function set_protocol( string $proto ) + { + if ($proto != 'https' && $proto != 'http') { + throw new \Exception('https and http are the only protocols that can be passed to set_protocol'); + } + $this->protocol = $proto; + } + + public function set_password( string $pass ) + { + $this->auth_type = 'pass'; + $this->auth = $pass; + } + + public function set_hash( string $hash ) + { + $this->auth_type = 'hash'; + $this->auth = \preg_replace("/(\n|\r|\s)/", '', $hash); + } + + public function hash_auth( string $user, string $hash ) + { + $this->set_hash( $hash ); + $this->user = $user; + } + + public function password_auth( string $user, string $pass ) + { + $this->set_password( $pass ); + $this->user = $user; + } + + public function set_http_client( string $client ) + { + if ( ( $client != 'curl' ) && ( $client != 'fopen' ) ) { + throw new \Exception('only curl and fopen and allowed http clients'); + } + $this->http_client = $client; + } + + /** + * Perform an XML-API Query + * + * This function will perform an XML-API Query and return the specified output format of the call being made + * + * @param string $function The XML-API call to execute + * @param array $vars An associative array of the parameters to be passed to the XML-API Calls + * @return mixed + */ + public function jsonapi_query( string $function, array $vars = array() ) + { + // Check to make sure all the data needed to perform the query is in place + if (!$function) { + throw new \Exception('jsonapi_query() requires a function to be passed to it'); + } + + if ($this->user == null) { + throw new \Exception('no user has been set'); + } + + if ($this->auth ==null) { + throw new \Exception('no authentication information has been set'); + } + + // Build the query: + + $query_type = '/json-api/'; + + $args = \http_build_query($vars, '', '&'); + $url = $this->protocol . '://' . $this->host . ':' . $this->port . $query_type . $function; + + if ($this->debug) { + \error_log('URL: ' . $url); + \error_log('DATA: ' . $args); + } + + // Set the $auth string + + $authstr = ''; + if ($this->auth_type == 'hash') { + $authstr = 'Authorization: WHM ' . $this->user . ':' . $this->auth . "\r\n"; + } elseif ($this->auth_type == 'pass') { + $authstr = 'Authorization: Basic ' . \base64_encode($this->user .':'. $this->auth) . "\r\n"; + } else { + throw new \Exception('invalid auth_type set'); + } + + if ($this->debug) { + \error_log("Authentication Header: " . $authstr ."\n"); + } + + // Perform the query (or pass the info to the functions that actually do perform the query) + + $response = ''; + if ($this->http_client == 'curl') { + $response = $this->curl_query($url, $args, $authstr); + } elseif ($this->http_client == 'fopen') { + $response = $this->fopen_query($url, $args, $authstr); + } + + // fix #1 + $aMatch = array(); + if ($response && false !== stripos($response, '') && + \preg_match('/HTTP-EQUIV[\s]?=[\s]?"refresh"/i', $response) && + \preg_match('/]+url[\s]?=[\s]?([^">]+)/i', $response, $aMatch) && + !empty($aMatch[1]) && 0 === \strpos(\trim($aMatch[1]), 'http')) + { + $url = \trim($aMatch[1]) . $query_type . $function; + if ($this->debug) { + \error_log('new URL: ' . $url); + } + + if ($this->http_client == 'curl') { + $response = $this->curl_query($url, $args, $authstr); + } elseif ($this->http_client == 'fopen') { + $response = $this->fopen_query($url, $args, $authstr); + } + } + // --- + + /* + * Post-Query Block + * Handle response, return proper data types, debug, etc + */ + + // print out the response if debug mode is enabled. + if ($this->debug) { + \error_log("RESPONSE:\n " . $response); + } + + // The only time a response should contain is in the case of authentication error + // cPanel 11.25 fixes this issue, but if is in the response, we'll error out. + + if (\stristr($response, '') == true) { + if (\stristr($response, 'Login Attempt Failed') == true) { + \error_log("Login Attempt Failed"); + + return; + } + if (\stristr($response, 'action="/login/"') == true) { + \error_log("Authentication Error"); + + return; + } + + return; + } + + return $response; + } + + private function curl_query( $url, $postdata, $authstr ) + { + $curl = \curl_init(); + \curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); + // Return contents of transfer on curl_exec + \curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + // Allow self-signed certs + \curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); + // Set the URL + \curl_setopt($curl, CURLOPT_URL, $url); + // Increase buffer size to avoid "funny output" exception + \curl_setopt($curl, CURLOPT_BUFFERSIZE, 131072); + + // Pass authentication header + $header[0] =$authstr . + "Content-Type: application/x-www-form-urlencoded\r\n" . + "Content-Length: " . strlen($postdata) . "\r\n" . "\r\n" . $postdata; + + \curl_setopt($curl, CURLOPT_HTTPHEADER, $header); + + \curl_setopt($curl, CURLOPT_POST, 1); + + $result = \curl_exec($curl); + if ($result == false) { + throw new \Exception("curl_exec threw error \"" . \curl_error($curl) . "\" for " . $url . "?" . $postdata ); + } + \curl_close($curl); + + return $result; + } + + private function fopen_query( $url, $postdata, $authstr ) + { + if ( !(ini_get('allow_url_fopen') ) ) { + throw new \Exception('fopen_query called on system without allow_url_fopen enabled in php.ini'); + } + + $opts = array( + 'http' => array( + 'allow_self_signed' => true, + 'method' => 'POST', + 'header' => $authstr . + "Content-Type: application/x-www-form-urlencoded\r\n" . + "Content-Length: " . strlen($postdata) . "\r\n" . + "\r\n" . $postdata + ) + ); + $context = \stream_context_create($opts); + + return \file_get_contents($url, false, $context); + } + + public function api2_query($user, $module, $function, $args = array()) + { + if (!isset($user) || !isset($module) || !isset($function) ) { + \error_log("api2_query requires that a username, module and function are passed to it"); + + return false; + } + if (!is_array($args)) { + \error_log("api2_query requires that an array is passed to it as the 4th parameter"); + + return false; + } + + $args['cpanel_jsonapi_user'] = $user; + $args['cpanel_jsonapi_module'] = $module; + $args['cpanel_jsonapi_func'] = $function; + $args['cpanel_jsonapi_apiversion'] = '2'; + + return $this->jsonapi_query('cpanel', $args); + } +}