cPanel Blog

August 2009

WHM Plugins

| No TrackBacks

We really have no information available on how to write WHM Plugins.  I have had 3 people ask me in the past 48 hours on how to write them, so I thought I might want to consolidate and post this knowledge.  A WHM plugin is merely a simple CGI application that has a couple of special comments in it to handle how it is displayed.  Any CGI language can be used here, however only perl will allow you access to some special functions that make permission handling much easier.

All WHM plugins must be placed at /usr/local/cpanel/whostmgr/docroot/cgi and must be prefixed with addon_ and end with .cgi.  These must be owned by root:root and be globally readable/executable (755), so don’t store any access credentials in these scripts - have them load from other files that are root-readable only.

As I mentioned earlier, there are a couple of special comments that need to be placed inside of WHM Addons.  The first one of these is the WHMADDON comment which sets how the plugin will be displayed in WHM:

#WHMADDON:appname:Display Name

Where it says appname it should be replaced with the actual file name of the application excluding addon_ and .cgi, so if you have addon_test.cgi, this would contain “test”.  Display Name refers to what well be displayed under the “Plugins” header of WHM.  An example of how a #WHMADDON should look for an application named “Sample Test App” would be:

 

#WHMADDON:test:Sample Test App

ACLs

There are two parts to ACLs with WHM Plugins, that control who can display it and who can access it.  The ACLS comment controls who will see the ACL in the Plugins section of WHM.  Then there is actually enforcing the ACL which is done via the Whostmgr::ACLS perl module.  If you are not familiar with what ACLs mean in the context of WHM, these refer to the permissions that the reseller has to various aspects in WHM such as the ability to create accounts or edit DNS zones.  These are indicated by a string like “list-accts” or “all”, you can view a list of these ACLs in /usr/local/cpanel/Whostmgr/ACLS.pm.  You can see what permissions a reseller has by looking at the /var/cpanel/resellers file.

Using the ACLS comment is pretty straight forward.  You simply add #ACLS:<acl name>, this only controls which users will see the plugin in the WHM Plugins section.  If this is not set it will be viewable by ALL resellers.

#ACLS:list-accts


With this ACL in place, only reseller accounts that have access to the list-accts ACL will be able to view this plugin.

The other type of ACL actually enforces permissions.  When this is not set, any reseller can visit cgi/addon_APPNAME.cgi and execute the application with root permissions, so this is a very critical step in WHM Plugin development. 

Inside of our product, we provide a module for performing this type of check called Whostmgr::ACLS.  This module has various functions relating to how ACLs work, but the only function we are concerned with is the checkacl() function.  This operates by being passed an acl name and returning 1 or 0 depending on whether the user has access to this ACL or not.  So if passed the “all” ACL (which is indicative of “root” access) and a reseller without root access tried to access the addon script, it would return 0.  This module requires some setup, since it is located outside of perl’s normal include path, /usr/local/cpanel has to be added to the include path using “use lib”.  Also this module has a constructor called “init_acls()” that has to be called.  As an example, here’s a chunk of code that will check for the “all” (root) acl and print ‘access denied” if the reseller accessing the script does not have permission to do so.

use lib '/usr/local/cpanel/';

use Whostmgr::ACLS ();

Whostmgr::ACLS::init_acls();



if ( !Whostmgr::ACLS::checkacl( 'list-accts' ) ) {

    print "Access Denied";

    exit;

}


Of course, this will only work when this is run inside of a perl script.  Inside of a PHP script /var/cpanel/resellers will have to be parsed manually.  If someone asks nicely, I may write this for you.

That should cover the basics of writing WHM plugins, there are of course other finer aspects of WHM plugins that can be gone into, however this covers the entire concept.

 

API basics and how to call API1 functions

| No TrackBacks

I have been noticing several people challenged with calling cPanel functions via our various ways of hooking into our APIs. Unfortunately, this isn't as cut and dry as just calling a function within a programming language.  Various factors, such as whether the call is being made from within cPanel or from a remote system, affect how this needs to be done.

