array( 'friendlyName' => 'Bearer Token', 'validators' => array( 'required' => 'required', ) ), 'test' => array( 'friendlyName' => 'Development API', 'type' => 'yesno', 'validators' => array( 'required' => 'required', ) ), 'newrecoverwrite' => array( 'friendlyName' => 'Overwrite NS & SOA upon creation', 'type' => 'yesno', 'validators' => array( 'required' => 'required', ) ), 'soaemail' => array( 'friendlyName' => 'SOA E-mail Address', ), 'defttl' => array( 'friendlyName' => 'Default TTL', ), ); private $dev_api = 'https://my-test.rcodezero.at/api/v1'; private $api = 'https://my.rcodezero.at/api/v1'; public $availableTypes = ['A', 'AAAA', 'NS', 'MX', 'CNAME', 'TXT', 'SRV', 'DNAME', 'SPF', 'PTR', 'ALIAS', 'NAPTR', 'CAA', 'SOA', 'DS', 'URI', 'TLSA', 'CERT', 'SSHFP', ]; private function get($function, $params = [], $method = 'GET') { $url = ($this->config['test'] == 'on' ? $this->dev_api.'/' : $this->api.'/').$function; $headers = ['Content-Type: application/json', 'Authorization: Bearer '.$this->config['token']]; $ch = curl_init(); $chOptions = array ( CURLOPT_URL => $url, CURLINFO_HEADER_OUT => false, CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_HTTPHEADER => $headers, CURLOPT_HEADER => false, ); if($method != 'GET') { $chOptions[CURLOPT_CUSTOMREQUEST] = $method; if(count($params) > 0) { $chOptions = $chOptions + [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($params)]; } } curl_setopt_array($ch, $chOptions); $out = curl_exec($ch); curl_close($ch); if(curl_errno($ch) != 0 || $out === false) { throw new exceptions\DNSSubmoduleException('cURL error: ' . (curl_error($ch)?: 'Unknown Error'), dns\SubmoduleExceptionCodes::CONNECTION_PROBLEM); } if(preg_match('/^\*(.)+\*$/', strtolower(trim($out)))) { throw new exceptions\DNSSubmoduleException(($out?:'Unknown Error'), dns\SubmoduleExceptionCodes::COMMAND_ERROR); } return $out; } public function testConnection() { $out = json_decode($this->get('zones')); if ( !$out ) { throw new exceptions\DNSSubmoduleException('Incorrect API Key', dns\SubmoduleExceptionCodes::CONNECTION_PROBLEM); } } public function getZoneID() { $zone = json_decode($this->get("zones/" . $this->domain, [])); if ( isset($zone->id) ) { return (string)$zone->id; } throw new exceptions\DNSSubmoduleException('Zone does not exists', dns\SubmoduleExceptionCodes::INVALID_PARAMETERS); } public function zoneExists() { try { $this->getZoneID(); return true; } catch (exceptions\DNSSubmoduleException $e) { if ( $e->getCode() === dns\SubmoduleExceptionCodes::INVALID_PARAMETERS ) { return false; } throw $e; } } public function getRecords( $recordType = false ) { $this->getZoneID(); $rrsets = json_decode($this->get("zones/$this->domain/rrsets", []), false)->data; $out = []; $rrsets = array_filter($rrsets, [self::class, 'filterBrokenRRSets']); /** @var dns\submodules\RCodeZero\RRSet $rrset */ foreach ( $rrsets as $rrset ) { $type = $rrset->type; $name = $rrset->name; $ttl = $rrset->ttl; $recordClass = 'MGModule\\DNSManager2\\mgLibs\\custom\\dns\\record\\type\\' . $type; $recordAdapterClass = 'MGModule\\DNSManager2\\mgLibs\\custom\\dns\\submodules\\RCodeZero\\Adapters\\' . $type . 'Adapter'; if ( ($recordType && $recordType !== $type) || !class_exists($recordClass) || !in_array($type, $this->availableTypes, true) ) continue; /** @var dns\submodules\RCodeZero\RecordFromServer $recordFromServer */ foreach ( $rrset->records as $recordId => $recordFromServer ) { if ( class_exists($recordAdapterClass) && method_exists($recordAdapterClass, 'createRdata') ) { /** @var dns\submodules\RCodeZero\Adapters\AbstractRCodeZeroAdapter $record */ $record = new $recordAdapterClass(); $record->name = IdnaHelper::idnaEncode($name); $record->type = $type; $record->line = implode('|', [$name, $type, $recordId]); $record->ttl = $ttl; $record->createRdata(IdnaHelper::idnaDecode($recordFromServer->content)); } else { $record = new Record(); $record->name = IdnaHelper::idnaEncode($name); $record->type = $type; $record->line = implode('|', [$name, $type, $recordId]); $record->ttl = $ttl; $additionalProps = explode(' ', IdnaHelper::idnaDecode($recordFromServer->content)); $record->rdata = new $recordClass(); foreach ( array_keys(get_object_vars($record->rdata)) as $index => $additionalProperty ) { $record->rdata->$additionalProperty = $additionalProps[$index]; } } $out[] = $record; } } usort($out, static function ( $a, $b ) { /** @var Record $a */ /** @var Record $b */ $recordAllignment = ['SOA', 'A', 'AAAA', 'NS', 'MX', 'TXT']; if ( !in_array($a->type, $recordAllignment, true) && !in_array($b->type, $recordAllignment, true) ) return 0; if ( !in_array($a->type, $recordAllignment, true) ) return 1; if ( !in_array($b->type, $recordAllignment, true) ) return -1; if ( $a->type === $b->type ) { return $a->name < $b->name ? -1 : 1; } return array_search($a->type, $recordAllignment, true) < array_search($b->type, $recordAllignment, true) ? -1 : 1; }); return $out; } /** * Generates Valid RRSet format for one record ( usefull when we can use replace function ) * * @param Record $record * @param string $changetype * * @return array */ private function recordToParamsArray( dns\record\Record $record, $changetype = 'REPLACE' ) { return [ 'rrsets' => [ 'name' => $record->name, 'type' => $record->type, 'ttl' => $record->ttl, 'changetype' => $changetype, 'comments' => [], 'records' => [$this->formatRecordToAPIFormat($record)] ] ]; } /** * @param Record $record * * @throws exceptions\DNSSubmoduleException */ public function addRecord( dns\record\Record $record ) { $records = $this->getRecords(); $record->name = IdnaHelper::idnaEncode($record->name); $records[] = $record; $params = $this->recordsToParamArray($records); $params = $this->replaceTargetRRSetTTL($params, $record); $r= json_decode($this->get("zones/$this->domain/rrsets", $params, 'PATCH')); if($r->status == 'failed') { throw new exceptions\DNSSubmoduleException($r->message); } } public function editRecord( dns\record\Record $record ) { $record->nameToAbsolute($this->domain); $record->name = IdnaHelper::idnaEncode($record->name); $records = $this->getRecords(); $recordsBeforeUpdate = $records; /** @var Record $records */ foreach ( $records as $index => $recordBeforeUpdate ) { if ( $recordBeforeUpdate->line === $record->line ) { $records[$index] = $record; break; } } $params = $this->recordsToParamArray($records); $params = $this->replaceTargetRRSetTTL($params, $record); $errorType = null; try { $this->removeRRsets($recordsBeforeUpdate); } catch( exceptions\DNSSubmoduleException $e ) { $errorType = self::ERR_WHILE_REMOVING; } try { $this->get("zones/$this->domain/rrsets", $params, 'PATCH'); } catch( exceptions\DNSSubmoduleException $e ) { $errorType = $errorType ? : self::ERR_WHILE_CREATING; $this->restoreOldRecords($recordsBeforeUpdate,$errorType); } } public function deleteRecord( dns\record\Record $record ) { if( $record->type === 'SOA' ) throw new exceptions\DNSSubmoduleException('You are not allowed to delete SOA record'); $record->nameToAbsolute($this->domain); $record->name = IdnaHelper::idnaEncode($record->name); $records = $this->getRecords(); $recordsBeforeUpdate = $records; /** @var Record $records */ $typesBefore = []; foreach ( $records as $index => $recordBeforeUpdate ) { $types[] = $recordBeforeUpdate->type; if ( $recordBeforeUpdate->line === $record->line ) { unset($records[$index]); break; } } $params = $this->recordsToParamArray($records); $errorType = null; try { $this->removeRRsets($recordsBeforeUpdate); } catch( exceptions\DNSSubmoduleException $e ) { $errorType = self::ERR_WHILE_REMOVING; } try { $r=$this->get("zones/$this->domain/rrsets", $params, 'PATCH'); } catch( exceptions\DNSSubmoduleException $e ) { $errorType = $errorType ? : self::ERR_WHILE_CREATING; $this->restoreOldRecords($recordsBeforeUpdate, $errorType); } } public function activateZone( $dnsZoneName = false ) { $this->get('zones', [ 'domain' => $this->domain, 'type' => 'master' ], 'POST'); if($this->config['newrecoverwrite']) { $nameservers = $this->getNameServers(); $records = $this->getRecords('NS'); $nsRemove = [['name' => $this->domain.'.', 'type' => 'NS', 'changetype' => 'delete']]; $this->get("zones/$this->domain/rrsets", $nsRemove, 'PATCH'); $nsAdd = [ ['name' => $this->domain.'.', 'type' => 'NS', 'ttl' => $this->config['defttl'] ? $this->config['defttl'] : $records[0]->ttl, 'changetype' => 'add', 'records'=>[ ['content' => $nameservers[0].'.'], ['content' => $nameservers[1].'.'] ] ] ]; $r=$this->get("zones/$this->domain/rrsets", $nsAdd, 'PATCH'); list($soarecord) = $this->getRecords('SOA'); $recordAdapterClass = '\MGModule\DNSManager2\mgLibs\custom\dns\submodules\RCodeZero\Adapters\SOAAdapter'; $soarecord->rdata->mname = $nameservers[0].'.' ? $nameservers[0].'.' : $soarecord->rdata->mname; $soarecord->rdata->rname = str_replace('@', '.', $this->config['soaemail']).'.'; $content = (new $recordAdapterClass)->parseContentToApiFormat($soarecord->rdata); $soaUpdate = [[ 'name' => $this->domain.'.', 'type' => 'SOA', 'ttl' => $this->config['defttl'] ? $this->config['defttl'] : $soarecord->ttl, 'changetype' => 'update', 'records' =>[ ['content' => $content ] ]]]; $r=$this->get("zones/$this->domain/rrsets", $soaUpdate, 'PATCH'); } } public function terminateZone() { $this->get("zones/$this->domain", [], 'DELETE'); } public function getZones() { $ret = json_decode($this->get("zones", [])); $out = []; foreach ( $ret->data as $zone ) { $masters = null; $out[(string)$zone->domain] = $masters; } return $out; } /** * Creates rrsets structure from Records array * @param array $records * @param string $changeType * @return mixed */ private function recordsToParamArray( $records, $changeType = 'update') { $names = $this->getUniqueNames($records); $types = $this->getUniqueTypes($records); if( $changeType === 'DELETE' ) { $types = array_diff($types, ['SOA']); } $out = []; foreach ( $names as $name ) { foreach ( $types as $type ) { $rrset = new RRSet(); $rrset->name = $name; $rrset->type = $type; $rrset->ttl = $this->getRRSetTTL($name, $type, $records); $rrset->changetype = $changeType; $rrset->records = $this->getRRsetForNameAndType($name, $type, $records); if ( $rrset->records ) { $out[] = $rrset; } } } return $out; } /** * @param array $records * @return array */ private function getUniqueNames( $records ) { $names = []; /** @var Record $record */ foreach ( $records as $record ) { $names[] = $record->name; } return array_unique($names); } /** * @param array $records * @return array */ private function getUniqueTypes( $records ) { $types = []; /** @var Record $record */ foreach ( $records as $record ) { $types[] = $record->type; } return array_unique($types); } /** * @param $name * @param $type * @param $records * @return array */ private function getRRsetForNameAndType( $name, $type, $records ) { $out = []; foreach ( $records as $record ) { if ( $record->name === $name && $record->type === $type ) { $out[] = $this->formatRecordToAPIFormat($record); } } return $out; } /** * @param Record $record * @return array */ private function formatRecordToAPIFormat( Record $record ) { $recordAdapterClass = '\MGModule\DNSManager2\mgLibs\custom\dns\submodules\RCodeZero\Adapters\\' . $record->type . 'Adapter'; if ( class_exists($recordAdapterClass) ) { $content = (new $recordAdapterClass)->parseContentToApiFormat($record->rdata); } else { $content = str_replace("\t",' ',$record->rdata->toString()); } return ['content' => $content, 'disabled' => false]; } /** * Function removes all RRsets from server * It's easier when we remove all and then add all since we can't update single record cuz REPLACE duplicates record * @param $records * @return bool * @throws exceptions\DNSSubmoduleException */ private function removeRRsets( $records ) { $removeParams = $this->recordsToParamArray($records, 'DELETE'); $this->get("zones/$this->domain/rrsets", $removeParams, 'PATCH'); if( count($this->getRecords()) ) { throw new exceptions\DNSSubmoduleException(self::ERR_WHILE_REMOVING); } return true; } /** * @param string $name * @param string $type * @param array $records * * @return int */ private function getRRSetTTL( $name, $type, array $records ) { foreach( $records as $record ) { if( $record->type === $type && $record->name === $name ) { return $record->ttl; } } return 3600; } /** * @param array $params * @param Record $record * * @return mixed */ private function replaceTargetRRSetTTL( $params, Record $record ) { /*** @var RRSet $rrset */ foreach( $params['rrsets'] as $index => $rrset ) { if( $rrset->name === $record->name && $rrset->type === $record->type ) { $params['rrsets'][$index]->ttl = $record->ttl ?: 3600; return $params; } } return $params; } /** * @param array $oldRecords * @param string $errorType * * @throws exceptions\DNSSubmoduleException */ private function restoreOldRecords( $oldRecords, $errorType = self::ERR_WHILE_CREATING ) { $errors = []; foreach( $oldRecords as $oldRecord ) { try { $this->get("zones/$this->domain", $this->recordToParamsArray($oldRecord), 'PATCH'); } catch( exceptions\DNSSubmoduleException $e ) { $errors[] = $e->getMessage(); } } if( count($errors) ) { $msg = $errorType === self::ERR_WHILE_REMOVING ? 'Zone corrupted contact administrator. Affected records: ' : 'Couldn\'t create records: '; throw new exceptions\DNSSubmoduleException($msg . implode('
', $errors)); } } /** * @param RRSet $rrset * * @return bool */ private function filterBrokenRRSets( $rrset ) { $domainToCheck = rtrim(IdnaHelper::idnaEncode($this->domain), ' .') . '.'; return (bool)preg_match('/' . preg_quote($domainToCheck, '/') . '$/', $rrset->name); } }