diff --git a/CHANGELOG.md b/CHANGELOG.md index e450c29..3601392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,3 +4,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.0] - 2019-07-17 +### Added +- Initial Release + +## [1.0.1] - 2019-07-18 +### Added +- Added zone_exists method to override parent and prevent issues with zoneAfxr method +### Fixed +- SOA creation defaults to YYYYMMDD01 instead of last two numbers being random +- Record updates will now properly increment the SOA Serial +- Record creation no longer removes same named and type records leaving only the new one +- Record deletion no longer removes all records of the same name and type +- Fixed record updates not updating all the time +### Changed +- Changed license from GPLv3 to MIT to allow for integration into apnscp + +## [1.0.2] - 2019-07-24 +### Fixed +- Fixed issue with NS definition using wrong constant +- Fixed warning using wrong macro \ No newline at end of file diff --git a/src/Module.php b/src/Module.php index 28221bd..78cda78 100644 --- a/src/Module.php +++ b/src/Module.php @@ -42,7 +42,7 @@ public function __construct() { parent::__construct(); $this->key = AUTH_PDNS_KEY; - $this->ns = AUTH_PDNS; + $this->ns = defined('AUTH_PDNS_NS') ? AUTH_PDNS_NS : AUTH_PDNS; // Backwards compatible $this->records = []; } @@ -244,122 +244,29 @@ public function zone_exists(string $zone): bool } /** - * Creates Record with Zone Creation (not currently implemented) - * - * @param $name - * @param null $ip - * - * @return array - */ - protected function createDefaultRecords($name, $ip = null): array - { - $rrsets = []; - $cnames = $this->defaultCnames; - - if (! is_null($ip)) - { - $records[] = [ - 'content' => $ip, - 'disabled' => false, - ]; - - $rrsets[] = [ - 'records' => $records, - 'name' => $this->makeCanonical($name), - 'ttl' => 14400, - 'type' => 'A', - ]; - } - - foreach ($cnames as $cname) - { - $records = [0 => [ - 'content' => $this->makeCanonical($name), - 'disabled' => false, - ]]; - - $rrsets[] = [ - 'records' => $records, - 'name' => $this->makeFqdn($name, $cname, true), - 'ttl' => 14400, - 'type' => 'CNAME', - ]; - } - - return $rrsets; - } - - /** - * Return a complete domain name with the subdomain and zone. - * Optionally returns the canonical domain with trailing period - * - * @param $zone - * @param $subdomain - * - * @param bool $makeCanonical - * - * @return string - */ - private function makeFqdn($zone, $subdomain, $makeCanonical = false): string - { - if (strpos($subdomain, $zone) === false) - { - $subdomain = implode('.', [$subdomain, $zone]); - } - - if ($makeCanonical) - { - return $this->makeCanonical($subdomain); - } - - return $subdomain; - } - - /** - * Get raw zone data - * - * @param string $domain - * - * @return null|string - */ - - protected function zoneAxfr($domain): ?string - { - try - { - $api = $this->makeApi(); - $axfrrec = $api->do('GET', 'servers/localhost/zones' . sprintf('/%s', $this->makeCanonical($domain)) . '/export'); - } - catch (ClientException $e) - { - warning("Failed to transfer DNS records from PowerDNS - try again later. Response code: %d", $e->getResponse()->getStatusCode()); - - return null; - } - - return $axfrrec['zone']; - } - - /** - * Modify a DNS record + * Add a DNS record * * @param string $zone - * @param Record $old - * @param Record $new + * @param string $subdomain + * @param string $rr + * @param string $param + * @param int $ttl * * @return bool */ - protected function atomicUpdate(string $zone, Record $old, Record $new): bool + public function add_record(string $zone, string $subdomain, string $rr, string $param, int $ttl = self::DNS_TTL): bool { - if (! $this->canonicalizeRecord($zone, $old['name'], $old['rr'], $old['parameter'], $old['ttl'])) + if (! $this->canonicalizeRecord($zone, $subdomain, $rr, $param, $ttl)) { return false; } - if (! $this->canonicalizeRecord($zone, $new['name'], $new['rr'], $new['parameter'], $new['ttl'])) - { - return false; - } + $record = new Record($zone, [ + 'name' => $subdomain, + 'rr' => $rr, + 'parameter' => $param, + 'ttl' => $ttl, + ]); try { @@ -368,162 +275,97 @@ protected function atomicUpdate(string $zone, Record $old, Record $new): bool $zoneData = $api->do('GET', 'servers/localhost/zones' . sprintf('/%s', $this->makeCanonical($zone))); $this->records = $zoneData['rrsets']; - $rrsets = $this->changeRecords($old, $new); + $rrsets = $this->addRecords($record); $api->do('PATCH', 'zones' . sprintf('/%s', $this->makeCanonical($zone)), ['rrsets' => $rrsets]); } catch (ClientException $e) { - return error("Failed to update record '%s' on zone '%s' (old - rr: '%s', param: '%s'; new - rr: '%s', param: '%s'): %s", - $old['name'], - $zone, - $old['rr'], - $old['parameter'], $new['name'] ?? $old['name'], $new['parameter'] ?? $old['parameter'], - $this->renderMessage($e) - ); + return error("Failed to create record '%s': %s", (string) $record, $this->renderMessage($e)); } - return true; + return $api->getResponse()->getStatusCode() === 204; // Returns 204 No Content on success. } /** - * Parse existing records for zone, find and remove the old record from the list and add the new one. + * Parse existing records for zone, add to records to ensure same named records are not removed * Refer to https://doc.powerdns.com/authoritative/http-api/zone.html#rrset under "changetype" * * @param \Opcenter\Dns\Record $record * * @return array */ - private function changeRecords(Record $old, Record $new): array + private function addRecords(Record $record): array { $return = []; - $oldName = $this->replaceMarker($old['zone'], $old['name']); - $newName = $this->replaceMarker($new['zone'], $new['name']); - - $added = false; // Did we add the record to an existing rrset? + $name = $name = $this->replaceMarker($record['zone'], $record['name']); foreach ($this->records as $rrset) { - if ($rrset['name'] === $oldName && $rrset['type'] === $old['rr']) - { - if (count($rrset['records']) > 1) // More than one record, loop through and delete just the record - { - foreach ($rrset['records'] as $k => $rrec) - { - if ($rrec['content'] === $old['parameter']) - { - unset($rrset['records'][ $k ]); - $rrset['records'] = array_values($rrset['records']); - } - } - } - else { - // Only 1 record, just delete the rrset. - $return[] = [ - 'records' => '', - 'name' => $oldName, - 'changetype' => 'DELETE', - 'type' => $old['rr'], - ]; - } - - } - - if ($rrset['name'] === $newName && $rrset['type'] === $new['rr']) + if ($rrset['name'] === $name && $rrset['type'] === $record['rr']) { - $rrset['records'][] = ['content' => $this->parseRecord($new), 'disabled' => false]; + $rrset['records'][] = ['content' => $this->parseRecord($record), 'disabled' => false]; $rrset['changetype'] = 'REPLACE'; $return[] = $rrset; - - $added = true; } } // No records match the name and type, let's create a new record set - if (true !== $added) + if (empty($return)) { - $return[] = $this->formatRecord($new); + $return[] = $this->formatRecord($record); } return $return; } /** - * Add a DNS record + * Replaces the @ with the fqdn * - * @param string $zone - * @param string $subdomain - * @param string $rr - * @param string $param - * @param int $ttl + * @param $zone + * @param $name * - * @return bool + * @return string */ - public function add_record(string $zone, string $subdomain, string $rr, string $param, int $ttl = self::DNS_TTL): bool + protected function replaceMarker($zone, $name): string { - if (! $this->canonicalizeRecord($zone, $subdomain, $rr, $param, $ttl)) - { - return false; - } - - $record = new Record($zone, [ - 'name' => $subdomain, - 'rr' => $rr, - 'parameter' => $param, - 'ttl' => $ttl, - ]); - - try + if ($name === '@') { - $api = $this->makeApi(); - // Get zone and rrsets, need to parse the existing rrsets to ensure proper addition of new records - $zoneData = $api->do('GET', 'servers/localhost/zones' . sprintf('/%s', $this->makeCanonical($zone))); - $this->records = $zoneData['rrsets']; - - $rrsets = $this->addRecords($record); - - $api->do('PATCH', 'zones' . sprintf('/%s', $this->makeCanonical($zone)), ['rrsets' => $rrsets]); + $name = $this->makeCanonical($zone); } - catch (ClientException $e) + else { - return error("Failed to create record '%s': %s", (string) $record, $this->renderMessage($e)); + $name = $this->makeFqdn($zone, $name, true); } - return $api->getResponse()->getStatusCode() === 204; // Returns 204 No Content on success. + return $name; } /** - * Parse existing records for zone, add to records to ensure same named records are not removed - * Refer to https://doc.powerdns.com/authoritative/http-api/zone.html#rrset under "changetype" + * Return a complete domain name with the subdomain and zone. + * Optionally returns the canonical domain with trailing period * - * @param \Opcenter\Dns\Record $record + * @param $zone + * @param $subdomain * - * @return array + * @param bool $makeCanonical + * + * @return string */ - private function addRecords(Record $record): array + private function makeFqdn($zone, $subdomain, $makeCanonical = false): string { - $return = []; - - $name = $name = $this->replaceMarker($record['zone'], $record['name']); - - foreach ($this->records as $rrset) + if (strpos($subdomain, $zone) === false) { - if ($rrset['name'] === $name && $rrset['type'] === $record['rr']) - { - $rrset['records'][] = ['content' => $this->parseRecord($record), 'disabled' => false]; - $rrset['changetype'] = 'REPLACE'; - $return[] = $rrset; - } + $subdomain = implode('.', [$subdomain, $zone]); } - // No records match the name and type, let's create a new record set - if (empty($return)) + if ($makeCanonical) { - $return[] = $this->formatRecord($record); + return $this->makeCanonical($subdomain); } - return $return; + return $subdomain; } /** @@ -724,34 +566,194 @@ private function removeRecords(Record $record): array } /** - * CNAME cannot be present in root + * Creates Record with Zone Creation (not currently implemented) + * + * @param $name + * @param null $ip + * + * @return array + */ + protected function createDefaultRecords($name, $ip = null): array + { + $rrsets = []; + $cnames = $this->defaultCnames; + + if (! is_null($ip)) + { + $records[] = [ + 'content' => $ip, + 'disabled' => false, + ]; + + $rrsets[] = [ + 'records' => $records, + 'name' => $this->makeCanonical($name), + 'ttl' => 14400, + 'type' => 'A', + ]; + } + + foreach ($cnames as $cname) + { + $records = [0 => [ + 'content' => $this->makeCanonical($name), + 'disabled' => false, + ]]; + + $rrsets[] = [ + 'records' => $records, + 'name' => $this->makeFqdn($name, $cname, true), + 'ttl' => 14400, + 'type' => 'CNAME', + ]; + } + + return $rrsets; + } + + /** + * Get raw zone data + * + * @param string $domain + * + * @return null|string + */ + + protected function zoneAxfr($domain): ?string + { + try + { + $api = $this->makeApi(); + $axfrrec = $api->do('GET', 'servers/localhost/zones' . sprintf('/%s', $this->makeCanonical($domain)) . '/export'); + } + catch (ClientException $e) + { + warn("Failed to transfer DNS records from PowerDNS - try again later. Response code: %d", $e->getResponse()->getStatusCode()); + + return null; + } + + return $axfrrec['zone']; + } + + /** + * Modify a DNS record + * + * @param string $zone + * @param Record $old + * @param Record $new * * @return bool */ - protected function hasCnameApexRestriction(): bool + protected function atomicUpdate(string $zone, Record $old, Record $new): bool { + if (! $this->canonicalizeRecord($zone, $old['name'], $old['rr'], $old['parameter'], $old['ttl'])) + { + return false; + } + + if (! $this->canonicalizeRecord($zone, $new['name'], $new['rr'], $new['parameter'], $new['ttl'])) + { + return false; + } + + try + { + $api = $this->makeApi(); + // Get zone and rrsets, need to parse the existing rrsets to ensure proper addition of new records + $zoneData = $api->do('GET', 'servers/localhost/zones' . sprintf('/%s', $this->makeCanonical($zone))); + $this->records = $zoneData['rrsets']; + + $rrsets = $this->changeRecords($old, $new); + + $api->do('PATCH', 'zones' . sprintf('/%s', $this->makeCanonical($zone)), ['rrsets' => $rrsets]); + } + catch (ClientException $e) + { + return error("Failed to update record '%s' on zone '%s' (old - rr: '%s', param: '%s'; new - rr: '%s', param: '%s'): %s", + $old['name'], + $zone, + $old['rr'], + $old['parameter'], $new['name'] ?? $old['name'], $new['parameter'] ?? $old['parameter'], + $this->renderMessage($e) + ); + } + return true; } /** - * Replaces the @ with the fqdn - * @param $zone - * @param $name + * Parse existing records for zone, find and remove the old record from the list and add the new one. + * Refer to https://doc.powerdns.com/authoritative/http-api/zone.html#rrset under "changetype" * - * @return string + * @param \Opcenter\Dns\Record $old + * @param \Opcenter\Dns\Record $new + * + * @return array */ - protected function replaceMarker($zone, $name) :string + private function changeRecords(Record $old, Record $new): array { - if ($name === '@') + $return = []; + + $oldName = $this->replaceMarker($old['zone'], $old['name']); + $newName = $this->replaceMarker($new['zone'], $new['name']); + + $added = false; // Did we add the record to an existing rrset? + + foreach ($this->records as $rrset) { - $name = $this->makeCanonical($zone); + if ($rrset['name'] === $oldName && $rrset['type'] === $old['rr']) + { + if (count($rrset['records']) > 1) // More than one record, loop through and delete just the record + { + foreach ($rrset['records'] as $k => $rrec) + { + if ($rrec['content'] === $old['parameter']) + { + unset($rrset['records'][ $k ]); + $rrset['records'] = array_values($rrset['records']); + } + } + } + else + { + // Only 1 record, just delete the rrset. + $return[] = [ + 'records' => '', + 'name' => $oldName, + 'changetype' => 'DELETE', + 'type' => $old['rr'], + ]; + } + } + + if ($rrset['name'] === $newName && $rrset['type'] === $new['rr']) + { + $rrset['records'][] = ['content' => $this->parseRecord($new), 'disabled' => false]; + $rrset['changetype'] = 'REPLACE'; + $return[] = $rrset; + + $added = true; + } } - else + + // No records match the name and type, let's create a new record set + if (true !== $added) { - $name = $this->makeFqdn($zone, $name, true); + $return[] = $this->formatRecord($new); } - return $name; + return $return; + } + + /** + * CNAME cannot be present in root + * + * @return bool + */ + protected function hasCnameApexRestriction(): bool + { + return true; } /**