Unverified Commit f0074e6a authored by Sam Williams's avatar Sam Williams Committed by GitHub
Browse files

Merge pull request #29 from Badcow/monolith

Merge Badcow DNS Parser into single library
parents 2a831087 b3a29ad5
......@@ -13,6 +13,7 @@ $finder = PhpCsFixer\Finder::create()
->in(__DIR__.'/lib')
->in(__DIR__.'/tests')
->exclude(__DIR__.'/tests/Resources')
->exclude(__DIR__.'/tests/Parser/Resources')
;
$config = PhpCsFixer\Config::create()
......
Badcow DNS Zone Library
=======================
This library constructs DNS zone records based on [RFC1035](https://tools.ietf.org/html/rfc1035) and subsequent standards.
The aim of this project is to create abstract object representations of DNS records in PHP. The project consists of various
classes representing DNS objects (such as `Zone`, `ResourceRecord`, and various `RData` types), a parser to convert BIND
style text files to the PHP objects, and builders to create aesthetically pleasing BIND records.
## Build Status
[![Build Status](https://travis-ci.org/Badcow/DNS.png)](https://travis-ci.org/Badcow/DNS) [![Code Coverage](https://scrutinizer-ci.com/g/Badcow/DNS/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/Badcow/DNS/?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Badcow/DNS/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Badcow/DNS/?branch=master)
......@@ -152,6 +153,7 @@ ipv6.domain IN AAAA ::1; This is an IPv6 domain.
* `AAAA`
* `APL`
* `CNAME`
* `CAA`
* `DNAME`
* `HINFO`
* `LOC`
......@@ -169,11 +171,13 @@ ipv6.domain IN AAAA ::1; This is an IPv6 domain.
## Parsing BIND Records
BIND Records can be parsed into PHP objects using [Badcow DNS Parser](https://github.com/Badcow/DNS-Parser).
`composer require "badcow/dns-parser"`
BIND Records can be parsed into PHP objects using `Badcow\DNS\Parser\Parser`
```php
$file = file_get_contents('/path/to/example.com.txt');
$zone = Badcow\DNS\Parser\Parser::parse('example.com.', $file); // Returns Badcow\DNS\Zone
```
$zone = Badcow\DNS\Parser\Parser::parse('example.com.', $file); //Badcow Zone Object
```
Simple as that.
More examples can be found in the [The Docs](docs/Parser)
Basic Usage
===========
```php
$file = file_get_contents('/path/to/example.com.txt');
$zone = Badcow\DNS\Parser\Parser::parse('example.com.', $file);
```
Simple as that.
## Example
### BIND Record
```text
$ORIGIN example.com.
$TTL 3600
@ IN SOA (
example.com. ; MNAME
post.example.com. ; RNAME
2014110501 ; SERIAL
3600 ; REFRESH
14400 ; RETRY
604800 ; EXPIRE
3600 ; MINIMUM
)
; NS RECORDS
@ NS ns1.nameserver.com.
@ NS ns2.nameserver.com.
info TXT "This is some additional \"information\""
; A RECORDS
sub.domain A 192.168.1.42 ; This is a local ip.
; AAAA RECORDS
ipv6.domain AAAA ::1 ; This is an IPv6 domain.
; MX RECORDS
@ MX 10 mail-gw1.example.net.
@ MX 20 mail-gw2.example.net.
@ MX 30 mail-gw3.example.net.
mail IN TXT "THIS IS SOME TEXT; WITH A SEMICOLON"
```
### Processing the record
```php
<?php
require_once '/path/to/vendor/autoload.php';
$file = file_get_contents('/path/to/example.com.txt');
$zone = Badcow\DNS\Parser\Parser::parse('example.com.', $file);
$zone->getName(); //Returns example.com.
foreach ($zone->getResourceRecords() as $record) {
$record->getName();
$record->getClass();
$record->getTtl();
$record->getRdata()->output();
}
```
\ No newline at end of file
Using Custom RData Handlers
===========================
Out-of-the-box, the library will handle most RData types that are regularly encountered. Occasionally, you may encounter
an unsupported type. You can add your own RData handler method for the record type. For example, you may want to support
the non-standard `SPF` record type, and return a `TXT` instance.
```php
$sfp = function (\ArrayIterator $iterator): Badcow\DNS\Rdata\TXT {
$string = '';
while ($iterator->valid()) {
$string .= $iterator->current() . ' ';
$iterator->next();
}
$string = trim($string, ' "'); //Remove whitespace and quotes
$sfp = new Badcow\DNS\Rdata\TXT;
$sfp->setText($string);
return $sfp;
};
$customHandlers = ['SFP' => $sfp];
$record = 'example.com. 7200 IN SFP "v=spf1 a mx ip4:69.64.153.131 include:_spf.google.com ~all"';
$parser = new \Badcow\DNS\Parser\Parser($customHandlers);
$zone = $parser->makeZone('example.com.', $record);
```
You can also overwrite the default handlers if you wish, as long as your handler method returns an instance of
`Badcow\DNS\Rdata\RdataInterface`.
\ No newline at end of file
<?php
/*
* 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;
class Normaliser
{
/**
* @var StringIterator
*/
private $string;
/**
* @var string
*/
private $normalisedString = '';
/**
* Normaliser constructor.
*
* @param string $zone
*/
public function __construct(string $zone)
{
//Remove Windows line feeds and tabs
$zone = str_replace([Tokens::CARRIAGE_RETURN, Tokens::TAB], ['', Tokens::SPACE], $zone);
$this->string = new StringIterator($zone);
}
/**
* @param string $zone
*
* @return string
*
* @throws ParseException
*/
public static function normalise(string $zone): string
{
return (new self($zone))->process();
}
/**
* @return string
*
* @throws ParseException
*/
public function process(): string
{
while ($this->string->valid()) {
$this->handleTxt();
$this->handleComment();
$this->handleMultiline();
$this->append();
}
$this->removeWhitespace();
return $this->normalisedString;
}
/**
* Ignores the comment section.
*/
private function handleComment(): void
{
if ($this->string->isNot(Tokens::SEMICOLON)) {
return;
}
while ($this->string->isNot(Tokens::LINE_FEED) && $this->string->valid()) {
$this->string->next();
}
}
/**
* Handle text inside of double quotations. When this function is called, the String pointer MUST be at the
* double quotation mark.
*
* @throws ParseException
*/
private function handleTxt(): void
{
if ($this->string->isNot(Tokens::DOUBLE_QUOTES)) {
return;
}
$this->append();
while ($this->string->isNot(Tokens::DOUBLE_QUOTES)) {
if (!$this->string->valid()) {
throw new ParseException('Unbalanced double quotation marks. End of file reached.');
}
//If escape character
if ($this->string->is(Tokens::BACKSLASH)) {
$this->append();
}
if ($this->string->is(Tokens::LINE_FEED)) {
throw new ParseException('Line Feed found within double quotation marks context.', $this->string);
}
$this->append();
}
}
/**
* Move multi-line records onto single line.
*
* @throws ParseException
*/
private function handleMultiline(): void
{
if ($this->string->isNot(Tokens::OPEN_BRACKET)) {
return;
}
$this->string->next();
while ($this->string->valid()) {
$this->handleTxt();
$this->handleComment();
if ($this->string->is(Tokens::LINE_FEED)) {
$this->string->next();
continue;
}
if ($this->string->is(Tokens::CLOSE_BRACKET)) {
$this->string->next();
return;
}
$this->append();
}
throw new ParseException('End of file reached. Unclosed bracket.');
}
/**
* Remove superfluous whitespace characters from string.
*/
private function removeWhitespace(): void
{
$string = preg_replace('/ {2,}/', Tokens::SPACE, $this->normalisedString);
$lines = [];
foreach (explode(Tokens::LINE_FEED, $string) as $line) {
if ('' !== $line = trim($line)) {
$lines[] = $line;
}
}
$this->normalisedString = implode(Tokens::LINE_FEED, $lines);
}
/**
* Add current entry to normalisedString and moves iterator to next entry.
*/
private function append()
{
$this->normalisedString .= $this->string->current();
$this->string->next();
}
}
<?php
/*
* 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;
class ParseException extends \Exception
{
/**
* @var StringIterator
*/
private $stringIterator;
/**
* ParseException constructor.
*
* @param string $message
* @param StringIterator|null $stringIterator
*/
public function __construct(string $message = '', StringIterator $stringIterator = null)
{
if (null !== $stringIterator) {
$this->stringIterator = $stringIterator;
$message .= sprintf(' [Line no: %d]', $this->getLineNumber());
}
parent::__construct($message);
}
/**
* Get line number of current entry on the StringIterator.
*
* @return int
*/
private function getLineNumber(): int
{
$pos = $this->stringIterator->key();
$this->stringIterator->rewind();
$lineNo = 1;
while ($this->stringIterator->key() < $pos) {
if ($this->stringIterator->is(Tokens::LINE_FEED)) {
++$lineNo;
}
$this->stringIterator->next();
}
return $lineNo;
}
}
<?php
/*
* 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\ResourceRecord;
use Badcow\DNS\Zone;
use Badcow\DNS\Rdata;
class Parser
{
/**
* @var string
*/
private $string;
/**
* @var string
*/
private $previousName;
/**
* @var Zone
*/
private $zone;
/**
* Array of methods that take an ArrayIterator and return an Rdata object. The array key is the Rdata type.
*
* @var array
*/
private $rdataHandlers = [];
/**
* Parser constructor.
*
* @param array $rdataHandlers
*/
public function __construct(array $rdataHandlers = [])
{
$this->rdataHandlers = array_merge(RdataHandlers::getHandlers(), $rdataHandlers);
}
/**
* @param string $name
* @param string $zone
*
* @return Zone
*
* @throws ParseException
*/
public static function parse(string $name, string $zone): Zone
{
return (new self())->makeZone($name, $zone);
}
/**
* @param $name
* @param $string
*
* @return Zone
*
* @throws ParseException
*/
public function makeZone($name, $string): Zone
{
$this->zone = new Zone($name);
$this->string = Normaliser::normalise($string);
foreach (explode(Tokens::LINE_FEED, $this->string) as $line) {
$this->processLine($line);
}
return $this->zone;
}
/**
* @param string $line
*
* @throws ParseException
*/
private function processLine(string $line): void
{
$iterator = new \ArrayIterator(explode(Tokens::SPACE, $line));
if ($this->isControlEntry($iterator)) {
$this->processControlEntry($iterator);
return;
}
$resourceRecord = new ResourceRecord();
$this->processResourceName($iterator, $resourceRecord);
$this->processTtl($iterator, $resourceRecord);
$this->processClass($iterator, $resourceRecord);
$resourceRecord->setRdata($this->extractRdata($iterator));
$this->zone->addResourceRecord($resourceRecord);
}
/**
* Processes control entries at the top of a BIND record, i.e. $ORIGIN, $TTL, $INCLUDE, etc.
*
* @param \ArrayIterator $iterator
*/
private function processControlEntry(\ArrayIterator $iterator): void
{
if ('$TTL' === strtoupper($iterator->current())) {
$iterator->next();
$this->zone->setDefaultTtl((int) $iterator->current());
}
}
/**
* Processes a ResourceRecord name.
*
* @param \ArrayIterator $iterator
* @param ResourceRecord $resourceRecord
*/
private function processResourceName(\ArrayIterator $iterator, ResourceRecord $resourceRecord): void
{
if ($this->isResourceName($iterator)) {
$this->previousName = $iterator->current();
$iterator->next();
}
$resourceRecord->setName($this->previousName);
}
/**
* Set RR's TTL if there is one.
*
* @param \ArrayIterator $iterator
* @param ResourceRecord $resourceRecord
*/
private function processTtl(\ArrayIterator $iterator, ResourceRecord $resourceRecord): void
{
if ($this->isTTL($iterator)) {
$resourceRecord->setTtl($iterator->current());
$iterator->next();
}
}
/**
* Set RR's class if there is one.
*
* @param \ArrayIterator $iterator
* @param ResourceRecord $resourceRecord
*/
private function processClass(\ArrayIterator $iterator, ResourceRecord $resourceRecord): void
{
if (Classes::isValid(strtoupper($iterator->current()))) {
$resourceRecord->setClass(strtoupper($iterator->current()));
$iterator->next();
}
}
/**
* Determine if iterant is a resource name.
*
* @param \ArrayIterator $iterator
*
* @return bool
*/
private function isResourceName(\ArrayIterator $iterator): bool
{
return !(
$this->isTTL($iterator) ||
Classes::isValid(strtoupper($iterator->current())) ||
RDataTypes::isValid(strtoupper($iterator->current()))
);
}
/**
* Determine if iterant is a control entry such as $TTL, $ORIGIN, $INCLUDE, etcetera.
*
* @param \ArrayIterator $iterator
*
* @return bool
*/
private function isControlEntry(\ArrayIterator $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).
*
* @param \ArrayIterator $iterator
*
* @return bool
*/
private function isTTL(\ArrayIterator $iterator): bool
{
return 1 === preg_match('/^\d+$/', $iterator->current());
}
/**
* @param \ArrayIterator $iterator
*
* @return RData\RDataInterface
*
* @throws ParseException
*/
private function extractRdata(\ArrayIterator $iterator): Rdata\RdataInterface
{
$type = strtoupper($iterator->current());
$iterator->next();
if (array_key_exists($type, $this->rdataHandlers)) {
return call_user_func($this->rdataHandlers[$type], $iterator);
}
return RdataHandlers::catchAll($type, $iterator);
}
}
<?php
/*
* 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;
class RDataTypes
{
/**
* @var array
*/
public static $names = [
self::TYPE_A,
self::TYPE_NS,
self::TYPE_CNAME,
self::TYPE_SOA,
self::TYPE_PTR,
self::TYPE_MX,