Have you ever wanted to implement your own dynamic DNS? Maybe you want to use a subdomain of your domain, or you just don’t want another 3rd party application running on your computer.
This is a project that’s been on my back burner for a while and I’ve finally gotten around to making it usable. I had implemented a cPanel-only version of this in the past, but it was less than ideal. Calling the cPanel API for each request was unnecessarily slow and it needed to be refactored. This time around, I created an interface for managing the DNS hosts, and I implemented a MySQL provider in addition to the cPanel provider. By using the MySQL provider as a cache of sorts for cPanel, the number of requests that actually go through to cPanel is minimized and the average response time is much shorter.
I included create and delete functions in the interface for completeness, but only implemented the read and update functions. My list of dynamic hosts is short and rarely changes, so I left those for future enhancement. Likewise for IPv6 support, I laid out most of the framework for it but I am not actually making use of it.
DISCLAIMER: I make no guarantees that this will work for you, nor will I provide support for it. I am putting it here in hopes that it may be a beneficial example for someone.
First, a class to represent a DNS host. This is the object that will be returned by a read request.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php class DynDnsHost { public $name; public $ipv4Address; public $ipv6Address; public $ttl; public $lastTouched; public $lastUpdated; } ?> |
Next, we need an interface for the DNS manager(s) to implement.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
<?php interface IDnsManager { const INVALID_HOST = 'Invalid Host'; const INVALID_IP_ADDRESS = 'Invalid IP Address'; const NOT_IMPLEMENTED = 'Not Implemented'; /** * Creates a new host with the specified name and IP address * * @param string $host Name of the host * @param string $ip IP address of the host * * @return bool true if the host was created, false if the creation failed */ public function createHost($host, $ip); /** * Read the specified host's information * * @param string $host Name of the host * * @return DynDnsHost object representing the requested host or NULL if the host wasn't found */ public function readHost($host); /** * Updates the specified host * * @param string $host Name of the host * @param string $ip New IP address of the host * * @return bool true if the host was updated, false if the update failed or the IP address didn't change */ public function updateHost($host, $ip); /** * Deletes the specified host * * @param string $host Name of the host * * @return bool true if the host was deleted, false if the deletion failed */ public function deleteHost($host); } ?> |
Then, a simple interface to track how much time was taken…
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php interface ITimeable { /** * Retrieve the time taken up by this class * * @return float Execution time of the class since its instantiation (wall clock time) */ public function getExecutionTime(); } ?> |
Now comes the interesting part. I implemented two DNS managers, one for MySQL and one for cPanel. cPanel is the core of the DNS manager, but the MySQL implementation will handle the caching.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
<?php /** * A class to handle caching DNS entries in MySQL * * @param string[] $mysqlInfo Array of MySQL configuration values: * $mysqlInfo['hostname'] * $mysqlInfo['username'] * $mysqlInfo['password'] * $mysqlInfo['database'] * $mysqlInfo['table'] */ class MysqlDnsManager implements IDnsManager, ITimeable { private $dbHandle; private $dbTable; private $executionTime; /***** Constructor / Destructor *****/ function __construct($mysqlInfo) { $startTime = microtime(true); $this->dbHandle = new \PDO("mysql:host={mysqlInfo['hostname']};dbname={mysqlInfo['database']}", $mysqlInfo['username'], $mysqlInfo['password']); $this->dbHandle->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); $this->dbTable = $mysqlInfo['table']; $this->executionTime = (microtime(true) - $startTime); } function __destruct() { $this->dbHandle = null; } /***** Public Functions *****/ public function createHost($host, $ip) { throw new \Exception(IDnsManager::NOT_IMPLEMENTED); } public function readHost($host) { $startTime = microtime(true); $stHandle = $this->dbHandle->prepare("SELECT * FROM $this->dbTable WHERE name = :name LIMIT 1"); $stHandle->setFetchMode(\PDO::FETCH_CLASS, '\DynDnsHost'); $stHandle->bindParam(':name', $host, \PDO::PARAM_STR); $stHandle->execute(); $dynHost = $stHandle->fetch(); $this->executionTime += (microtime(true) - $startTime); return $dynHost; } public function updateHost($host, $ip) { $dynHost = $this->readHost($host); $updated = false; $startTime = microtime(true); if (empty($dynHost)) { throw new \Exception(IDnsManager::INVALID_HOST); } if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $addrType = 'ipv4Address'; } else if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $addrType = 'ipv6Address'; } else { throw new \Exception(IDnsManager::INVALID_IP_ADDRESS); } if ($ip != $dynHost->$addrType) { $stHandle = $this->dbHandle->prepare("UPDATE $this->dbTable SET $addrType = :ip, lastUpdated = NOW(), lastTouched = lastUpdated WHERE name = :name LIMIT 1"); $stHandle->bindParam(':ip', $ip, \PDO::PARAM_STR); $updated = true; } else { $stHandle = $this->dbHandle->prepare("UPDATE $this->dbTable SET lastTouched = NOW() WHERE name = :name LIMIT 1"); } $stHandle->bindParam(':name', $host, \PDO::PARAM_STR); $stHandle->execute(); $this->executionTime += (microtime(true) - $startTime); return $updated; } public function deleteHost($host) { throw new \Exception(IDnsManager::NOT_IMPLEMENTED); } public function getExecutionTime() { return $this->executionTime; } } ?> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
<?php /** * A class to handle DNS entries in cPanel * * @param mixed[] $cpanelInfo Array of cPanel configuration values: * $cpanelInfo['hostname'] * $cpanelInfo['port'] * $cpanelInfo['username'] * $cpanelInfo['password'] * $cpanelInfo['zone'] // example.com * $cpanelInfo['subdomain'] // remote * @param boolean $sslVerify Set to false to disable verification of cPanel SSL certificates */ class CpanelDnsManager implements IDnsManager, ITimeable { private $curl; private $cpanelUrl; private $zoneDomain; private $subDomain; private $executionTime; /***** Constructor / Destructor *****/ function __construct($cpanelInfo, $sslVerify = true) { $startTime = microtime(true); $this->curl = curl_init(); if ($this->curl === false) { throw new \Exception('Failed to initialize cURL'); } /* Store the result instead of displaying it */ curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); /* Treat HTTP codes > 400 as failures */ curl_setopt($this->curl, CURLOPT_FAILONERROR, true); if ($sslVerify === false) { /* Allow self-signed certs */ curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, false); /* Allow certs that do not match the hostname */ curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, false); } /* Configure the authentication parameters */ curl_setopt($this->curl, CURLOPT_HTTPHEADER, array('Authorization: Basic '.base64_encode($cpanelInfo['username'].':'.$cpanelInfo['password']))); $this->cpanelUrl = sprintf('https://%s:%d', $cpanelInfo['hostname'], $cpanelInfo['port']); $this->zoneDomain = $cpanelInfo['zone']; $this->subDomain = $cpanelInfo['subdomain']; $this->executionTime = (microtime(true) - $startTime); } function __destruct() { curl_close($this->curl); } /***** Public Functions *****/ public function createHost($host, $ip) { throw new \Exception(IDnsManager::NOT_IMPLEMENTED); } public function readHost($host) { $startTime = microtime(true); $hostRecords = $this->getHostRecords($host); if (!empty($hostRecords)) { $dynHost = new \DynDnsHost(); $dynHost->name = $host; $dynHost->ttl = $hostRecords[0]['ttl']; $dynHost->lastTouched = ''; $dynHost->lastUpdated = ''; /* Iterate through the records to get each address */ foreach($hostRecords as $record) { if ($record['type'] == 'A') { $dynHost->ipv4Address = $record['address']; } if ($record['type'] == 'AAAA') { $dynHost->ipv6Address = $record['address']; } } } else { $dynHost = null; } $this->executionTime += (microtime(true) - $startTime); return $dynHost; } public function updateHost($host, $ip) { $startTime = microtime(true); $updated = false; $hostRecords = $this->getHostRecords($host); if (empty($hostRecords)) { throw new \Exception(IDnsManager::INVALID_HOST); } if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $recordType = 'A'; } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $recordType = 'AAAA'; } else { throw new \Exception(IDnsManager::INVALID_IP_ADDRESS); } foreach ($hostRecords as $record) { if ($record['type'] == $recordType) { if ($record['address'] != $ip) { $updateParams = array( 'cpanel_jsonapi_module' => 'ZoneEdit', 'cpanel_jsonapi_func' => 'edit_zone_record', 'domain' => $this->zoneDomain, 'line' => $record['line'], 'type' => $record['type'], 'address' => $ip ); $result = $this->cpanelRequest($updateParams); if ($result) { $updated = true; } } } } $this->executionTime += (microtime(true) - $startTime); return $updated; } public function deleteHost($host) { throw new \Exception(IDnsManager::NOT_IMPLEMENTED); } public function getExecutionTime() { return $this->executionTime; } /***** Private Functions *****/ private function getHostRecords($host) { $fetchzoneParams = array( 'cpanel_jsonapi_module' => 'ZoneEdit', 'cpanel_jsonapi_func' => 'fetchzone_records', 'domain' => $this->zoneDomain, 'name' => sprintf('%s.%s.%s.', $host, $this->subDomain, $this->zoneDomain), 'customonly' => 1 ); $result = $this->cpanelRequest($fetchzoneParams); if (!isset($result['data'])) { throw new \Exception('No zone data returned'); } return $result['data']; } private function cpanelRequest($params) { curl_setopt($this->curl, CURLOPT_URL, $this->cpanelUrl.'/json-api/cpanel?'.http_build_query($params)); $result = curl_exec($this->curl); // Check for valid result if ($result === false) { throw new \Exception(curl_error($this->curl)); } // Attempt to process result $jsonResult = json_decode($result, true); // Descend into the result tree if (isset($jsonResult['cpanelresult'])) { $jsonResult = $jsonResult['cpanelresult']; } else { throw new \Exception('Could not decode JSON response'); } return $jsonResult; } } ?> |
We also could use a simple class to pull the two of these together.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
<?php class DynDnsManager implements IDnsManager, ITimeable { private $mysqlDnsManager; private $cpanelDnsManager; function __construct($cpanelInfo, $mysqlInfo = null) { if (!empty($mysqlInfo)) { $this->mysqlDnsManager = new MysqlDnsManager($mysqlInfo); } $this->cpanelDnsManager = new CpanelDnsManager($cpanelInfo); } function __destruct() { } public function createHost($host, $ip) { throw new \Exception(IDnsManager::NOT_IMPLEMENTED); } public function readHost($host) { $dynHost = null; if ($this->mysqlDnsManager instanceof IDnsManager) { $dynHost = $this->mysqlDnsManager->readHost($host); } /* Couldn't find it in MySQL? Check cPanel just in case */ if (empty($dynHost)) { $dynHost = $this->cpanelDnsManager->readHost($host); } return $dynHost; } public function updateHost($host, $ip) { if ($this->mysqlDnsManager instanceof IDnsManager) { $hostUpdated = $this->mysqlDnsManager->updateHost($host, $ip); } else { /* Force the update to go to cPanel since MySQL isn't available */ $hostUpdated = true; } /* updateHost returns true if an update was made, so pass the update along */ if ($hostUpdated === true) { $hostUpdated = $this->cpanelDnsManager->updateHost($host, $ip); } return $hostUpdated; } public function deleteHost($host) { throw new \Exception(IDnsManager::NOT_IMPLEMENTED); } public function getExecutionTime() { $executionTime = 0.0; if ($this->mysqlDnsManager instanceof ITimeable) { $executionTime += $this->mysqlDnsManager->getExecutionTime(); } if ($this->cpanelDnsManager instanceof ITimeable) { $executionTime += $this->cpanelDnsManager->getExecutionTime(); } return $executionTime; } } ?> |
Finally, a simple example of how to make use of it.
Obviously, this isn’t a complete example and it provides no input validation. Please don’t use it directly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
<?php /* Plain text output */ header('Content-type: text/plain'); /* Enable autoloading of classes */ spl_autoload_register(); $mysqlInfo['hostname'] = 'localhost'; $mysqlInfo['username'] = 'username'; $mysqlInfo['password'] = 'password'; $mysqlInfo['database'] = 'dyndns'; $mysqlInfo['table'] = 'dynhosts'; $cpanelInfo['hostname'] = 'localhost'; $cpanelInfo['port'] = 2083; $cpanelInfo['username'] = 'username'; $cpanelInfo['password'] = 'password'; $cpanelInfo['zone'] = 'example.com'; $cpanelInfo['subdomain'] = 'remote'; if (!empty($_REQUEST['host'])) { $host = $_REQUEST['host']; } if (!empty($_REQUEST['ip'])) { $ip = $_REQUEST['ip']; } try { $dnsManager = new DynDnsManager($cpanelInfo, $mysqlInfo); switch ($_SERVER['REQUEST_METHOD']) { case 'GET': print_r($dnsManager->readHost($host)); break; case 'POST': if ($dnsManager->updateHost($host, $ip)) { echo "Host updated successfully\n"; } else { echo "Host not updated\n"; } break; default: die("Unsupported method!\n"); break; } printf("Time: %.6f sec\n", $dnsManager->getExecutionTime()); } catch (Exception $e) { die($e->getMessage()); } ?> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Copyright (C) 2015 MHstudio This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. |
Leave a Reply