| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621 |
- <?php
- namespace MGModule\DNSManager2\mgLibs\custom\dns\submodules;
- use Exception;
- use \MGModule\DNSManager2\mgLibs\custom\dns;
- use MGModule\DNSManager2\mgLibs\custom\dns\submodules\RCodeZero\RRSet;
- use \MGModule\DNSManager2\mgLibs\custom\dns\exceptions;
- use \MGModule\DNSManager2\mgLibs\custom\dns\interfaces;
- use \MGModule\DNSManager2\mgLibs\custom\dns\record\Record;
- use MGModule\DNSManager2\mgLibs\custom\helpers\IdnaHelper;
- class RCodeZero extends dns\SubmoduleAbstract implements
- interfaces\SubmoduleTTLInterface, interfaces\SubmoduleImportInterface
- {
- const ERR_WHILE_REMOVING = 'errRemovingRecord';
- const ERR_WHILE_CREATING = 'errCreatingRecord';
- public $configFields = array(
- 'token' => 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('<br />', $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);
- }
- }
|