This commit is contained in:
the-djmaze 2024-03-19 17:31:32 +01:00
parent d59563cf1b
commit 173a172cf8
3 changed files with 477 additions and 0 deletions

View file

@ -0,0 +1,121 @@
<?php
/**
* TODO: convert to https://api.docs.cpanel.net/openapi/cpanel/operation/passwd_pop/
* https://github.com/CpanelInc/xmlapi-php
*/
use SnappyMail\SensitiveString;
class ChangePasswordCPanelDriver
{
const
NAME = 'cPanel',
DESCRIPTION = 'Change passwords in cPanel.';
private \RainLoop\Config\Plugin $oConfig;
private \MailSo\Log\Logger $oLogger;
function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger)
{
$this->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;
}
}

View file

@ -0,0 +1,17 @@
<?php
class ChangePasswordCPanelPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Change Password cPanel',
VERSION = '2.36',
RELEASE = '2024-03-17',
REQUIRED = '2.36.0',
CATEGORY = 'Security',
DESCRIPTION = 'Extension to allow users to change their passwords through cPanel';
public function Supported() : string
{
return 'Use Change Password plugin';
}
}

View file

@ -0,0 +1,339 @@
<?php
/**
* cPanel XMLAPI Client Class
*
* This class allows for easy interaction with cPanel's XML-API allow functions within the XML-API to be called
* by calling funcions within this class
*
* LICENSE:
*
* Copyright (c) 2012, cPanel, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided
* that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this list of conditions and the
* following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other materials provided with the distribution.
* * Neither the name of the cPanel, Inc. nor the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @category Cpanel
* @package xmlapi
* @copyright 2012 cPanel, Inc.
* @license http://sdk.cpanel.net/license/bsd.html
* @version Release: 1.0.13
* @link http://twiki.cpanel.net/twiki/bin/view/AllDocumentation/AutomationIntegration/XmlApi
* @since Class available since release 0.1
*/
namespace cPanel;
class jsonapi
{
// should debugging statements be printed?
private bool $debug = false;
// The host to connect to
private string $host = '127.0.0.1';
// the port to connect to
private int $port = 2087;
// should be the literal strings http or https
private string $protocol = 'https';
// literal strings hash or password
private ?string $auth_type = null;
// the actual password or hash
private ?string $auth = null;
// username to authenticate as
private ?string $user = null;
// The HTTP Client to use
private string $http_client = 'curl';
public function __construct(string $host, ?string $user = null, ?string $password = null )
{
if ( ( $user != null ) && ( \strlen( $user ) < 9 ) ) {
$this->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, '<html>') &&
\preg_match('/HTTP-EQUIV[\s]?=[\s]?"refresh"/i', $response) &&
\preg_match('/<meta [^>]+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 <html> is in the case of authentication error
// cPanel 11.25 fixes this issue, but if <html> is in the response, we'll error out.
if (\stristr($response, '<html>') == 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);
}
}