Current File : //proc/self/root/usr/local/jetapps/var/lib/3rdparty/Badcow/DNS/Parser/Parser.php |
<?php
declare(strict_types=1);
/*
* This file is part of Badcow DNS Library.
*
* (c) Samuel Williams <sam@badcow.co>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Badcow\DNS\Parser;
use Badcow\DNS\Classes;
use Badcow\DNS\Rdata\Factory;
use Badcow\DNS\Rdata\RdataInterface;
use Badcow\DNS\Rdata\Types;
use Badcow\DNS\ResourceRecord;
use Badcow\DNS\Zone;
use Exception;
class Parser
{
/**
* @var Zone
*/
private $zone;
/**
* Array of methods that take an ArrayIterator and return an Rdata object. The array key is the Rdata type.
*
* @var callable[]
*/
private $rdataHandlers = [];
/**
* @var ResourceRecord
*/
private $currentResourceRecord;
/**
* @var string
*/
private $lastStatedDomain;
/**
* @var int
*/
private $lastStatedTtl;
/**
* @var string
*/
private $lastStatedClass;
/**
* @var string the current ORIGIN value, defaults to the Zone name
*/
private $origin;
/**
* @var int|null the currently defined default TTL
*/
private $ttl;
/**
* @var ZoneFileFetcherInterface|null Used to get the contents of files included through the directive
*/
private $fetcher;
/**
* @var int
*/
private $commentOptions;
/**
* @var bool tracks if the class has already been set on a particular line
*/
private $classHasBeenSet = false;
/**
* @var bool tracks if the TTL has already been set on a particular line
*/
private $ttlHasBeenSet = false;
/**
* @var bool tracks if the resource name has already been set on a particular line
*/
private $nameHasBeenSet = false;
/**
* @var bool tracks if the type has already been set on a particular line
*/
private $typeHasBeenSet = false;
/**
* Parser constructor.
*/
public function __construct(array $rdataHandlers = [], ?ZoneFileFetcherInterface $fetcher = null)
{
$this->rdataHandlers = $rdataHandlers;
$this->fetcher = $fetcher;
}
/**
* @throws ParseException
*/
public static function parse(string $name, string $zone, int $commentOptions = Comments::NONE): Zone
{
return (new self())->makeZone($name, $zone, $commentOptions);
}
/**
* @throws ParseException
*/
public function makeZone(string $name, string $string, int $commentOptions = Comments::NONE): Zone
{
$this->zone = new Zone($name);
$this->origin = $name;
$this->lastStatedDomain = $name;
$this->commentOptions = $commentOptions;
$this->processZone($string);
return $this->zone;
}
/**
* @throws ParseException
*/
private function processZone(string $zone): void
{
$normalisedZone = Normaliser::normalise($zone, $this->commentOptions);
foreach (explode(Tokens::LINE_FEED, $normalisedZone) as $line) {
$this->processLine($line);
}
}
/**
* @throws ParseException
*/
private function processLine(string $line): void
{
list($entry, $comment) = $this->extractComment($line);
$this->currentResourceRecord = new ResourceRecord();
$this->currentResourceRecord->setComment($comment);
if ('' === $entry) {
$this->zone->addResourceRecord($this->currentResourceRecord);
return;
}
$iterator = new ResourceRecordIterator($entry);
if ($this->isControlEntry($iterator)) {
$this->processControlEntry($iterator);
return;
}
$this->processEntry($iterator);
$this->zone->addResourceRecord($this->currentResourceRecord);
$this->ttlHasBeenSet = false;
$this->classHasBeenSet = false;
$this->nameHasBeenSet = false;
$this->typeHasBeenSet = false;
}
/**
* @throws ParseException
*/
private function processEntry(ResourceRecordIterator $iterator): void
{
if ($this->isTTL($iterator)) {
$this->currentResourceRecord->setTtl(TimeFormat::toSeconds($iterator->current()));
$this->ttlHasBeenSet = true;
$iterator->next();
$this->processEntry($iterator);
return;
}
if ($this->isClass($iterator)) {
$this->currentResourceRecord->setClass(strtoupper($iterator->current()));
$this->classHasBeenSet = true;
$iterator->next();
$this->processEntry($iterator);
return;
}
if ($this->isResourceName($iterator) && null === $this->currentResourceRecord->getName()) {
$this->currentResourceRecord->setName($this->appendOrigin($iterator->current()));
$this->nameHasBeenSet = true;
$iterator->next();
$this->processEntry($iterator);
return;
}
if ($this->isType($iterator)) {
$this->currentResourceRecord->setRdata($this->extractRdata($iterator));
$this->typeHasBeenSet = true;
$this->populateNullValues();
return;
}
throw new ParseException(sprintf('Could not parse entry "%s".', (string) $iterator));
}
/**
* If no domain-name, TTL, or class is set on the record, populate object with last stated value (RFC-1035).
* If $TTL has been set, then that value will fill the resource records TTL (RFC-2308).
*
* @see https://www.ietf.org/rfc/rfc1035 Section 5.1
* @see https://tools.ietf.org/html/rfc2308 Section 4
*/
private function populateNullValues(): void
{
if (empty($this->currentResourceRecord->getName())) {
$this->currentResourceRecord->setName($this->lastStatedDomain);
} else {
$this->lastStatedDomain = $this->currentResourceRecord->getName();
}
if (null === $this->currentResourceRecord->getTtl()) {
$this->currentResourceRecord->setTtl($this->ttl ?? $this->lastStatedTtl);
} else {
$this->lastStatedTtl = $this->currentResourceRecord->getTtl();
}
if (null === $this->currentResourceRecord->getClass()) {
$this->currentResourceRecord->setClass($this->lastStatedClass);
} else {
$this->lastStatedClass = $this->currentResourceRecord->getClass();
}
}
/**
* Append the $ORIGIN to a subdomain if:
* 1) the current $ORIGIN is different, and
* 2) the subdomain is not already fully qualified, or
* 3) the subdomain is '@'.
*
* @param string $subdomain the subdomain to which the $ORIGIN needs to be appended
*
* @return string The concatenated string of the subdomain.$ORIGIN
*/
private function appendOrigin(string $subdomain): string
{
if ($this->origin === $this->zone->getName()) {
return $subdomain;
}
if ('.' === substr($subdomain, -1, 1)) {
return $subdomain;
}
if ('.' === $this->origin) {
return $subdomain.'.';
}
if ('@' === $subdomain) {
return $this->origin;
}
return $subdomain.'.'.$this->origin;
}
/**
* Processes control entries at the top of a BIND record, i.e. $ORIGIN, $TTL, $INCLUDE, etc.
*
* @throws ParseException
*/
private function processControlEntry(ResourceRecordIterator $iterator): void
{
if ('$TTL' === strtoupper($iterator->current())) {
$iterator->next();
$this->ttl = TimeFormat::toSeconds($iterator->current());
if (null === $this->zone->getDefaultTtl()) {
$this->zone->setDefaultTtl($this->ttl);
}
}
if ('$ORIGIN' === strtoupper($iterator->current())) {
$iterator->next();
$this->origin = (string) $iterator->current();
}
if ('$INCLUDE' === strtoupper($iterator->current())) {
$iterator->next();
$this->includeFile($iterator);
}
}
/**
* @throws ParseException
*/
private function includeFile(ResourceRecordIterator $iterator): void
{
if (null === $this->fetcher) {
return;
}
list($path, $domain) = $this->extractIncludeArguments($iterator->getRemainingAsString());
//Copy the state of the parser so as to revert back once included file has been parsed.
$_lastStatedDomain = $this->lastStatedDomain;
$_lastStatedClass = $this->lastStatedClass;
$_lastStatedTtl = $this->lastStatedTtl;
$_origin = $this->origin;
$_ttl = $this->ttl;
//Parse the included record.
$this->origin = $domain ?? $_origin;
$childRecord = $this->fetcher->fetch($path);
if (null !== $this->currentResourceRecord->getComment()) {
$childRecord = Tokens::SEMICOLON.$this->currentResourceRecord->getComment().Tokens::LINE_FEED.$childRecord;
}
$this->processZone($childRecord);
//Revert the parser.
$this->lastStatedDomain = $_lastStatedDomain;
$this->lastStatedClass = $_lastStatedClass;
$this->lastStatedTtl = $_lastStatedTtl;
$this->origin = $_origin;
$this->ttl = $_ttl;
}
/**
* @param string $string the string proceeding the $INCLUDE directive
*
* @return array an array containing [$path, $domain]
*/
private function extractIncludeArguments(string $string): array
{
$s = new StringIterator($string);
$path = '';
$domain = null;
while ($s->valid()) {
$path .= $s->current();
$s->next();
if ($s->is(Tokens::SPACE)) {
$s->next();
$domain = $s->getRemainingAsString();
}
if ($s->is(Tokens::BACKSLASH)) {
$s->next();
}
}
return [$path, $domain];
}
/**
* Determine if iterant is a resource name.
*/
private function isResourceName(ResourceRecordIterator $iterator): bool
{
if ($this->nameHasBeenSet) {
return false;
}
// Look ahead and determine if the next token is a TTL, Class, or valid Type.
$iterator->next();
if (!$iterator->valid()) {
return false;
}
$isName = $this->isTTL($iterator) ||
$this->isClass($iterator, 'DOMAIN') ||
$this->isType($iterator);
$iterator->prev();
if (!$isName) {
return false;
}
if (0 === $iterator->key()) {
return true;
}
return false;
}
/**
* Determine if iterant is a class.
*
* @param string|null $origin the previously assumed resource record parameter, either 'TTL' or NULL
*/
private function isClass(ResourceRecordIterator $iterator, $origin = null): bool
{
if ($this->classHasBeenSet) {
return false;
}
if (!Classes::isValid($iterator->current())) {
return false;
}
$iterator->next();
if ('TTL' === $origin) {
$isClass = $this->isType($iterator);
} else {
$isClass = $this->isTTL($iterator, 'CLASS') || $this->isType($iterator);
}
$iterator->prev();
return $isClass;
}
/**
* Determine if current iterant is an Rdata type string.
*/
private function isType(ResourceRecordIterator $iterator): bool
{
if ($this->typeHasBeenSet) {
return false;
}
return Types::isValid(strtoupper($iterator->current())) || array_key_exists($iterator->current(), $this->rdataHandlers);
}
/**
* Determine if iterant is a control entry such as $TTL, $ORIGIN, $INCLUDE, etcetera.
*/
private function isControlEntry(ResourceRecordIterator $iterator): bool
{
return 1 === preg_match('/^\$[A-Z0-9]+/i', $iterator->current());
}
/**
* Determine if the iterant is a TTL (i.e. it is an integer after domain-name).
*
* @param string $origin the previously assumed resource record parameter, either 'CLASS' or NULL
*/
private function isTTL(ResourceRecordIterator $iterator, $origin = null): bool
{
if ($this->ttlHasBeenSet) {
return false;
}
if (!TimeFormat::isTimeFormat($iterator->current())) {
return false;
}
if ($iterator->key() < 1) {
return false;
}
$iterator->next();
if ('CLASS' === $origin) {
$isTtl = $this->isType($iterator);
} else {
$isTtl = $this->isClass($iterator, 'TTL') || $this->isType($iterator);
}
$iterator->prev();
return $isTtl;
}
/**
* Split a DNS zone line into a resource record and a comment.
*
* @return array [$entry, $comment]
*/
private function extractComment(string $rr): array
{
$string = new StringIterator($rr);
$entry = '';
$comment = null;
while ($string->valid()) {
//If a semicolon is within double quotes, it will not be treated as the beginning of a comment.
$entry .= $this->extractDoubleQuotedText($string);
if ($string->is(Tokens::SEMICOLON)) {
$string->next();
$comment = $string->getRemainingAsString();
break;
}
$entry .= $string->current();
$string->next();
}
return [$entry, $comment];
}
/**
* Extract text within double quotation context.
*/
private function extractDoubleQuotedText(StringIterator $string): string
{
if ($string->isNot(Tokens::DOUBLE_QUOTES)) {
return '';
}
$entry = $string->current();
$string->next();
while ($string->isNot(Tokens::DOUBLE_QUOTES)) {
//If the current char is a backslash, treat the next char as being escaped.
if ($string->is(Tokens::BACKSLASH)) {
$entry .= $string->current();
$string->next();
}
$entry .= $string->current();
$string->next();
}
return $entry;
}
/**
* @throws ParseException
*/
private function extractRdata(ResourceRecordIterator $iterator): RdataInterface
{
$type = strtoupper($iterator->current());
$iterator->next();
if (array_key_exists($type, $this->rdataHandlers)) {
return $this->callRdataHandler($type, $iterator);
}
try {
return Factory::textToRdataType($type, $iterator->getRemainingAsString());
} catch (Exception $exception) {
throw new ParseException(sprintf('Could not extract Rdata from resource record "%s".', (string) $iterator), null, $exception);
}
}
private function callRdataHandler(string $type, ResourceRecordIterator $iterator): RdataInterface
{
$rdataInterface = call_user_func($this->rdataHandlers[$type], $iterator);
if (!$rdataInterface instanceof RdataInterface) {
throw new \UnexpectedValueException(sprintf('Rdata handler must return instance of Badcow\DNS\Rdata\RdataInterface; "%s" returned instead.', gettype($rdataInterface)));
}
return $rdataInterface;
}
}