To help you understand this, I will begin covering these topics in a series of posts on cPanel's various functions and how they work. In this first post of the series, I will discuss the basics of cPanel's APIs, and how to call API1 functions.

The basics

Before we can even begin going over how to call cPanel's API functions, we need to discuss the various API types.

For the most part, cPanel's API is divided into two sub-systems; API1 and API2. The difference between these is in how they are called and how they return data.

API1 will normally print data to the cPanel interface.  This works well when the functions are called via cpanel tags (covered later in this article), but won't return much useful data when called via the XML API, livePHP or CGI scripts. 

API2, on the other hand, is a much more robust system, capable of returning complex data structures that can be parsed into templates contained within a cp tag. API2 calls, as they do actually return data, will always return relevant information when called via the XML API, livePHP or CGI scripts.

One of my favorite features of API2 is that it uses named-based parameters that translate well into URL parameters.



Calling API1 functions

API1 functions can be called via a tag. As previously mentioned, these print data to the cPanel interface. cpanel tags use the following format:


<cpanel Module="function( params )">



So, if you wanted to call Ftp::ftpservername(), which is used to print out which FTP server is being used, it would be called with the following syntax:

<cpanel Ftp="ftpservername()">

Now, you will not want all functions to actually display data. For example, Mysql::adddb is generally something that you do not want printing data, for security reasons. Instead you want to check for error handling.

Any data being printed from this function can be suppressed in the browser via HTML comments, like so:

<!--Module="function()"-->


Sending input to the API1 function

So, in order to pass data to an API function, we will need a way to pull in the data.

Inside of cPanel's HTML parsing, we have access to certain variables. The main variables that you need to be aware of are $FORM and $CPERROR.

$FORM is merely a variable that is populated with either GET or POST data passed to the page from the browser. This variable is how cPanel passes data around from page to page. To access it, you call the $FORM variable the same way that you would call a hash in Perl ($FORM{'element'}).

So, for example, if you had a page called add_mysql_db that was passed the following: 


add_mysql_db.html?db=dbname


It would contain the following:


<cpanel Mysql="addb( $FORM{'db'} )">

The <cpanelif> tag

Along with the cpanel tag, there is also support for some really basic logic within this system, via the <cpanelif> tag. This tag allows for checking of basic boolean logic to see if a variable is set; if true, it will send whatever is contained between the <cpanelif> tags to the browser.  <cpanelif> tags cannot be nested in any way.

To call cpanelif, you will do something like the following:

<cpanelif $VAR{''}>

HTML CODE HERE

</cpanelif>

This is useful when you want to display an error message within cPanel's interface.  These are populated into the $CPERROR{$context} variable. 

Unfortunately, context is defined on the back-end on a per-module basis, so generally $context will be the same as whatever module you are calling.  In the case of our previous MySQL example, if we wanted to check for an error message, we would want to do the following:


    <cpanel Mysql="adddb( $FORM{'db'} )">

    <cpanelif $CPERROR{'mysql'}>

      ERROR: $CPERROR{'mysql'}

    </cpanelif>

Of course, you would want to also want to do !$CPERROR{'mysql'} around any success messages to make sure you don't end up with "ERROR: errormsg This was successful".

These are the basics of how to call API1 via .html files. You can always use the XML API to call these functions as well, but because this is API1, you may not get any useful data.

 

Using WHM remote authentication

| No TrackBacks

One thing that I have noticed while working with other people developing software that interacts with WHM’s XML API is that they always use basic HTTP authentication. It is okay to use basic authentication, but it is held to the same security restrictions in place for people using browsers. When working with cPanel in a remote fashion, having to work around these restrictions is unnecessary. Inside of our DNS clustering system, we developed a solution for just this problem called WHM Remote Access Keys or “WHM auth”.

