MH Studio

Development and tinkering

Recent Posts

  • Headlight HID Retrofit
  • Solar Monitoring With Python
  • Grid-Assisted Solar, pt. 2
  • Grid-Assisted Solar, pt. 1
  • Reverse-engineering TPMS sensors

Archives

  • July 2017
  • June 2017
  • May 2017
  • April 2017
  • March 2017
  • February 2017
  • November 2015

Categories

  • gadgets
  • house
  • programming
  • tinkering
  • vehicles

Powered by Genesis

Dynamic DNS Manager in PHP

2015-11-26 by Mitchel Leave a Comment

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.

DynDnsHost.php
PHP
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.

IDnsManager.php
PHP
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…

ITimeable.php
PHP
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.

MysqlDnsManager.php
PHP
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;
}
}
 
?>

CpanelDnsManager.php
PHP
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.

DynDnsManager.php
PHP
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.

PHP
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());
}
 
?>

LICENSE
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/>.

Filed Under: programming

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *