mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-09-20 07:35:55 +08:00
Resolve #485
This commit is contained in:
parent
d59563cf1b
commit
173a172cf8
121
plugins/change-password-cpanel/driver.php
Normal file
121
plugins/change-password-cpanel/driver.php
Normal 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;
|
||||
}
|
||||
}
|
17
plugins/change-password-cpanel/index.php
Normal file
17
plugins/change-password-cpanel/index.php
Normal 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';
|
||||
}
|
||||
}
|
339
plugins/change-password-cpanel/jsonapi.php
Normal file
339
plugins/change-password-cpanel/jsonapi.php
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue