| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470 |
- <?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',
- 'validators' => [
- 'required' => 'required'
- ],
- ],
- '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',
- ]
- ],
- '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', 'CAA', 'DS', 'HINFO', 'NS', 'MX', 'CNAME', 'DNAME', 'TXT', 'SRV', 'AFSDB', '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->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->reloadZone();
- return $this->synchronizeSlaves($this->getDomainWithoutDot(), 'Terminate');
- }
- /**
- * 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();
- return $this->ssh2->exec('systemctl reload bind9');
- }
- /**
- * 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 )
- {
- return (($index = strpos($line, ';')) !== false) ? substr($line, 0, $index) : $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
- ];
- $recordsArray[] = implode(' ', $recordArray) . ' ' . $record->rdata->toString();
- }
- 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->sftp->login($this->config['username'], $this->config['password']) )
- {
- 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');
- }
- //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;
- //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;
- $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;
- }
- }
|