Hello. The geeky pill of today is about how to get rid of DynDNS and feel like those punks when they make their DIY t-shirt with a political message on it. Or something like that.
See, many providers nowadays are providing hosting based on cPanel and a certain number of subdomains that you can set up through the front end. What you may learn today is that cPanel also offers an API that could let you do this automatically.
The API calls themselves are quite easy to understand and apply, but the troublesome part was about the authentication. Now, cPanel actually does offer several ways to connect, but the only one that really worked for me was the first one, which goes like this:
- Log into the cPanel API;
- Once you get redirected to the front end, you get a URL like this:
https://mc2dn.name:2082/cpsess1234567890/frontend/x3/index.html
- Take the
cpsess
value from that URL; - Make your API call using the cpsess value, for example:
http://yourdomain.example:2082/cpsess1234567890/json-api/cpanel?cpanel_jsonapi_module=ZoneEdit&cpanel...
As a starting point I’ve used a great blog article, which provides also a ready-made PHP script with everything set to go (and this will be your primary point of reference to set the whole thing up, actually). The only problem with that solution is that the request is based on one of the several authentication systems that my provider wasn’t allowing for some reason. In order to work around it, I’ve basically instructed cURL to behave like a browser.
Step 0: Does it actually work for me?
First thing off, you need to decide where to put your subdomains. Are they going off the main domain, say host.yourdomain.example
? Or are you going to dedicate a subdomain to it, for example host.ddns.yourdomain.example
? If your choice goes to the latter, then you need to create a subdomain in cPanel first, since our script will only work with the host definitions.
Once ready, next thing you need to do is to test if the API works from your browser. If it doesn’t, why should you waste time any further? Try to open http://yourdomain.example:2082/json-api/cpanel
, and see if you get this as a result:
{"cpanelresult":{"apiversion":"2","error":"api call failed. Module name is required.","data":{"reason":"api call failed. Module name is required.","result":"0"},"type":"text"}}
If you get an error like 401 Access Denied
instead, then check with your provider.
Step 1: Preparation.
The script in the original article is setting up a class, but for the sake of clarify I will go with normal PHP functions. Let’s start with the very first definition, including the cURL object:
// Plain text output header('Content-type: text/plain'); // Initialize constants define('CP_HOSTNAME', "http://yourdomain.example:2082"); define('CP_USERNAME', "username"); define('CP_PASSWORD', "password"); define('CP_COOKIE', "cookie.txt"); define('CP_DOMAIN', "yourdomain.example"); define('CP_DDNSDOMAIN', ".ddns.yourdomain.example."); // Define the API parameters define('DYNDNS_READ', 'read'); define('DYNDNS_UPDATE', 'update'); // Initialize variables $cpSessID = FALSE; $ch = curl_init(); curl_setopt_array($ch, array( CURLOPT_SSL_VERIFYPEER => false, // Allow self-signed certs CURLOPT_SSL_VERIFYHOST => false, // Allow certs that do not match the hostname CURLOPT_RETURNTRANSFER => true, // Return contents CURLOPT_HEADER => false, CURLOPT_COOKIESESSION => true, //CURLOPT_FOLLOWLOCATION => true, // Doesn't work with safe mode ));
Next thing, we set up the most important function: cpanelRequest()
. We will use this function for both the login (with no parameters) and the actual API call.
function cpanelRequest($params = NULL) { $isLoginReq = (is_null($params)); if ((!$isLoginReq) && ($cpSessID)) { // We are preparing a normal request $url = CP_HOSTNAME."/$cpSessID/json-api/cpanel?".http_build_query($params); curl_setopt($ch, CURLOPT_COOKIEFILE, CP_COOKIE); } else { // We are preparing the initial login $url = CP_HOSTNAME."/login"; curl_setopt($ch, CURLOPT_POSTFIELDS, "user=".CP_USERNAME."&pass=".CP_PASSWORD); curl_setopt($ch, CURLOPT_COOKIEJAR, CP_COOKIE); } curl_setopt($ch, CURLOPT_URL, $url); // Fire the request $result = curl_exec($ch); // There are no errors unless defined otherwise $error = false; // Check for valid result if ($result === false) { echo curl_error($ch)."\n"; // If curl didn't return anything, there's nothing else to check return false; } // Check for error code $errorNum = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (($errorNo != '301') && ($errorNum != '200')) { echo "API Error: $errNum\n"; $error = true; } // Check if we have the cpsess token value somewhere // (basically either 'url' or 'redirect_url') if ($isLoginReq) { if (preg_match("/.*?\/(cpsess.*?)\/.*?/is", implode(curl_getinfo($ch)), $matches)) $cpSessID = $matches[1]; else $error = true; // We don't need anything else at this point return (!$error); } // Attempt to parse the JSON result $jsonResult = json_decode($result, true); if (empty($jsonResult)) { echo "Invalid JSON: \n".strip_tags($result)."\n"; return false; } // Check for cpanelresult object if (isset($jsonResult['cpanelresult'])) { $jsonResult = $jsonResult['cpanelresult']; } else { echo "Can't parse the cpanelresult object\n"; $error = true; } // Check for cpanel error if (isset($jsonResult['error'])) { echo $jsonResult['error']."\n"; $error = true; } if ($error) return false; else return $jsonResult; } }
Once this is done, the rest of the script depends on you.
Step 2: Define the basic API calls.
What do you want to do with this API? Do you want to allow the user to just work on a define set of subdomains, or do you want to let them create some? Do you want them to be able to specify any IP address or only the one they call from?
In this case we’ll work on a defined set of subdomains and we’ll give the opportunity to define the IP address or not. Remember that the original article has a more comprehensive script that includes far more functions, but my purpose here is to explain how I did it for myself, and how you can do it as well.
First thing off, let’s set up another internal call to cPanel that will be useful in whatever case: cpanelGetHost()
. What it does is just this: it checks if the host you want to work with is already defined in the subdomain.
function cpanelGetHost($host) { $fetchzoneParams = array( 'cpanel_jsonapi_module' => 'ZoneEdit', 'cpanel_jsonapi_func' => 'fetchzone_records', 'domain' => CP_DOMAIN, 'customonly' => 1 ); $result = cpanelRequest($fetchzoneParams); if (empty($result['data'])) return false; // Get the list of DNS records $zoneFile = $result['data']; $hosts = array(); foreach ($zoneFile as $line) { // Looks for a matching host across // the A records if ( ($line['type'] == 'A') && ($host == DYNDNS_ALLHOSTS || (strcasecmp($line['name'], $host.CP_DDNSDOMAIN) === 0)) ) { $hosts[] = $line; } } if (!empty($hosts)) return $hosts; else echo "No hosts found\n"; return false; }
Next one is dynUpdateHost()
. This one does exactly what it says: it updates an existing entry with a new IP address.
function dynUpdateHost($host, $ip) { $hosts = cpanelGetHost($host); if ($hosts === false) return false; foreach ($hosts as $hostInfo) { if ($hostInfo['address'] == $ip) { echo "No update required: {$hostInfo['name']} ($ip)\n"; return true; } $result = cpanelRequest(array( 'cpanel_jsonapi_module' => 'ZoneEdit', 'cpanel_jsonapi_func' => 'edit_zone_record', 'domain' => CP_DOMAIN, 'Line' => $hostInfo['Line'], 'type' => $hostInfo['type'], 'address' => $ip )); if ($result) echo "Update successful: {$hostInfo['name']} ($ip)\n"; else echo "Update failed: {$hostInfo['name']}\n"; } }
Where should we get this IP address? We’ll leave this part outside the function itself, in the main routine later on. But before that: another command you may want to make available is dynReadHost()
. I will put the code here just for the sake of it, but in fact I can’t think of any other reason to use it other than testing whether the script works.
function dynReadHost($host) { $hosts = cpanelGetHost($host); if (!empty($hosts)) { $tabs = 0; foreach($hosts as $h) { $ht = 1 + (strlen($h['name']) / 8); if($ht > $tabs) $tabs = $ht; } echo "Name".str_repeat("\t", $tabs)."TTL\tClass\tType\tRecord\n"; foreach ($hosts as $hostInfo) { echo $hostInfo['name']."\t"; echo $hostInfo['ttl']."\t"; echo $hostInfo['class']."\t"; echo $hostInfo['type']."\t"; echo $hostInfo['record']."\n"; } } }
Step 3: Prepare the main call.
Now that we’re done with the foundation, it’s time to think about the interface. How should the syntax look like? We will follow the article again and require a GET query with the following parameters:
- action: can be read or update (mandatory)
- host: a specific subdomain (mandatory)
- ip: a valid IP address (optional)
Here is the code to accomplish that:
// Must have an action if (empty($_GET['action'])) die('No action specified'); // Make sure a host was specified if (empty($_GET['host'])) die('Must specify host'); else $host = $_GET['host']; // Use server value for IP if none was specified if (empty($_GET['ip'])) $ip = $_SERVER['REMOTE_ADDR']; else $ip = $_GET['ip']; // Validate IP address if (!filter_var($ip, FILTER_VALIDATE_IP)) die('Invalid IP address'); // Process the requested action switch ($_GET['action']) { case DYNDNS_READ: dynReadHost($host); break; case DYNDNS_UPDATE: dynUpdateHost($host, $ip); break; default: die('Not implemented'); }
Step 4: Final refinements.
Once this is done, voilà, the script is ready to be used. The only thing missing is a little authentication routine at the very top of the script, to prevent anybody to just use your DDNS service without you knowing. Here is an idea to start with:
if ((!isset($_SERVER['PHP_AUTH_USER'])) || ($_SERVER['PHP_AUTH_USER'] != 'username') || ($_SERVER['PHP_AUTH_PW'] != 'password')) { header('WWW-Authenticate: Basic realm="Dynamic DNS Service"'); header('HTTP/1.0 401 Unauthorized'); echo "Try again?"; exit; }
Again, the article provides a more complex system, with user-based authentication and authorization, so I would suggest to have a look at it.
FAQ #1: I get 401 Access Denied from the script!?
If you tested the API fine from your browser but the script returns a 401 error, it’s quite likely that the server is blacklisted by the firewall. You need to ask your ISP to make sure that the IP address of the server is whitelisted. If your provider is the same as mine, then good luck with letting them understand that you’re trying to access cPanel from within the server itself, they will go banana.
Leave a Reply to Jai Cancel reply