RCodeZero.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. <?php
  2. namespace MGModule\DNSManager2\mgLibs\custom\dns\submodules;
  3. use Exception;
  4. use \MGModule\DNSManager2\mgLibs\custom\dns;
  5. use MGModule\DNSManager2\mgLibs\custom\dns\submodules\RCodeZero\RRSet;
  6. use \MGModule\DNSManager2\mgLibs\custom\dns\exceptions;
  7. use \MGModule\DNSManager2\mgLibs\custom\dns\interfaces;
  8. use \MGModule\DNSManager2\mgLibs\custom\dns\record\Record;
  9. use MGModule\DNSManager2\mgLibs\custom\helpers\IdnaHelper;
  10. class RCodeZero extends dns\SubmoduleAbstract implements
  11. interfaces\SubmoduleTTLInterface, interfaces\SubmoduleImportInterface
  12. {
  13. const ERR_WHILE_REMOVING = 'errRemovingRecord';
  14. const ERR_WHILE_CREATING = 'errCreatingRecord';
  15. public $configFields = array(
  16. 'token' => array(
  17. 'friendlyName' => 'Bearer Token',
  18. 'validators' => array(
  19. 'required' => 'required',
  20. )
  21. ),
  22. 'test' => array(
  23. 'friendlyName' => 'Development API',
  24. 'type' => 'yesno',
  25. 'validators' => array(
  26. 'required' => 'required',
  27. )
  28. ),
  29. 'newrecoverwrite' => array(
  30. 'friendlyName' => 'Overwrite NS & SOA upon creation',
  31. 'type' => 'yesno',
  32. 'validators' => array(
  33. 'required' => 'required',
  34. )
  35. ),
  36. 'soaemail' => array(
  37. 'friendlyName' => 'SOA E-mail Address',
  38. ),
  39. 'defttl' => array(
  40. 'friendlyName' => 'Default TTL',
  41. ),
  42. );
  43. private $dev_api = 'https://my-test.rcodezero.at/api/v1';
  44. private $api = 'https://my.rcodezero.at/api/v1';
  45. public $availableTypes = ['A', 'AAAA', 'NS', 'MX', 'CNAME', 'TXT', 'SRV', 'DNAME', 'SPF', 'PTR', 'ALIAS', 'NAPTR', 'CAA', 'SOA', 'DS', 'URI', 'TLSA', 'CERT', 'SSHFP', ];
  46. private function get($function, $params = [], $method = 'GET')
  47. {
  48. $url = ($this->config['test'] == 'on' ?
  49. $this->dev_api.'/' :
  50. $this->api.'/').$function;
  51. $headers = ['Content-Type: application/json', 'Authorization: Bearer '.$this->config['token']];
  52. $ch = curl_init();
  53. $chOptions = array (
  54. CURLOPT_URL => $url,
  55. CURLINFO_HEADER_OUT => false,
  56. CURLOPT_RETURNTRANSFER => true,
  57. CURLOPT_SSL_VERIFYPEER => false,
  58. CURLOPT_SSL_VERIFYHOST => false,
  59. CURLOPT_HTTPHEADER => $headers,
  60. CURLOPT_HEADER => false,
  61. );
  62. if($method != 'GET')
  63. {
  64. $chOptions[CURLOPT_CUSTOMREQUEST] = $method;
  65. if(count($params) > 0)
  66. {
  67. $chOptions = $chOptions + [
  68. CURLOPT_POST => true,
  69. CURLOPT_POSTFIELDS => json_encode($params)];
  70. }
  71. }
  72. curl_setopt_array($ch, $chOptions);
  73. $out = curl_exec($ch);
  74. curl_close($ch);
  75. if(curl_errno($ch) != 0 || $out === false) {
  76. throw new exceptions\DNSSubmoduleException('cURL error: ' . (curl_error($ch)?: 'Unknown Error'), dns\SubmoduleExceptionCodes::CONNECTION_PROBLEM);
  77. }
  78. if(preg_match('/^\*(.)+\*$/', strtolower(trim($out))))
  79. {
  80. throw new exceptions\DNSSubmoduleException(($out?:'Unknown Error'), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
  81. }
  82. return $out;
  83. }
  84. public function testConnection()
  85. {
  86. $out = json_decode($this->get('zones'));
  87. if ( !$out )
  88. {
  89. throw new exceptions\DNSSubmoduleException('Incorrect API Key', dns\SubmoduleExceptionCodes::CONNECTION_PROBLEM);
  90. }
  91. }
  92. public function getZoneID()
  93. {
  94. $zone = json_decode($this->get("zones/" . $this->domain, []));
  95. if ( isset($zone->id) )
  96. {
  97. return (string)$zone->id;
  98. }
  99. throw new exceptions\DNSSubmoduleException('Zone does not exists', dns\SubmoduleExceptionCodes::INVALID_PARAMETERS);
  100. }
  101. public function zoneExists()
  102. {
  103. try
  104. {
  105. $this->getZoneID();
  106. return true;
  107. }
  108. catch (exceptions\DNSSubmoduleException $e)
  109. {
  110. if ( $e->getCode() === dns\SubmoduleExceptionCodes::INVALID_PARAMETERS )
  111. {
  112. return false;
  113. }
  114. throw $e;
  115. }
  116. }
  117. public function getRecords( $recordType = false )
  118. {
  119. $this->getZoneID();
  120. $rrsets = json_decode($this->get("zones/$this->domain/rrsets", []), false)->data;
  121. $out = [];
  122. $rrsets = array_filter($rrsets, [self::class, 'filterBrokenRRSets']);
  123. /** @var dns\submodules\RCodeZero\RRSet $rrset */
  124. foreach ( $rrsets as $rrset )
  125. {
  126. $type = $rrset->type;
  127. $name = $rrset->name;
  128. $ttl = $rrset->ttl;
  129. $recordClass = 'MGModule\\DNSManager2\\mgLibs\\custom\\dns\\record\\type\\' . $type;
  130. $recordAdapterClass = 'MGModule\\DNSManager2\\mgLibs\\custom\\dns\\submodules\\RCodeZero\\Adapters\\' . $type . 'Adapter';
  131. if ( ($recordType && $recordType !== $type) || !class_exists($recordClass) || !in_array($type, $this->availableTypes, true) ) continue;
  132. /** @var dns\submodules\RCodeZero\RecordFromServer $recordFromServer */
  133. foreach ( $rrset->records as $recordId => $recordFromServer )
  134. {
  135. if ( class_exists($recordAdapterClass) && method_exists($recordAdapterClass, 'createRdata') )
  136. {
  137. /** @var dns\submodules\RCodeZero\Adapters\AbstractRCodeZeroAdapter $record */
  138. $record = new $recordAdapterClass();
  139. $record->name = IdnaHelper::idnaEncode($name);
  140. $record->type = $type;
  141. $record->line = implode('|', [$name, $type, $recordId]);
  142. $record->ttl = $ttl;
  143. $record->createRdata(IdnaHelper::idnaDecode($recordFromServer->content));
  144. }
  145. else
  146. {
  147. $record = new Record();
  148. $record->name = IdnaHelper::idnaEncode($name);
  149. $record->type = $type;
  150. $record->line = implode('|', [$name, $type, $recordId]);
  151. $record->ttl = $ttl;
  152. $additionalProps = explode(' ', IdnaHelper::idnaDecode($recordFromServer->content));
  153. $record->rdata = new $recordClass();
  154. foreach ( array_keys(get_object_vars($record->rdata)) as $index => $additionalProperty )
  155. {
  156. $record->rdata->$additionalProperty = $additionalProps[$index];
  157. }
  158. }
  159. $out[] = $record;
  160. }
  161. }
  162. usort($out, static function ( $a, $b )
  163. {
  164. /** @var Record $a */
  165. /** @var Record $b */
  166. $recordAllignment = ['SOA', 'A', 'AAAA', 'NS', 'MX', 'TXT'];
  167. if ( !in_array($a->type, $recordAllignment, true) && !in_array($b->type, $recordAllignment, true) ) return 0;
  168. if ( !in_array($a->type, $recordAllignment, true) ) return 1;
  169. if ( !in_array($b->type, $recordAllignment, true) ) return -1;
  170. if ( $a->type === $b->type )
  171. {
  172. return $a->name < $b->name ? -1 : 1;
  173. }
  174. return array_search($a->type, $recordAllignment, true) < array_search($b->type, $recordAllignment, true) ? -1 : 1;
  175. });
  176. return $out;
  177. }
  178. /**
  179. * Generates Valid RRSet format for one record ( usefull when we can use replace function )
  180. *
  181. * @param Record $record
  182. * @param string $changetype
  183. *
  184. * @return array
  185. */
  186. private function recordToParamsArray( dns\record\Record $record, $changetype = 'REPLACE' )
  187. {
  188. return [
  189. 'rrsets' => [
  190. 'name' => $record->name,
  191. 'type' => $record->type,
  192. 'ttl' => $record->ttl,
  193. 'changetype' => $changetype,
  194. 'comments' => [],
  195. 'records' => [$this->formatRecordToAPIFormat($record)]
  196. ]
  197. ];
  198. }
  199. /**
  200. * @param Record $record
  201. *
  202. * @throws exceptions\DNSSubmoduleException
  203. */
  204. public function addRecord( dns\record\Record $record )
  205. {
  206. $records = $this->getRecords();
  207. $record->name = IdnaHelper::idnaEncode($record->name);
  208. $records[] = $record;
  209. $params = $this->recordsToParamArray($records);
  210. $params = $this->replaceTargetRRSetTTL($params, $record);
  211. $r= json_decode($this->get("zones/$this->domain/rrsets", $params, 'PATCH'));
  212. if($r->status == 'failed')
  213. {
  214. throw new exceptions\DNSSubmoduleException($r->message);
  215. }
  216. }
  217. public function editRecord( dns\record\Record $record )
  218. {
  219. $record->nameToAbsolute($this->domain);
  220. $record->name = IdnaHelper::idnaEncode($record->name);
  221. $records = $this->getRecords();
  222. $recordsBeforeUpdate = $records;
  223. /** @var Record $records */
  224. foreach ( $records as $index => $recordBeforeUpdate )
  225. {
  226. if ( $recordBeforeUpdate->line === $record->line )
  227. {
  228. $records[$index] = $record;
  229. break;
  230. }
  231. }
  232. $params = $this->recordsToParamArray($records);
  233. $params = $this->replaceTargetRRSetTTL($params, $record);
  234. $errorType = null;
  235. try
  236. {
  237. $this->removeRRsets($recordsBeforeUpdate);
  238. }
  239. catch( exceptions\DNSSubmoduleException $e )
  240. {
  241. $errorType = self::ERR_WHILE_REMOVING;
  242. }
  243. try
  244. {
  245. $this->get("zones/$this->domain/rrsets", $params, 'PATCH');
  246. }
  247. catch( exceptions\DNSSubmoduleException $e )
  248. {
  249. $errorType = $errorType ? : self::ERR_WHILE_CREATING;
  250. $this->restoreOldRecords($recordsBeforeUpdate,$errorType);
  251. }
  252. }
  253. public function deleteRecord( dns\record\Record $record )
  254. {
  255. if( $record->type === 'SOA' ) throw new exceptions\DNSSubmoduleException('You are not allowed to delete SOA record');
  256. $record->nameToAbsolute($this->domain);
  257. $record->name = IdnaHelper::idnaEncode($record->name);
  258. $records = $this->getRecords();
  259. $recordsBeforeUpdate = $records;
  260. /** @var Record $records */
  261. $typesBefore = [];
  262. foreach ( $records as $index => $recordBeforeUpdate )
  263. {
  264. $types[] = $recordBeforeUpdate->type;
  265. if ( $recordBeforeUpdate->line === $record->line )
  266. {
  267. unset($records[$index]);
  268. break;
  269. }
  270. }
  271. $params = $this->recordsToParamArray($records);
  272. $errorType = null;
  273. try
  274. {
  275. $this->removeRRsets($recordsBeforeUpdate);
  276. }
  277. catch( exceptions\DNSSubmoduleException $e )
  278. {
  279. $errorType = self::ERR_WHILE_REMOVING;
  280. }
  281. try
  282. {
  283. $r=$this->get("zones/$this->domain/rrsets", $params, 'PATCH');
  284. }
  285. catch( exceptions\DNSSubmoduleException $e )
  286. {
  287. $errorType = $errorType ? : self::ERR_WHILE_CREATING;
  288. $this->restoreOldRecords($recordsBeforeUpdate, $errorType);
  289. }
  290. }
  291. public function activateZone( $dnsZoneName = false )
  292. {
  293. $this->get('zones', [
  294. 'domain' => $this->domain,
  295. 'type' => 'master'
  296. ], 'POST');
  297. if($this->config['newrecoverwrite'])
  298. {
  299. $nameservers = $this->getNameServers();
  300. $records = $this->getRecords('NS');
  301. $nsRemove = [['name' => $this->domain.'.', 'type' => 'NS', 'changetype' => 'delete']];
  302. $this->get("zones/$this->domain/rrsets", $nsRemove, 'PATCH');
  303. $nsAdd = [
  304. ['name' => $this->domain.'.',
  305. 'type' => 'NS',
  306. 'ttl' => $this->config['defttl'] ? $this->config['defttl'] : $records[0]->ttl,
  307. 'changetype' => 'add',
  308. 'records'=>[
  309. ['content' => $nameservers[0].'.'],
  310. ['content' => $nameservers[1].'.']
  311. ]
  312. ]
  313. ];
  314. $r=$this->get("zones/$this->domain/rrsets", $nsAdd, 'PATCH');
  315. list($soarecord) = $this->getRecords('SOA');
  316. $recordAdapterClass = '\MGModule\DNSManager2\mgLibs\custom\dns\submodules\RCodeZero\Adapters\SOAAdapter';
  317. $soarecord->rdata->mname = $nameservers[0].'.' ? $nameservers[0].'.' : $soarecord->rdata->mname;
  318. $soarecord->rdata->rname = str_replace('@', '.', $this->config['soaemail']).'.';
  319. $content = (new $recordAdapterClass)->parseContentToApiFormat($soarecord->rdata);
  320. $soaUpdate = [[
  321. 'name' => $this->domain.'.',
  322. 'type' => 'SOA',
  323. 'ttl' => $this->config['defttl'] ? $this->config['defttl'] : $soarecord->ttl,
  324. 'changetype' => 'update', 'records' =>[
  325. ['content' =>
  326. $content
  327. ]
  328. ]]];
  329. $r=$this->get("zones/$this->domain/rrsets", $soaUpdate, 'PATCH');
  330. }
  331. }
  332. public function terminateZone()
  333. {
  334. $this->get("zones/$this->domain", [], 'DELETE');
  335. }
  336. public function getZones()
  337. {
  338. $ret = json_decode($this->get("zones", []));
  339. $out = [];
  340. foreach ( $ret->data as $zone )
  341. {
  342. $masters = null;
  343. $out[(string)$zone->domain] = $masters;
  344. }
  345. return $out;
  346. }
  347. /**
  348. * Creates rrsets structure from Records array
  349. * @param array $records
  350. * @param string $changeType
  351. * @return mixed
  352. */
  353. private function recordsToParamArray( $records, $changeType = 'update')
  354. {
  355. $names = $this->getUniqueNames($records);
  356. $types = $this->getUniqueTypes($records);
  357. if( $changeType === 'DELETE' )
  358. {
  359. $types = array_diff($types, ['SOA']);
  360. }
  361. $out = [];
  362. foreach ( $names as $name )
  363. {
  364. foreach ( $types as $type )
  365. {
  366. $rrset = new RRSet();
  367. $rrset->name = $name;
  368. $rrset->type = $type;
  369. $rrset->ttl = $this->getRRSetTTL($name, $type, $records);
  370. $rrset->changetype = $changeType;
  371. $rrset->records = $this->getRRsetForNameAndType($name, $type, $records);
  372. if ( $rrset->records )
  373. {
  374. $out[] = $rrset;
  375. }
  376. }
  377. }
  378. return $out;
  379. }
  380. /**
  381. * @param array $records
  382. * @return array
  383. */
  384. private function getUniqueNames( $records )
  385. {
  386. $names = [];
  387. /** @var Record $record */
  388. foreach ( $records as $record )
  389. {
  390. $names[] = $record->name;
  391. }
  392. return array_unique($names);
  393. }
  394. /**
  395. * @param array $records
  396. * @return array
  397. */
  398. private function getUniqueTypes( $records )
  399. {
  400. $types = [];
  401. /** @var Record $record */
  402. foreach ( $records as $record )
  403. {
  404. $types[] = $record->type;
  405. }
  406. return array_unique($types);
  407. }
  408. /**
  409. * @param $name
  410. * @param $type
  411. * @param $records
  412. * @return array
  413. */
  414. private function getRRsetForNameAndType( $name, $type, $records )
  415. {
  416. $out = [];
  417. foreach ( $records as $record )
  418. {
  419. if ( $record->name === $name && $record->type === $type )
  420. {
  421. $out[] = $this->formatRecordToAPIFormat($record);
  422. }
  423. }
  424. return $out;
  425. }
  426. /**
  427. * @param Record $record
  428. * @return array
  429. */
  430. private function formatRecordToAPIFormat( Record $record )
  431. {
  432. $recordAdapterClass = '\MGModule\DNSManager2\mgLibs\custom\dns\submodules\RCodeZero\Adapters\\' . $record->type . 'Adapter';
  433. if ( class_exists($recordAdapterClass) )
  434. {
  435. $content = (new $recordAdapterClass)->parseContentToApiFormat($record->rdata);
  436. }
  437. else
  438. {
  439. $content = str_replace("\t",' ',$record->rdata->toString());
  440. }
  441. return ['content' => $content, 'disabled' => false];
  442. }
  443. /**
  444. * Function removes all RRsets from server
  445. * It's easier when we remove all and then add all since we can't update single record cuz REPLACE duplicates record
  446. * @param $records
  447. * @return bool
  448. * @throws exceptions\DNSSubmoduleException
  449. */
  450. private function removeRRsets( $records )
  451. {
  452. $removeParams = $this->recordsToParamArray($records, 'DELETE');
  453. $this->get("zones/$this->domain/rrsets", $removeParams, 'PATCH');
  454. if( count($this->getRecords()) )
  455. {
  456. throw new exceptions\DNSSubmoduleException(self::ERR_WHILE_REMOVING);
  457. }
  458. return true;
  459. }
  460. /**
  461. * @param string $name
  462. * @param string $type
  463. * @param array $records
  464. *
  465. * @return int
  466. */
  467. private function getRRSetTTL( $name, $type, array $records )
  468. {
  469. foreach( $records as $record )
  470. {
  471. if( $record->type === $type && $record->name === $name )
  472. {
  473. return $record->ttl;
  474. }
  475. }
  476. return 3600;
  477. }
  478. /**
  479. * @param array $params
  480. * @param Record $record
  481. *
  482. * @return mixed
  483. */
  484. private function replaceTargetRRSetTTL( $params, Record $record )
  485. {
  486. /*** @var RRSet $rrset */
  487. foreach( $params['rrsets'] as $index => $rrset )
  488. {
  489. if( $rrset->name === $record->name && $rrset->type === $record->type )
  490. {
  491. $params['rrsets'][$index]->ttl = $record->ttl ?: 3600;
  492. return $params;
  493. }
  494. }
  495. return $params;
  496. }
  497. /**
  498. * @param array $oldRecords
  499. * @param string $errorType
  500. *
  501. * @throws exceptions\DNSSubmoduleException
  502. */
  503. private function restoreOldRecords( $oldRecords, $errorType = self::ERR_WHILE_CREATING )
  504. {
  505. $errors = [];
  506. foreach( $oldRecords as $oldRecord )
  507. {
  508. try
  509. {
  510. $this->get("zones/$this->domain", $this->recordToParamsArray($oldRecord), 'PATCH');
  511. }
  512. catch( exceptions\DNSSubmoduleException $e )
  513. {
  514. $errors[] = $e->getMessage();
  515. }
  516. }
  517. if( count($errors) )
  518. {
  519. $msg = $errorType === self::ERR_WHILE_REMOVING ? 'Zone corrupted contact administrator. Affected records: ' : 'Couldn\'t create records: ';
  520. throw new exceptions\DNSSubmoduleException($msg . implode('<br />', $errors));
  521. }
  522. }
  523. /**
  524. * @param RRSet $rrset
  525. *
  526. * @return bool
  527. */
  528. private function filterBrokenRRSets( $rrset )
  529. {
  530. $domainToCheck = rtrim(IdnaHelper::idnaEncode($this->domain), ' .') . '.';
  531. return (bool)preg_match('/' . preg_quote($domainToCheck, '/') . '$/', $rrset->name);
  532. }
  533. }