From 26cba21e5dc9eee10fd4fc3a8577de716d567e9f Mon Sep 17 00:00:00 2001 From: Troy Siedsma Date: Wed, 17 Jul 2019 13:45:43 -0500 Subject: [PATCH] Fixed issues, updated license and ready for release --- ISSUES.md | 113 ---------- LICENSE | 2 +- README.md | 4 +- src/Module.php | 572 ++++++++++++++++++++++++++----------------------- 4 files changed, 310 insertions(+), 381 deletions(-) delete mode 100644 ISSUES.md diff --git a/ISSUES.md b/ISSUES.md deleted file mode 100644 index 43359ec..0000000 --- a/ISSUES.md +++ /dev/null @@ -1,113 +0,0 @@ -## Issues List: ## -1) Deleting a Site deletes the zone and then tries to delete records but they were already deleted with the zone - -```ERROR : Failed to transfer DNS records from PowerDNS - try again later. Response code: 404 -DEBUG : 0.68447: Dns_Module -> _delete -DEBUG : 0.04320: Mysql_Module -> _delete -DEBUG : 0.00006: Ipinfo_Module -> _delete -DEBUG : Service config `billing' disabled for site - disabling calling hook `_delete' on `Billing_Module' -DEBUG : Service config `pgsql' disabled for site - disabling calling hook `_delete' on `Pgsql_Module' -DEBUG : 0.00054: Site_Module -> _delete -ERROR : Failed to transfer DNS records from PowerDNS - try again later. Response code: 404 -``` - -Full Backtrace: -``` -DEBUG : Running hooks for `testdomain.com' (user: `testuser') -DEBUG : 0.00007: Sql_Module -> _delete -DEBUG : 0.00285: Php_Module -> _delete -DEBUG : 0.00042: Letsencrypt_Module -> _delete -DEBUG : 0.00004: Dav_Module -> _delete -DEBUG : 0.01276: Majordomo_Module -> _delete -DEBUG : 0.00329: Crontab_Module -> _delete -DEBUG : 0.00012: Spamfilter_Module -> _delete -DEBUG : 0.00008: Email_Module -> _delete -DEBUG : 0.00009: Ssh_Module -> _delete -DEBUG : 0.00004: Ftp_Module -> _delete -ERROR : Auth_Module::_getAPIQueryFragment: cannot get billing invoice for API key - 0. Error_Reporter::add_error("Auth_Module::_getAPIQueryFragment: cannot get billing invoice for API key", ) - [/usr/local/apnscp/lib/log_wrapper.php:63] - 1. error("cannot get billing invoice for API key") - [/usr/local/apnscp/lib/modules/auth.php:322] - 2. Auth_Module->_getAPIQueryFragment() - [/usr/local/apnscp/lib/modules/auth.php:950] - 3. Auth_Module->_get_api_keys_real("testuser") - [/usr/local/apnscp/lib/modules/auth.php:1082] - 4. Auth_Module->get_api_keys() - [/usr/local/apnscp/lib/modules/auth.php:1057] - 5. Auth_Module->_delete() - [/usr/local/apnscp/lib/Util/Account/Hooks.php:142] - 6. Util_Account_Hooks::_process("delete", ) - [/usr/local/apnscp/lib/Util/Account/Hooks.php:49] - 7. Util_Account_Hooks::run("delete") - [/usr/local/apnscp/bin/DeleteDomain:35] - -DEBUG : 0.00313: Auth_Module -> _delete -DEBUG : 0.00060: Aliases_Module -> _delete -DEBUG : 0.00004: Diskquota_Module -> _delete -DEBUG : Service config `tomcat' disabled for site - disabling calling hook `_delete' on `Tomcat_Module' -DEBUG : 0.00736: User_Module -> _delete -DEBUG : 0.00011: Bandwidth_Module -> _delete -DEBUG : 0.00006: Ssl_Module -> _delete -DEBUG : 0.00166: Cgroup_Module -> _delete -DEBUG : 0.03925: Web_Module -> _delete -DEBUG : 0.63373: Dns_Module -> _delete -DEBUG : 0.02480: Mysql_Module -> _delete -DEBUG : 0.00006: Ipinfo_Module -> _delete -DEBUG : Service config `billing' disabled for site - disabling calling hook `_delete' on `Billing_Module' -DEBUG : Service config `pgsql' disabled for site - disabling calling hook `_delete' on `Pgsql_Module' -DEBUG : 0.00035: Site_Module -> _delete -ERROR : Opcenter\Dns\Providers\Powerdns\Module::zoneAxfr: Failed to transfer DNS records from PowerDNS - try again later. Response code: 404 - 0. Error_Reporter::add_error("Opcenter\Dns\Providers\Powerdns\Module::zoneAxfr: Failed to transfer DNS records from PowerDNS - try again later. Response code: %d", [404]) - [/usr/local/apnscp/lib/log_wrapper.php:63] - 1. error("Failed to transfer DNS records from PowerDNS - try again later. Response code: %d", 404) - [/usr/local/apnscp/resources/playbooks/addins/apnscp-powerdns/src/Module.php:477] - 2. Opcenter\Dns\Providers\Powerdns\Module->zoneAxfr("testdomain.com") - [/usr/local/apnscp/lib/modules/dns.php:295] - 3. Dns_Module->zone_exists("testdomain.com") - [/usr/local/apnscp/lib/Module/Skeleton/Standard.php:144] - 4. Module\Skeleton\Standard->_invoke("zone_exists", ["testdomain.com"]) - [/usr/local/apnscp/lib/Module/Skeleton/Webhooks.php:35] - 5. Module\Skeleton\Webhooks->_invoke("zone_exists", ["testdomain.com"]) - [/usr/local/apnscp/lib/apnscpfunction.php:734] - 6. apnscpFunctionInterceptor->call("dns_zone_exists", ["testdomain.com"]) - [/usr/local/apnscp/lib/apnscpFunctionInterceptorTrait.php:34] - 7. Module\Skeleton\Standard->__call("dns_zone_exists", ["testdomain.com"]) - [/usr/local/apnscp/lib/modules/email.php:959] - 8. Email_Module->remove_virtual_transport("testdomain.com") - [/usr/local/apnscp/lib/Module/Skeleton/Standard.php:144] - 9. Module\Skeleton\Standard->_invoke("remove_virtual_transport", ["testdomain.com"]) - [/usr/local/apnscp/lib/Module/Skeleton/Webhooks.php:35] -10. Module\Skeleton\Webhooks->_invoke("remove_virtual_transport", ["testdomain.com"]) - [/usr/local/apnscp/lib/apnscpfunction.php:734] -11. apnscpFunctionInterceptor->call("email_remove_virtual_transport", ["testdomain.com"]) - [/usr/local/apnscp/lib/apnscpfunction.php:685] -12. apnscpFunctionInterceptor->__call("email_remove_virtual_transport", ["testdomain.com"]) - [/usr/local/apnscp/lib/Opcenter/Service/Validators/Mail/Enabled.php:57] -13. Opcenter\Service\Validators\Mail\Enabled->depopulate(Opcenter\SiteConfiguration) - [/usr/local/apnscp/lib/Opcenter/Account/Delete.php:110] -14. Opcenter\Account\Delete->exec() - [/usr/local/apnscp/bin/DeleteDomain:43] - -INFO : Removing port `1' assigned to `site1' - 0. Error_Reporter::add_info("Removing port `%d' assigned to `%s'", [1, "site1"]) - [/usr/local/apnscp/lib/log_wrapper.php:87] - 1. info("Removing port `%d' assigned to `%s'", 1, "site1") - [/usr/local/apnscp/lib/Opcenter/Service/Validators/Ssh/PortIndex.php:97] - 2. Opcenter\Service\Validators\Ssh\PortIndex->depopulate(Opcenter\SiteConfiguration) - [/usr/local/apnscp/lib/Opcenter/Account/Delete.php:110] - 3. Opcenter\Account\Delete->exec() - [/usr/local/apnscp/bin/DeleteDomain:43] - -WARNING : Opcenter\Service\Validators\Pgsql\Enabled::depopulate(): unable to lookup postgresql user for `site1' - 0. Error_Reporter::add_warning("Opcenter\Service\Validators\Pgsql\Enabled::depopulate(): unable to lookup postgresql user for `%s'", ["site1"]) - [/usr/local/apnscp/lib/log_wrapper.php:75] - 1. warn("unable to lookup postgresql user for `%s'", "site1") - [/usr/local/apnscp/lib/Opcenter/Service/Validators/Pgsql/Enabled.php:90] - 2. Opcenter\Service\Validators\Pgsql\Enabled->depopulate(Opcenter\SiteConfiguration) - [/usr/local/apnscp/lib/Opcenter/Account/Delete.php:110] - 3. Opcenter\Account\Delete->exec() - [/usr/local/apnscp/bin/DeleteDomain:43] -``` - -Seems to be trying to delete records after deleting the zone which deletes the records... The 404 is a result of the zone not existing in PowerDNS anymore. diff --git a/LICENSE b/LICENSE index f288702..e72bfdd 100644 --- a/LICENSE +++ b/LICENSE @@ -671,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read -. +. \ No newline at end of file diff --git a/README.md b/README.md index e55c3cb..e2fe1e0 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ pdns: ``` * `uri` value is the hostname of your master PowerDNS server running the HTTP API webserver (with a trailing slash) * `key` value is the **API Key** in `pdns.conf` on the master nameserver. -* `nameservers` value is a comma delimited list of nameservers. There is not currently a limit for the number of nameservers you may use, 2-5 is typical and should be geographically distributed per RFC 2182. +* `ns` value is a list of nameservers as in the example above. Put nameservers on their own lines prefixed with a hyphen and indented accordingly. There is not currently a limit for the number of nameservers you may use, 2-5 is typical and should be geographically distributed per RFC 2182. ### Setting as default @@ -158,4 +158,4 @@ See also: [Creating a provider](https://hq.apnscp.com/apnscp-pre-alpha-technical ## Contributing -Submit a PR and have fun! +Submit a PR and have fun! \ No newline at end of file diff --git a/src/Module.php b/src/Module.php index 741078a..64df744 100644 --- a/src/Module.php +++ b/src/Module.php @@ -14,13 +14,6 @@ use Module\Provider\Contracts\ProviderInterface; use Opcenter\Dns\Record; -/** @TODO - * Fix Deleting Records / The PowerDNS API logic is weird. I feel like deleting a record could delete other records with the same name... - * Fix Adding Records not showing on page until reload - * Test Changing Records - * Test addon domains - * Fix MX creation - Default account creation MX records fail - */ class Module extends \Dns_Module implements ProviderInterface { use \NamespaceUtilitiesTrait; @@ -52,51 +45,38 @@ public function __construct() } /** - * Add a DNS record + * Add DNS zone to service * - * @param string $zone - * @param string $subdomain - * @param string $rr - * @param string $param - * @param int $ttl + * @param string $domain + * @param string $ip * * @return bool */ - public function add_record(string $zone, string $subdomain, string $rr, string $param, int $ttl = self::DNS_TTL): bool + public function add_zone_backend(string $domain, string $ip): bool { - if (! $this->canonicalizeRecord($zone, $subdomain, $rr, $param, $ttl)) - { - return false; - } - - $record = new Record($zone, [ - 'name' => $subdomain, - 'rr' => $rr, - 'parameter' => $param, - 'ttl' => $ttl, - ]); - - if ($record['name'] === '@') - { - $record['name'] = ''; - } - + $domain = rtrim($domain, '\.'); + /** + * @var Zones $api + */ + $api = $this->makeApi(); try { - $api = $this->makeApi(); - $rrsets = $this->formatRecord($record); - $ret = $api->do('PATCH', 'zones/' . $this->canonical($zone), ['rrsets' => $rrsets]); // returns empty or zero??? - $this->addCache($record); + $resp = $api->do('POST', 'servers/localhost/zones', [ + 'account' => null, + 'kind' => 'native', + 'soa_edit_api' => 'INCEPTION-INCREMENT', + 'masters' => [], + 'name' => $this->makeCanonical($domain), + 'nameservers' => [], + 'rrsets' => array_merge($this->createSOA($domain, $this->ns[0], 'hostmaster@' . $domain), $this->createNS($domain, $this->ns)), + ]); } catch (ClientException $e) { - info(json_encode(['rec' => $record->toArray(), 'ret' => $ret, 'rrsets' => $rrsets], JSON_PRETTY_PRINT)); - info($e->getMessage()); - - return error("Failed to create record '%s': %s", (string) $record, $this->renderMessage($e)); + return error("Failed to add zone '%s', error: %s", $domain, $this->renderMessage($e)); } - return true; // this sucks... + return true; } /** @@ -109,79 +89,6 @@ private function makeApi(): Api return new Api(); } - /** - * Format a PowerDNS record prior to sending - * - * @param Record $r - * - * @return array - */ - protected function formatRecord(Record $r): ?array - { - $type = strtoupper($r['rr']); - $ttl = $r['ttl'] ?? static::DNS_TTL; - - $content = ''; - $priority = null; - $name = null; - - switch ($type) - { - case 'A': - case 'AAAA': - case 'CNAME': - case 'TXT': - case 'NS': - case 'PTR': - $content = $r['parameter']; - break; - case 'MX': - $priority = (int) $r->getMeta('priority'); - $content = sprintf( - '%d %s', - $r->getMeta('priority'), - $this->canonical($r->getMeta('data')) - ); - break; - case 'SRV': - $content = sprintf( - // protocol | service | target | priority | weight | port - '%s %s %s %d %d %d', - $r->getMeta('protocol'), - $r->getMeta('service'), - $r->getMeta('data'), - (int) $r->getMeta('priority'), - (int) $r->getMeta('weight'), - (int) $r->getMeta('port') - ); - break; - case 'CAA': - $content = sprintf( - // tag | target - '%s %s', - $r->getMeta('tag'), - $r->getMeta('data') - ); - break; - default: - fatal("Unsupported DNS RR type '%s'", $type); - } - - $rrsets[] = [ - 'records' => [0 => [ - 'content' => $content, - 'disabled' => false, - ]], - 'name' => $name ?? $this->canonical(implode('.', [$r['name'], $r['zone']])), - 'ttl' => $ttl, - 'type' => $type, - 'prio' => $priority ?? 0, - 'changetype' => 'REPLACE', - ]; - - return $rrsets; - } - /** * returns canonical domain (e.g. always returns root dot) * @@ -189,13 +96,13 @@ protected function formatRecord(Record $r): ?array * * @return string */ - private function canonical($name) + private function makeCanonical($name) { if (empty($name)) // Sometimes the name is empty and ltrim throws a fit about it { return $name; } - $name = ltrim($name, '.'); + $name = trim($name, '.'); if (substr($name, -1) !== '.') { return $name . '.'; @@ -205,127 +112,14 @@ private function canonical($name) } /** - * Extract JSON message if present - * - * @param ClientException $e - * - * @return string - */ - private function renderMessage(ClientException $e): string - { - - $body = \Error_Reporter::silence(function () use ($e) { - return \json_decode($e->getResponse()->getBody()->getContents(), true); - }); - if (! $body || ! ($reason = array_get($body, 'errors.0.reason'))) - { - return $e->getMessage(); - } - - return $reason; - } - - /** - * Remove a DNS record - * - * @param string $zone - * @param string $subdomain - * @param string $rr - * @param string|null $param - * - * @return bool - */ - public function remove_record(string $zone, string $subdomain, string $rr, string $param = null): bool - { - $zone = trim($zone, '.'); - if (! $canonicalZone = $this->canonicalizeRecord($zone, $subdomain, $rr, $param)) - { - return false; - } - - $record = new Record($zone, [ - 'name' => $subdomain, - 'rr' => $rr, - 'parameter' => $param, - ]); - - // Not sure how this is used -// $id = $this->getRecordId($record); - - if ($record['name'] === '@') - { - $record['name'] = $this->canonical($zone); - } else { - $record['name'] = $this->canonical(implode('.', [$subdomain, $zone])); - } - - $api = $this->makeApi(); - -// if (! $id) -// { -// $fqdn = implode('.', [$subdomain, $zone]); -// -// return error("Record '%s' (rr: '%s', param: '%s') does not exist", $fqdn, $rr, $param); -// } - - $rrsets[] = [ - 'records' => '', - 'name' => $record['name'], - 'changetype' => 'DELETE', - 'type' => $record['rr'], - ]; - - try - { - $ret = $api->do('PATCH', "zones/${zone}", ['rrsets' => $rrsets]); - } - catch (ClientException $e) - { - $fqdn = implode('.', [$subdomain, $zone]); - - return error("Failed to delete record '%s' type %s", $fqdn, $rr); - } - array_forget($this->zoneCache[ $record->getZone() ], $this->getCacheKey($record)); - - return $api->getResponse()->getStatusCode() === 200; - } - - /** - * Add DNS zone to service + * Create SOA record for specified domain/zone * - * @param string $domain - * @param string $ip + * @param $name + * @param $primary + * @param $soa_contact * - * @return bool + * @return array */ - public function add_zone_backend(string $domain, string $ip): bool - { - $domain = trim($domain, '.'); - /** - * @var Zones $api - */ - $api = $this->makeApi(); - try - { - $resp = $api->do('POST', 'servers/localhost/zones', [ - 'account' => null, - 'kind' => 'native', - 'soa_edit_api' => 'INCEPTION-INCREMENT', - 'masters' => [], - 'name' => $this->canonical($domain), - 'nameservers' => [], - // 'rrsets' => array_merge($this->createSOA($domain, $this->ns[0], 'hostmaster@' . $domain), $this->createNS($domain, $this->ns), $this->createDefaultRecords($domain, $ip)), - 'rrsets' => array_merge($this->createSOA($domain, $this->ns[0], 'hostmaster@' . $domain), $this->createNS($domain, $this->ns)), - ]); - } - catch (ClientException $e) - { - return error("Failed to add zone '%s', error: %s", $domain, $this->renderMessage($e)); - } - - return true; - } - protected function createSOA($name, $primary, $soa_contact) { $rrsets = [ @@ -334,14 +128,14 @@ protected function createSOA($name, $primary, $soa_contact) 'content' => sprintf( // primary | contact | serial | refresh | retry | expire | ttl '%s %s %s 3600 1800 604800 600', - $this->canonical($primary), - $this->canonical($soa_contact), + $this->makeCanonical($primary), + $this->makeCanonical($soa_contact), date('Ymd') . sprintf('%02d', rand(0, 99)) ), 'disabled' => false, ], ], - 'name' => $this->canonical($name), + 'name' => $this->makeCanonical($name), 'ttl' => 86400, 'type' => 'SOA', ]; @@ -349,6 +143,14 @@ protected function createSOA($name, $primary, $soa_contact) return [$rrsets]; } + /** + * Create NS records for the specified domain/zone + * + * @param $name + * @param array $nameservers + * + * @return array + */ protected function createNS($name, array $nameservers): array { $rrsets = $records = []; @@ -356,14 +158,14 @@ protected function createNS($name, array $nameservers): array foreach ($nameservers as $nameserver) { $records[] = [ - 'content' => $this->canonical($nameserver), + 'content' => $this->makeCanonical($nameserver), 'disabled' => false, ]; } $rrsets[] = [ 'records' => $records, - 'name' => $this->canonical($name), + 'name' => $this->makeCanonical($name), 'ttl' => 86400, 'type' => 'NS', ]; @@ -371,6 +173,27 @@ protected function createNS($name, array $nameservers): array return $rrsets; } + /** + * Extract JSON message if present + * + * @param ClientException $e + * + * @return string + */ + private function renderMessage(ClientException $e): string + { + + $body = \Error_Reporter::silence(function () use ($e) { + return \json_decode($e->getResponse()->getBody()->getContents(), true); + }); + if (! $body || ! ($reason = array_get($body, 'errors.0.reason'))) + { + return $e->getMessage(); + } + + return $reason; + } + /** * Remove DNS zone from nameserver * @@ -393,6 +216,14 @@ public function remove_zone_backend(string $domain): bool return true; } + /** + * Creates Record with Zone Creation (not currently implemented) + * + * @param $name + * @param null $ip + * + * @return array + */ protected function createDefaultRecords($name, $ip = null): array { $rrsets = []; @@ -407,7 +238,7 @@ protected function createDefaultRecords($name, $ip = null): array $rrsets[] = [ 'records' => $records, - 'name' => $this->canonical($name), + 'name' => $this->makeCanonical($name), 'ttl' => 14400, 'type' => 'A', ]; @@ -416,13 +247,13 @@ protected function createDefaultRecords($name, $ip = null): array foreach ($cnames as $cname) { $records = [0 => [ - 'content' => $this->canonical($name), + 'content' => $this->makeCanonical($name), 'disabled' => false, ]]; $rrsets[] = [ 'records' => $records, - 'name' => $this->canonical(implode('.', [$cname, $name])), + 'name' => $this->makeFqdn($name, $cname, true), 'ttl' => 14400, 'type' => 'CNAME', ]; @@ -431,6 +262,32 @@ protected function createDefaultRecords($name, $ip = null): array 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 * @@ -440,6 +297,7 @@ protected function createDefaultRecords($name, $ip = null): array */ protected function zoneAxfr($domain): ?string { + $domain = rtrim($domain, '\.'); // @todo hold records in cache and synthesize AXFR $client = $this->makeApi(); @@ -471,14 +329,17 @@ protected function zoneAxfr($domain): ?string { if ($e->getResponse()->getStatusCode() === 422) { - // zone doesn't exist - return null; + return null; // This really shouldn't happen } + if ($e->getResponse()->getStatusCode() === 404) + { + return null; // No zone here! + } + error("Failed to transfer DNS records from PowerDNS - try again later. Response code: %d", $e->getResponse()->getStatusCode()); return null; } - $this->zoneCache[ $domain ] = []; foreach ($records['rrsets'] as $r) { foreach ($r['records'] as $record) @@ -498,17 +359,7 @@ protected function zoneAxfr($domain): ?string default: $parameter = $record['content']; } - $hostname = $this->canonical($r['name']); - $preamble[] = $hostname . "\t" . $r['ttl'] . "\tIN\t" . $r['type'] . "\t" . $parameter; - - $this->addCache(new Record($domain, - [ - 'name' => $hostname, - 'rr' => $r['type'], - 'ttl' => $r['ttl'] ?? static::DNS_TTL, - 'parameter' => $parameter, - ] - )); + $preamble[] = $r['name'] . "\t" . $r['ttl'] . "\tIN\t" . $r['type'] . "\t" . $parameter; } } $axfrrec = implode("\n", $preamble); @@ -543,24 +394,21 @@ protected function atomicUpdate(string $zone, Record $old, Record $new): bool { return false; } - if (! $this->getRecordId($old)) - { - return error("failed to find record ID in PowerDNS zone '%s' - does '%s' (rr: '%s', parameter: '%s') exist?", - $zone, $old['name'], $old['rr'], $old['parameter']); - } + + $old['ttl'] = null; + if (! $this->canonicalizeRecord($zone, $new['name'], $new['rr'], $new['parameter'], $new['ttl'])) { return false; } - $api = $this->makeApi(); try { $merged = clone $old; $new = $merged->merge($new); - $id = $this->getRecordId($old); - $ret = $api->do('PATCH', "zones/" . sprintf('%s', $this->canonical($zone)), ['rrsets' => $this->formatRecord($old)]); + $this->add_record($zone, $new['name'], $new['rr'], $new['parameter'], $new['ttl']); + $this->remove_record($zone, $old['name'], $new['rr'], $new['parameter']); } catch (ClientException $e) { @@ -572,12 +420,185 @@ protected function atomicUpdate(string $zone, Record $old, Record $new): bool $this->renderMessage($e) ); } - array_forget($this->zoneCache[ $old->getZone() ], $this->getCacheKey($old)); - $this->addCache($new); return true; } + /** + * Add a DNS record + * + * @param string $zone + * @param string $subdomain + * @param string $rr + * @param string $param + * @param int $ttl + * + * @return bool + */ + public function add_record(string $zone, string $subdomain, string $rr, string $param, int $ttl = self::DNS_TTL): bool + { + if (! $this->canonicalizeRecord($zone, $subdomain, $rr, $param, $ttl)) + { + return false; + } + + $record = new Record($zone, [ + 'name' => $subdomain, + 'rr' => $rr, + 'parameter' => $param, + 'ttl' => $ttl, + ]); + + try + { + $api = $this->makeApi(); + $rrsets = $this->formatRecord($record); + $ret = $api->do('PATCH', 'zones/' . $this->makeCanonical($zone), ['rrsets' => $rrsets]); // returns empty or zero??? + } + catch (ClientException $e) + { +// info(json_encode(['rec' => $record->toArray(), 'rrsets' => $rrsets], JSON_PRETTY_PRINT)); +// info($e->getMessage()); + + return error("Failed to create record '%s': %s", (string) $record, $this->renderMessage($e)); + } + + return true; // this sucks... + } + + /** + * Format a PowerDNS record prior to sending + * + * @param Record $r + * + * @return array + */ + protected function formatRecord(Record $r): ?array + { + $type = strtoupper($r['rr']); + $ttl = $r['ttl'] ?? static::DNS_TTL; + + $content = ''; + $priority = null; + $name = null; + + switch ($type) + { + case 'A': + case 'AAAA': + case 'CNAME': + case 'TXT': + case 'NS': + case 'PTR': + $content = $r['parameter']; + break; + case 'MX': + $priority = (int) $r->getMeta('priority'); + $content = sprintf( + '%d %s', + $r->getMeta('priority'), + $this->makeCanonical($r->getMeta('data')) + ); + break; + case 'SRV': + $content = sprintf( + // protocol | service | target | priority | weight | port + '%s %s %s %d %d %d', + $r->getMeta('protocol'), + $r->getMeta('service'), + $r->getMeta('data'), + (int) $r->getMeta('priority'), + (int) $r->getMeta('weight'), + (int) $r->getMeta('port') + ); + break; + case 'CAA': + $content = sprintf( + // tag | target + '%s %s', + $r->getMeta('tag'), + $r->getMeta('data') + ); + break; + default: + fatal("Unsupported DNS RR type '%s'", $type); + } + + if ($r['name'] === '@') + { + $r['name'] = ''; + } + + $rrsets[] = [ + 'records' => [0 => [ + 'content' => $content, + 'disabled' => false, + ]], + 'name' => $name ?? $this->makeFqdn($r['zone'], $r['name'], true), + 'ttl' => $ttl, + 'type' => $type, + 'prio' => $priority ?? 0, + 'changetype' => 'REPLACE', + ]; + + return $rrsets; + } + + /** + * Remove a DNS record + * + * @param string $zone + * @param string $subdomain + * @param string $rr + * @param string|null $param + * + * @return bool + */ + public function remove_record(string $zone, string $subdomain, string $rr, string $param = null): bool + { + $zone = rtrim($zone, '\.'); + if (! $canonicalZone = $this->canonicalizeRecord($zone, $subdomain, $rr, $param)) + { + return false; + } + + $record = new Record($zone, [ + 'name' => $subdomain, + 'rr' => $rr, + 'parameter' => $param, + ]); + + if ($record['name'] === '@') + { + $name = $this->makeCanonical($zone); + } + else + { + $name = $this->makeFqdn($zone, $subdomain, true); + } + + $rrsets[] = [ + 'records' => '', + 'name' => $name, + 'changetype' => 'DELETE', + 'type' => $record['rr'], + ]; + + try + { + $api = $this->makeApi(); + $ret = $api->do('PATCH', "zones/${zone}", ['rrsets' => $rrsets]); + } + catch (ClientException $e) + { + $fqdn = $this->makeFqdn($zone, $subdomain); + + return error("Failed to delete record '%s' type %s", $fqdn, $rr); + } + + return $api->getResponse()->getStatusCode() === 200; + } + /** * CNAME cannot be present in root * @@ -587,4 +608,25 @@ protected function hasCnameApexRestriction(): bool { return true; } + + /** + * Strip the zone and trailing . from a subdomain/name entry + * + * @param $zone + * @param $subdomain + * + * @return string + */ + private function stripName($zone, $subdomain): string + { + $zone = rtrim($zone, '\.'); + $subdomain = rtrim($subdomain, '\.'); + + if (strpos($subdomain, $zone) !== false) + { + $subdomain = str_replace($zone, '', $subdomain); + } + + return rtrim($subdomain, '\.'); + } }