The way that WHM auth works is by passing a key inside of the HTTP headers to cpsrvd (cPanel’s HTTP daemon). Your access key can be accessed and regenerated via Setup Remote Access Keys in WHM, or viewed via the file system at ~/.accesshash. These work both for root and resellers with support for cPanel users coming in the future.

When sending a WHM auth header to WHM, you’ll need to add the following as an HTTP header:


Authorization: WHM $user:$hash

where $hash is your access hash, stripped of all new lines.

When working with this functionality inside of scripts, it’s generally easiest to use an HTTP library for adding these headers. For example, if you wanted to use WHM auth inside of PHP & curl, you would simply add the following to the curl object before query:

$hash = “81a ….. 0af”;		# Set up the Hash
$hash = preg_replace(‘(/r|/n)’, “”, $hash); # Strip newlines from the hash
$auth_header[0] = “Authorization: WHM $username:$hash”; # set up the Header Array
$curl_setopt($curl, CURLOPT_HTTPHEADER, $auth_header); # tell curl to use the header array

Of course, not everyone wants to use PHP for handling remote interactions, and I personally would not feel proper discussing how to authenticate to WHM without talking about Perl and LWP.

As always with Perl, there is more than one way to do it, so we will simply discuss the most simple. When calling LWP’s get function, you simply make the second argument a hash named “Authorization” with a value of something similar to

WHM $user:$hash.
my $access_hash = “81a … 0af”;	# set up the accesshash
$access_hash =~ s/(\n|\r)//g;	# Remove newlines from the accesshash
my $auth_string = “WHM $user:$access_hash”;	# create the authentication string
$response = $lwp->get( $url, Authorization => $auth_string ); # send auth header with req

At this point, you can treat $response like a normal HTTP::Response object.

Inside of cPanel 11.25, there are numerous new security features being implemented. These changes can break both cPanel plugins and remote management applications (like billing systems) that integrate with cPanel. Luckily, the changes are all optional; however, I would hate to see addons preventing people from enabling new features &mdash; like session tokens, which help prevent XSRF attacks. So, stripped from an email I sent out to third-party developers earlier this week, here are some details regarding these changes.

Security tokens

The first of these changes is the inclusion of security tokens.With  this optional feature of 11.25, URLs will now contain <em>cpsess</em>, which has been put in place to help mitigate XSRF attacks.

Absolute URLs will no longer be allowed; you will need to ensure that you are using relative URLs within your product. For non-browser systems that interact with cPanel/WHM and webmail using Basic HTTP authentication, these security measures may be bypassed by ensuring that no session cookies are sent with the request. To enable this setting, go to the Tweak Settings screen in WHM and locate this option:

Require security tokens for all interfaces. This will greatly improve the security of cPanel and WHM against XSRF attacks, but may break integration with other systems, login applications, billing software and third party themes.

Click Save.

Source IP check

The second change that you should be aware of is Source IP Check, which is a security question verification interface inside of cPanel.

This feature will prompt users to define questions on their first login after enabling the feature (which is set to Off by default). Then, users will be prompted to answer the security questions on future logins from new IP addresses.

To configure this, go to $ip:2087/securitypolicy_enable and check the "Limit logins to verified IP addresses" option. It is important to note that this will affect XML API requests unless the Apply security policies to XML-API requests option is disabled.

This change should mostly affect XML API users. To solve other problems, we have modified the Source IP Check feature so that we enable the last login IP when the feature is enabled and no white list is found. This should reduce user annoyance a bit, and it prevents every frame from showing the security questions screen.

Blank referer checks

There are also some changes to the way cPanel handles blank referer checks, which make them more accurate. Basically, if a page is sent with a blank referer inside of an existing session, it will trigger an XSRF prevention page. Yet again, this is not affected inside of sessions that do not use cookies and are authenticated via either HTTP auth or WHM auth.

Testing and support

In order to test your software with these new features, you will need to update your software to the latest beta by editing /etc/cpudpate.conf and changing the CPANEL= line to read CPANEL=beta. These changes should also be available within the EDGE tree by 8/10/09.

 

