| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595 |
- <?php
- namespace MGModule\DNSManager2\mgLibs\custom\dns\submodules;
- use Exception;
- use \MGModule\DNSManager2\mgLibs\custom\dns;
- use MGModule\DNSManager2\mgLibs\custom\dns\exceptions\DNSSubmoduleException;
- use MGModule\DNSManager2\mgLibs\custom\dns\exceptions\DNSSubmoduleHiddenException;
- use \MGModule\DNSManager2\mgLibs\custom\dns\interfaces;
- use MGModule\DNSManager2\mgLibs\custom\dns\SubmoduleExceptionCodes;
- use MGModule\DNSManager2\mgLibs\custom\dns\utils\Patterns;
- use MGModule\DNSManager2\mgLibs\custom\helpers\IdnaHelper;
- use MGModule\DNSManager2\mgLibs\custom\helpers\TimeUnitsAliassesHelper;
- use phpseclib\Crypt\RSA;
- use phpseclib\Net\SFTP;
- use phpseclib\Net\SSH2;
- /**
- * Class Bind9 support for Bind9 server
- * @package MGModule\DNSManager2\mgLibs\custom\dns\submodules
- */
- class Bind9 extends dns\SubmoduleAbstract implements
- interfaces\SubmoduleImportInterface,
- interfaces\SubmoduleTTLInterface
- {
- public $configFields = [
- 'hostname' => [
- 'friendlyName' => 'Hostname/IP',
- 'placeholder' => 'example.com',
- 'validators' => [
- 'required' => 'required',
- ]
- ],
- 'username' => [
- 'friendlyName' => 'Username',
- 'validators' => [
- 'required' => 'required',
- ]
- ],
- 'password' => [
- 'friendlyName' => 'User Password',
- 'type' => 'password',
- ],
- 'default_ip' => [
- 'friendlyName' => 'Default IP',
- 'placeholder' => '10.10.10.10',
- 'validators' => [
- 'required' => 'required',
- 'pattern' => Patterns::IP4_OR_IP6,
- ]
- ],
- 'rsa' => [
- 'friendlyName' => 'RSA Private Key',
- 'type' => 'textarea',
- ],
- 'ssl' => [
- 'friendlyName' => 'Enable SSL',
- 'type' => 'yesno',
- ],
- 'pathtoconfig' => [
- 'friendlyName' => 'Path To Bind9 Config Directory',
- 'placeholder' => '/etc/bind/',
- 'validators' => [
- 'required' => 'required',
- ]
- ],
- 'pathtobackup' => [
- 'friendlyName' => 'Path To Backup Directory',
- 'placeholder' => '/etc/bind/backups/',
- 'validators' => [
- 'required' => 'required',
- ]
- ],
- 'reloadService' => [
- 'friendlyName' => 'Reload Service Name',
- 'type' => 'select',
- 'options' => [
- 'bind' => 'bind9',
- 'named' => 'named'
- ],
- 'help' => 'Required for reloading service after zone activation'
- ],
- 'rname' => [
- 'friendlyName' => 'Admin Email (RNAME)',
- 'placeholder' => 'admin.example.com.',
- 'validators' => [
- 'required' => 'required'
- ],
- ],
- 'refresh' => [
- 'friendlyName' => 'Refresh',
- 'placeholder' => '900',
- 'validators' => [
- 'required' => 'required'
- ],
- ],
- 'retry' => [
- 'friendlyName' => 'Retry',
- 'placeholder' => '600',
- 'validators' => [
- 'required' => 'required'
- ],
- ],
- 'expire' => [
- 'friendlyName' => 'Expire',
- 'placeholder' => '86400',
- 'validators' => [
- 'required' => 'required'
- ],
- ],
- 'notify_slaves' => [
- 'friendlyName' => 'Notify Slaves',
- 'type' => 'yesno',
- ],
- 'master_ip' => [
- 'friendlyName' => 'Master IP',
- 'placeholder' => '10.10.10.10',
- 'help' => 'Required when notify slaves are active'
- ],
- 'slaves' => [
- 'friendlyName' => 'Slaves IP List',
- 'type' => 'textarea',
- 'placeholder' => '10.10.10.4, 10.10.10.5, 10.10.10.6'
- ],
- 'customSettings' => [
- 'friendlyName' => 'Additional Settings',
- 'type' => 'textarea',
- 'help' => 'It will be placed in named.conf.local as plain text make sure your format is valid',
- 'placeholder' => " max-journal-size 50k;\n min-retry-time 100;"
- ]
- ];
- public $availableTypes = ['A', 'AAAA','AFSDB','ALIAS', 'CAA', 'CNAME', 'DNAME','DS', 'HINFO', 'NS', 'MX', 'TXT', 'SRV', 'NAPTR', 'RP', 'SOA'];
- private $soa = [];
- /**
- * @var SSH2
- */
- private $ssh2;
- /**
- * @var SFTP
- */
- private $sftp;
- /**
- * @var string
- */
- private $bindConfigFile;
- /**
- * @var string
- */
- private $zoneFilePath;
- /**
- * @var string
- */
- private $zoneFile;
- /**
- * @var array
- */
- private $records;
- /**
- * @var int
- */
- private $ttl;
- /**
- * Test connection with Server
- * @throws DNSSubmoduleException if fails to connect
- */
- public function testConnection()
- {
- if ( !$this->ssh2 ) $this->establishSSH2Connection();
- if ( !$this->fileExists($this->parsePathToConfig()) )
- {
- throw new DNSSubmoduleException('File: ' . $this->parsePathToConfig() . ' doesn\'t exists', SubmoduleExceptionCodes::CONNECTION_PROBLEM);
- }
- if ( !$this->directoryExists($this->parsePathToConfig(true) . 'zones') && !$this->createDirectory($this->parsePathToConfig(true) . 'zones') )
- {
- throw new DNSSubmoduleException('This module requires writable directory: ' . $this->parsePathToConfig(true) . 'zones');
- }
- if ( !$this->directoryExists($this->config['pathtobackup']) )
- {
- throw new DNSSubmoduleException('Directory: ' . $this->config['pathtobackup'] . ' doesn\'t exists', SubmoduleExceptionCodes::CONNECTION_PROBLEM);
- }
- $tempFileName = $this->parsePathToConfig(true) . uniqid('dnsm2temp', false);
- if ( $this->upload($tempFileName, 'Lorem ipsum') )
- {
- $this->removeFile($tempFileName);
- }
- else
- {
- throw new DNSSubmoduleException('Account you specified don\'t have read/write prmissions in bind directory', SubmoduleExceptionCodes::CONNECTION_PROBLEM);
- }
- return true;
- }
- /**
- * Gets records from server
- * @param bool $recordTypeFilter
- * @return array
- * @throws DNSSubmoduleException
- */
- public function getRecords( $recordTypeFilter = false )
- {
- $this->records = $this->records ? : $this->parseFileToStructure($recordTypeFilter);
- return $this->records;
- }
- /**
- * Adds a record to server
- * @param dns\record\Record $record
- * @return bool
- * @throws DNSSubmoduleException
- * @throws DNSSubmoduleHiddenException
- */
- public function addRecord( dns\record\Record $record )
- {
- //Since addRecord can be called in loop we need to download last good configuration every run
- //So when somenone will try to create wrong record the function will revert file one step back instead of to point when loop started
- $this->updateZoneVariables();
- //We just add this to structure and parse it later so no need to do it multiple times since it will give same result
- if ( !$this->records )
- {
- $this->records = $this->parseFileToStructure();
- }
- if(($record->type === 'SOA') && ($soaRecordPositon = array_search('SOA', array_column($this->records, 'type'), true)) !== false)
- {
- $record->line = $this->records[$soaRecordPositon]->line;
- return $this->editRecord($record);
- }
- $record->name = $this->replaceNameToBind9Record($record->name);
- if ( !$this->validateName($record->name) )
- {
- throw new DNSSubmoduleHiddenException('The Record ' . $record->name . ' name is invalid and can\'t be saved');
- }
- //Before adding we should check if the record is valid
- $record->rdata->validate();
- $this->records[] = $record;
- $this->backupConfig($this->bindConfigFile);
- $this->backupZone($this->zoneFile);
- $this->incrementSOASerial();
- $this->upload($this->zoneFilePath, $this->parseStructureToFile());
- $zoneValidation = $this->checkZoneChanges();
- if ( $zoneValidation !== true )
- {
- $this->upload($this->zoneFilePath, $this->zoneFile);
- throw new DNSSubmoduleHiddenException($record->__toString() .' -> '.$zoneValidation);
- }
- $this->reloadZone();
- return false;
- }
- /**
- * Edits record by matching it with line
- * @param dns\record\Record $record
- * @return bool
- * @throws DNSSubmoduleException
- * @throws DNSSubmoduleHiddenException
- */
- public function editRecord( dns\record\Record $record )
- {
- if ( !$this->zoneFile )
- {
- $this->updateZoneVariables();
- }
- $record->name = $this->replaceNameToBind9Record($record->name);
- if ( !$this->validateName($record->name) )
- {
- throw new DNSSubmoduleHiddenException('Invalid Record Name: ' . $record->name);
- }
- //Validate data sent by user
- $record->rdata->validate();
- if ( !$this->records )
- {
- $this->records = $this->parseFileToStructure();
- }
- /** @var dns\record\Record $structRecord */
- foreach ( $this->records as $index => $structRecord )
- {
- if ( (int)$structRecord->line === (int)$record->line )
- {
- $record->ttl = $record->ttl ? : 14400;
- $this->records[$index] = $record;
- }
- }
- $this->backupConfig($this->bindConfigFile);
- $this->backupZone($this->zoneFile);
- $this->incrementSOASerial();
- if ( !$this->upload($this->zoneFilePath, $this->parseStructureToFile()) ) throw new DNSSubmoduleException('We were unable to edit record: ' . $record->type . ' in file: ' . $this->zoneFilePath);
- $zoneValidation = $this->checkZoneChanges();
- if ( $zoneValidation !== true )
- {
- $this->upload($this->zoneFilePath, $this->zoneFile);
- throw new DNSSubmoduleException($zoneValidation);
- }
- $this->reloadZone();
- return true;
- }
- /**
- * Removes record by it's line
- * @param dns\record\Record $record
- * @throws DNSSubmoduleException
- */
- public function deleteRecord( dns\record\Record $record )
- {
- if ( !$this->zoneFilePath )
- {
- $this->updateZoneVariables();
- }
- if ( !$this->records )
- {
- $this->records = $this->parseFileToStructure();
- }
- /** @var dns\record\Record $structRecord */
- foreach ( $this->records as $index => $structRecord )
- {
- if ( (int)$structRecord->line === (int)$record->line )
- {
- unset($this->records[$index]);
- }
- }
- $this->backupConfig($this->bindConfigFile);
- $this->backupZone($this->zoneFile);
- $this->incrementSOASerial();
- $this->upload($this->zoneFilePath, $this->parseStructureToFile());
- $this->reloadZone();
- }
- /**
- * Checks if zone exists
- * @return bool
- * @throws DNSSubmoduleException
- */
- public function zoneExists()
- {
- return array_key_exists($this->getDomainWithoutDot(), $this->getZones());
- }
- public function activateZone()
- {
- $configFile = $this->downloadFile($this->parsePathToConfig());
- $zoneFileName = $this->parsePathToConfig(true);
- $zoneFileName .= 'zones/db.' . $this->getDomainWithoutDot();
- //Get template config file and parse
- $path = __DIR__ . '/bind9/sampleZoneMasterConfigFile.txt';
- $zoneConfigString = file_get_contents($path);
- if ( $this->config['notify_slaves'] === 'on' && trim($this->config['slaves']))
- {
- $notifyStatus = 'yes';
- $slaveIpsArray = explode(',', $this->config['slaves']);
- $slaveIps = implode(';', $slaveIpsArray) . ((count($slaveIpsArray)) ? ';' : '');
- }
- else
- {
- $notifyStatus = 'no';
- $slaveIps = '';
- }
- $zoneConfigString = str_replace(
- [
- '{$domain}',
- '{$file}',
- '{$notify}',
- '{$slaveips}',
- '{$clientCustom}'
- ],
- [
- $this->getDomainWithoutDot(),
- $zoneFileName,
- $notifyStatus,
- $slaveIps,
- $this->config['customSettings']
- ],
- $zoneConfigString);
- $configFile .= $zoneConfigString;
- //Get template zone file and parse
- $path = __DIR__ . '/bind9/sampleZoneFile.txt';
- $zoneFileString = file_get_contents($path);
- $ipReplacement = filter_var($this->ip) ? $this->ip : $this->config['default_ip'];
- $zoneFileString = str_replace(
- [
- '{$domain}',
- '{$ttl}',
- '{$mname}',
- '{$rname}',
- '{$refresh}',
- '{$retry}',
- '{$expire}',
- '{$ip}'
- ],
- [
- $this->getDomainReplacement(),
- 14400,
- $this->getDomainReplacement($this->server->getNameservers(1)->name),
- $this->config['rname'],
- $this->config['refresh'],
- $this->config['retry'],
- $this->config['expire'],
- $ipReplacement
- ],
- $zoneFileString);
- $this->backupConfig($configFile);
- $pathToConfig = $this->parsePathToConfig();
- if ( !$this->upload($pathToConfig, $configFile) ) throw new DNSSubmoduleException('We couldn\'t create file: ' . $pathToConfig);
- if ( !$this->upload($zoneFileName, $zoneFileString) ) throw new DNSSubmoduleException('We couldn\'t create file: ' . $zoneFileName);
- $this->reloadBind9();
- $this->synchronizeSlaves($this->getDomainWithoutDot());
- }
- /**
- * @param string $domain
- * @param string $type
- * @return bool
- * @throws DNSSubmoduleException
- */
- protected function synchronizeSlaves( $domain, $type = 'Activate' )
- {
- if(!trim($this->config['slaves'])) return;
- $slaves = explode(',', $this->config['slaves']);
- foreach ( $slaves as $slaveIp )
- {
- $slaveIp = trim($slaveIp);
- if(!filter_var($slaveIp,FILTER_VALIDATE_IP))
- {
- continue;
- }
- $this->establishSFTPConnection(trim($slaveIp));
- $this->establishSSH2Connection(trim($slaveIp));
- $configFile = $this->downloadFile($this->parsePathToConfig());
- $zoneFileName = 'db.' . $domain;
- if ( $type === 'Activate' )
- {
- $path = __DIR__ . '/bind9/sampleZoneSlaveConfigFile.txt';
- $zoneConfigString = file_get_contents($path);
- $zoneConfigString = str_replace(
- [
- '{$domain}',
- '{$masterIp}',
- '{$file}'
- ],
- [
- $domain,
- $this->config['master_ip'],
- $zoneFileName
- ], $zoneConfigString);
- $configFile .= $zoneConfigString;
- $this->upload($this->parsePathToConfig(), $configFile);
- }
- elseif ( $type === 'Terminate' )
- {
- $regexDomain = '/zone\s+"' . preg_quote($domain, '/') . '\.*"\s+.+?\n};/msi';
- $configFile = preg_replace($regexDomain, '', $configFile, 1);
- $this->upload($this->parsePathToConfig(), $configFile);
- $this->removeFile('/var/cache/bind/db.'.$this->getDomainWithoutDot());
- }
- $this->reloadBind9();
- }
- $this->sftp = null;
- $this->ssh2 = null;
- return true;
- }
- /**
- * Removes zone
- * @return bool
- * @throws DNSSubmoduleException
- */
- public function terminateZone()
- {
- $this->updateZoneVariables();
- if ( !$this->zoneFilePath )
- {
- throw new DNSSubmoduleException('Missing file property in bind9 zone config file');
- }
- //Remove from main file
- $regexDomain = '/zone\s+"' . preg_quote($this->getDomainWithoutDot(), '/') . '\.*"\s+.+?\n};/msi';
- $configFile = preg_replace($regexDomain, '', $this->bindConfigFile, 1);
- $this->backupConfig($configFile);
- $this->backupZone($this->zoneFile);
- //Remove zone file
- if ( !$this->removeFile($this->zoneFilePath) )
- {
- throw new DNSSubmoduleException('Can\'t delete zone file wrong file or no permissions');
- }
- //Remove zone from config
- $pathToConfig = $this->parsePathToConfig();
- if ( !$this->upload($pathToConfig, $configFile) ) throw new DNSSubmoduleException('We couldn\'t remove zone from config file in path: ' . $pathToConfig);
- $this->synchronizeSlaves($this->getDomainWithoutDot(), 'Terminate');
- $this->reloadBind9();
- return true;
- }
- /**
- * Gets Zones
- * @return array
- * @throws DNSSubmoduleException
- */
- public function getZones()
- {
- $zoneFile = $this->downloadFile($this->parsePathToConfig());
- $values = explode("\n", $zoneFile);
- $out = [];
- foreach ( $values as $nol => $value )
- {
- if ( preg_match('/in-addr\.arpa/', $value) )
- {
- continue;
- }
- if ( !preg_match('/zone\s"(.+)"/', $value, $domain) )
- {
- continue;
- }
- if ( !filter_var($domain[1], FILTER_VALIDATE_DOMAIN) )
- {
- continue;
- }
- $domain[1] = rtrim($domain[1], '.');
- $out[$domain[1]] = '';
- }
- return $out;
- }
- /**
- * Uploads string to file specified in first parameter to server
- *
- * @param string $fileLoc Location of the remote file
- * @param string $stringFile String with \n sepearation to be placed in file
- *
- * @return bool
- * @throws DNSSubmoduleException if can't connect to host
- */
- private function upload( $fileLoc, $stringFile )
- {
- if ( !$this->sftp ) $this->establishSFTPConnection();
- return $this->sftp->put($fileLoc, $stringFile, SFTP::SOURCE_STRING);
- }
- /**
- * Reloads the zone on remote
- * @return boolean
- * @throws DNSSubmoduleException
- */
- private function reloadZone()
- {
- if ( !$this->ssh2 ) $this->establishSSH2Connection();
- return $this->ssh2->exec('rndc reload ' . $this->getDomainWithoutDot());
- }
- /**
- * Reloads bind 9 (required in zone activation)
- * @return string
- * @throws DNSSubmoduleException
- */
- private function reloadBind9()
- {
- if( !$this->ssh2 ) $this->establishSSH2Connection();
- $command = 'systemctl reload ';
- if( $this->config['reloadService'] === 'named' )
- {
- $command .= 'named';
- }
- else
- {
- $command .= 'bind9';
- }
- return $this->ssh2->exec($command);
- }
- /**
- * Checks if file exists on remote
- * @param string $file file
- * @return bool
- * @throws DNSSubmoduleException if couldn't connect
- */
- private function fileExists( $file )
- {
- if ( !$this->sftp ) $this->establishSFTPConnection();
- return ($this->sftp->get($file) !== false);
- }
- /**
- * Removes file from remote
- * @param string $fileLocation Location of the file to be removed
- *
- * @return bool
- * @throws DNSSubmoduleException when he can't find file or have no permissions
- */
- private function removeFile( $fileLocation )
- {
- if ( !$this->sftp ) $this->establishSFTPConnection();
- return $this->sftp->delete($fileLocation);
- }
- /**
- * Gets file from server
- * @param string $file downlaods file from remote
- * @return mixed
- * @throws DNSSubmoduleException
- */
- private function downloadFile( $file )
- {
- if ( !$this->sftp ) $this->establishSFTPConnection();
- return $this->sftp->get($file);
- }
- /**
- * Checks if file exists on remote
- * @param string $dir absolute dir
- * @return bool
- * @throws DNSSubmoduleException
- */
- private function directoryExists( $dir )
- {
- if ( !$this->sftp ) $this->establishSFTPConnection();
- return $this->sftp->chdir($dir);
- }
- /**
- * Creates Directory
- * @param string $dir
- * @return bool depending on result
- * @throws DNSSubmoduleException if couldn't connect to remote
- */
- private function createDirectory( $dir )
- {
- if ( !$this->sftp ) $this->establishSFTPConnection();
- return $this->sftp->mkdir($dir);
- }
- /**
- * Backups the zone file to backup/zones/ directory
- * @param $data
- * @return bool
- * @throws DNSSubmoduleException
- */
- private function backupZone( $data )
- {
- if ( !$this->sftp ) $this->establishSFTPConnection();
- $backupDir = rtrim($this->config['pathtobackup'], '/') . '/zones/';
- if ( !$this->directoryExists($backupDir) )
- {
- $this->createDirectory($backupDir);
- }
- $domain = str_replace('.', '', $this->getDomainWithoutDot());
- $backupDir .= $domain . '/';
- if ( !$this->directoryExists($backupDir) )
- {
- $this->createDirectory($backupDir);
- }
- $filename = date('YmdHis') . '.txt';
- return $this->upload($backupDir . $filename, $data);
- }
- /**
- * Backups the config file to backup/config
- * @param $data
- * @return bool
- * @throws DNSSubmoduleException
- */
- private function backupConfig( $data )
- {
- if ( !$this->sftp ) $this->establishSFTPConnection();
- $backupDir = rtrim($this->config['pathtobackup'], '/') . '/config/';
- if ( !$this->directoryExists($backupDir) && !$this->createDirectory($backupDir) )
- {
- throw new DNSSubmoduleException('We couldn\'t create diretory for backup config');
- }
- $filename = date('YmdHis') . '.txt';
- return $this->upload($backupDir . $filename, $data);
- }
- /**
- * Parses the config path given by user to valid one
- * @param bool $dir Specifies should function return the dir name or file name
- * @return string
- */
- public function parsePathToConfig( $dir = false )
- {
- $path = $this->config['pathtoconfig'];
- if ( $dir ) return rtrim(str_replace('named.conf.local', '', $path), '/') . '/';
- //If there is no slash at the end and it's not config file we add slash
- if ( substr($path, -1, 1) !== '/' && strpos($path, 'named.conf.local') === false )
- {
- $path .= '/';
- }
- //If there is no name of config at the end we add the default
- if ( strpos($path, 'named.conf.local') === false )
- {
- $path .= 'named.conf.local';
- }
- return $path;
- }
- /**
- * Adds to array of soa properties property which will be later used for creating record
- *
- * @param mixed $soaProp Property to be addded
- * @throws DNSSubmoduleException if there is to many elements in array
- */
- private function addSOAInformation( $soaProp )
- {
- $this->soa[] = $soaProp;
- if ( count($this->soa) > 12 )
- {
- throw new DNSSubmoduleException('There is problably missing ")" in your SOA record definition');
- }
- }
- /**
- * Creates SOA record from properties previously addded to $this->soa
- *
- * @param int|string $defTtl Default ttl of zone if not specified soa ttl will be set to 14400
- *
- * @return dns\record\Record
- * @throws DNSSubmoduleException
- */
- public function createSOAFromArray( $defTtl = 14400 )
- {
- $recordTypeIndex = $this->getIndexOfRecordType($this->soa, 'SOA');
- $record = new dns\record\Record();
- $record->name = $this->soa[0];
- $record->type = 'SOA';
- $ttlAndClass = $this->getTTLAndClassFromValues($recordTypeIndex, $this->soa, $defTtl);
- $record->ttl = $ttlAndClass['ttl'];
- $record->class = $ttlAndClass['class'];
- //Make sure we have name|class|ttl to ommit later checking for it
- if ( $recordTypeIndex === 2 )
- {
- array_splice($this->soa, 2, 1, [$record->class, $record->ttl]);
- }
- $soaRecord = new dns\record\type\SOA();
- $soaRecord->mname = $this->soa[4];
- $soaRecord->rname = $this->soa[5];
- $soaRecord->serial = TimeUnitsAliassesHelper::convertToSeconds($this->soa[6]);
- $soaRecord->refresh = TimeUnitsAliassesHelper::convertToSeconds($this->soa[7]);
- $soaRecord->retry = TimeUnitsAliassesHelper::convertToSeconds($this->soa[8]);
- $soaRecord->expire = TimeUnitsAliassesHelper::convertToSeconds($this->soa[9]);
- $soaRecord->minimum = TimeUnitsAliassesHelper::convertToSeconds($this->soa[10]);
- $record->rdata = $soaRecord;
- $record->customData = $record->rdlength = '';
- $record->line = $this->soa['line'];
- return $record;
- }
- /**
- * Gets location of zone file from main config of bind
- * @param string $bindConfigFile bindConfigFile in string downloaded from bind server
- * @param string $zoneName Name of zone to search
- *
- * @return string
- * @throws DNSSubmoduleException if can't find zone or when there is no path specified
- */
- private function getZoneFileLocationFrombindConfigFile( $bindConfigFile, $zoneName )
- {
- $preg = '/zone\s+"' . preg_quote($zoneName, '/') . '\.*".+?\n};/msi';
- if ( !preg_match($preg, $bindConfigFile, $matches) )
- {
- throw new DNSSubmoduleException('We couldn\'t find ' . $zoneName . ' in your bindconfig file');
- }
- if ( !preg_match('/file\s"(.+?)"/mi', $matches[0], $zonePath) )
- {
- throw new DNSSubmoduleException('We couldn\'t find path in your ' . $zoneName . ' config section in your bind config file');
- }
- return $zonePath[1];
- }
- /**
- * Convert Time aliasses to normal int seconds
- * @param $line
- *
- * @return string|string[]|null
- * @throws Exception
- */
- private function convertTime( $line )
- {
- $values = preg_split('/\s+/', $line);
- foreach ( $values as &$value )
- {
- if ( preg_match('/(\d+)([MHDW])/i', $value, $match) )
- {
- $replacement = TimeUnitsAliassesHelper::convertToSeconds($match[0]);
- $value = preg_replace('/(\d+)([MHDW])/i', $replacement, $value);
- }
- }
- return implode(' ', $values);
- }
- /**
- * Replaces @ and whitespaces to domain names with dot attached
- * @param string $line
- * @return string
- */
- private function replaceName( $line )
- {
- //Explode line to array
- $values = preg_split('/\s+/', $line);
- //Record name is blank/whitespace/@ so we remove place there zone name
- if ( $values[0] === '@' || !trim($values[0]) )
- {
- $values[0] = $this->getDomainReplacement();
- $line = implode(' ', $values);
- }
- return $line;
- }
- /**
- * Creates record name with @ if record name matches domain and trims domain from the end
- * @param string $name
- * @return string
- */
- private function replaceNameToBind9Record( $name )
- {
- $name = trim(preg_replace('/\.?' . preg_quote(IdnaHelper::idnaDecode($this->getDomainWithoutDot())) . '\.+$/', '', $name, 1));
- return $name === '' ? '@' : $name;
- }
- /**
- * Filters the records by their type
- * @param array $lines
- * @param string $recordTypeFilter
- * @return array
- */
- private function filterRecords( $lines, $recordTypeFilter )
- {
- foreach ( $lines as $nol => $line )
- {
- //Explode line to array
- $values = preg_split('/\s+/', $line);
- //Get record type from array
- $recordType = $this->getRecordType($values);
- //If filter is set and record doesn't match we remove it
- if ( $recordType !== $recordTypeFilter )
- {
- unset($lines[$nol]);
- continue;
- }
- }
- return $lines;
- }
- /**
- * Removes everything in line after ";"
- * @param string $line
- * @return false|string
- */
- private function removeComments( $line )
- {
- //Replaces everything after ; but only if semicolon is not between quotation marks
- //I spent on it about 1 hour plz don't remove
- return preg_replace('/(?:^\s*;|(?:;(?!.+"))).+/', '', $line);
- }
- /**
- * Returns the index of type property in record
- * @param $values
- * @param $recordType
- * @return false|int|string
- */
- private function getIndexOfRecordType( $values, $recordType )
- {
- return array_search($recordType, $values, false);
- }
- /** Returns record type
- * @param array $values exploded line by \s
- * @return bool
- */
- private function getRecordType( $values )
- {
- foreach ( $values as $value )
- {
- if ( in_array($value, $this->availableTypes, false) )
- {
- return $value;
- }
- }
- return false;
- }
- /**
- * Function looks for $TTL in file
- * @param array $values exploded zone file by "\n"
- * @return int|null
- */
- private function findDefaultTtl( array $values )
- {
- $ttl = null;
- foreach ( $values as $line )
- {
- if ( !preg_match('/\$TTL\s+(\d+)/', $line, $match) )
- {
- continue;
- }
- $ttl = (int)$match[1];
- break;
- }
- return $ttl;
- }
- /** Gets SOA record from zone file
- * @param array $lines
- * @param int $ttl
- * @return dns\record\Record|null
- * @throws DNSSubmoduleException
- */
- private function getSoaRecord( array $lines, $ttl = 14400 )
- {
- $out = null;
- foreach ( $lines as $nol => $line )
- {
- $values = preg_split('/\s+/', $line);
- $recordType = $this->getRecordType($values);
- if ( $recordType === 'SOA' )
- {
- //Check if SOA is multiline
- $multilineSoa = strpos($line, '(') !== false && strpos($line, ')') === false;
- $this->soa['line'] = (int)$nol;
- break;
- }
- }
- if ( !isset($this->soa['line']) ) return $out;
- if ( isset($multilineSoa) )
- {
- $lines[$this->soa['line']] = $this->replaceName($lines[$this->soa['line']]);
- if ( $multilineSoa )
- {
- //We don't know length of SOA so we go from it's beggining to the end of the file
- foreach ( range($this->soa['line'], count($lines) - 1) as $i )
- {
- //Spliting by whitespace
- $values = preg_split('/\s+/', $lines[$i]);
- //We add all values from line if for some reason someone specified more than one
- foreach ( $values as $soaProp )
- {
- if ( ($soaProp === '(') || !trim($soaProp) )
- {
- continue;
- }
- if ( $soaProp !== ')' )
- {
- $this->addSOAInformation($soaProp);
- }
- else
- {
- $out = $this->createSOAFromArray($ttl);
- }
- }
- //We found end of the soa we can leave
- if ( strpos($lines[$i], ')') !== false )
- {
- break;
- }
- }
- }
- else
- {
- //Spliting by whitespace
- $values = preg_split('/\s+/', $lines[$this->soa['line']]);
- //We add all values from line
- foreach ( $values as $soaProp )
- {
- if ( $soaProp !== '(' && $soaProp !== ')' && $soaProp && $soaProp !== ' ' )
- {
- $this->addSOAInformation($soaProp);
- }
- }
- $out = $this->createSOAFromArray($ttl);
- }
- }
- return $out;
- }
- /** Removes comments, empty lines,$ORIGIN and $TTL lines and converts time aliases to seconds
- * @param array $lines
- * @return array
- * @throws Exception when can't convert time
- */
- private function setAndRemoveUsefullValues( $lines )
- {
- foreach ( $lines as $nol => $line )
- {
- if ( !trim($line) )
- {
- unset($lines[$nol]);
- continue;
- }
- $lines[$nol] = $this->removeComments($line);
- if ( strpos($line, '$ORIGIN') !== false )
- {
- unset($lines[$nol]);
- continue;
- }
- if ( preg_match('/\$TTL\s+(\d+)/', $line, $match) )
- {
- $this->ttl = (int)$match[1];
- unset($lines[$nol]);
- continue;
- }
- //All time aliasses to seconds
- $lines[$nol] = $this->convertTime($lines[$nol]);
- }
- return $lines;
- }
- /**
- * Generate the bind9 file from structure
- * @return string
- */
- private function parseStructureToFile()
- {
- $recordsArray = [
- '$ORIGIN ' . $this->getDomainReplacement(),
- '$TTL ' . $this->ttl
- ];
- usort($this->records, static function ( $a, $b )
- {
- $recordAllignment = ['SOA', 'A', 'AAAA', 'NS', 'MX', 'TXT'];
- if ( !in_array($a->type, $recordAllignment, true) && !in_array($b->type, $recordAllignment, true) ) return 0;
- if ( !in_array($a->type, $recordAllignment, true) ) return 1;
- if ( !in_array($b->type, $recordAllignment, true) ) return -1;
- if ( $a->type === $b->type )
- {
- return $a->name < $b->name ? -1 : 1;
- }
- return array_search($a->type, $recordAllignment, true) < array_search($b->type, $recordAllignment, true) ? -1 : 1;
- });
- /** @var dns\record\Record $record */
- foreach ( $this->records as $record )
- {
- $record->name = $this->replaceNameToBind9Record($record->name);
- $recordArray = [
- $record->name,
- $record->class,
- $record->ttl,
- $record->type
- ];
- $recordRdata = $record->rdata->toString();
- if( strlen($recordRdata) > 255 )
- {
- $recordsArray = array_merge($recordsArray, $this->splitMultilineRecord($recordArray,$recordRdata));
- }
- else
- {
- $recordsArray[] = implode(' ', $recordArray) . ' ' .$recordRdata;
- }
- }
- return implode("\n", $recordsArray) . "\n";
- }
- /**
- * Removes soa from file checking for it beeing multiline
- *
- * @param array $lines file sp
- * @param int $offset the location of soa in lines
- * @return array
- */
- private function removeSoaFromLine( array $lines, $offset = 0 )
- {
- $multilineSoa = (strpos($lines[$offset], '(') !== false) && (strpos($lines[$offset], ')') === false);
- if ( $multilineSoa )
- {
- //Removes lines until finds closing round bracket
- foreach ( range($offset, count($lines) - 1) as $i )
- {
- $templn = $lines[$i];
- unset($lines[$i]);
- if ( strpos($templn, ')') !== false )
- {
- break;
- }
- }
- }
- else
- {
- unset($lines[$offset]);
- }
- return $lines;
- }
- /**
- * Function checks if there is $ORIGIN in file and matches zone name
- * Not used cause of the $ORIGIN beeing not required in file
- * @param $lines
- * @return mixed|null
- * @throws DNSSubmoduleException
- */
- private function validateFile( $lines )
- {
- $name = null;
- foreach ( $lines as $line )
- {
- if ( !preg_match('/\$ORIGIN\s+(.+)/', $line, $match) )
- {
- continue;
- }
- $name = $match[1];
- break;
- }
- if ( !$name )
- {
- throw new DNSSubmoduleException('Missing $ORIGIN in your file');
- }
- if ( $name !== $this->getDomainWithoutDot() && $name !== $this->getDomainReplacement() )
- {
- throw new DNSSubmoduleException('$ORIGIN in file doesn\'t match the zone name');
- }
- return $name;
- }
- /**
- * Since it's ain't easy task here is function for it
- * @param int $indexOfRecordType
- * @param array $values
- * @param int $defaultTTL
- * @param string $defaultClass
- * @return array
- * @throws DNSSubmoduleException
- */
- private function getTTLAndClassFromValues( $indexOfRecordType, array $values, $defaultTTL, $defaultClass = 'IN' )
- {
- $out = [];
- //We have to check where is ttl and class cause they can be shuffled or one of them can be ommited
- if ( $indexOfRecordType === 3 )
- {
- if ( is_numeric(trim($values[2])) )
- {
- $out['class'] = $values[1];
- $out['ttl'] = (int)$values[2];
- }
- else
- {
- $out['class'] = $values[2];
- $out['ttl'] = (int)$values[1];
- }
- }
- elseif ( $indexOfRecordType === 2 )
- {
- //If there is only 2 we will have to check which one
- if ( is_numeric(trim($values[1])) )
- {
- $out['ttl'] = (int)$values[1];
- $out['class'] = $defaultClass;
- }
- else
- {
- $out['class'] = $values[1];
- $out['ttl'] = (int)$defaultTTL;
- }
- }
- else
- {
- throw new DNSSubmoduleException('Error while processing your record: ' . $values[0] . ' your record values are in wrong order');
- }
- return $out;
- }
- /**
- * Validates the domain name if there is no dot it wasn't the propper domain name
- * @param $name
- * @return bool
- */
- private function validateName( $name )
- {
- return substr($name, -1, 1) !== '.';
- }
- /**
- * @param array $lines lines of file
- * @param int $line number of line to edit
- * @param int $counter specifies offset from which start removing files
- * @return array with unset lines
- * @throws DNSSubmoduleException if we couldn't find the ")" in file
- */
- private function clearMultiline( $lines, $line, $counter = 0 )
- {
- //Make sure to don't look for lines that are not in file
- $linesToEndOfFile = count($lines) - $line;
- while ( $counter < $linesToEndOfFile )
- {
- if ( $counter > 10 )
- {
- throw new DNSSubmoduleException('Missing closing round bracket in zone file in SOA record you tried to edit');
- }
- //We have to save it somewhere before deleting it
- $currentLine = $lines[$line + $counter];
- unset($lines[$line + $counter]);
- if ( strpos($currentLine, ')') !== false )
- {
- break;
- }
- $counter++;
- }
- return $lines;
- }
- /**
- * For matching and replacing purposes returns domain name with dot attached
- * @param null|string $domain If provided function will append dot to parameter domain instead of global zone
- * @return string
- */
- private function getDomainReplacement( $domain = null )
- {
- $targetDomain = $domain ? : $this->domain;
- return rtrim($targetDomain, '.') . '.';
- }
- /**
- * For matching purposes removes dot (if exist) from the end of the domain
- * @param null|string $domain If provided function will trim dot from parameter domain instead of global zone
- * @return string
- */
- private function getDomainWithoutDot( $domain = null )
- {
- $targetDomain = $domain ? : $this->domain;
- return rtrim($targetDomain, '.');
- }
- /**
- * Sets path to zone and config and downloads file
- * @return bool
- * @throws DNSSubmoduleException
- */
- private function updateZoneVariables()
- {
- $this->bindConfigFile = $this->downloadFile($this->parsePathToConfig());
- $this->zoneFilePath = $this->getZoneFileLocationFrombindConfigFile($this->bindConfigFile, $this->getDomainWithoutDot());
- $this->zoneFile = $this->downloadFile($this->zoneFilePath);
- return true;
- }
- /**Creates SSH2 instance
- * @param null $ip
- * @return SSH2
- * @throws DNSSubmoduleException
- */
- private function establishSSH2Connection( $ip = null )
- {
- $connctIp = $ip ? : $this->config['hostname'];
- $this->ssh2 = new SSH2($connctIp);
- if ( $this->config['rsa'] )
- {
- $auth = new RSA();
- $auth->loadKey($this->config['rsa']);
- }
- else
- {
- $auth = $this->config['password'];
- }
- if ( !$this->ssh2->login($this->config['username'], $auth) )
- {
- throw new DNSSubmoduleException('Wrong authentication details');
- }
- return $this->ssh2;
- }
- /** Creates SFTP Instance
- * @param null $ip
- * @return SFTP
- * @throws DNSSubmoduleException
- */
- private function establishSFTPConnection( $ip = null )
- {
- $connctIp = $ip ? : $this->config['hostname'];
- $this->sftp = new SFTP($connctIp);
- if ( $this->config['rsa'] )
- {
- $auth = new RSA();
- $auth->loadKey($this->config['rsa']);
- }
- else
- {
- $auth = $this->config['password'];
- }
- if ( !$this->sftp->login($this->config['username'], $auth) )
- {
- throw new DNSSubmoduleException('We couldn\'t upload changes to your server check permissions for given account');
- }
- return $this->sftp;
- }
- /**
- * @param $recordTypeFilter
- * @return array
- * @throws DNSSubmoduleException
- */
- private function parseFileToStructure( $recordTypeFilter = false )
- {
- $this->updateZoneVariables();
- $lines = explode("\n", $this->zoneFile);
- $out = [];
- // $this->validateFile($lines);
- $lines = $this->setAndRemoveUsefullValues($lines);
- $this->ttl = $this->ttl ? : 14400;
- $soa = $this->getSoaRecord($lines, $this->ttl);
- //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)
- if ( $soa )
- {
- $out[] = $soa;
- $lines = $this->removeSoaFromLine($lines, $soa->line);
- }
- //If multiple SOA records we throw exception
- if ( $this->getSoaRecord($lines) )
- {
- throw new DNSSubmoduleException('You can have only one SOA record in file');
- }
- $lines = $this->trimUnnecessaryBrackets($lines);
- $lines = $this->convertMultilineRecords($lines);
- //If filter records types is set we filter any other out
- if ( $recordTypeFilter )
- {
- $lines = $this->filterRecords($lines, $recordTypeFilter);
- }
- //Everything else in file should be record
- foreach( $lines as $nol => $line )
- {
- if( !trim($line) ) continue;
- $record = $this->buildRecord($line, $nol);
- if( $record )
- {
- $out[] = $record;
- }
- }
- return $out;
- }
- /**
- * Since Bin9 requires to update this value every time
- */
- private function incrementSOASerial()
- {
- foreach ( $this->records as &$structRecord )
- {
- if ( $structRecord->type === 'SOA' )
- {
- $structRecord->rdata->serial++;
- }
- }
- return $this;
- }
- /**
- * Runs named-checkzone on domain
- * @return string
- * @throws DNSSubmoduleException
- */
- private function checkZoneChanges()
- {
- $this->establishSSH2Connection();
- $pathToZone = $this->zoneFilePath;
- $result = $this->ssh2->exec("named-checkzone {$this->getDomainWithoutDot()} {$pathToZone}");
- if ( $this->ssh2->getExitStatus() )
- {
- return $result;
- }
- return true;
- }
- private function splitMultilineRecord( array $recordArray, string $recordRdata )
- {
- $out = [];
- $out[] = implode(' ', $recordArray) . ' (';
- foreach( str_split($recordRdata, 200) as $splittedRdata )
- {
- $out[] = '"' . trim($splittedRdata, '"') . '"';
- }
- $out[count($out) - 1] .= ')';
- return $out;
- }
- private function trimUnnecessaryBrackets( array $lines )
- {
- foreach( $lines as &$line )
- {
- if( strpos($line, '(') !== false && strpos($line, ')') !== false )
- {
- $line = str_replace(['(', ')'], '', $line);
- }
- }
- return $lines;
- }
- private function buildRecord( $line, $nol )
- {
- //Replace @/blank/whitespace in first val for valid domain names
- $line = $this->replaceName($line);
- $values = preg_split('/\s+/', $line);
- $recordType = $this->getRecordType($values);
- $indexOfRecordType = $this->getIndexOfRecordType($values, $recordType);
- //If record type is not supported
- if( !$recordType )
- {
- throw new DNSSubmoduleException('We don\'t support this record type in line: ' . ($nol + 1));
- }
- $record = new dns\record\Record();
- $record->name = $values[0];
- $record->line = $nol;
- $record->type = $values[$indexOfRecordType];
- $record->customData = $record->rdlength = '';
- $ttlandclass = $this->getTTLAndClassFromValues($indexOfRecordType, $values, $this->ttl);
- $record->class = $ttlandclass['class'];
- $record->ttl = $ttlandclass['ttl'];
- //We split string by whitespace but txt record type has spaces in one of the params so we have to merge them
- if( $recordType === 'TXT' )
- {
- preg_match('/".+"/', $line, $match);
- $values[$indexOfRecordType + 1] = str_replace("\t", ' ', $match[0]);
- }
- $className = 'MGModule\DNSManager2\mgLibs\custom\dns\record\type\\' . $recordType;
- $additionalVars = array_keys(get_class_vars($className));
- $recordTypeClass = new $className;
- foreach( $additionalVars as $index => $prop )
- {
- $recordTypeClass->$prop = $values[$indexOfRecordType + 1 + $index];
- }
- $record->rdata = $recordTypeClass;
- return $record;
- }
- private function convertMultilineRecords( array $lines )
- {
- $joinToLine = false;
- foreach( $lines as $nol => $line )
- {
- //If we are currently joining values and there is no closing bracket just join it
- if( $joinToLine !== false && strpos($line, ')') === false )
- {
- $lines[$joinToLine] .= trim($line);
- unset($lines[$nol]);
- continue;
- }
- //If there is closing bracket and we are joining values we add trimmed value without closing bracket and end joining
- if( $joinToLine !== false && strpos($line, ')') !== false )
- {
- $lines[$joinToLine] .= trim(str_replace(')', '', $line));
- $joinToLine = false;
- unset($lines[$nol]);
- continue;
- }
- //If there is opening bracket in line and no closing brackets we start joining values
- if( strpos($line, '(') !== false && strpos($line, ')') === false )
- {
- $lines[$nol] = str_replace('(', '', $line);
- $joinToLine = $nol;
- }
- }
- //Since we join values from multiple lines they are connected with double double quotes
- //We want to create single record so we have to remove this double quotes
- return array_map(static function( $line ) {
- return str_replace('""', '', $line);
- }, $lines);
- }
- }
|