From: Nico Kaiser Date: Tue, 18 Jun 2013 12:59:11 +0000 (+0200) Subject: Initial 2007 version X-Git-Tag: v1.0.0~15 X-Git-Url: http://git.neszt.hu/?a=commitdiff_plain;h=48db3394987e32a96f9bb2f775302bb4c3d88134;p=Dyndns%2F.git Initial 2007 version --- 48db3394987e32a96f9bb2f775302bb4c3d88134 diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..c1ce6b4 --- /dev/null +++ b/README.txt @@ -0,0 +1,163 @@ +Introduction +============ + +This script takes the same parameters as the original members.dyndns.org +server does. It can update a BIND DNS server via "nsupdate". + +As it uses the same syntax as the original DynDNS.org servers do, a dynamic DNS server equipped with +this script can be used with DynDNS compatible clients without having to modify anything on the +client side. + + +Installation +============ + +To mimic the original DynDNS.org behavior, the Script's URL must be + + http://members.dyndns.org/nic/update + +You may have to adjust your own DNS configuration to make "members.dyndns.org" point to your own +Server and you Web Servers configuration to make "/nic/update" call the PHP script provided in this +package. + + +Furthermore, to be able to dynamically update the BIND DNS server, DNS key must be generated with +the command: + + dnskeygen -n dyndns.example.com -H 512 -h + +(Where "dyndns.example.com" is the key name) +The resulting key (look at the "Key:" line in the resulting Kdyndns.example.com.+157+00000.private) +must be copied to both, the config.php file (along with the key name, see there for details), and +the BIND configuration (see below). + + +The key has to be added to the BIND configuration (named.conf), as well as a DNS zone: + + +key dyndns.example.com. { + algorithm HMAC-MD5; + secret "bvZ....K5A=="; +}; + +zone "dyndns.example.com" { + type master; + file "dyndns.example.com.zone"; + allow-update { + key dyndns.example.com.; + }; +}; + +In this case, the zone is also called "dyndns.example.com". The (initial) dyndns.example.com.zone +file (located in BIND's cache directory) looks like this: + +$TTL 1h +@ IN SOA dyndns.example.com. root.example.com. ( + 2007111501 ; serial + 1h ; refresh + 15m ; retry + 7d ; expiration + 1h ; minimum + ) + NS + +Remember to change access rights so BIND is able to write to this file. + + +PHP script configuration +------------------------ + +The PHP script is called by the DynDNS client, it validates the input and calls "nsupdate" to +finally update the DNS with the new data. Its configuration is rather simple, the user database is +implemented as text file "dyndns.user" with each line containing + + : + +Where is crypt'ed like in Apache's htpasswd files. +Hosts are assigned to users in using the file "dyndns.hosts": + + :(,,,...) + +(So users can update multiple hosts, and a host can be updated by multiple users). + + +The location of these files must be specified in "config.php". For security reasons, don't place +them in your Document root, otherwise every Web user can read them. + + + +Implementation +============== + +Here you can find details on which capabilities of the DynDNS specification are implemented. + +Hostname: members.dyndns.org +HTTP ports: 80, 8245 +HTTPS port: 443 (not supported!) + + +Usage +----- + +Authentication in URL: + +http://username:password@members.dyndns.org/nic/update?hostname=yourhostname&myip=ipaddress&wildcard=NOCHG&mx=NOCHG&backmx=NOCHG + + +Raw HTTP GET Request: + +GET /nic/update?hostname=yourhostname&myip=ipaddress&wildcard=NOCHG&mx=NOCHG&backmx=NOCHG HTTP/1.0 +Host: members.dyndns.org +Authorization: Basic base-64-authorization +User-Agent: Company - Device - Version Number + +Fragment base-64-authorization should be represented by Base 64 encoded username:password string. + + +Implemented fields +------------------ + +hostname + Comma separated list of hostnames that you wish to update (up to 20 hostnames per request). + This is a required field. + Example: hostname=test.dyndns.org,customtest.dyndns.org + +myip + IP address to set for the update. + (If this parameter is not specified, the best IP address the server can determine will be used) + +(See http://www.dyndns.com/developers/specs/syntax.html for more details) + + +Return Codes +------------ + +good + The update was successful, and the hostname is now updated. + +badauth + The username and password pair do not match a real user. + +notfqdn + The hostname specified is not a fully-qualified domain name (not in the form hostname.dyndns.org + or domain.com). + +nohost + The hostname specified does not exist in this user account (or is not in the service specified in + the system parameter) + +badagent + The user agent was not sent or HTTP method is not permitted (we recommend use of GET request method). + +dnserr + DNS error encountered + +911 + There is a problem or scheduled maintenance on our side. + +(See http://www.dyndns.com/developers/specs/return.html for more details) + + + +Nico Kaiser +nico@siriux.net diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..21ebd29 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,8 @@ +TODO +==== + +- Implement Wildcards +- Implement NOCHG +- Implement more features from DynDNS.org +- Provide Apache templates (for mod_rewrite, etc.) + diff --git a/conf/apache/Dyndns.conf b/conf/apache/Dyndns.conf new file mode 100644 index 0000000..8f866dc --- /dev/null +++ b/conf/apache/Dyndns.conf @@ -0,0 +1,17 @@ +# +# This is the Apache configuration for the Dyndns package. +# +# An alias is set from /nic to /usr/local/Dyndns/htdocs and a Rewrite rule +# is added for the /nic/update URL. +# +Alias /nic /usr/local/Dyndns/htdocs + + + Options FollowSymLinks + AllowOverride None + Order allow,deny + Allow from all + RewriteEngine On + RewriteBase /nic + RewriteRule ^update$ /nic/index.php + diff --git a/conf/bind/Dyndns.conf.include b/conf/bind/Dyndns.conf.include new file mode 100644 index 0000000..ce6d33c --- /dev/null +++ b/conf/bind/Dyndns.conf.include @@ -0,0 +1,14 @@ + +key dyndns.example.com. { + algorithm HMAC-MD5; + secret "bvZfFHkl16wNGL/LuEUAqvlBeue9lw7C8GkHnQucN6jpKDMjOu29zFR6LlO5YlpN +zYquDBmDSPVddX9SuFIK5A=="; +}; + +zone "dyndns.org" { + type master; + file "dyndns.org.zone"; + allow-update { + key dyndns.example.com.; + }; +}; \ No newline at end of file diff --git a/conf/bind/keys/Kdyndns.example.com.+157+00000.key b/conf/bind/keys/Kdyndns.example.com.+157+00000.key new file mode 100644 index 0000000..4711196 --- /dev/null +++ b/conf/bind/keys/Kdyndns.example.com.+157+00000.key @@ -0,0 +1 @@ +dyndns.example.com. IN KEY 513 3 157 bvZfFHkl16wNGL/LuEUAqvlBeue9lw7C8GkHnQucN6jpKDMjOu29zFR6LlO5YlpNzYquDBmDSPVddX9SuFIK5A== diff --git a/conf/bind/keys/Kdyndns.example.com.+157+00000.private b/conf/bind/keys/Kdyndns.example.com.+157+00000.private new file mode 100644 index 0000000..4bd7e72 --- /dev/null +++ b/conf/bind/keys/Kdyndns.example.com.+157+00000.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 157 (HMAC) +Key: bvZfFHkl16wNGL/LuEUAqvlBeue9lw7C8GkHnQucN6jpKDMjOu29zFR6LlO5YlpNzYquDBmDSPVddX9SuFIK5A== diff --git a/conf/dyndns.hosts b/conf/dyndns.hosts new file mode 100644 index 0000000..12f07b8 --- /dev/null +++ b/conf/dyndns.hosts @@ -0,0 +1,5 @@ +client1.dyndns.example.com:user1,user2 +client2.dyndns.example.com:user1,user2 +user1.dyndns.example.com:user1 +user2.dyndns.example.com:user2 +*.u1.dyndns.example.com:user1 diff --git a/conf/dyndns.user b/conf/dyndns.user new file mode 100644 index 0000000..12885a1 --- /dev/null +++ b/conf/dyndns.user @@ -0,0 +1,2 @@ +user1:mypassword123 +user2:anotherpassword diff --git a/htdocs/classes/Dyndns.class.php b/htdocs/classes/Dyndns.class.php new file mode 100644 index 0000000..3e46f5c --- /dev/null +++ b/htdocs/classes/Dyndns.class.php @@ -0,0 +1,239 @@ + + * @version $Revision: 13 $ + */ +class Dyndns { + + /** + * Storage for all configuration variables, set in config.php + * @var array + * @access private + */ + var $_config; + + /** + * The user logged in + * @var string + * @access private + */ + var $_user; + + /** + * IP the hostnames should point to + * @var string + * @access private + */ + var $_myIp; + + /** + * Hostnames that should be updated + * @var array + * @access private + */ + var $_hostnames; + + /** + * Debug buffer + * @var string + * @access private + */ + var $_debugBuffer; + + + function Dyndns() { + /* Default config settings */ + $this->_config = array ( + 'hostsFile' => 'dyndns.hosts', /* Location of the hosts database */ + 'userFile' => 'dyndns.user', /* Location of the user database */ + 'debugFile' => 'dyndns.log', /* Debug file */ + 'debug' => false, /* Enable debugging */ + + 'bind.server' => false, + 'bind.zone' => '', + 'bind.ttl' => 300, + 'bind.key' => '', + ); + } + + /** + * Initializes the Dyndns script + */ + function init() { + $this->_users = new DyndnsUsers($this->_config['userFile']); + $this->_hosts = new DyndnsHosts($this->_config['hostsFile']); + + $this->_checkHttpMethod(); + $this->_checkAuthentication(); + + /* Get IP address, fallback to REMOTE_ADDR */ + $this->_myIp = DyndnsHelper::getMyIp(); + if (array_key_exists('myip', $_REQUEST)) { + if (DyndnsHelper::checkValidIp($_REQUEST['myip'])) { + $this->_myIp = $_REQUEST['myip']; + } else { + $this->debug('Invalid parameter myip. Using default REMOTE_ADDR'); + } + } + + /* Get hostnames to be updated */ + $this->_hostnames = array (); + if (array_key_exists('hostname', $_REQUEST) && ($_REQUEST['hostname'] != '')) { + $this->_hostnames = explode(',', strtolower($_REQUEST['hostname'])); + $this->_checkHostnames(); + } else { + $this->_returnCode('notfqdn'); + } + + $this->_updateHosts(); + + /* Return "good" code as everything seems to be ok now */ + $this->_returnCode('good'); + } + + /** + * Store a value in the config table + * + * @param string $key + * @param mixed $value + */ + function setConfig($key, $value) { + $this->_config[$key] = $value; + } + + /** + * Get a value from the config table + * + * @return mixed an arbitrary value + */ + function getConfig($key) { + return $this->_config[$key]; + } + + /** + * Checks if the HTTP method is supported. Currently, only GET is supported, + * all other methods will result in a "badagent" code. + * + * @access private + */ + function _checkHttpMethod() { + /* Only HTTP method "GET" is allowed here */ + if ($_SERVER['REQUEST_METHOD'] != 'GET') { + $this->debug('ERROR: HTTP method ' . $_SERVER['REQUEST_METHOD'] . ' is not allowed.'); + $this->_returnCode('badagent', Array ('HTTP/1.0 405 Method Not Allowed')); + } + } + + /** + * Handles authentication. Requests HTTP authentication and if user/pw is submitted + * check if they are valid + * + * @access private + */ + function _checkAuthentication() { + /* Request user/pw if not submitted yet */ + if (!isset($_SERVER['PHP_AUTH_USER'])) { + $this->debug('No authentication data sent'); + $this->_returnCode('badauth', Array ( + 'WWW-Authenticate: Basic realm="DynDNS API Access"', + 'HTTP/1.0 401 Unauthorized') + ); + } + $user = strtolower($_SERVER['PHP_AUTH_USER']); + $password = $_SERVER['PHP_AUTH_PW']; + if (! $this->_users->checkCredentials($user, $password)) { + $this->_returnCode('badauth', Array ('HTTP/1.0 403 Forbidden')); + } + $this->_user = $user; + } + + /** + * Checks if all hostnames are valid (FQDN) and belong to the user + * + * @access private + */ + function _checkHostnames() { + foreach ($this->_hostnames as $hostname) { + if (! DyndnsHelper::checkValidHost($hostname)) { + $this->_returnCode('notfqdn'); + } + if (! $this->_hosts->checkUserHost($this->_user, $hostname)) { + $this->_returnCode('nohost'); + } + } + } + + /** + * Updates all hosts + * + * @param string hostname Hostname + * @param string myip IP address + */ + function _updateHosts() { + foreach ($this->_hostnames as $hostname) { + if (! $this->_hosts->update($hostname, $this->_myIp) ) { + $this->_returnCode('dnserr'); + } + } + /* Flush host database (write to hosts file) */ + if (! $this->_hosts->flush()) { + $this->_returnCode('dnserr'); + } + } + + /** + * Returns a "Return code". The program exits after output. + * + * @param string code Return code (like "notfqdn") + * @param array additionalHeaders HTTP headers to be added + * @param string debugMessage Message for the debug log + */ + function _returnCode($code, $additionalHeaders = Array (), $debugMessage = "") { + foreach ($additionalHeaders as $header) { + header($header); + } + $this->debug('Sending return code: ' . $code); + echo $code; + $this->_shutdown(); + } + + /** + * Shuts down, closes files, writes debug output, etc. + * + * @access private + */ + function _shutdown() { + /* Write debug buffer */ + if ( ($this->_debugBuffer != "") && ($this->_config['debug'])) { + if ($fh = @fopen($this->_config['debugFile'], 'a')) { + fwrite($fh, $this->_debugBuffer); + fclose($fh); + } + } + exit; + } + + /** + * Saves a debug message (if debugging is turned on) + * + * @param string message Debug message + */ + function debug($message) { + $this->_debugBuffer .= date('M j G:i:s') . ' Dyndns: ' . $message . "\n"; + } +} +?> \ No newline at end of file diff --git a/htdocs/classes/DyndnsHelper.class.php b/htdocs/classes/DyndnsHelper.class.php new file mode 100644 index 0000000..b8e915f --- /dev/null +++ b/htdocs/classes/DyndnsHelper.class.php @@ -0,0 +1,79 @@ + + * @version $Revision: 13 $ + * @static + */ +class DyndnsHelper { + + /** + * Simple function to check valid IP address + * + * @param string IP address + * @return boolean True if IP is valid + */ + function checkValidIp($ip) { + if (! eregi("^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$", $ip)) + return false; + $tmp = explode(".", $ip); + foreach ($tmp as $sub) { + $sub = $sub * 1; + if ($sub < 0 || $sub > 256) return true; + } + return true; + } + + /** + * Simple function to check valid Hostname (FQDN) + * + * @param string Hostname + * @return boolean True if Hostname is valid + */ + function checkValidHost($hostname) { + return eregi('^[a-z0-9.-]+$', $hostname); + } + + /** + * Tries to get the IPv4 of the client + * + * @param access public + * @return string ip + */ + function getMyIp() { + $ip = $_SERVER['REMOTE_ADDR']; + /* Some IPv6 Servers add ::ffff: */ + $ip = preg_replace('/^::ffff:/', '', $ip); + return $ip; + } + + /** + * Compares if two hostnames are the same, with regard to a wildcard + * + * @param string host1 + * @param string host2 + * @return boolean true or false + */ + function compareHosts($host1, $host2, $wildcard = false) { + $a = explode('.', $host1); + $b = explode('.', $host2); + if (count($a) != count($b)) + return false; + for ($i = 0; $i < count($a); $i++) { + if (($wildcard === false) or (($a[$i] != $wildcard) and ($b[$i] != $wildcard))) + if ($a[$i] != $b[$i]) + return false; + } + return true; + } +} +?> \ No newline at end of file diff --git a/htdocs/classes/DyndnsHosts.class.php b/htdocs/classes/DyndnsHosts.class.php new file mode 100644 index 0000000..8c1639d --- /dev/null +++ b/htdocs/classes/DyndnsHosts.class.php @@ -0,0 +1,194 @@ + + * @version $Revision: 13 $ + */ +class DyndnsHosts { + + /** + * Filename of the hosts file (dyndns.hosts) + * @var string + * @access private + */ + var $_hostsFile; + + /** + * Host/Users array: 'hostname' => array ('user1', 'user2', ...) + * @var array + * @access private + */ + var $_hosts; + + /** + * List of updates in the format 'hostname' => 'ip' + * @var array + * @access + */ + var $_updates; + + /** + * This is true if the status / user files were read + * @var boolean + * @access private + */ + var $_initialized; + + + function DyndnsHosts($hostsFile) { + $this->_hostsFile = $hostsFile; + $this->_initialized = false; + $this->_updates = array (); + } + + /** + * Adds an update to the list + */ + function update($hostname, $ip) { + if (! $this->_initialized) + $this->_init(); + + $GLOBALS['dyndns']->debug('Update: '.$hostname . ':'.$ip); + $this->_updates[$hostname] = $ip; + return true; + } + + /** + * Checks if the host belongs to the user + * + * @param string user Username + * @param string hostname Hostname + * @return boolean TRUE if the host belongs to the user + */ + function checkUserHost($user, $hostname) { + if ($hostname == 'members.dyndns.org') { + $GLOBALS['dyndns']->debug('Cannot change members.dyndns.org'); + return false; + } + if (! DyndnsHelper::checkValidHost($hostname)) { + $GLOBALS['dyndns']->debug('Invalid host: ' . $hostname); + return false; + } + if (! $this->_initialized) + $this->_init(); + if (is_array($this->_hosts)) { + foreach ($this->_hosts as $line) { + if (preg_match("/^(.*?):(.*)/", $line, $matches)) { + if (DyndnsHelper::compareHosts($matches[1], $hostname, '*') && + in_array($user, explode(',', strtolower($matches[2])))) { + return true; + } + } + } + } + $GLOBALS['dyndns']->debug('Host '.$hostname.' does not belong to user '.$user); + return false; + } + + /** + * Write cached changes to the status file + * + * @access public + */ + function flush() { + return $this->_updateBind(); + } + + /** + * Initializes the user and status list from the file + * + * @access private + */ + function _init() { + if ($this->_initialized) return; + $this->_readHostsFile(); + if (! is_array($this->_hosts)) { + $this->_hosts = Array (); + } + $this->_initialized = true; + } + + /** + * Reads the contents of $_hostsFile into $_hosts + * + * @access private + */ + function _readHostsFile() { + $lines = @file($this->_hostsFile); + if (is_array($lines)) { + $this->_hosts = $lines; + } else { + $GLOBALS['dyndns']->debug('Empty hosts file: "' . $this->hostsFile . '"'); + } + } + + /** + * Sends DNS Updates to BIND server + * + * @access private + */ + function _updateBind() { + $server = $GLOBALS['dyndns']->getConfig('bind.server'); + $zone = $GLOBALS['dyndns']->getConfig('bind.zone'); + $ttl = $GLOBALS['dyndns']->getConfig('bind.ttl') * 1; + $key = $GLOBALS['dyndns']->getConfig('bind.key'); + + if (! DyndnsHelper::checkValidHost($server)) { + $GLOBALS['dyndns']->debug('ERROR: Invalid bind.server config value'); + return false; + } + if (! DyndnsHelper::checkValidHost($zone)) { + $GLOBALS['dyndns']->debug('ERROR: Invalid bind.zone config value'); + return false; + } + if (! is_int($ttl)) { + $GLOBALS['dyndns']->debug('Invalid bind.ttl config value. Setting to default 300.'); + $ttl = 300; + } + if ($ttl < 60) { + $GLOBALS['dyndns']->debug('bind.ttl is too low. Setting to default 300.'); + $ttl = 300; + } + if (! eregi('^[a-z0-9.-=/]+$', $key)) { + $GLOBALS['dyndns']->debug('ERROR: Invalid bind.key config value'); + return false; + } + + /* Create temp file with nsupdate commands */ + $tempfile = tempnam('/tmp', 'Dyndns'); + $fh = @fopen($tempfile, 'w'); + if (! $fh) { + $GLOBALS['dyndns']->debug('ERROR: Could not open temporary file'); + return false; + } + fwrite($fh, "server $server\n"); + fwrite($fh, "zone $zone\n"); + $ttl = $GLOBALS['dyndns']->getConfig('bind.ttl'); + foreach ($this->_updates as $host => $ip) { + fwrite($fh, "update delete $host A\n"); + fwrite($fh, "update add $host $ttl A $ip\n"); + } + fwrite($fh, "send\n"); + fclose($fh); + + /* Execute nsupdate */ + $result = exec('/usr/bin/nsupdate -y ' . $key . ' ' . $tempfile . ' 2>&1'); + unlink($tempfile); + if ($result != '') { + $GLOBALS['dyndns']->debug('ERROR: nsupdate returns: ' . $result); + return false; + } + + return true; + } +} +?> \ No newline at end of file diff --git a/htdocs/classes/DyndnsUsers.class.php b/htdocs/classes/DyndnsUsers.class.php new file mode 100644 index 0000000..62c90f6 --- /dev/null +++ b/htdocs/classes/DyndnsUsers.class.php @@ -0,0 +1,60 @@ + + * @version $Revision: 13 $ + */ +class DyndnsUsers { + + /** + * Filename of the users file + * @var string + * @access private + */ + var $_userFile; + + + function DyndnsUsers($userFile) { + $this->_userFile = $userFile; + } + + /** + * Checks user credentials + * + * @param string user + * @param string password + * @access private + */ + function checkCredentials($user, $password) { + $lines = @file($this->_userFile); + if (is_array($lines)) { + foreach ($lines as $line) { + if (preg_match("/^(.*?):(.*)/", $line, $matches)) { + if (strtolower($matches[1]) == strtolower($user)) { + $salt = substr($matches[2], 0, 2); + if (crypt($password, $salt) == $matches[2]) { + $GLOBALS['dyndns']->debug('Login successful for user ' . $user); + return TRUE; + } else { + $GLOBALS['dyndns']->debug('Wrong password for user: ' . $user); + } + } + } + } + } else { + $GLOBALS['dyndns']->debug('Empty user file: "' . $this->_userFile . '"'); + } + $GLOBALS['dyndns']->debug('Unknown user: ' . $user); + return FALSE; + } +} +?> \ No newline at end of file diff --git a/htdocs/config.php b/htdocs/config.php new file mode 100644 index 0000000..c7205c0 --- /dev/null +++ b/htdocs/config.php @@ -0,0 +1,59 @@ +setConfig('hostsFile', 'conf/dyndns.hosts'); + + +/* + * Location of the user database + */ +$dyndns->setConfig('userFile', 'conf/dyndns.user'); + + +/* + * Enable debugging? + */ +$dyndns->setConfig('debug', true); + + +/* + * Debug filename + */ +$dyndns->setConfig('debugFile', '/tmp/dyndns.log'); + + +/* + * Secret Key for BIND nsupdate + * : + */ +$dyndns->setConfig('bind.key', 'dyndns.example.com:bvZfFHkl16wNGL/LuEUAqvlBeue9lw7C8GkHnQucN6jpKDMjOu29zFR6LlO5YlpNzYquDBmDSPVddX9SuFIK5A=='); + + +/* + * Address of the BIND server. You can specify any remote DNS server here, + * if the server allows you to update data using bind.key + */ +$dyndns->setConfig('bind.server', 'localhost'); + + +/* + * The BIND zone which retrieves the updates + */ +$dyndns->setConfig('bind.zone', 'dyndns.example.com'); + + +/* + * Dynamic DNS entries will get this TTL + */ +$dyndns->setConfig('bind.ttl', '300'); + +?> \ No newline at end of file diff --git a/htdocs/index.php b/htdocs/index.php new file mode 100644 index 0000000..5b60f3f --- /dev/null +++ b/htdocs/index.php @@ -0,0 +1,33 @@ + + * @version $Revision: 13 $ + */ + +error_reporting(E_ALL); +require_once(dirname(__FILE__) . '/classes/Dyndns.class.php'); + +$GLOBALS['dyndns'] =& new Dyndns(); +$dyndns =& $GLOBALS['dyndns']; + +@include(dirname(__FILE__). '/config.php'); + +$dyndns->init(); + +?> \ No newline at end of file