Writing an FTP password trap in Perl

Password traps are probably the type of plugin that generates the most support requests at the moment. When creating a password trap, it is important that you use a Function Hook rather than a Custom Event Handler. The reason for this is that function hooks are executed as root, while custom event handlers are executed as the user. If you are not familiar with function hooks, please read the Documentation.

Function hooks reside as scripts in the subdirectories of /usr/local/cpanel/hooks/. Inside of the hooks/ directory are subdirectories for each module of cPanel, such as ftp/ or email/. When an API call is made, the corresponding module's subdirectory is checked for a script. If one exists, the script is passed XML data that contains both the parameters passed to the API call and information about the user. The XML data looks something like this:

	<cpanelevent>
<errors></errors>
<event>FUNCTION NAME</event>
<module>MODULE NAME</module>
<params>
<param0></param0>
<param1></param1>
<param2></param2>
...
</params>
</cpanelevent>
<CPDATA>
<BWLIMIT>unlimited</BWLIMIT>
.. Data from /var/cpanel/users/USERNAME
<USER>cptest</USER>
</CPDATA>


As you can see from the example, each piece of information is held between tags called containers. In this example, the container merely correlates to data contained within the /var/cpanel/users/ directory. The parameters used to call the function are contained within the container.

In order to read this data in in Perl, we will need to have the following bit of code:

my $xml; 
while()
{
$xml .= $_;
}


This piece of code will assign data to the $xml variable, reading one line at a time. Once the data is contained within the variable, we'll need to transform it into a hash reference using XML::Simple's XMLin function.

my $call_info = XMLin($xml);

Once the information you need has been transformed into a hash, you can access the parameters like any normal hash. In the case of cpanelevent, it looks like the following:

{
'params' => {
'param5' => 'public_html/wierdo',
'param4' => {},
'param0' => 'wierdo',
'param2' => 'public_html/wierdo',
'param3' => 'unlimited',
'param1' => 'Tm$qgIAwrH4A'
},
'errors' => {},
'event' => 'addftp',
'module' => 'ftp'
}


As you can see from the example, each parameter is named using the "param" prefix. These parameters will show up on any API1 call. To make sorting this information easier, we will need to define what information we want to save into a couple of scalar variables. For example:

my $system_user = $api_call->{'CPDATA'}->{'USER'}; 
my $ftp_user = $api_call->{'cpanelevent'}->{'params'}->{'param0'};
my $ftp_password = $api_call->{'cpanelevent'}->{'params'}->{'param1'};


At this point, we've worked out how to access all that data that we need. Now, we'll need to figure out where to log it. For the purposes of this article, we'll use
YAML::Syck. YAML is fast, extremely lightweight, and YAML::Syck can already be found on every cPanel system. But before we can really begin working with YAML::Syck, we'll need to work on our data structure. The following example should work well:

{
system_user => {
ftp_user => ftp_password,
ftp_user2 => ftp_password2
},
system_user2 => {
anotheruser => anotherpassword
}
}

At this point we'll need to load the YAML data from a file (if it exists):

my $datastore = [];
if (-e "/root/ftp_accounts") {
$datastore = LoadFile("/root/ftp_accounts");
}

And then add the data to the hash:

$datastore->{ $system_user }->{ $ftp_user } = $ftp_password; 

At this point, our data structure has been set up and is loading from a file, so all we have to do is save it to the file when it completes adding the data to the hash.

DumpFile("/root/ftp_accounts", $datastore);

Now that the script is finished, we'll simply need to upload it, because this password trap affects cPanel's FTP module, /usr/local/cpanel/hooks/ftp/addftp and /usr/local/cpanel/hooks/ftp/passwdftp. Once this has been done, /root/ftp_accounts will contain a complete list of FTP accounts on the system after they have been created or had their passwords changed.

If you wish to download the information in this tutorial, it is available here.

