PowerDNSV4.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958
  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\dnssec\DnsSec;
  6. use MGModule\DNSManager2\mgLibs\custom\dns\submodules\PowerDNSv4\RRSet;
  7. use MGModule\DNSManager2\mgLibs\custom\dns\utils\Patterns;
  8. use \MGModule\DNSManager2\mgLibs\custom\dns\exceptions;
  9. use \MGModule\DNSManager2\mgLibs\custom\dns\interfaces;
  10. use \MGModule\DNSManager2\mgLibs\custom\dns\record\Record;
  11. use MGModule\DNSManager2\mgLibs\custom\helpers\IdnaHelper;
  12. class PowerDNSV4 extends dns\SubmoduleAbstract implements
  13. interfaces\SubmoduleTTLInterface, interfaces\SubmoduleImportInterface, interfaces\SubmoduleRDNSInterface, interfaces\SubmoduleDNSSecInterface
  14. {
  15. const ERR_WHILE_REMOVING = 'errRemovingRecord';
  16. const ERR_WHILE_CREATING = 'errCreatingRecord';
  17. public $configFields = [
  18. 'hostname' => [
  19. 'friendlyName' => 'Hostname/IP',
  20. 'validators' => [
  21. 'required' => 'required',
  22. ]
  23. ],
  24. 'port' => [
  25. 'friendlyName' => 'Default Port',
  26. 'value' => '8081'
  27. ],
  28. 'ssl' => [
  29. 'friendlyName' => 'SSL',
  30. 'type' => 'yesno',
  31. ],
  32. 'apikey' => [
  33. 'friendlyName' => 'API Key',
  34. 'validators' => [
  35. 'required' => 'required',
  36. ]
  37. ],
  38. 'domain_type' => [
  39. 'friendlyName' => 'Domain Type',
  40. 'type' => 'select',
  41. 'options' => ['MASTER' => 'MASTER', 'SLAVE' => 'SLAVE', 'NATIVE' => 'NATIVE'],
  42. ],
  43. 'notify_slaves' => [
  44. 'friendlyName' => 'Notify Slaves',
  45. 'type' => 'yesno',
  46. ],
  47. 'zone_account' => [
  48. 'friendlyName' => 'Zone Account',
  49. ],
  50. 'increment_soa_serial'=>[
  51. 'friendlyName' => 'Manually Increment SOA Serial',
  52. 'type' => 'yesno',
  53. ],
  54. 'default_ip' => [
  55. 'friendlyName' => 'Default IP',
  56. 'validators' => [
  57. 'required' => 'required',
  58. 'pattern' => Patterns::IP4_OR_IP6,
  59. ]
  60. ],
  61. ];
  62. public $availableTypes = ['MINFO', 'A', 'AAAA', 'NS', 'MX', 'CNAME', 'TXT', 'SRV', 'DNAME', 'SPF', 'PTR', 'ALIAS', 'AFSDB', 'HINFO', 'LOC', 'NAPTR', 'RP', 'CAA', 'SOA', 'DNSKEY', 'DS', 'RRSIG', 'URI', 'TLSA', 'SMIMEA', 'CERT', 'SSHFP', 'NSEC3PARAM', 'NSEC3', 'NSEC'];
  63. private $zoneID = false;
  64. private function get( $function, $params = [], $customRequest = null )
  65. {
  66. $headers[] = 'X-API-Key: ' . $this->config['apikey'];
  67. $port = $this->config['port'] ? : 8081;
  68. $url = $this->config['ssl'] ? 'https' : 'http';
  69. $url .= '://' . $this->config['hostname'] . ':' . $port . '/api/v1/' . $function;
  70. $this->log('REQUEST: ' . $url);
  71. $ch = curl_init();
  72. $chOptions = [
  73. CURLOPT_URL => trim($url),
  74. CURLOPT_RETURNTRANSFER => 1,
  75. CURLOPT_SSL_VERIFYPEER => 0,
  76. CURLOPT_SSL_VERIFYHOST => 0,
  77. CURLOPT_TIMEOUT => 30,
  78. CURLOPT_HTTPHEADER => $headers
  79. ];
  80. if ( $params )
  81. {
  82. $body = json_encode($params);
  83. $body = str_replace(
  84. [
  85. ':{',
  86. '},',
  87. '}}',
  88. '}],{'
  89. ],
  90. [
  91. ':[{',
  92. '}],',
  93. '}]}',
  94. '},{'
  95. ], $body);
  96. $chOptions[CURLOPT_POST] = 1;
  97. $chOptions[CURLOPT_POSTFIELDS] = $body;
  98. }
  99. if ( $customRequest )
  100. {
  101. $chOptions[CURLOPT_CUSTOMREQUEST] = $customRequest;
  102. }
  103. curl_setopt_array($ch, $chOptions);
  104. $out = curl_exec($ch);
  105. $this->log('RESPONSE: ' . $out);
  106. if ( strpos($out, '"error"') !== false)
  107. {
  108. $out = json_decode($out);
  109. throw new exceptions\DNSSubmoduleException(str_replace('\032', ' ', $out->error), dns\SubmoduleExceptionCodes::INVALID_PARAMETERS);
  110. }
  111. if ( strpos($out, 'Unauthorized') !== false )
  112. {
  113. throw new exceptions\DNSSubmoduleException('Incorrect API Key', dns\SubmoduleExceptionCodes::CONNECTION_PROBLEM);
  114. }
  115. if ( curl_errno($ch) )
  116. {
  117. throw new exceptions\DNSSubmoduleException('cURL Error: ' . curl_errno($ch) . ' - ' . curl_error($ch), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
  118. }
  119. $info = curl_getinfo($ch);
  120. if($info['http_code'] >= 300)
  121. {
  122. throw new exceptions\DNSSubmoduleException($this->parseHttpCodeError($info['http_code']), $info['http_code']);
  123. }
  124. curl_close($ch);
  125. return $out;
  126. }
  127. private function parseHttpCodeError($code)
  128. {
  129. switch($code)
  130. {
  131. case 400:
  132. $error = 'Bad request to the API';
  133. break;
  134. case 404:
  135. $error = 'API not enabled or invalid URL';
  136. break;
  137. default:
  138. $error = 'The server returned '.$code.' error';
  139. }
  140. return $error;
  141. }
  142. public function testConnection()
  143. {
  144. $out = $this->get('servers');
  145. }
  146. private function getServerID()
  147. {
  148. $out = json_decode($this->get('servers'));
  149. return $out[0]->id;
  150. }
  151. public function getZoneID()
  152. {
  153. if ( $this->zoneID !== false )
  154. {
  155. return $this->zoneID;
  156. }
  157. $serverId = $this->getServerID();
  158. try
  159. {
  160. $zone = json_decode($this->get("servers/$serverId/zones/" . $this->domain, []));
  161. }
  162. catch (exceptions\DNSSubmoduleException $e)
  163. {
  164. if($e->getCode() == 404)
  165. {
  166. throw new exceptions\DNSSubmoduleException('Zone does not exists', dns\SubmoduleExceptionCodes::INVALID_PARAMETERS);
  167. }
  168. }
  169. if ( isset($zone->id) )
  170. {
  171. $this->zoneID = (string)$zone->id;
  172. return (string)$zone->id;
  173. }
  174. throw new exceptions\DNSSubmoduleException('Zone does not exists', dns\SubmoduleExceptionCodes::INVALID_PARAMETERS);
  175. }
  176. public function zoneExists()
  177. {
  178. try
  179. {
  180. $this->getZoneID();
  181. return true;
  182. }
  183. catch (exceptions\DNSSubmoduleException $e)
  184. {
  185. if ( $e->getCode() === dns\SubmoduleExceptionCodes::INVALID_PARAMETERS )
  186. {
  187. return false;
  188. }
  189. throw $e;
  190. }
  191. }
  192. public function getRecords( $recordType = false )
  193. {
  194. $serverId = $this->getServerID();
  195. $zoneId = $this->getZoneID();
  196. $rrsets = json_decode($this->get("servers/$serverId/zones/$zoneId", []), false)->rrsets;
  197. $out = [];
  198. $rrsets = array_filter($rrsets, [self::class, 'filterBrokenRRSets']);
  199. /** @var dns\submodules\PowerDNSv4\RRSet $rrset */
  200. foreach ( $rrsets as $rrset )
  201. {
  202. $type = $rrset->type;
  203. $name = $rrset->name;
  204. $ttl = $rrset->ttl;
  205. $recordClass = 'MGModule\\DNSManager2\\mgLibs\\custom\\dns\\record\\type\\' . $type;
  206. $recordAdapterClass = 'MGModule\\DNSManager2\\mgLibs\\custom\\dns\\submodules\\PowerDNSv4\\Adapters\\' . $type . 'Adapter';
  207. if ( ($recordType && $recordType !== $type) || !class_exists($recordClass) || !in_array($type, $this->availableTypes, true) ) continue;
  208. /** @var dns\submodules\PowerDNSv4\RecordFromServer $recordFromServer */
  209. foreach ( $rrset->records as $recordId => $recordFromServer )
  210. {
  211. if ( class_exists($recordAdapterClass) && method_exists($recordAdapterClass, 'createRdata') )
  212. {
  213. /** @var dns\submodules\PowerDNSv4\Adapters\AbstractPowerDNSv4Adapter $record */
  214. $record = new $recordAdapterClass();
  215. $record->name = IdnaHelper::idnaEncode($name);
  216. $record->type = $type;
  217. $record->line = implode('|', [$name, $type, $recordId]);
  218. $record->ttl = $ttl;
  219. $record->class = 'IN';
  220. $record->createRdata(IdnaHelper::idnaDecode($recordFromServer->content));
  221. }
  222. else
  223. {
  224. $record = new Record();
  225. $record->name = IdnaHelper::idnaEncode($name);
  226. $record->type = $type;
  227. $record->line = implode('|', [$name, $type, $recordId]);
  228. $record->ttl = $ttl;
  229. $record->class = 'IN';
  230. $additionalProps = explode(' ', IdnaHelper::idnaDecode($recordFromServer->content));
  231. $record->rdata = new $recordClass();
  232. foreach ( array_keys(get_object_vars($record->rdata)) as $index => $additionalProperty )
  233. {
  234. $record->rdata->$additionalProperty = $additionalProps[$index];
  235. }
  236. }
  237. $out[] = $record;
  238. }
  239. }
  240. usort($out, static function ( $a, $b )
  241. {
  242. /** @var Record $a */
  243. /** @var Record $b */
  244. $recordAllignment = ['SOA', 'A', 'AAAA', 'NS', 'MX', 'TXT'];
  245. if ( !in_array($a->type, $recordAllignment, true) && !in_array($b->type, $recordAllignment, true) ) return 0;
  246. if ( !in_array($a->type, $recordAllignment, true) ) return 1;
  247. if ( !in_array($b->type, $recordAllignment, true) ) return -1;
  248. if ( $a->type === $b->type )
  249. {
  250. return $a->name < $b->name ? -1 : 1;
  251. }
  252. return array_search($a->type, $recordAllignment, true) < array_search($b->type, $recordAllignment, true) ? -1 : 1;
  253. });
  254. return $out;
  255. }
  256. /**
  257. * Generates Valid RRSet format for one record ( usefull when we can use replace function )
  258. *
  259. * @param Record $record
  260. * @param string $changetype
  261. *
  262. * @return array
  263. */
  264. private function recordToParamsArray( dns\record\Record $record, $changetype = 'REPLACE' )
  265. {
  266. return [
  267. 'rrsets' => [
  268. 'name' => $record->name,
  269. 'type' => $record->type,
  270. 'ttl' => $record->ttl,
  271. 'changetype' => $changetype,
  272. 'comments' => [],
  273. 'records' => [$this->formatRecordToPowerDnsFormat($record)]
  274. ]
  275. ];
  276. }
  277. /**
  278. * @param Record $record
  279. *
  280. * @throws exceptions\DNSSubmoduleException
  281. */
  282. public function addRecord( dns\record\Record $record )
  283. {
  284. $serverId = $this->getServerID();
  285. $zoneId = $this->getZoneID();
  286. $records = $this->getRecords();
  287. $record->name = IdnaHelper::idnaEncode($record->nameToAbsolute($this->domain));
  288. $records[] = $record;
  289. if( $this->config['increment_soa_serial'] === 'on' )
  290. {
  291. $records = $this->incrementSOASerial($records);
  292. }
  293. $params = $this->recordsToParamArray($records);
  294. $params = $this->replaceTargetRRSetTTL($params, $record);
  295. $this->get("servers/$serverId/zones/$zoneId", $params, 'PATCH');
  296. if ( $this->isSigned() )
  297. {
  298. $this->rectify();
  299. }
  300. $this->checkAndRunNotifySlaves($serverId, $zoneId);
  301. }
  302. public function editRecord( dns\record\Record $record )
  303. {
  304. $serverId = $this->getServerID();
  305. $zoneId = $this->getZoneID();
  306. $record->nameToAbsolute($this->domain);
  307. $record->name = IdnaHelper::idnaEncode($record->name);
  308. $records = $this->getRecords();
  309. $recordsBeforeUpdate = $records;
  310. /** @var Record $records */
  311. foreach ( $records as $index => $recordBeforeUpdate )
  312. {
  313. if ( $recordBeforeUpdate->line === $record->line )
  314. {
  315. $records[$index] = $record;
  316. break;
  317. }
  318. }
  319. if( $this->config['increment_soa_serial'] === 'on' )
  320. {
  321. $records = $this->incrementSOASerial($records);
  322. }
  323. $params = $this->recordsToParamArray($records);
  324. $params = $this->replaceTargetRRSetTTL($params, $record);
  325. $errorType = null;
  326. try
  327. {
  328. $this->removeRRsets($recordsBeforeUpdate);
  329. }
  330. catch( exceptions\DNSSubmoduleException $e )
  331. {
  332. $errorType = self::ERR_WHILE_REMOVING;
  333. }
  334. try
  335. {
  336. $this->get("servers/$serverId/zones/$zoneId", $params, 'PATCH');
  337. }
  338. catch( exceptions\DNSSubmoduleException $e )
  339. {
  340. $errorType = $errorType ? : self::ERR_WHILE_CREATING;
  341. $this->restoreOldRecords($recordsBeforeUpdate,$errorType);
  342. }
  343. if ( $this->isSigned() )
  344. {
  345. $this->rectify();
  346. }
  347. }
  348. public function deleteRecord( dns\record\Record $record )
  349. {
  350. if( $record->type === 'SOA' ) throw new exceptions\DNSSubmoduleException('You are not allowed to delete SOA record');
  351. $serverId = $this->getServerID();
  352. $zoneId = $this->getZoneID();
  353. $record->nameToAbsolute($this->domain);
  354. $record->name = IdnaHelper::idnaEncode($record->name);
  355. $records = $this->getRecords();
  356. $recordsBeforeUpdate = $records;
  357. /** @var Record $records */
  358. foreach ( $records as $index => $recordBeforeUpdate )
  359. {
  360. if ( $recordBeforeUpdate->line === $record->line )
  361. {
  362. unset($records[$index]);
  363. break;
  364. }
  365. }
  366. $params = $this->recordsToParamArray($records);
  367. $errorType = null;
  368. try
  369. {
  370. $this->removeRRsets($recordsBeforeUpdate);
  371. }
  372. catch( exceptions\DNSSubmoduleException $e )
  373. {
  374. $errorType = self::ERR_WHILE_REMOVING;
  375. }
  376. try
  377. {
  378. $this->get("servers/$serverId/zones/$zoneId", $params, 'PATCH');
  379. }
  380. catch( exceptions\DNSSubmoduleException $e )
  381. {
  382. $errorType = $errorType ? : self::ERR_WHILE_CREATING;
  383. $this->restoreOldRecords($recordsBeforeUpdate, $errorType);
  384. }
  385. if ( $this->isSigned() )
  386. {
  387. $this->rectify();
  388. }
  389. $this->checkAndRunNotifySlaves($serverId, $zoneId);
  390. }
  391. public function activateZone( $dnsZoneName = false )
  392. {
  393. $serverId = $this->getServerID();
  394. $params = [
  395. 'name' => rtrim($this->domain, '.') . '.',
  396. 'kind' => $this->config['domain_type'],
  397. 'nameservers' => [],
  398. ];
  399. if ( !empty($this->config['zone_account']) )
  400. {
  401. $params['account'] = $this->config['zone_account'];
  402. }
  403. if ( $dnsZoneName )
  404. {
  405. $params['name'] = rtrim($dnsZoneName, '.') . '.';
  406. }
  407. return $this->get("servers/$serverId/zones", $params);
  408. }
  409. public function terminateZone()
  410. {
  411. $serverId = $this->getServerID();
  412. $zoneId = $this->getZoneID();
  413. $this->get("servers/$serverId/zones/$zoneId", [], 'DELETE');
  414. }
  415. public function getZones()
  416. {
  417. $serverId = $this->getServerID();
  418. $ret = json_decode($this->get("servers/$serverId/zones", []));
  419. $out = [];
  420. foreach ( $ret as $zone )
  421. {
  422. $masters = null;
  423. if ( !empty($zone->masters) )
  424. {
  425. $masters = implode(', ', $zone->masters);
  426. }
  427. $out[substr((string)$zone->name, 0, -1)] = $masters;
  428. }
  429. return $out;
  430. }
  431. public function updateRDNS( $ip, $ttl = false, $value = false )
  432. {
  433. $revDnsZoneName = dns\utils\ReverseDNSHelper::reverseZoneName($ip);
  434. $zoneId = $this->getRevDNSZoneID($revDnsZoneName);
  435. $serverId = $this->getServerID();
  436. if ( !$zoneId )
  437. {
  438. $this->activateZone($revDnsZoneName);
  439. $zoneId = $this->getRevDNSZoneID($revDnsZoneName);
  440. }
  441. //We don't allow to replace name of the rdns record so we can simply use patch
  442. $revRecord = $this->createPowerDnsPTRRecord($ip, $ttl, $value, $revDnsZoneName);
  443. $this->get("servers/$serverId/zones/$zoneId", $this->recordToParamsArray($revRecord), 'PATCH');
  444. $this->checkAndRunNotifySlaves($serverId, $zoneId);
  445. }
  446. private function createPowerDnsPTRRecord( $ip, $ttl, $value, $dnsZoneName )
  447. {
  448. $record = new Record();
  449. $record->name = $this->getLastPartOfIp($ip, true) . rtrim($dnsZoneName, '.') . '.';
  450. $record->ttl = $ttl;
  451. $record->type = 'PTR';
  452. $record->rdata = new dns\record\type\PTR();
  453. $record->rdata->ptrdname = $value;
  454. return $record;
  455. }
  456. public function removeRDNS( $ip )
  457. {
  458. $revDnsZoneName = dns\utils\ReverseDNSHelper::reverseZoneName($ip);
  459. $zoneId = $this->getRevDNSZoneID($revDnsZoneName);
  460. if ( !$zoneId )
  461. {
  462. return false;
  463. }
  464. $record = $this->getRDNSRecord($ip);
  465. if ( $record === false )
  466. {
  467. return false;
  468. }
  469. $this->deleteRecord($record);
  470. return true;
  471. }
  472. private function getLastPartOfIp( $ip, $withDot = false )
  473. {
  474. $ipLastPart = dns\utils\ReverseDNSHelper::reverseRecordName($ip);
  475. return $withDot ? $ipLastPart . '.' : $ipLastPart;
  476. }
  477. public function getRDNSRecord( $ip )
  478. {
  479. $clonedZone = $this->getClone($ip);
  480. $records = $clonedZone->getRecords('PTR');
  481. $revZoneName = $this->getLastPartOfIp($ip, true) . rtrim(dns\utils\ReverseDNSHelper::reverseZoneName($ip), '.') . '.';
  482. foreach ( $records as $record )
  483. {
  484. if ( $record->name === $revZoneName ) return $record;
  485. }
  486. return false;
  487. }
  488. private function getRevDNSZoneID( $zoneName )
  489. {
  490. $zoneName = rtrim($zoneName, '.') . '.';
  491. $serverId = $this->getServerID();
  492. $out = json_decode($this->get("servers/$serverId/zones", []));
  493. foreach ( $out as $zone )
  494. {
  495. if ( $zone->name === $zoneName )
  496. {
  497. $this->zoneID = (string)$zone->id;
  498. return (string)$zone->id;
  499. }
  500. }
  501. return false;
  502. }
  503. private function checkAndRunNotifySlaves( $serverID = null, $zoneID = null )
  504. {
  505. if ( $serverID !== null && $zoneID !== null && $this->config['notify_slaves'] === 'on' )
  506. {
  507. $this->get('servers/' . $serverID . '/zones/' . $zoneID . '/notify', [], 'PUT');
  508. }
  509. }
  510. //DNS SEC
  511. public function getSignKeys()
  512. {
  513. $dnssec = new \MGModule\DNSManager2\mgLibs\custom\dns\dnssec\DnsSec();
  514. $serverId = $this->getServerID();
  515. $zoneId = $this->getZoneID();
  516. $ret = json_decode($this->get("servers/$serverId/zones/$zoneId/cryptokeys", []));
  517. foreach ( $ret as $record )
  518. {
  519. foreach ( $record->ds as $dsRecord )
  520. {
  521. $ex = explode(' ', $dsRecord);
  522. $ds = new dns\record\type\DS();
  523. $ds->setKeytag($ex[0]);
  524. $ds->setAlgorithm($ex[1]);
  525. $ds->setDigestType($ex[2]);
  526. $ds->setDigest($ex[3]);
  527. $dnssec->addDs($ds);
  528. }
  529. switch ( strtolower($record->keytype) )
  530. {
  531. case 'csk':
  532. $dnsKeyEx = explode(' ', $record->dnskey);
  533. $dnskey = new dns\record\type\DNSKEY();
  534. $dnskey->setFlags($dnsKeyEx[0]);
  535. $dnskey->setProtocol($dnsKeyEx[1]);
  536. $dnskey->setAlgorithm($dnsKeyEx[2]);
  537. $dnskey->setPublicKey($dnsKeyEx[3]);
  538. $zoneKey = new dns\dnssec\CSK();
  539. $zoneKey->setId($record->id);
  540. $zoneKey->setBits($record->bits);
  541. $zoneKey->setDnsKey($dnskey);
  542. $dnssec->addKey($zoneKey);
  543. break;
  544. case 'ksk':
  545. case 'zsk':
  546. $dnsKeyEx = explode(' ', $record->dnskey);
  547. $dnskey = new dns\record\type\DNSKEY();
  548. $dnskey->setFlags($dnsKeyEx[0]);
  549. $dnskey->setProtocol($dnsKeyEx[1]);
  550. $dnskey->setAlgorithm($dnsKeyEx[2]);
  551. $dnskey->setPublicKey($dnsKeyEx[3]);
  552. $dnskey2 = new dns\record\type\DNSKEY();
  553. $dnskey2->setFlags($dnsKeyEx[0]);
  554. $dnskey2->setProtocol($dnsKeyEx[1]);
  555. $dnskey2->setAlgorithm($dnsKeyEx[2]);
  556. $ksk = new dns\dnssec\KSK();
  557. $ksk->setId($record->id);
  558. $ksk->setBits($record->bits);
  559. $ksk->setDnsKey($dnskey);
  560. $zsk = new dns\dnssec\ZSK();
  561. $zsk->setId($record->id);
  562. $zsk->setBits($record->bits);
  563. $zsk->setDnsKey($dnskey2);
  564. $dnssec->addKey($ksk);
  565. $dnssec->addKey($zsk);
  566. break;
  567. }
  568. }
  569. return $dnssec;
  570. }
  571. /**
  572. * Sign DNS zone
  573. */
  574. public function sign()
  575. {
  576. $this->changeStatus(true);
  577. }
  578. /**
  579. * Unsign DNS zone
  580. */
  581. public function unsign()
  582. {
  583. $this->changeStatus(false);
  584. }
  585. private function changeStatus( $status = true, $dnsZoneName = null )
  586. {
  587. $serverId = $this->getServerID();
  588. $zoneId = $this->getZoneID();
  589. $params = [
  590. 'dnssec' => (boolean)$status,
  591. ];
  592. if ( $dnsZoneName )
  593. {
  594. $params['name'] = rtrim($dnsZoneName, '.') . '.';
  595. }
  596. $this->get("servers/$serverId/zones/$zoneId", $params, 'PUT');
  597. }
  598. /**
  599. * Rectify DNS zone
  600. */
  601. public function rectify()
  602. {
  603. $serverId = $this->getServerID();
  604. $zoneId = $this->getZoneID();
  605. $this->get("servers/$serverId/zones/$zoneId/rectify", [], 'PUT');
  606. }
  607. /**
  608. *
  609. */
  610. public function isSigned()
  611. {
  612. try
  613. {
  614. $serverId = $this->getServerID();
  615. $zoneId = $this->getZoneID();
  616. return json_decode($this->get("servers/$serverId/zones/$zoneId", []))->dnssec;
  617. }
  618. catch ( Exception $ex)
  619. {
  620. return false;
  621. }
  622. }
  623. /**
  624. * Creates rrsets structure from Records array
  625. * @param array $records
  626. * @param string $changeType
  627. * @return mixed
  628. */
  629. private function recordsToParamArray( $records, $changeType = 'REPLACE')
  630. {
  631. $names = $this->getUniqueNames($records);
  632. $types = $this->getUniqueTypes($records);
  633. if( $changeType === 'DELETE' )
  634. {
  635. $types = array_diff($types, ['SOA']);
  636. }
  637. $out = ['rrsets' => []];
  638. foreach ( $names as $name )
  639. {
  640. foreach ( $types as $type )
  641. {
  642. $rrset = new RRSet();
  643. $rrset->name = $name;
  644. $rrset->type = $type;
  645. $rrset->ttl = $this->getRRSetTTL($name, $type, $records);
  646. $rrset->changetype = $changeType;
  647. $rrset->comments = [];
  648. $rrset->records = $this->getRRsetForNameAndType($name, $type, $records);
  649. if ( $rrset->records )
  650. {
  651. $out['rrsets'][] = $rrset;
  652. }
  653. }
  654. }
  655. return $out;
  656. }
  657. /**
  658. * @param array $records
  659. * @return array
  660. */
  661. private function getUniqueNames( $records )
  662. {
  663. $names = [];
  664. /** @var Record $record */
  665. foreach ( $records as $record )
  666. {
  667. $names[] = $record->name;
  668. }
  669. return array_unique($names);
  670. }
  671. /**
  672. * @param array $records
  673. * @return array
  674. */
  675. private function getUniqueTypes( $records )
  676. {
  677. $types = [];
  678. /** @var Record $record */
  679. foreach ( $records as $record )
  680. {
  681. $types[] = $record->type;
  682. }
  683. return array_unique($types);
  684. }
  685. /**
  686. * @param $name
  687. * @param $type
  688. * @param $records
  689. * @return array
  690. */
  691. private function getRRsetForNameAndType( $name, $type, $records )
  692. {
  693. $out = [];
  694. foreach ( $records as $record )
  695. {
  696. if ( $record->name === $name && $record->type === $type )
  697. {
  698. $out[] = $this->formatRecordToPowerDnsFormat($record);
  699. }
  700. }
  701. return $out;
  702. }
  703. /**
  704. * @param Record $record
  705. * @return array
  706. */
  707. private function formatRecordToPowerDnsFormat( Record $record )
  708. {
  709. $recordAdapterClass = 'MGModule\\DNSManager2\\mgLibs\\custom\\dns\\submodules\\PowerDNSv4\\Adapters\\' . $record->type . 'Adapter';
  710. if ( class_exists($recordAdapterClass) && method_exists($recordAdapterClass, 'parseContentToApiFormat') )
  711. {
  712. $content = (new $recordAdapterClass)->parseContentToApiFormat($record->rdata);
  713. }
  714. else
  715. {
  716. $content = str_replace("\t",' ',$record->rdata->toString());
  717. }
  718. return ['content' => $content, 'disabled' => false];
  719. }
  720. /**
  721. * Function removes all RRsets from server
  722. * It's easier when we remove all and then add all since we can't update single record cuz REPLACE duplicates record
  723. * @param $records
  724. * @return bool
  725. * @throws exceptions\DNSSubmoduleException
  726. */
  727. private function removeRRsets( $records )
  728. {
  729. $serverId = $this->getServerID();
  730. $zoneId = $this->getZoneID();
  731. $removeParams = $this->recordsToParamArray($records, 'DELETE');
  732. $this->get("servers/$serverId/zones/$zoneId", $removeParams, 'PATCH');
  733. if( count($this->getRecords()) )
  734. {
  735. throw new exceptions\DNSSubmoduleException(self::ERR_WHILE_REMOVING);
  736. }
  737. return true;
  738. }
  739. /**
  740. * @param string $name
  741. * @param string $type
  742. * @param array $records
  743. *
  744. * @return int
  745. */
  746. private function getRRSetTTL( $name, $type, array $records )
  747. {
  748. foreach( $records as $record )
  749. {
  750. if( $record->type === $type && $record->name === $name )
  751. {
  752. return $record->ttl;
  753. }
  754. }
  755. return 3600;
  756. }
  757. /**
  758. * @param array $params
  759. * @param Record $record
  760. *
  761. * @return mixed
  762. */
  763. private function replaceTargetRRSetTTL( $params, Record $record )
  764. {
  765. /*** @var RRSet $rrset */
  766. foreach( $params['rrsets'] as $index => $rrset )
  767. {
  768. if( $rrset->name === $record->name && $rrset->type === $record->type )
  769. {
  770. $params['rrsets'][$index]->ttl = $record->ttl ?: 3600;
  771. return $params;
  772. }
  773. }
  774. return $params;
  775. }
  776. /**
  777. * @param array $oldRecords
  778. * @param string $errorType
  779. *
  780. * @throws exceptions\DNSSubmoduleException
  781. */
  782. private function restoreOldRecords( $oldRecords, $errorType = self::ERR_WHILE_CREATING )
  783. {
  784. $serverId = $this->getServerID();
  785. $zoneId = $this->getZoneID();
  786. $errors = [];
  787. foreach( $oldRecords as $oldRecord )
  788. {
  789. try
  790. {
  791. $this->get("servers/$serverId/zones/$zoneId", $this->recordToParamsArray($oldRecord), 'PATCH');
  792. }
  793. catch( exceptions\DNSSubmoduleException $e )
  794. {
  795. $errors[] = $e->getMessage();
  796. }
  797. }
  798. if( count($errors) )
  799. {
  800. $msg = $errorType === self::ERR_WHILE_REMOVING ? 'Zone corrupted contact administrator. Affected records: ' : 'Couldn\'t create records: ';
  801. throw new exceptions\DNSSubmoduleException($msg . implode('<br />', $errors));
  802. }
  803. }
  804. /**
  805. * @param RRSet $rrset
  806. *
  807. * @return bool
  808. */
  809. private function filterBrokenRRSets( $rrset )
  810. {
  811. $domainToCheck = rtrim(IdnaHelper::idnaEncode($this->domain), ' .') . '.';
  812. return (bool)preg_match('/' . preg_quote($domainToCheck, '/') . '$/', $rrset->name);
  813. }
  814. /**
  815. * @param array $records
  816. *
  817. * @return array
  818. */
  819. public function incrementSOASerial( array $records )
  820. {
  821. /** @var Record $record */
  822. foreach( $records as &$record )
  823. {
  824. if( $record->type === 'SOA' )
  825. {
  826. $record->rdata->serial++;
  827. return $records;
  828. }
  829. }
  830. }
  831. }