Bind9.php 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595
  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\exceptions\DNSSubmoduleException;
  6. use MGModule\DNSManager2\mgLibs\custom\dns\exceptions\DNSSubmoduleHiddenException;
  7. use \MGModule\DNSManager2\mgLibs\custom\dns\interfaces;
  8. use MGModule\DNSManager2\mgLibs\custom\dns\SubmoduleExceptionCodes;
  9. use MGModule\DNSManager2\mgLibs\custom\dns\utils\Patterns;
  10. use MGModule\DNSManager2\mgLibs\custom\helpers\IdnaHelper;
  11. use MGModule\DNSManager2\mgLibs\custom\helpers\TimeUnitsAliassesHelper;
  12. use phpseclib\Crypt\RSA;
  13. use phpseclib\Net\SFTP;
  14. use phpseclib\Net\SSH2;
  15. /**
  16. * Class Bind9 support for Bind9 server
  17. * @package MGModule\DNSManager2\mgLibs\custom\dns\submodules
  18. */
  19. class Bind9 extends dns\SubmoduleAbstract implements
  20. interfaces\SubmoduleImportInterface,
  21. interfaces\SubmoduleTTLInterface
  22. {
  23. public $configFields = [
  24. 'hostname' => [
  25. 'friendlyName' => 'Hostname/IP',
  26. 'placeholder' => 'example.com',
  27. 'validators' => [
  28. 'required' => 'required',
  29. ]
  30. ],
  31. 'username' => [
  32. 'friendlyName' => 'Username',
  33. 'validators' => [
  34. 'required' => 'required',
  35. ]
  36. ],
  37. 'password' => [
  38. 'friendlyName' => 'User Password',
  39. 'type' => 'password',
  40. ],
  41. 'default_ip' => [
  42. 'friendlyName' => 'Default IP',
  43. 'placeholder' => '10.10.10.10',
  44. 'validators' => [
  45. 'required' => 'required',
  46. 'pattern' => Patterns::IP4_OR_IP6,
  47. ]
  48. ],
  49. 'rsa' => [
  50. 'friendlyName' => 'RSA Private Key',
  51. 'type' => 'textarea',
  52. ],
  53. 'ssl' => [
  54. 'friendlyName' => 'Enable SSL',
  55. 'type' => 'yesno',
  56. ],
  57. 'pathtoconfig' => [
  58. 'friendlyName' => 'Path To Bind9 Config Directory',
  59. 'placeholder' => '/etc/bind/',
  60. 'validators' => [
  61. 'required' => 'required',
  62. ]
  63. ],
  64. 'pathtobackup' => [
  65. 'friendlyName' => 'Path To Backup Directory',
  66. 'placeholder' => '/etc/bind/backups/',
  67. 'validators' => [
  68. 'required' => 'required',
  69. ]
  70. ],
  71. 'reloadService' => [
  72. 'friendlyName' => 'Reload Service Name',
  73. 'type' => 'select',
  74. 'options' => [
  75. 'bind' => 'bind9',
  76. 'named' => 'named'
  77. ],
  78. 'help' => 'Required for reloading service after zone activation'
  79. ],
  80. 'rname' => [
  81. 'friendlyName' => 'Admin Email (RNAME)',
  82. 'placeholder' => 'admin.example.com.',
  83. 'validators' => [
  84. 'required' => 'required'
  85. ],
  86. ],
  87. 'refresh' => [
  88. 'friendlyName' => 'Refresh',
  89. 'placeholder' => '900',
  90. 'validators' => [
  91. 'required' => 'required'
  92. ],
  93. ],
  94. 'retry' => [
  95. 'friendlyName' => 'Retry',
  96. 'placeholder' => '600',
  97. 'validators' => [
  98. 'required' => 'required'
  99. ],
  100. ],
  101. 'expire' => [
  102. 'friendlyName' => 'Expire',
  103. 'placeholder' => '86400',
  104. 'validators' => [
  105. 'required' => 'required'
  106. ],
  107. ],
  108. 'notify_slaves' => [
  109. 'friendlyName' => 'Notify Slaves',
  110. 'type' => 'yesno',
  111. ],
  112. 'master_ip' => [
  113. 'friendlyName' => 'Master IP',
  114. 'placeholder' => '10.10.10.10',
  115. 'help' => 'Required when notify slaves are active'
  116. ],
  117. 'slaves' => [
  118. 'friendlyName' => 'Slaves IP List',
  119. 'type' => 'textarea',
  120. 'placeholder' => '10.10.10.4, 10.10.10.5, 10.10.10.6'
  121. ],
  122. 'customSettings' => [
  123. 'friendlyName' => 'Additional Settings',
  124. 'type' => 'textarea',
  125. 'help' => 'It will be placed in named.conf.local as plain text make sure your format is valid',
  126. 'placeholder' => " max-journal-size 50k;\n min-retry-time 100;"
  127. ]
  128. ];
  129. public $availableTypes = ['A', 'AAAA','AFSDB','ALIAS', 'CAA', 'CNAME', 'DNAME','DS', 'HINFO', 'NS', 'MX', 'TXT', 'SRV', 'NAPTR', 'RP', 'SOA'];
  130. private $soa = [];
  131. /**
  132. * @var SSH2
  133. */
  134. private $ssh2;
  135. /**
  136. * @var SFTP
  137. */
  138. private $sftp;
  139. /**
  140. * @var string
  141. */
  142. private $bindConfigFile;
  143. /**
  144. * @var string
  145. */
  146. private $zoneFilePath;
  147. /**
  148. * @var string
  149. */
  150. private $zoneFile;
  151. /**
  152. * @var array
  153. */
  154. private $records;
  155. /**
  156. * @var int
  157. */
  158. private $ttl;
  159. /**
  160. * Test connection with Server
  161. * @throws DNSSubmoduleException if fails to connect
  162. */
  163. public function testConnection()
  164. {
  165. if ( !$this->ssh2 ) $this->establishSSH2Connection();
  166. if ( !$this->fileExists($this->parsePathToConfig()) )
  167. {
  168. throw new DNSSubmoduleException('File: ' . $this->parsePathToConfig() . ' doesn\'t exists', SubmoduleExceptionCodes::CONNECTION_PROBLEM);
  169. }
  170. if ( !$this->directoryExists($this->parsePathToConfig(true) . 'zones') && !$this->createDirectory($this->parsePathToConfig(true) . 'zones') )
  171. {
  172. throw new DNSSubmoduleException('This module requires writable directory: ' . $this->parsePathToConfig(true) . 'zones');
  173. }
  174. if ( !$this->directoryExists($this->config['pathtobackup']) )
  175. {
  176. throw new DNSSubmoduleException('Directory: ' . $this->config['pathtobackup'] . ' doesn\'t exists', SubmoduleExceptionCodes::CONNECTION_PROBLEM);
  177. }
  178. $tempFileName = $this->parsePathToConfig(true) . uniqid('dnsm2temp', false);
  179. if ( $this->upload($tempFileName, 'Lorem ipsum') )
  180. {
  181. $this->removeFile($tempFileName);
  182. }
  183. else
  184. {
  185. throw new DNSSubmoduleException('Account you specified don\'t have read/write prmissions in bind directory', SubmoduleExceptionCodes::CONNECTION_PROBLEM);
  186. }
  187. return true;
  188. }
  189. /**
  190. * Gets records from server
  191. * @param bool $recordTypeFilter
  192. * @return array
  193. * @throws DNSSubmoduleException
  194. */
  195. public function getRecords( $recordTypeFilter = false )
  196. {
  197. $this->records = $this->records ? : $this->parseFileToStructure($recordTypeFilter);
  198. return $this->records;
  199. }
  200. /**
  201. * Adds a record to server
  202. * @param dns\record\Record $record
  203. * @return bool
  204. * @throws DNSSubmoduleException
  205. * @throws DNSSubmoduleHiddenException
  206. */
  207. public function addRecord( dns\record\Record $record )
  208. {
  209. //Since addRecord can be called in loop we need to download last good configuration every run
  210. //So when somenone will try to create wrong record the function will revert file one step back instead of to point when loop started
  211. $this->updateZoneVariables();
  212. //We just add this to structure and parse it later so no need to do it multiple times since it will give same result
  213. if ( !$this->records )
  214. {
  215. $this->records = $this->parseFileToStructure();
  216. }
  217. if(($record->type === 'SOA') && ($soaRecordPositon = array_search('SOA', array_column($this->records, 'type'), true)) !== false)
  218. {
  219. $record->line = $this->records[$soaRecordPositon]->line;
  220. return $this->editRecord($record);
  221. }
  222. $record->name = $this->replaceNameToBind9Record($record->name);
  223. if ( !$this->validateName($record->name) )
  224. {
  225. throw new DNSSubmoduleHiddenException('The Record ' . $record->name . ' name is invalid and can\'t be saved');
  226. }
  227. //Before adding we should check if the record is valid
  228. $record->rdata->validate();
  229. $this->records[] = $record;
  230. $this->backupConfig($this->bindConfigFile);
  231. $this->backupZone($this->zoneFile);
  232. $this->incrementSOASerial();
  233. $this->upload($this->zoneFilePath, $this->parseStructureToFile());
  234. $zoneValidation = $this->checkZoneChanges();
  235. if ( $zoneValidation !== true )
  236. {
  237. $this->upload($this->zoneFilePath, $this->zoneFile);
  238. throw new DNSSubmoduleHiddenException($record->__toString() .' -> '.$zoneValidation);
  239. }
  240. $this->reloadZone();
  241. return false;
  242. }
  243. /**
  244. * Edits record by matching it with line
  245. * @param dns\record\Record $record
  246. * @return bool
  247. * @throws DNSSubmoduleException
  248. * @throws DNSSubmoduleHiddenException
  249. */
  250. public function editRecord( dns\record\Record $record )
  251. {
  252. if ( !$this->zoneFile )
  253. {
  254. $this->updateZoneVariables();
  255. }
  256. $record->name = $this->replaceNameToBind9Record($record->name);
  257. if ( !$this->validateName($record->name) )
  258. {
  259. throw new DNSSubmoduleHiddenException('Invalid Record Name: ' . $record->name);
  260. }
  261. //Validate data sent by user
  262. $record->rdata->validate();
  263. if ( !$this->records )
  264. {
  265. $this->records = $this->parseFileToStructure();
  266. }
  267. /** @var dns\record\Record $structRecord */
  268. foreach ( $this->records as $index => $structRecord )
  269. {
  270. if ( (int)$structRecord->line === (int)$record->line )
  271. {
  272. $record->ttl = $record->ttl ? : 14400;
  273. $this->records[$index] = $record;
  274. }
  275. }
  276. $this->backupConfig($this->bindConfigFile);
  277. $this->backupZone($this->zoneFile);
  278. $this->incrementSOASerial();
  279. if ( !$this->upload($this->zoneFilePath, $this->parseStructureToFile()) ) throw new DNSSubmoduleException('We were unable to edit record: ' . $record->type . ' in file: ' . $this->zoneFilePath);
  280. $zoneValidation = $this->checkZoneChanges();
  281. if ( $zoneValidation !== true )
  282. {
  283. $this->upload($this->zoneFilePath, $this->zoneFile);
  284. throw new DNSSubmoduleException($zoneValidation);
  285. }
  286. $this->reloadZone();
  287. return true;
  288. }
  289. /**
  290. * Removes record by it's line
  291. * @param dns\record\Record $record
  292. * @throws DNSSubmoduleException
  293. */
  294. public function deleteRecord( dns\record\Record $record )
  295. {
  296. if ( !$this->zoneFilePath )
  297. {
  298. $this->updateZoneVariables();
  299. }
  300. if ( !$this->records )
  301. {
  302. $this->records = $this->parseFileToStructure();
  303. }
  304. /** @var dns\record\Record $structRecord */
  305. foreach ( $this->records as $index => $structRecord )
  306. {
  307. if ( (int)$structRecord->line === (int)$record->line )
  308. {
  309. unset($this->records[$index]);
  310. }
  311. }
  312. $this->backupConfig($this->bindConfigFile);
  313. $this->backupZone($this->zoneFile);
  314. $this->incrementSOASerial();
  315. $this->upload($this->zoneFilePath, $this->parseStructureToFile());
  316. $this->reloadZone();
  317. }
  318. /**
  319. * Checks if zone exists
  320. * @return bool
  321. * @throws DNSSubmoduleException
  322. */
  323. public function zoneExists()
  324. {
  325. return array_key_exists($this->getDomainWithoutDot(), $this->getZones());
  326. }
  327. public function activateZone()
  328. {
  329. $configFile = $this->downloadFile($this->parsePathToConfig());
  330. $zoneFileName = $this->parsePathToConfig(true);
  331. $zoneFileName .= 'zones/db.' . $this->getDomainWithoutDot();
  332. //Get template config file and parse
  333. $path = __DIR__ . '/bind9/sampleZoneMasterConfigFile.txt';
  334. $zoneConfigString = file_get_contents($path);
  335. if ( $this->config['notify_slaves'] === 'on' && trim($this->config['slaves']))
  336. {
  337. $notifyStatus = 'yes';
  338. $slaveIpsArray = explode(',', $this->config['slaves']);
  339. $slaveIps = implode(';', $slaveIpsArray) . ((count($slaveIpsArray)) ? ';' : '');
  340. }
  341. else
  342. {
  343. $notifyStatus = 'no';
  344. $slaveIps = '';
  345. }
  346. $zoneConfigString = str_replace(
  347. [
  348. '{$domain}',
  349. '{$file}',
  350. '{$notify}',
  351. '{$slaveips}',
  352. '{$clientCustom}'
  353. ],
  354. [
  355. $this->getDomainWithoutDot(),
  356. $zoneFileName,
  357. $notifyStatus,
  358. $slaveIps,
  359. $this->config['customSettings']
  360. ],
  361. $zoneConfigString);
  362. $configFile .= $zoneConfigString;
  363. //Get template zone file and parse
  364. $path = __DIR__ . '/bind9/sampleZoneFile.txt';
  365. $zoneFileString = file_get_contents($path);
  366. $ipReplacement = filter_var($this->ip) ? $this->ip : $this->config['default_ip'];
  367. $zoneFileString = str_replace(
  368. [
  369. '{$domain}',
  370. '{$ttl}',
  371. '{$mname}',
  372. '{$rname}',
  373. '{$refresh}',
  374. '{$retry}',
  375. '{$expire}',
  376. '{$ip}'
  377. ],
  378. [
  379. $this->getDomainReplacement(),
  380. 14400,
  381. $this->getDomainReplacement($this->server->getNameservers(1)->name),
  382. $this->config['rname'],
  383. $this->config['refresh'],
  384. $this->config['retry'],
  385. $this->config['expire'],
  386. $ipReplacement
  387. ],
  388. $zoneFileString);
  389. $this->backupConfig($configFile);
  390. $pathToConfig = $this->parsePathToConfig();
  391. if ( !$this->upload($pathToConfig, $configFile) ) throw new DNSSubmoduleException('We couldn\'t create file: ' . $pathToConfig);
  392. if ( !$this->upload($zoneFileName, $zoneFileString) ) throw new DNSSubmoduleException('We couldn\'t create file: ' . $zoneFileName);
  393. $this->reloadBind9();
  394. $this->synchronizeSlaves($this->getDomainWithoutDot());
  395. }
  396. /**
  397. * @param string $domain
  398. * @param string $type
  399. * @return bool
  400. * @throws DNSSubmoduleException
  401. */
  402. protected function synchronizeSlaves( $domain, $type = 'Activate' )
  403. {
  404. if(!trim($this->config['slaves'])) return;
  405. $slaves = explode(',', $this->config['slaves']);
  406. foreach ( $slaves as $slaveIp )
  407. {
  408. $slaveIp = trim($slaveIp);
  409. if(!filter_var($slaveIp,FILTER_VALIDATE_IP))
  410. {
  411. continue;
  412. }
  413. $this->establishSFTPConnection(trim($slaveIp));
  414. $this->establishSSH2Connection(trim($slaveIp));
  415. $configFile = $this->downloadFile($this->parsePathToConfig());
  416. $zoneFileName = 'db.' . $domain;
  417. if ( $type === 'Activate' )
  418. {
  419. $path = __DIR__ . '/bind9/sampleZoneSlaveConfigFile.txt';
  420. $zoneConfigString = file_get_contents($path);
  421. $zoneConfigString = str_replace(
  422. [
  423. '{$domain}',
  424. '{$masterIp}',
  425. '{$file}'
  426. ],
  427. [
  428. $domain,
  429. $this->config['master_ip'],
  430. $zoneFileName
  431. ], $zoneConfigString);
  432. $configFile .= $zoneConfigString;
  433. $this->upload($this->parsePathToConfig(), $configFile);
  434. }
  435. elseif ( $type === 'Terminate' )
  436. {
  437. $regexDomain = '/zone\s+"' . preg_quote($domain, '/') . '\.*"\s+.+?\n};/msi';
  438. $configFile = preg_replace($regexDomain, '', $configFile, 1);
  439. $this->upload($this->parsePathToConfig(), $configFile);
  440. $this->removeFile('/var/cache/bind/db.'.$this->getDomainWithoutDot());
  441. }
  442. $this->reloadBind9();
  443. }
  444. $this->sftp = null;
  445. $this->ssh2 = null;
  446. return true;
  447. }
  448. /**
  449. * Removes zone
  450. * @return bool
  451. * @throws DNSSubmoduleException
  452. */
  453. public function terminateZone()
  454. {
  455. $this->updateZoneVariables();
  456. if ( !$this->zoneFilePath )
  457. {
  458. throw new DNSSubmoduleException('Missing file property in bind9 zone config file');
  459. }
  460. //Remove from main file
  461. $regexDomain = '/zone\s+"' . preg_quote($this->getDomainWithoutDot(), '/') . '\.*"\s+.+?\n};/msi';
  462. $configFile = preg_replace($regexDomain, '', $this->bindConfigFile, 1);
  463. $this->backupConfig($configFile);
  464. $this->backupZone($this->zoneFile);
  465. //Remove zone file
  466. if ( !$this->removeFile($this->zoneFilePath) )
  467. {
  468. throw new DNSSubmoduleException('Can\'t delete zone file wrong file or no permissions');
  469. }
  470. //Remove zone from config
  471. $pathToConfig = $this->parsePathToConfig();
  472. if ( !$this->upload($pathToConfig, $configFile) ) throw new DNSSubmoduleException('We couldn\'t remove zone from config file in path: ' . $pathToConfig);
  473. $this->synchronizeSlaves($this->getDomainWithoutDot(), 'Terminate');
  474. $this->reloadBind9();
  475. return true;
  476. }
  477. /**
  478. * Gets Zones
  479. * @return array
  480. * @throws DNSSubmoduleException
  481. */
  482. public function getZones()
  483. {
  484. $zoneFile = $this->downloadFile($this->parsePathToConfig());
  485. $values = explode("\n", $zoneFile);
  486. $out = [];
  487. foreach ( $values as $nol => $value )
  488. {
  489. if ( preg_match('/in-addr\.arpa/', $value) )
  490. {
  491. continue;
  492. }
  493. if ( !preg_match('/zone\s"(.+)"/', $value, $domain) )
  494. {
  495. continue;
  496. }
  497. if ( !filter_var($domain[1], FILTER_VALIDATE_DOMAIN) )
  498. {
  499. continue;
  500. }
  501. $domain[1] = rtrim($domain[1], '.');
  502. $out[$domain[1]] = '';
  503. }
  504. return $out;
  505. }
  506. /**
  507. * Uploads string to file specified in first parameter to server
  508. *
  509. * @param string $fileLoc Location of the remote file
  510. * @param string $stringFile String with \n sepearation to be placed in file
  511. *
  512. * @return bool
  513. * @throws DNSSubmoduleException if can't connect to host
  514. */
  515. private function upload( $fileLoc, $stringFile )
  516. {
  517. if ( !$this->sftp ) $this->establishSFTPConnection();
  518. return $this->sftp->put($fileLoc, $stringFile, SFTP::SOURCE_STRING);
  519. }
  520. /**
  521. * Reloads the zone on remote
  522. * @return boolean
  523. * @throws DNSSubmoduleException
  524. */
  525. private function reloadZone()
  526. {
  527. if ( !$this->ssh2 ) $this->establishSSH2Connection();
  528. return $this->ssh2->exec('rndc reload ' . $this->getDomainWithoutDot());
  529. }
  530. /**
  531. * Reloads bind 9 (required in zone activation)
  532. * @return string
  533. * @throws DNSSubmoduleException
  534. */
  535. private function reloadBind9()
  536. {
  537. if( !$this->ssh2 ) $this->establishSSH2Connection();
  538. $command = 'systemctl reload ';
  539. if( $this->config['reloadService'] === 'named' )
  540. {
  541. $command .= 'named';
  542. }
  543. else
  544. {
  545. $command .= 'bind9';
  546. }
  547. return $this->ssh2->exec($command);
  548. }
  549. /**
  550. * Checks if file exists on remote
  551. * @param string $file file
  552. * @return bool
  553. * @throws DNSSubmoduleException if couldn't connect
  554. */
  555. private function fileExists( $file )
  556. {
  557. if ( !$this->sftp ) $this->establishSFTPConnection();
  558. return ($this->sftp->get($file) !== false);
  559. }
  560. /**
  561. * Removes file from remote
  562. * @param string $fileLocation Location of the file to be removed
  563. *
  564. * @return bool
  565. * @throws DNSSubmoduleException when he can't find file or have no permissions
  566. */
  567. private function removeFile( $fileLocation )
  568. {
  569. if ( !$this->sftp ) $this->establishSFTPConnection();
  570. return $this->sftp->delete($fileLocation);
  571. }
  572. /**
  573. * Gets file from server
  574. * @param string $file downlaods file from remote
  575. * @return mixed
  576. * @throws DNSSubmoduleException
  577. */
  578. private function downloadFile( $file )
  579. {
  580. if ( !$this->sftp ) $this->establishSFTPConnection();
  581. return $this->sftp->get($file);
  582. }
  583. /**
  584. * Checks if file exists on remote
  585. * @param string $dir absolute dir
  586. * @return bool
  587. * @throws DNSSubmoduleException
  588. */
  589. private function directoryExists( $dir )
  590. {
  591. if ( !$this->sftp ) $this->establishSFTPConnection();
  592. return $this->sftp->chdir($dir);
  593. }
  594. /**
  595. * Creates Directory
  596. * @param string $dir
  597. * @return bool depending on result
  598. * @throws DNSSubmoduleException if couldn't connect to remote
  599. */
  600. private function createDirectory( $dir )
  601. {
  602. if ( !$this->sftp ) $this->establishSFTPConnection();
  603. return $this->sftp->mkdir($dir);
  604. }
  605. /**
  606. * Backups the zone file to backup/zones/ directory
  607. * @param $data
  608. * @return bool
  609. * @throws DNSSubmoduleException
  610. */
  611. private function backupZone( $data )
  612. {
  613. if ( !$this->sftp ) $this->establishSFTPConnection();
  614. $backupDir = rtrim($this->config['pathtobackup'], '/') . '/zones/';
  615. if ( !$this->directoryExists($backupDir) )
  616. {
  617. $this->createDirectory($backupDir);
  618. }
  619. $domain = str_replace('.', '', $this->getDomainWithoutDot());
  620. $backupDir .= $domain . '/';
  621. if ( !$this->directoryExists($backupDir) )
  622. {
  623. $this->createDirectory($backupDir);
  624. }
  625. $filename = date('YmdHis') . '.txt';
  626. return $this->upload($backupDir . $filename, $data);
  627. }
  628. /**
  629. * Backups the config file to backup/config
  630. * @param $data
  631. * @return bool
  632. * @throws DNSSubmoduleException
  633. */
  634. private function backupConfig( $data )
  635. {
  636. if ( !$this->sftp ) $this->establishSFTPConnection();
  637. $backupDir = rtrim($this->config['pathtobackup'], '/') . '/config/';
  638. if ( !$this->directoryExists($backupDir) && !$this->createDirectory($backupDir) )
  639. {
  640. throw new DNSSubmoduleException('We couldn\'t create diretory for backup config');
  641. }
  642. $filename = date('YmdHis') . '.txt';
  643. return $this->upload($backupDir . $filename, $data);
  644. }
  645. /**
  646. * Parses the config path given by user to valid one
  647. * @param bool $dir Specifies should function return the dir name or file name
  648. * @return string
  649. */
  650. public function parsePathToConfig( $dir = false )
  651. {
  652. $path = $this->config['pathtoconfig'];
  653. if ( $dir ) return rtrim(str_replace('named.conf.local', '', $path), '/') . '/';
  654. //If there is no slash at the end and it's not config file we add slash
  655. if ( substr($path, -1, 1) !== '/' && strpos($path, 'named.conf.local') === false )
  656. {
  657. $path .= '/';
  658. }
  659. //If there is no name of config at the end we add the default
  660. if ( strpos($path, 'named.conf.local') === false )
  661. {
  662. $path .= 'named.conf.local';
  663. }
  664. return $path;
  665. }
  666. /**
  667. * Adds to array of soa properties property which will be later used for creating record
  668. *
  669. * @param mixed $soaProp Property to be addded
  670. * @throws DNSSubmoduleException if there is to many elements in array
  671. */
  672. private function addSOAInformation( $soaProp )
  673. {
  674. $this->soa[] = $soaProp;
  675. if ( count($this->soa) > 12 )
  676. {
  677. throw new DNSSubmoduleException('There is problably missing ")" in your SOA record definition');
  678. }
  679. }
  680. /**
  681. * Creates SOA record from properties previously addded to $this->soa
  682. *
  683. * @param int|string $defTtl Default ttl of zone if not specified soa ttl will be set to 14400
  684. *
  685. * @return dns\record\Record
  686. * @throws DNSSubmoduleException
  687. */
  688. public function createSOAFromArray( $defTtl = 14400 )
  689. {
  690. $recordTypeIndex = $this->getIndexOfRecordType($this->soa, 'SOA');
  691. $record = new dns\record\Record();
  692. $record->name = $this->soa[0];
  693. $record->type = 'SOA';
  694. $ttlAndClass = $this->getTTLAndClassFromValues($recordTypeIndex, $this->soa, $defTtl);
  695. $record->ttl = $ttlAndClass['ttl'];
  696. $record->class = $ttlAndClass['class'];
  697. //Make sure we have name|class|ttl to ommit later checking for it
  698. if ( $recordTypeIndex === 2 )
  699. {
  700. array_splice($this->soa, 2, 1, [$record->class, $record->ttl]);
  701. }
  702. $soaRecord = new dns\record\type\SOA();
  703. $soaRecord->mname = $this->soa[4];
  704. $soaRecord->rname = $this->soa[5];
  705. $soaRecord->serial = TimeUnitsAliassesHelper::convertToSeconds($this->soa[6]);
  706. $soaRecord->refresh = TimeUnitsAliassesHelper::convertToSeconds($this->soa[7]);
  707. $soaRecord->retry = TimeUnitsAliassesHelper::convertToSeconds($this->soa[8]);
  708. $soaRecord->expire = TimeUnitsAliassesHelper::convertToSeconds($this->soa[9]);
  709. $soaRecord->minimum = TimeUnitsAliassesHelper::convertToSeconds($this->soa[10]);
  710. $record->rdata = $soaRecord;
  711. $record->customData = $record->rdlength = '';
  712. $record->line = $this->soa['line'];
  713. return $record;
  714. }
  715. /**
  716. * Gets location of zone file from main config of bind
  717. * @param string $bindConfigFile bindConfigFile in string downloaded from bind server
  718. * @param string $zoneName Name of zone to search
  719. *
  720. * @return string
  721. * @throws DNSSubmoduleException if can't find zone or when there is no path specified
  722. */
  723. private function getZoneFileLocationFrombindConfigFile( $bindConfigFile, $zoneName )
  724. {
  725. $preg = '/zone\s+"' . preg_quote($zoneName, '/') . '\.*".+?\n};/msi';
  726. if ( !preg_match($preg, $bindConfigFile, $matches) )
  727. {
  728. throw new DNSSubmoduleException('We couldn\'t find ' . $zoneName . ' in your bindconfig file');
  729. }
  730. if ( !preg_match('/file\s"(.+?)"/mi', $matches[0], $zonePath) )
  731. {
  732. throw new DNSSubmoduleException('We couldn\'t find path in your ' . $zoneName . ' config section in your bind config file');
  733. }
  734. return $zonePath[1];
  735. }
  736. /**
  737. * Convert Time aliasses to normal int seconds
  738. * @param $line
  739. *
  740. * @return string|string[]|null
  741. * @throws Exception
  742. */
  743. private function convertTime( $line )
  744. {
  745. $values = preg_split('/\s+/', $line);
  746. foreach ( $values as &$value )
  747. {
  748. if ( preg_match('/(\d+)([MHDW])/i', $value, $match) )
  749. {
  750. $replacement = TimeUnitsAliassesHelper::convertToSeconds($match[0]);
  751. $value = preg_replace('/(\d+)([MHDW])/i', $replacement, $value);
  752. }
  753. }
  754. return implode(' ', $values);
  755. }
  756. /**
  757. * Replaces @ and whitespaces to domain names with dot attached
  758. * @param string $line
  759. * @return string
  760. */
  761. private function replaceName( $line )
  762. {
  763. //Explode line to array
  764. $values = preg_split('/\s+/', $line);
  765. //Record name is blank/whitespace/@ so we remove place there zone name
  766. if ( $values[0] === '@' || !trim($values[0]) )
  767. {
  768. $values[0] = $this->getDomainReplacement();
  769. $line = implode(' ', $values);
  770. }
  771. return $line;
  772. }
  773. /**
  774. * Creates record name with @ if record name matches domain and trims domain from the end
  775. * @param string $name
  776. * @return string
  777. */
  778. private function replaceNameToBind9Record( $name )
  779. {
  780. $name = trim(preg_replace('/\.?' . preg_quote(IdnaHelper::idnaDecode($this->getDomainWithoutDot())) . '\.+$/', '', $name, 1));
  781. return $name === '' ? '@' : $name;
  782. }
  783. /**
  784. * Filters the records by their type
  785. * @param array $lines
  786. * @param string $recordTypeFilter
  787. * @return array
  788. */
  789. private function filterRecords( $lines, $recordTypeFilter )
  790. {
  791. foreach ( $lines as $nol => $line )
  792. {
  793. //Explode line to array
  794. $values = preg_split('/\s+/', $line);
  795. //Get record type from array
  796. $recordType = $this->getRecordType($values);
  797. //If filter is set and record doesn't match we remove it
  798. if ( $recordType !== $recordTypeFilter )
  799. {
  800. unset($lines[$nol]);
  801. continue;
  802. }
  803. }
  804. return $lines;
  805. }
  806. /**
  807. * Removes everything in line after ";"
  808. * @param string $line
  809. * @return false|string
  810. */
  811. private function removeComments( $line )
  812. {
  813. //Replaces everything after ; but only if semicolon is not between quotation marks
  814. //I spent on it about 1 hour plz don't remove
  815. return preg_replace('/(?:^\s*;|(?:;(?!.+"))).+/', '', $line);
  816. }
  817. /**
  818. * Returns the index of type property in record
  819. * @param $values
  820. * @param $recordType
  821. * @return false|int|string
  822. */
  823. private function getIndexOfRecordType( $values, $recordType )
  824. {
  825. return array_search($recordType, $values, false);
  826. }
  827. /** Returns record type
  828. * @param array $values exploded line by \s
  829. * @return bool
  830. */
  831. private function getRecordType( $values )
  832. {
  833. foreach ( $values as $value )
  834. {
  835. if ( in_array($value, $this->availableTypes, false) )
  836. {
  837. return $value;
  838. }
  839. }
  840. return false;
  841. }
  842. /**
  843. * Function looks for $TTL in file
  844. * @param array $values exploded zone file by "\n"
  845. * @return int|null
  846. */
  847. private function findDefaultTtl( array $values )
  848. {
  849. $ttl = null;
  850. foreach ( $values as $line )
  851. {
  852. if ( !preg_match('/\$TTL\s+(\d+)/', $line, $match) )
  853. {
  854. continue;
  855. }
  856. $ttl = (int)$match[1];
  857. break;
  858. }
  859. return $ttl;
  860. }
  861. /** Gets SOA record from zone file
  862. * @param array $lines
  863. * @param int $ttl
  864. * @return dns\record\Record|null
  865. * @throws DNSSubmoduleException
  866. */
  867. private function getSoaRecord( array $lines, $ttl = 14400 )
  868. {
  869. $out = null;
  870. foreach ( $lines as $nol => $line )
  871. {
  872. $values = preg_split('/\s+/', $line);
  873. $recordType = $this->getRecordType($values);
  874. if ( $recordType === 'SOA' )
  875. {
  876. //Check if SOA is multiline
  877. $multilineSoa = strpos($line, '(') !== false && strpos($line, ')') === false;
  878. $this->soa['line'] = (int)$nol;
  879. break;
  880. }
  881. }
  882. if ( !isset($this->soa['line']) ) return $out;
  883. if ( isset($multilineSoa) )
  884. {
  885. $lines[$this->soa['line']] = $this->replaceName($lines[$this->soa['line']]);
  886. if ( $multilineSoa )
  887. {
  888. //We don't know length of SOA so we go from it's beggining to the end of the file
  889. foreach ( range($this->soa['line'], count($lines) - 1) as $i )
  890. {
  891. //Spliting by whitespace
  892. $values = preg_split('/\s+/', $lines[$i]);
  893. //We add all values from line if for some reason someone specified more than one
  894. foreach ( $values as $soaProp )
  895. {
  896. if ( ($soaProp === '(') || !trim($soaProp) )
  897. {
  898. continue;
  899. }
  900. if ( $soaProp !== ')' )
  901. {
  902. $this->addSOAInformation($soaProp);
  903. }
  904. else
  905. {
  906. $out = $this->createSOAFromArray($ttl);
  907. }
  908. }
  909. //We found end of the soa we can leave
  910. if ( strpos($lines[$i], ')') !== false )
  911. {
  912. break;
  913. }
  914. }
  915. }
  916. else
  917. {
  918. //Spliting by whitespace
  919. $values = preg_split('/\s+/', $lines[$this->soa['line']]);
  920. //We add all values from line
  921. foreach ( $values as $soaProp )
  922. {
  923. if ( $soaProp !== '(' && $soaProp !== ')' && $soaProp && $soaProp !== ' ' )
  924. {
  925. $this->addSOAInformation($soaProp);
  926. }
  927. }
  928. $out = $this->createSOAFromArray($ttl);
  929. }
  930. }
  931. return $out;
  932. }
  933. /** Removes comments, empty lines,$ORIGIN and $TTL lines and converts time aliases to seconds
  934. * @param array $lines
  935. * @return array
  936. * @throws Exception when can't convert time
  937. */
  938. private function setAndRemoveUsefullValues( $lines )
  939. {
  940. foreach ( $lines as $nol => $line )
  941. {
  942. if ( !trim($line) )
  943. {
  944. unset($lines[$nol]);
  945. continue;
  946. }
  947. $lines[$nol] = $this->removeComments($line);
  948. if ( strpos($line, '$ORIGIN') !== false )
  949. {
  950. unset($lines[$nol]);
  951. continue;
  952. }
  953. if ( preg_match('/\$TTL\s+(\d+)/', $line, $match) )
  954. {
  955. $this->ttl = (int)$match[1];
  956. unset($lines[$nol]);
  957. continue;
  958. }
  959. //All time aliasses to seconds
  960. $lines[$nol] = $this->convertTime($lines[$nol]);
  961. }
  962. return $lines;
  963. }
  964. /**
  965. * Generate the bind9 file from structure
  966. * @return string
  967. */
  968. private function parseStructureToFile()
  969. {
  970. $recordsArray = [
  971. '$ORIGIN ' . $this->getDomainReplacement(),
  972. '$TTL ' . $this->ttl
  973. ];
  974. usort($this->records, static function ( $a, $b )
  975. {
  976. $recordAllignment = ['SOA', 'A', 'AAAA', 'NS', 'MX', 'TXT'];
  977. if ( !in_array($a->type, $recordAllignment, true) && !in_array($b->type, $recordAllignment, true) ) return 0;
  978. if ( !in_array($a->type, $recordAllignment, true) ) return 1;
  979. if ( !in_array($b->type, $recordAllignment, true) ) return -1;
  980. if ( $a->type === $b->type )
  981. {
  982. return $a->name < $b->name ? -1 : 1;
  983. }
  984. return array_search($a->type, $recordAllignment, true) < array_search($b->type, $recordAllignment, true) ? -1 : 1;
  985. });
  986. /** @var dns\record\Record $record */
  987. foreach ( $this->records as $record )
  988. {
  989. $record->name = $this->replaceNameToBind9Record($record->name);
  990. $recordArray = [
  991. $record->name,
  992. $record->class,
  993. $record->ttl,
  994. $record->type
  995. ];
  996. $recordRdata = $record->rdata->toString();
  997. if( strlen($recordRdata) > 255 )
  998. {
  999. $recordsArray = array_merge($recordsArray, $this->splitMultilineRecord($recordArray,$recordRdata));
  1000. }
  1001. else
  1002. {
  1003. $recordsArray[] = implode(' ', $recordArray) . ' ' .$recordRdata;
  1004. }
  1005. }
  1006. return implode("\n", $recordsArray) . "\n";
  1007. }
  1008. /**
  1009. * Removes soa from file checking for it beeing multiline
  1010. *
  1011. * @param array $lines file sp
  1012. * @param int $offset the location of soa in lines
  1013. * @return array
  1014. */
  1015. private function removeSoaFromLine( array $lines, $offset = 0 )
  1016. {
  1017. $multilineSoa = (strpos($lines[$offset], '(') !== false) && (strpos($lines[$offset], ')') === false);
  1018. if ( $multilineSoa )
  1019. {
  1020. //Removes lines until finds closing round bracket
  1021. foreach ( range($offset, count($lines) - 1) as $i )
  1022. {
  1023. $templn = $lines[$i];
  1024. unset($lines[$i]);
  1025. if ( strpos($templn, ')') !== false )
  1026. {
  1027. break;
  1028. }
  1029. }
  1030. }
  1031. else
  1032. {
  1033. unset($lines[$offset]);
  1034. }
  1035. return $lines;
  1036. }
  1037. /**
  1038. * Function checks if there is $ORIGIN in file and matches zone name
  1039. * Not used cause of the $ORIGIN beeing not required in file
  1040. * @param $lines
  1041. * @return mixed|null
  1042. * @throws DNSSubmoduleException
  1043. */
  1044. private function validateFile( $lines )
  1045. {
  1046. $name = null;
  1047. foreach ( $lines as $line )
  1048. {
  1049. if ( !preg_match('/\$ORIGIN\s+(.+)/', $line, $match) )
  1050. {
  1051. continue;
  1052. }
  1053. $name = $match[1];
  1054. break;
  1055. }
  1056. if ( !$name )
  1057. {
  1058. throw new DNSSubmoduleException('Missing $ORIGIN in your file');
  1059. }
  1060. if ( $name !== $this->getDomainWithoutDot() && $name !== $this->getDomainReplacement() )
  1061. {
  1062. throw new DNSSubmoduleException('$ORIGIN in file doesn\'t match the zone name');
  1063. }
  1064. return $name;
  1065. }
  1066. /**
  1067. * Since it's ain't easy task here is function for it
  1068. * @param int $indexOfRecordType
  1069. * @param array $values
  1070. * @param int $defaultTTL
  1071. * @param string $defaultClass
  1072. * @return array
  1073. * @throws DNSSubmoduleException
  1074. */
  1075. private function getTTLAndClassFromValues( $indexOfRecordType, array $values, $defaultTTL, $defaultClass = 'IN' )
  1076. {
  1077. $out = [];
  1078. //We have to check where is ttl and class cause they can be shuffled or one of them can be ommited
  1079. if ( $indexOfRecordType === 3 )
  1080. {
  1081. if ( is_numeric(trim($values[2])) )
  1082. {
  1083. $out['class'] = $values[1];
  1084. $out['ttl'] = (int)$values[2];
  1085. }
  1086. else
  1087. {
  1088. $out['class'] = $values[2];
  1089. $out['ttl'] = (int)$values[1];
  1090. }
  1091. }
  1092. elseif ( $indexOfRecordType === 2 )
  1093. {
  1094. //If there is only 2 we will have to check which one
  1095. if ( is_numeric(trim($values[1])) )
  1096. {
  1097. $out['ttl'] = (int)$values[1];
  1098. $out['class'] = $defaultClass;
  1099. }
  1100. else
  1101. {
  1102. $out['class'] = $values[1];
  1103. $out['ttl'] = (int)$defaultTTL;
  1104. }
  1105. }
  1106. else
  1107. {
  1108. throw new DNSSubmoduleException('Error while processing your record: ' . $values[0] . ' your record values are in wrong order');
  1109. }
  1110. return $out;
  1111. }
  1112. /**
  1113. * Validates the domain name if there is no dot it wasn't the propper domain name
  1114. * @param $name
  1115. * @return bool
  1116. */
  1117. private function validateName( $name )
  1118. {
  1119. return substr($name, -1, 1) !== '.';
  1120. }
  1121. /**
  1122. * @param array $lines lines of file
  1123. * @param int $line number of line to edit
  1124. * @param int $counter specifies offset from which start removing files
  1125. * @return array with unset lines
  1126. * @throws DNSSubmoduleException if we couldn't find the ")" in file
  1127. */
  1128. private function clearMultiline( $lines, $line, $counter = 0 )
  1129. {
  1130. //Make sure to don't look for lines that are not in file
  1131. $linesToEndOfFile = count($lines) - $line;
  1132. while ( $counter < $linesToEndOfFile )
  1133. {
  1134. if ( $counter > 10 )
  1135. {
  1136. throw new DNSSubmoduleException('Missing closing round bracket in zone file in SOA record you tried to edit');
  1137. }
  1138. //We have to save it somewhere before deleting it
  1139. $currentLine = $lines[$line + $counter];
  1140. unset($lines[$line + $counter]);
  1141. if ( strpos($currentLine, ')') !== false )
  1142. {
  1143. break;
  1144. }
  1145. $counter++;
  1146. }
  1147. return $lines;
  1148. }
  1149. /**
  1150. * For matching and replacing purposes returns domain name with dot attached
  1151. * @param null|string $domain If provided function will append dot to parameter domain instead of global zone
  1152. * @return string
  1153. */
  1154. private function getDomainReplacement( $domain = null )
  1155. {
  1156. $targetDomain = $domain ? : $this->domain;
  1157. return rtrim($targetDomain, '.') . '.';
  1158. }
  1159. /**
  1160. * For matching purposes removes dot (if exist) from the end of the domain
  1161. * @param null|string $domain If provided function will trim dot from parameter domain instead of global zone
  1162. * @return string
  1163. */
  1164. private function getDomainWithoutDot( $domain = null )
  1165. {
  1166. $targetDomain = $domain ? : $this->domain;
  1167. return rtrim($targetDomain, '.');
  1168. }
  1169. /**
  1170. * Sets path to zone and config and downloads file
  1171. * @return bool
  1172. * @throws DNSSubmoduleException
  1173. */
  1174. private function updateZoneVariables()
  1175. {
  1176. $this->bindConfigFile = $this->downloadFile($this->parsePathToConfig());
  1177. $this->zoneFilePath = $this->getZoneFileLocationFrombindConfigFile($this->bindConfigFile, $this->getDomainWithoutDot());
  1178. $this->zoneFile = $this->downloadFile($this->zoneFilePath);
  1179. return true;
  1180. }
  1181. /**Creates SSH2 instance
  1182. * @param null $ip
  1183. * @return SSH2
  1184. * @throws DNSSubmoduleException
  1185. */
  1186. private function establishSSH2Connection( $ip = null )
  1187. {
  1188. $connctIp = $ip ? : $this->config['hostname'];
  1189. $this->ssh2 = new SSH2($connctIp);
  1190. if ( $this->config['rsa'] )
  1191. {
  1192. $auth = new RSA();
  1193. $auth->loadKey($this->config['rsa']);
  1194. }
  1195. else
  1196. {
  1197. $auth = $this->config['password'];
  1198. }
  1199. if ( !$this->ssh2->login($this->config['username'], $auth) )
  1200. {
  1201. throw new DNSSubmoduleException('Wrong authentication details');
  1202. }
  1203. return $this->ssh2;
  1204. }
  1205. /** Creates SFTP Instance
  1206. * @param null $ip
  1207. * @return SFTP
  1208. * @throws DNSSubmoduleException
  1209. */
  1210. private function establishSFTPConnection( $ip = null )
  1211. {
  1212. $connctIp = $ip ? : $this->config['hostname'];
  1213. $this->sftp = new SFTP($connctIp);
  1214. if ( $this->config['rsa'] )
  1215. {
  1216. $auth = new RSA();
  1217. $auth->loadKey($this->config['rsa']);
  1218. }
  1219. else
  1220. {
  1221. $auth = $this->config['password'];
  1222. }
  1223. if ( !$this->sftp->login($this->config['username'], $auth) )
  1224. {
  1225. throw new DNSSubmoduleException('We couldn\'t upload changes to your server check permissions for given account');
  1226. }
  1227. return $this->sftp;
  1228. }
  1229. /**
  1230. * @param $recordTypeFilter
  1231. * @return array
  1232. * @throws DNSSubmoduleException
  1233. */
  1234. private function parseFileToStructure( $recordTypeFilter = false )
  1235. {
  1236. $this->updateZoneVariables();
  1237. $lines = explode("\n", $this->zoneFile);
  1238. $out = [];
  1239. // $this->validateFile($lines);
  1240. $lines = $this->setAndRemoveUsefullValues($lines);
  1241. $this->ttl = $this->ttl ? : 14400;
  1242. $soa = $this->getSoaRecord($lines, $this->ttl);
  1243. //If SOA in file we add it to out records and remove it so it makes it easier to parse file later (since soa can be multiline)
  1244. if ( $soa )
  1245. {
  1246. $out[] = $soa;
  1247. $lines = $this->removeSoaFromLine($lines, $soa->line);
  1248. }
  1249. //If multiple SOA records we throw exception
  1250. if ( $this->getSoaRecord($lines) )
  1251. {
  1252. throw new DNSSubmoduleException('You can have only one SOA record in file');
  1253. }
  1254. $lines = $this->trimUnnecessaryBrackets($lines);
  1255. $lines = $this->convertMultilineRecords($lines);
  1256. //If filter records types is set we filter any other out
  1257. if ( $recordTypeFilter )
  1258. {
  1259. $lines = $this->filterRecords($lines, $recordTypeFilter);
  1260. }
  1261. //Everything else in file should be record
  1262. foreach( $lines as $nol => $line )
  1263. {
  1264. if( !trim($line) ) continue;
  1265. $record = $this->buildRecord($line, $nol);
  1266. if( $record )
  1267. {
  1268. $out[] = $record;
  1269. }
  1270. }
  1271. return $out;
  1272. }
  1273. /**
  1274. * Since Bin9 requires to update this value every time
  1275. */
  1276. private function incrementSOASerial()
  1277. {
  1278. foreach ( $this->records as &$structRecord )
  1279. {
  1280. if ( $structRecord->type === 'SOA' )
  1281. {
  1282. $structRecord->rdata->serial++;
  1283. }
  1284. }
  1285. return $this;
  1286. }
  1287. /**
  1288. * Runs named-checkzone on domain
  1289. * @return string
  1290. * @throws DNSSubmoduleException
  1291. */
  1292. private function checkZoneChanges()
  1293. {
  1294. $this->establishSSH2Connection();
  1295. $pathToZone = $this->zoneFilePath;
  1296. $result = $this->ssh2->exec("named-checkzone {$this->getDomainWithoutDot()} {$pathToZone}");
  1297. if ( $this->ssh2->getExitStatus() )
  1298. {
  1299. return $result;
  1300. }
  1301. return true;
  1302. }
  1303. private function splitMultilineRecord( array $recordArray, string $recordRdata )
  1304. {
  1305. $out = [];
  1306. $out[] = implode(' ', $recordArray) . ' (';
  1307. foreach( str_split($recordRdata, 200) as $splittedRdata )
  1308. {
  1309. $out[] = '"' . trim($splittedRdata, '"') . '"';
  1310. }
  1311. $out[count($out) - 1] .= ')';
  1312. return $out;
  1313. }
  1314. private function trimUnnecessaryBrackets( array $lines )
  1315. {
  1316. foreach( $lines as &$line )
  1317. {
  1318. if( strpos($line, '(') !== false && strpos($line, ')') !== false )
  1319. {
  1320. $line = str_replace(['(', ')'], '', $line);
  1321. }
  1322. }
  1323. return $lines;
  1324. }
  1325. private function buildRecord( $line, $nol )
  1326. {
  1327. //Replace @/blank/whitespace in first val for valid domain names
  1328. $line = $this->replaceName($line);
  1329. $values = preg_split('/\s+/', $line);
  1330. $recordType = $this->getRecordType($values);
  1331. $indexOfRecordType = $this->getIndexOfRecordType($values, $recordType);
  1332. //If record type is not supported
  1333. if( !$recordType )
  1334. {
  1335. throw new DNSSubmoduleException('We don\'t support this record type in line: ' . ($nol + 1));
  1336. }
  1337. $record = new dns\record\Record();
  1338. $record->name = $values[0];
  1339. $record->line = $nol;
  1340. $record->type = $values[$indexOfRecordType];
  1341. $record->customData = $record->rdlength = '';
  1342. $ttlandclass = $this->getTTLAndClassFromValues($indexOfRecordType, $values, $this->ttl);
  1343. $record->class = $ttlandclass['class'];
  1344. $record->ttl = $ttlandclass['ttl'];
  1345. //We split string by whitespace but txt record type has spaces in one of the params so we have to merge them
  1346. if( $recordType === 'TXT' )
  1347. {
  1348. preg_match('/".+"/', $line, $match);
  1349. $values[$indexOfRecordType + 1] = str_replace("\t", ' ', $match[0]);
  1350. }
  1351. $className = 'MGModule\DNSManager2\mgLibs\custom\dns\record\type\\' . $recordType;
  1352. $additionalVars = array_keys(get_class_vars($className));
  1353. $recordTypeClass = new $className;
  1354. foreach( $additionalVars as $index => $prop )
  1355. {
  1356. $recordTypeClass->$prop = $values[$indexOfRecordType + 1 + $index];
  1357. }
  1358. $record->rdata = $recordTypeClass;
  1359. return $record;
  1360. }
  1361. private function convertMultilineRecords( array $lines )
  1362. {
  1363. $joinToLine = false;
  1364. foreach( $lines as $nol => $line )
  1365. {
  1366. //If we are currently joining values and there is no closing bracket just join it
  1367. if( $joinToLine !== false && strpos($line, ')') === false )
  1368. {
  1369. $lines[$joinToLine] .= trim($line);
  1370. unset($lines[$nol]);
  1371. continue;
  1372. }
  1373. //If there is closing bracket and we are joining values we add trimmed value without closing bracket and end joining
  1374. if( $joinToLine !== false && strpos($line, ')') !== false )
  1375. {
  1376. $lines[$joinToLine] .= trim(str_replace(')', '', $line));
  1377. $joinToLine = false;
  1378. unset($lines[$nol]);
  1379. continue;
  1380. }
  1381. //If there is opening bracket in line and no closing brackets we start joining values
  1382. if( strpos($line, '(') !== false && strpos($line, ')') === false )
  1383. {
  1384. $lines[$nol] = str_replace('(', '', $line);
  1385. $joinToLine = $nol;
  1386. }
  1387. }
  1388. //Since we join values from multiple lines they are connected with double double quotes
  1389. //We want to create single record so we have to remove this double quotes
  1390. return array_map(static function( $line ) {
  1391. return str_replace('""', '', $line);
  1392. }, $lines);
  1393. }
  1394. }