diff --git a/model/zone.php b/model/zone.php index da23521..dd127c1 100644 --- a/model/zone.php +++ b/model/zone.php @@ -134,7 +134,6 @@ public function add_or_update_resource_record_set(ResourceRecordSet $rrset) { $change_record->type = $rrset->type; $change_record->content = $record->content; $change_record->disabled = $record->disabled; - $change_record->{'set-ptr'} = $record->{'set-ptr'}; $change->records[] = $change_record; } $change->comments = array(); @@ -807,10 +806,8 @@ private function process_rrset_action($update, &$trash, &$revs_missing, &$revs_u $rr = new ResourceRecord; $rr->content = $record->content; $rr->disabled = ($record->enabled === 'No' || $record->enabled === false); - if(!$autocreate_ptr || $rr->disabled) { - $rr->{'set-ptr'} = false; - } else { - $rr->{'set-ptr'} = $zone_dir->check_reverse_record_zone($rrset->name, $rrset->type, $rr->content, $revs_missing, $revs_updated); + if($autocreate_ptr && !$rr->disabled) { + $zone_dir->create_reverse_record($rrset->name, $rrset->type, $rr->content, $rrset->ttl, $revs_missing, $revs_updated); } $rrset->add_resource_record($rr); } @@ -840,16 +837,19 @@ private function process_rrset_action($update, &$trash, &$revs_missing, &$revs_u $rrset->ttl = DNSTime::expand($update->ttl); $record_count = 0; foreach($update->records as $record) { - if(!empty($record->delete)) continue; + if(!empty($record->delete)) { + if(!empty($record->remove_ptr) && ($update->oldtype == 'A' || $update->oldtype == 'AAAA')) { + $zone_dir->delete_reverse_record($update->oldname, $update->oldtype, $record->content, $revs_updated); + } + continue; + } $record_count++; $record->content = DNSContent::encode($record->content, $update->type, $this->name); $rr = new ResourceRecord; $rr->content = $record->content; $rr->disabled = ($record->enabled === 'No' || $record->enabled === false); - if(!$autocreate_ptr || $rr->disabled) { - $rr->{'set-ptr'} = false; - } else { - $rr->{'set-ptr'} = $zone_dir->check_reverse_record_zone($rrset->name, $rrset->type, $rr->content, $revs_missing, $revs_updated); + if($autocreate_ptr && !$rr->disabled) { + $zone_dir->create_reverse_record($rrset->name, $rrset->type, $rr->content, $rrset->ttl, $revs_missing, $revs_updated); } $rrset->add_resource_record($rr); } @@ -871,8 +871,14 @@ private function process_rrset_action($update, &$trash, &$revs_missing, &$revs_u if(!isset($this->rrsets[$update->oldname.' '.$update->oldtype])) { throw new BadData('Tried to delete a non-existent resource recordset: '.$update->oldname.' '.$update->oldtype.'.'); } - $change->before = serialize($this->rrsets[$update->oldname.' '.$update->oldtype]); - $this->delete_resource_record_set($this->rrsets[$update->oldname.' '.$update->oldtype]); + $rrset = $this->rrsets[$update->oldname.' '.$update->oldtype]; + $change->before = serialize($rrset); + if(!empty($update->remove_ptr) && ($update->oldtype == 'A' || $update->oldtype == 'AAAA')) { + foreach($rrset->list_resource_records() as $rr) { + $zone_dir->delete_reverse_record($update->oldname, $update->oldtype, $rr->content, $revs_updated); + } + } + $this->delete_resource_record_set($rrset); break; } return $change; diff --git a/model/zonedirectory.php b/model/zonedirectory.php index b4ea1d1..656cb05 100644 --- a/model/zonedirectory.php +++ b/model/zonedirectory.php @@ -224,14 +224,18 @@ public function list_accounts() { } /** - * Check the list of zones to see if a suitable reverse zone exists for the forward record. + * Check the list of zones to see if a suitable reverse zone exists for the forward record, and if + * so create/update the matching PTR record via a direct call to the PowerDNS API. + * PowerDNS's own "set-ptr" record attribute that used to do this automatically has been removed, + * so DNS UI must create the PTR record itself. * @param string $name of DNS record * @param string $type of DNS record * @param string $address that DNS record points to + * @param int $ttl to use for the PTR record * @param array $revs_missing keep track of reverse zones that are missing * @param array $revs_updated keep track of reverse zones that will be updated */ - public function check_reverse_record_zone($name, $type, $address, &$revs_missing, &$revs_notify) { + public function create_reverse_record($name, $type, $address, $ttl, &$revs_missing, &$revs_notify) { global $zone_dir, $active_user; if($type == 'A') { @@ -270,6 +274,25 @@ public function check_reverse_record_zone($name, $type, $address, &$revs_missing } } } + // Create the PTR record directly via the PowerDNS API + $ptr_rrset = new ResourceRecordSet; + $ptr_rrset->name = $reverse_address; + $ptr_rrset->type = 'PTR'; + $ptr_rrset->ttl = $ttl; + $ptr_record = new ResourceRecord; + $ptr_record->content = $name; + $ptr_record->disabled = false; + $ptr_rrset->add_resource_record($ptr_record); + try { + $reverse_zone->add_or_update_resource_record_set($ptr_rrset); + $reverse_zone->commit_changes(); + } catch(ResourceRecordInvalid $e) { + $alert = new UserAlert; + $alert->content = "Failed to create reverse record for $address pointing to $name: ".$e->getMessage(); + $alert->class = 'warning'; + $active_user->add_alert($alert); + return false; + } // Add reverse zone to list of zones to send a notify for $revs_notify[$reverse_zone->pdns_id] = $reverse_zone; return true; @@ -284,6 +307,109 @@ public function check_reverse_record_zone($name, $type, $address, &$revs_missing return false; } + /** + * Locate the reverse (PTR) RRset for a forward record's address, if one exists. + * Searches for an appropriate reverse zone by starting with the full domain name, and + * removing subdomains until a match is found or there is nothing left to remove. + * @param string $type of forward DNS record (A or AAAA) + * @param string $address that the forward DNS record points to + * @return array|null array(Zone $reverse_zone, ResourceRecordSet $rrset), or null if none found + */ + private function find_reverse_rrset($type, $address) { + global $zone_dir; + + if($type == 'A') { + $reverse_address = implode('.', array_reverse(explode('.', $address))).'.in-addr.arpa.'; + } elseif($type == 'AAAA') { + $address = ipv6_address_expand($address); + $reverse_address = implode('.', array_reverse(str_split(str_replace(':', '', $address)))).'.ip6.arpa.'; + } else { + return null; + } + $reverse_zone_name = $reverse_address; + do { + try { + $reverse_zone = $zone_dir->get_zone_by_name($reverse_zone_name); + foreach($reverse_zone->list_resource_record_sets() as $rrset) { + if($rrset->name == $reverse_address && $rrset->type == 'PTR') { + return array($reverse_zone, $rrset); + } + } + return null; + } catch(ZoneNotFound $e) { + } + } while($this->remove_subdomain($reverse_zone_name)); + return null; + } + + /** + * Check whether a reverse (PTR) record exists pointing back at the given forward record. + * @param string $name of DNS record + * @param string $type of DNS record + * @param string $address that DNS record points to + * @return bool true if a matching PTR record exists + */ + public function has_reverse_record($name, $type, $address) { + $found = $this->find_reverse_rrset($type, $address); + if(is_null($found)) return false; + list(, $rrset) = $found; + foreach($rrset->list_resource_records() as $rr) { + if($rr->content == $name) return true; + } + return false; + } + + /** + * Check the list of zones to see if a suitable reverse zone exists for the forward record, and if + * so remove the matching PTR record via a direct call to the PowerDNS API. + * @param string $name of DNS record whose PTR record should be removed + * @param string $type of DNS record + * @param string $address that DNS record pointed to + * @param array $revs_notify keep track of reverse zones that will be updated + * @return bool true if a PTR record was found and removed + */ + public function delete_reverse_record($name, $type, $address, &$revs_notify) { + global $active_user; + + $found = $this->find_reverse_rrset($type, $address); + if(is_null($found)) return false; + list($reverse_zone, $rrset) = $found; + $remaining_rrs = array(); + $matched = false; + foreach($rrset->list_resource_records() as $rr) { + if($rr->content == $name) { + $matched = true; + } else { + $remaining_rrs[] = $rr; + } + } + if(!$matched) return false; + try { + if(count($remaining_rrs) == 0) { + $reverse_zone->delete_resource_record_set($rrset); + } else { + $new_rrset = new ResourceRecordSet; + $new_rrset->name = $rrset->name; + $new_rrset->type = $rrset->type; + $new_rrset->ttl = $rrset->ttl; + foreach($remaining_rrs as $rr) { + $new_rrset->add_resource_record($rr); + } + $reverse_zone->add_or_update_resource_record_set($new_rrset); + } + $reverse_zone->commit_changes(); + } catch(ResourceRecordInvalid $e) { + $alert = new UserAlert; + $alert->content = "Failed to remove reverse record for $address pointing to $name: ".$e->getMessage(); + $alert->class = 'warning'; + $active_user->add_alert($alert); + return false; + } + // Add reverse zone to list of zones to send a notify for + $revs_notify[$reverse_zone->pdns_id] = $reverse_zone; + return true; + } + /** * Given a DNS name, remove the bottom-level subdomain from it. * @param string $address DNS name diff --git a/public_html/extra.js b/public_html/extra.js index 777846b..1038887 100644 --- a/public_html/extra.js +++ b/public_html/extra.js @@ -109,10 +109,34 @@ $(function() { button.append(' Delete'); tr.removeClass('delete'); tr.data('delete', false); + tr.data('remove-ptr', false); + update_changed(button); } else { button.text('Undelete'); tr.addClass('delete'); tr.data('delete', true); + if(!tr.data('newrow') && (tr.data('type') == 'A' || tr.data('type') == 'AAAA')) { + var content = $('td.content input', tr).val(); + // Only bother asking if a matching reverse (PTR) record actually exists + $.ajax({ + url: '../api/v2/zones/' + encodeURIComponent(form.data('zone')) + '/reverse-records', + method: 'GET', + data: {name: tr.data('name'), type: tr.data('type'), address: content}, + dataType: 'json' + }).done(function(response) { + if(response && response.exists) { + tr.data('remove-ptr', confirm('A reverse (PTR) record exists pointing to ' + content + '. Also remove it?')); + } else { + tr.data('remove-ptr', false); + } + }).fail(function() { + tr.data('remove-ptr', false); + }).always(function() { + update_changed(button); + }); + return; + } + tr.data('remove-ptr', false); } update_changed(button); } @@ -309,10 +333,15 @@ $(function() { rrchanged = true; rrspan.appendChild(document.createTextNode(' ')); var span = document.createElement('span'); - $(span).text('Resource record deleted.'); + var deletetext = 'Resource record deleted.'; + if($(this).data('remove-ptr')) deletetext += ' Reverse (PTR) record will also be removed.'; + $(span).text(deletetext); $(span).addClass('text-warning'); rrspan.appendChild(span); record['delete'] = true; + if(update.type == 'A' || update.type == 'AAAA') { + record['remove_ptr'] = !!$(this).data('remove-ptr'); + } } else { activerows++; if(record['enabled'] == 'Yes') enabledrows++; diff --git a/views/api.php b/views/api.php index 537d1ad..ba16466 100644 --- a/views/api.php +++ b/views/api.php @@ -28,6 +28,10 @@ } else { $api->zone_changes($router->vars['id']); } + break; + case 'reverse-records': + $api->zone_reverse_records($router->vars['id']); + break; } } else { $api->zone($router->vars['id']); @@ -148,6 +152,21 @@ public function update_zone_rrsets($zone_name) { $this->output(null); } + public function zone_reverse_records($zone_name) { + $this->options(array('GET' => 'check_zone_reverse_record'), array($zone_name)); + } + + public function check_zone_reverse_record($zone_name) { + global $zone_dir, $active_user; + $zone = $zone_dir->get_zone_by_name($zone_name); + if(!$active_user->admin && !$active_user->access_to($zone)) throw new AccessDenied; + if(!isset($_GET['name']) || !isset($_GET['type']) || !isset($_GET['address'])) throw new BadData('Missing required parameters.'); + if($_GET['type'] != 'A' && $_GET['type'] != 'AAAA') throw new BadData('Type must be A or AAAA.'); + $name = utf8_to_punycode(DNSName::canonify($_GET['name'], $zone->name)); + $exists = $zone_dir->has_reverse_record($name, $_GET['type'], $_GET['address']); + $this->output(array('exists' => $exists)); + } + public function zone_changes($zone_name) { $this->options(array('GET' => 'list_zone_changes'), array($zone_name)); }