Mostly the XML API is used for account management; however, there are other features in it that simplify system administration. The most obvious of these functions is the loadavg call.

Last year, I wrote a class for working with the XML API from PHP. This class returns SimpleXML objects for each XML API call made. This makes development of remote cPanel interactions extremely simple.

I'm going to go over how to build a quick-and-dirty multi-server load average monitoring system in PHP using this class.

For this script, we'll store access credentials in an XML file class named monitor.xml. The reason for using XML is so that we can store this data in an easy-to-read/modify format (even programmatically). This is far simpler than using an array of associative arrays for doing this, as access hashes tend to be large.

This is the schema that I have decided upon using:

<monitor>
<server>
<ip>...</ip>
<accesshash>..</accesshash>
<user>..</user>
</server>
<server>
..
</server>
</monitor>

If you are not familiar with access hashes, these correspond to the hash stored in either ~/.accesshash or Setup Remote Access Keys in WHM.

To load this data structure inside of our script, we want to do the following:


$conf = simplexml_load_file("monitor.xml");

Once the configuration of this script has been set in place, we can start on the actual logic of this script, which in this case will consist (mostly) of a loop over all of the servers in the XML.


foreach ( $conf->server as $server ) {
# Logic Here
}

With the configuration and iteration worked out, we will want to include and instantiate the XML API class. This class only takes one parameter in its constructor: the host of the server it's managing.


$conf = simplexml_load_file("monitor.xml");
include("xmlapi.php.inc");
foreach ( $conf->server as $server ) {
$xmlapi = new xmlapi( $server->ip );
}

Next, we will want to set up how to authenticate to the XML API.

Generally speaking, WHM Auth should always be used for any type of automation for numerous reasons, namely because it does not have the same security restrictions as Basic Auth or Cookie Auth. The only reason this shouldn't be done is client-side applications where you may not necessarily know the username and password.

To do this with the XML API PHP class, you should use the hash_auth function, which will set the appropriate headers.


$conf = simplexml_load_file("monitor.xml");
include("xmlapi.php.inc");
foreach ( $conf->server as $server ) {
$xmlapi = new xmlapi( $server->ip );
$xmlapi->hash_auth( $server->user, $server->accesshash);
}

Once this has been set up, we are ready to run whatever commands we need to run. Using this class, we can call loadavg by just calling the loadavg method within the XML API class.


$conf = simplexml_load_file("monitor.xml");
include("xmlapi.php.inc");
foreach ( $conf->server as $server ) {
$xmlapi = new xmlapi( $server->ip );
$xmlapi->hash_auth( $server->user, $server->accesshash);
$loadavg = $xmlapi->loadavg;
}

At this point, $loadavg will contain the loadavg information, similar to what's in /proc/cpuinfo, but in a SimpleXML format. All we have to do now is display this data:


$conf = simplexml_load_file("monitor.xml");
include("xmlapi.php.inc");
foreach ( $conf->server as $server ) {
$xmlapi = new xmlapi( $server->ip );
$xmlapi->hash_auth( $server->user, $server->accesshash);
$loadavg = $xmlapi->loadavg;
print "<br>" . $server->ip . ": " . $loadavg->one . ", " . $loadavg->five . ", " . $loadavg->fifteen;
}

We are ready to run this script. Once executed, you should see something like the following for each server in monitor.xml:


127.0.0.1: 0.00, 0.00, 0.00


Now that this has been written, there is a HUGE security issue within this script that needs to be addressed. The following line is our offending code:


$conf = simplexml_load_file("monitor.xml");

This is an issue, as it is loading monitor.xml from within a document root (for example, $USERHOME/public_html/monitor.xml). This means that anyone could download this file and then authenticate to your server's WHM account. Instead, this file will need to be stored in a secure location outside of the document root, such as ~/monitor.xml.

The other concern with this is that this file should always have permissions of 400, never readable by other users. This script should only be executed as suPHP or off of a shared hosting system, so that it cannot be read by other users on the system.