The Community Forums

Interact with an entire community of cPanel & WHM users!
  1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.

Security Question, xml-api, api2

Discussion in 'cPanel Developers' started by willsmelser, Dec 23, 2009.

  1. willsmelser

    willsmelser Member

    Joined:
    Jun 11, 2008
    Messages:
    7
    Likes Received:
    0
    Trophy Points:
    1
    I am adding some functionality to a CMS system I have for my customers, where I will add ability to add email accounts and such.

    I do not give people access to cpanel for several reasons, so I will use the xml-api to add what I need.

    I am using the php class and everything works fine, but I have to use the root user and pass to get changes to work. Im not compfortable saving the root password in files on the server and in user folders especially.

    Is there a way to use cpanel logins for the xml-api? I was reading a little about using key and hash combo....which I feel much better about.

    Is there a way to limit commands that can be run in the api(s)?

    Can I block extenal IP addresses making HTTP requests for the api?

    Can I use cpanel userid/pass for performing tasks on their accounts instead of the root user?

    A little guidence would be much appreciated.
     
  2. willsmelser

    willsmelser Member

    Joined:
    Jun 11, 2008
    Messages:
    7
    Likes Received:
    0
    Trophy Points:
    1
    Well, what I decided to do

    Incase anyone else come across this same issues of securit, here is the route I went:

    1) made a "powerUser". Moved all accounts under a single reseller. So I dont have to use the root to use the api.

    2) modified my /scripts/postwwwacct file to add into the home directory of "powerUser" a file called acctList.txt (using this name for demo purposes)

    3) That file holds user:md5(pass) of new accounts created

    4) Added into my /capnel3_skel/public_html/somedir/sendAPIrequests.php

    5) This file makes curl POST requests to the powerUser account. It uses the actual domain, since I didnt want to edit httpd.conf to create new vhost. You would need to do this if your power user does not have an actual registed domain and DNS that point back to the server.

    6) This file also sends in POST vars 'user:md5(password)' and a other api args that are needed for my api class.

    7) Added a file into my "superUser" that accepts requests from IPs within my ip pool. An array of values that I will have to maintain by hand. I doubt Ill ever be adding any more IPs, but who knows. Just gotta remeber I gotta edit this file.

    8) This file check against the file that holds user:md5(pass) value pairs for requests made to it. Also, validates where the request was made from, looking for valid local IPs.

    9) Next this file uses a saved hash base64_encode('superUser:superpassword'); to make a request to 127.0.0.1 ip via my api.
    note: my api is just a simplified version of the php xml_api.

    So to recap.

    1) no user can make requests to change other users permissions. Nobody accesses the cpanel API directly. Each user password is exposed, but no more than it already was for making mysql connections and such.

    2) Actual API calls are called from an internal file that cannot be accessed via external requests. Should add some .htaccess to improve this.

    3) Not using root password, and not storing super user password or name for that matter. Using a base64_encoded string that is stored in the file.

    Seems likely to be somewhat secure...and if someone does hack 1 account, they can only mess up that account.

    ULTIMATELY: I have decided to go a different route since the API does not allow deletion of email forwarders (currently). So I will alter my postwwwacct script to move the /etc/valiases/domain.com to /home/domain/valiases and add a sym link back from there. Then the user will have access to making changes to their aliases file.

    Here are the 3 files I use for Curl solution. I will probably use this in future as I add API functionality to my CMS.
     
  3. willsmelser

    willsmelser Member

    Joined:
    Jun 11, 2008
    Messages:
    7
    Likes Received:
    0
    Trophy Points:
    1
    user file to make requests:
    PHP:
    //have to parse root for usename
    $name explode('/',$_SERVER['DOCUMENT_ROOT']);
    $user $name[2];
    $pass md5(USER_PASS);

    $action 'addforwarder';
    $fwdTo  'someemail@email.com';
    $domain 'somedomain.com';
    $newAcct'newacct@somedomain.com';

    $postArgs = array('user'=>$user,'pass'=>$pass,'action'=>$action,'domain'=>$domain,'newacct'=>$newAcct,'fwdacct'=>$fwdTo);
    $poststr http_build_query($postArgs,'','&');

    $url 'http://superUser.com/myapi.php';

    $ch curl_init();

    curl_setopt($chCURLOPT_SSL_VERIFYPEER0);
    curl_setopt($chCURLOPT_SSL_VERIFYHOST0);//allow self-signed certs
    curl_setopt($chCURLOPT_RETURNTRANSFER1);// return content of transfer

    curl_setopt($chCURLOPT_URL$url);
    curl_setopt($chCURLOPT_RETURNTRANSFER1);

    curl_setopt($chCURLOPT_HEADERfalse );
    curl_setopt($chCURLOPT_TIMEOUT5);
    curl_setopt($chCURLOPT_CONNECTTIMEOUT5);
    curl_setopt($chCURLOPT_POSTcount($postArgs));
    curl_setopt($chCURLOPT_POSTFIELDS$poststr);
     
    $contents curl_exec($ch);

    curl_close ($ch);

    echo 
    $contents//returns JSON data
    API that handles request from user
    PHP:
    //only except requests from the local server
    $validIps = array('72.167.999.999','72.167.999.999');
    if(!
    in_array($_SERVER['REMOTE_ADDR'],$validIps)){ echo 'Invalid IP making request.'; exit;}


    include 
    $_SERVER['DOCUMENT_ROOT'].'/../miofiles/acct/class_simpleXMLapi.php';
    $accDir $_SERVER['DOCUMENT_ROOT'].'/../miofiles/acct/';

    /*
    $testapi = new simpleXMLapi($account, $root_user,$root_pass,$ip);
    $testapi->api2_query('Email','addforward',array('email'=>$email_user,'domain'=>$email_domain,'fwdopt'=>'fwd','fwdemail'=>'willsmelser@gmail.com'));
    //$testapi->api_query('applist','',array());
    */

    //validate user
    $data $_POST//just so I could use GET during testing
    $acct $data['user'];
    $pass $data['pass'];

    $miodata file_get_contents($accDir.'mioinfo.txt');

    $valid false;
    foreach(
    explode("\n",$miodata) as $users){
        if( 
    trim($acct.':'.$pass) == trim($users)){
            
    $valid true;
            break;
        }
    }

    if(!
    $valid){echo '{"save":false,"error":"Invalid user information"}';exit;}

    $ip '127.0.0.1';
    $user 'superUser';
    $authhash 'aXNiYWasdfasdfajhas==&k';

    $api = new simpleXMLapi($acctnull$authhash$ip);

    $return '{"save":false,"error":"No action sent, or error with api."}';
    switch(
    $data['action']){
        case 
    'addforwarder':
            
    $api->api2_query('Email','addforward',array('email'=>$data['newacct'],'domain'=>$data['domain'],'fwdopt'=>'fwd','fwdemail'=>$data['fwdacct']));
            
    $result = ($api->getParsedResponse(array('cpanelresult','event','result')) == 1) ? true false;
            
    $newemail $api->getParsedResponse(array('cpanelresult','data','email'));
            
    $newfwd   $api->getParsedResponse(array('cpanelresult','data','forward'));
            if(
    $result){
                
    $return '{"save":true,"newAcct":"'.$newemail.'","newfwd":"'.$newfwd.'"}';
            }else{
                
    $return '{"save":false,"error":"There was an error saving the new email"}';
            }
            break;
    }
    //$api->printErrors();
    //$api->printCurlInfo();

    echo $return;
     
  4. willsmelser

    willsmelser Member

    Joined:
    Jun 11, 2008
    Messages:
    7
    Likes Received:
    0
    Trophy Points:
    1
    [/PHP]

    my class file...extends the cpanel xml_api and uses some
    polymorph to change requests. Have tested using the extended api yet. Doubt the extended methods will work.
    PHP:
    class simpleXMLapi extends xmlapi{
        private 
    $user '';
        private 
    $acct '';
        private 
    $authstr '';
        private 
    $ip '';
        
        private 
    $port 2087;
        private 
    $respType '/xml-api/';
        private 
    $hideAuth true;
        
        public 
    $cInfo = array();
        public 
    $err = array();
        
        protected 
    $lastResponse;
        
        public function 
    simpleXMLapi($account$user$passOrHash$ipOrDom){
            
    $this->user $user;
            
    $this->acct $account;
            
    $this->ip $ipOrDom;
            
            
    //set the auth string based on password recieved
            
    $this->authstr = (strlen($passOrHash) < 20) ? 
                
    'Authorization: Basic ' base64_encode($user .':'$passOrHash) :
                
    'Authorization: Basic ' $passOrHash;
        }
        
        public function 
    api2_query($module$function$args){
            if(!
    is_array($args)){
                
    array_push($this->err'Invalid $args sent.  Must be an array with key=>value pairs');
                return 
    false;
            }
            
            
    //setup args for 
            
    $args['user'] = $this->acct;
            
    $args['cpanel_xmlapi_module'] = $module;
            
    $args['cpanel_xmlapi_func'] = $function;
            
    $args['cpanel_xmlapi_apiversion'] = '2';
            
            
    //build url
            
    $url 'https://' $this->ip ':' $this->port $this->respType 'cpanel';
            
            
    //perform the Curl Request
            
    $result $this->curl_query($url$args$this->authstr);
        }
        public function 
    api_query($function$args=array()){
            
    //build url
            
    $url 'https://' $this->ip ':' $this->port $this->respType $function;
            return 
    $this->curl_query($url,$args$this->authstr);
        }
        private function 
    curl_query$url$args$authstr ) {
            
    //for Curl post need the key=>value ina string of key=val&ke2=val2
            
    $poststr http_build_query($args'''&');

            
    $header[0]  = $authstr "\r\n";
            
    $header[0] .= 'Content-Type: application/x-www-form-urlencoded';
            
            
    $curl curl_init();
             
    curl_setopt($curlCURLOPT_SSL_VERIFYPEER0);
            
    curl_setopt($curlCURLOPT_SSL_VERIFYHOST0);//allow self-signed certs
            
    curl_setopt($curlCURLOPT_RETURNTRANSFER1);// return content of transfer
            
    curl_setopt($curlCURLOPT_URL$url); //set url
        
            
    curl_setopt($curlCURLOPT_TIMEOUT5);
            
    curl_setopt($curlCURLOPT_CONNECTTIMEOUT5);
            
            
    curl_setopt($curlCURLOPT_HTTPHEADER$header);        
            
    curl_setopt($curlCURLOPT_POSTcount($args)); //send number of post vars via the header
            
    curl_setopt($curlCURLOPT_POSTFIELDS$poststr); //send the post var string

            
    $content curl_exec($curl);
            
            
    //collect data about the transaction...pushed to $this->err array
            
    $this->gatherCurlData(curl_errno($curl), curl_error($curl), curl_getinfo($curl), $header$url$authstr$poststr$content);
            
            
    //close connection
            
    curl_close($curl);
            
    $this->lastResponse $content;
            
            return 
    $content;
        }
        public function 
    getParsedArray(){
            return 
    $this->my_xml2array($this->lastResponse);
        }
        public function 
    getUnparsedResponse(){
            return 
    $this->lastResponse;
        }
        
    /**
         * Used to parse the response
         * @param $search
         * @return unknown_type
         */
        
    public function getParsedResponse($search = array('event','result')){
            if(
    strlen($this->lastResponse) == 0){array_push($this->err,'No data to parse');return false;}
            
    $parsed $this->my_xml2array($this->lastResponse);
            
            
    $name array_shift($search);
            return 
    $this->findXMLinfo($parsed,$name,$search);
        }
        
    /**
         * XML can be a little tricky, so this just finds the data we are looking for
         * such as did it work or not...aka $search=array('cpanelresult','event','result')
         * @param $arr Array An xml doc put into array format via my_xml2array
         * @param $name String The name of the xml node
         * @param $search String The nested xml node(s) you are searching for
         * @return mixed The value node of the inner most xml search node
         */
        
    private function findXMLinfo($arr$name$search){
            foreach(
    $arr as $val){
                if(
    $val['name'] == $name){
                    if(
    count($search) && !isset($val['value'])){
                        
    $temp array_shift($search);
                        return 
    $this->findXMLinfo($val$temp$search);
                    }else{
                        return 
    $val['value'];
                    }
                }
            }
        }
        
    /**
         * Function used to seperate out the gathering of data from the curl_query method
         * @param $errnum
         * @param $errmsg
         * @param $info
         * @param $header
         * @param $url
         * @param $authstr
         * @param $poststr
         * @param $content
         * @return unknown_type
         */
        
    private function gatherCurlData($errnum$errmsg$info$header$url$authstr$poststr$content){
            
    //put errors and transaction info to $this->err collector
            
    array_push($this->cInfo,"URL used:\n\t$url");
            
    array_push($this->cInfo,"HEADER used:\n".http_build_query($header,'',"\n"));
            
    array_push($this->cInfo,"Post String Used:\n\t$poststr");
            
    array_push($this->cInfo,"Curl Response:\n\t".$content);
            if(!
    $this->hideAutharray_push($this->err,"Auth Used:\n\t$authstr");
            
            
    array_push($this->err,"Curl Error: \n\t".$errnum.':'.$errmsg);
            
    array_push($this->err,"Curl Info Returned : \n\t".http_build_query($info,'',"\n"));
            
            return 
    true;
        }
        
    /**
         * Prints Errors stored in the array $this->err
         * @return unknown_type
         */
        
    public function printErrors(){
            
    $str '';
            foreach(
    $this->err as $err){
                
    $str .= "$err\n************************************************\n";
            }
            
    $str preg_replace('/(\n|\r)/',"<br/>\n",$str);
            echo 
    $str;
        }
        public function 
    printCurlInfo(){
            
    $str '';
            foreach(
    $this->cInfo as $info){
                
    $str .= "$info\n************************************************\n";
            }
            
    $str preg_replace('/(\n|\r)/',"<br/>\n",$str);
            echo 
    $str;
        }
        function 
    my_xml2array($contents
        { 
            
    $xml_values = array();  
            
    $parser xml_parser_create(''); 
            if(!
    $parser
                return 
    false
        
            
    xml_parser_set_option($parserXML_OPTION_TARGET_ENCODING'UTF-8'); 
            
    xml_parser_set_option($parserXML_OPTION_CASE_FOLDING0); 
            
    xml_parser_set_option($parserXML_OPTION_SKIP_WHITE1); 
            
    xml_parse_into_struct($parsertrim($contents), $xml_values); 
            
    xml_parser_free($parser); 
            if (!
    $xml_values
                return array(); 
            
            
    $xml_array = array(); 
            
    $last_tag_ar =& $xml_array
            
    $parents = array(); 
            
    $last_counter_in_tag = array(1=>0); 
            foreach (
    $xml_values as $data
            { 
                switch(
    $data['type']) 
                { 
                    case 
    'open'
                        
    $last_counter_in_tag[$data['level']+1] = 0
                        
    $new_tag = array('name' => $data['tag']); 
                        if(isset(
    $data['attributes'])) 
                            
    $new_tag['attributes'] = $data['attributes']; 
                        if(isset(
    $data['value']) && trim($data['value'])) 
                            
    $new_tag['value'] = trim($data['value']); 
                        
    $last_tag_ar[$last_counter_in_tag[$data['level']]] = $new_tag
                        
    $parents[$data['level']] =& $last_tag_ar
                        
    $last_tag_ar =& $last_tag_ar[$last_counter_in_tag[$data['level']]++]; 
                        break; 
                    case 
    'complete'
                        
    $new_tag = array('name' => $data['tag']); 
                        if(isset(
    $data['attributes'])) 
                            
    $new_tag['attributes'] = $data['attributes']; 
                        if(isset(
    $data['value']) && trim($data['value'])) 
                            
    $new_tag['value'] = trim($data['value']); 
        
                        
    $last_count count($last_tag_ar)-1
                        
    $last_tag_ar[$last_counter_in_tag[$data['level']]++] = $new_tag
                        break; 
                    case 
    'close'
                        
    $last_tag_ar =& $parents[$data['level']]; 
                        break; 
                    default: 
                        break; 
                }; 
            } 
            return 
    $xml_array
        }
    }
     
Loading...

Share This Page