Skip to content

Commit

Permalink
Finer controls for AlignedBuilder
Browse files Browse the repository at this point in the history
* [Issue #85](#85) - `Badow\DNS\AlignedBuilder` now has finer controls. You can now
  define the order of rendering Resource Records and add or change Rdata output formatters (see `Docs/AlignedZoneBuilder`.
* `Badow\DNS\AlignedBuilder` cannot be called statically anymore. It must be instantiated.
* Update documentation
  • Loading branch information
samuelwilliams authored Nov 30, 2020
1 parent dfe0cd0 commit 94ecc1c
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 23 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG-4.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ must instantiate the object first, and then call `fromText` method. All paramete
child or subdomain zone files.
* [PR #82](https://github.com/Badcow/DNS/pull/82) - Fix character escaping in TXT records. (Thank you, [@fbett](https://github.com/fbett))
* [Issue #84](https://github.com/Badcow/DNS/issues/84) - `TXT::toText()` now splits string into 255-byte chunks. (Thank you, [@fbett](https://github.com/fbett))
* [Issue #85](https://github.com/Badcow/DNS/issues/85) - `Badow\DNS\AlignedBuilder` now has finer controls. You can now
define the order of rendering Resource Records and add or change Rdata output formatters (see `Docs/AlignedZoneBuilder`.
* `Badow\DNS\AlignedBuilder` cannot be called statically anymore. It must be instantiated.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ $zone->addResourceRecord($a6);
$zone->addResourceRecord($ns2);
$zone->addResourceRecord($mx1);

echo AlignedBuilder::build($zone);
$builder = new AlignedBuilder();
echo $builder->build($zone);
```

### Output
Expand Down
217 changes: 217 additions & 0 deletions docs/AlignedZoneBuilder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
AlignedZoneBuilder
==================
The `Badcow\DNS\AlignedZoneBuilder` class takes a `Badcow\DNS\Zone` and creates aesthetically pleasing BIND style zone
record.

## Example
```php
require_once '/path/to/vendor/autoload.php';

use Badcow\DNS\Classes;
use Badcow\DNS\Zone;
use Badcow\DNS\Rdata\Factory;
use Badcow\DNS\ResourceRecord;
use Badcow\DNS\AlignedBuilder;

$zone = new Zone('example.com.');
$zone->setDefaultTtl(3600);

$soa = new ResourceRecord;
$soa->setName('@');
$soa->setClass(Classes::INTERNET);
$soa->setRdata(Factory::Soa(
'example.com.',
'post.example.com.',
'2014110501',
3600,
14400,
604800,
3600
));

$ns1 = new ResourceRecord;
$ns1->setName('@');
$ns1->setClass(Classes::INTERNET);
$ns1->setRdata(Factory::Ns('ns1.nameserver.com.'));

$ns2 = new ResourceRecord;
$ns2->setName('@');
$ns2->setClass(Classes::INTERNET);
$ns2->setRdata(Factory::Ns('ns2.nameserver.com.'));

$a = new ResourceRecord;
$a->setName('sub.domain');
$a->setRdata(Factory::A('192.168.1.42'));
$a->setComment('This is a local ip.');

$a6 = new ResourceRecord;
$a6->setName('ipv6.domain');
$a6->setRdata(Factory::Aaaa('::1'));
$a6->setComment('This is an IPv6 domain.');

$mx1 = new ResourceRecord;
$mx1->setName('@');
$mx1->setRdata(Factory::Mx(10, 'mail-gw1.example.net.'));

$mx2 = new ResourceRecord;
$mx2->setName('@');
$mx2->setRdata(Factory::Mx(20, 'mail-gw2.example.net.'));

$mx3 = new ResourceRecord;
$mx3->setName('@');
$mx3->setRdata(Factory::Mx(30, 'mail-gw3.example.net.'));

$zone->addResourceRecord($soa);
$zone->addResourceRecord($mx2);
$zone->addResourceRecord($ns1);
$zone->addResourceRecord($mx3);
$zone->addResourceRecord($a);
$zone->addResourceRecord($a6);
$zone->addResourceRecord($ns2);
$zone->addResourceRecord($mx1);

$builder = new AlignedBuilder();
echo $builder->build($zone);
```

### Output
```txt
$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
@ IN NS ns1.nameserver.com.
@ IN NS ns2.nameserver.com.
; 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.
```

## Customisations
### Resource Record Order
You can change the order in which the Resource Records are rendered, e.g.
```php
$alignedBuilder = new \Badcow\DNS\AlignedBuilder();
$myNewOrder = ['SOA', 'A', 'MX', 'AAAA', 'NS'];
$alignedBuilder->setOrder($myNewOrder);
echo $alignedBuilder->build($zone);
```
#### Output
```txt
$ORIGIN example.com.
$TTL 3600
@ IN SOA (
example.com. ; MNAME
post.example.com. ; RNAME
2014110501 ; SERIAL
3600 ; REFRESH
14400 ; RETRY
604800 ; EXPIRE
3600 ; MINIMUM
)
; A RECORDS
sub.domain A 192.168.1.42; This is a local ip.
; MX RECORDS
@ MX 10 mail-gw1.example.net.
@ MX 20 mail-gw2.example.net.
@ MX 30 mail-gw3.example.net.
; AAAA RECORDS
ipv6.domain AAAA ::1; This is an IPv6 domain.
; NS RECORDS
@ IN NS ns1.nameserver.com.
@ IN NS ns2.nameserver.com.
```

### Adding special handlers

It may be the case that you want to define (or change) the way that some Rdata is formatted; you can define custom Rdata
formatters in the AlignedBuilder. The parameters that are exposed to the callable are:
* `\Badcow\DNS\Rdata\RdataInterface $rdata` This is the Rdata that needs special handling.
* `int $padding` the amount of spaces before the start of the Rdata column

Below is an example where `TXT` rdata is split over multiple lines:
```php
function specialTxtFormatter(Badcow\DNS\Rdata\TXT $rdata, int $padding): string
{
//If the text length is less than or equal to 50 characters, just return it unaltered.
if (strlen($rdata->getText()) <= 50) {
return sprintf('"%s"', addcslashes($rdata->getText(), '"\\'));
}

$returnVal = "(\n";
$chunks = str_split($rdata->getText(), 50);
foreach ($chunks as $chunk) {
$returnVal .= str_repeat(' ', $padding).
sprintf('"%s"', addcslashes($chunk, '"\\')).
"\n";
}
$returnVal .= str_repeat(' ', $padding) . ")";

return $returnVal;
}

$zone = new Badcow\DNS\Zone('example.com.');
$zone->setDefaultTtl(3600);

$txt = new Badcow\DNS\ResourceRecord;
$txt->setName('txt.example.com.');
$txt->setClass('IN');
$txt->setRdata(Badcow\DNS\Rdata\Factory::Txt(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque ac suscipit risus. Curabitur ac urna et quam'.
'porttitor bibendum ut ac ipsum. Duis congue diam sed velit interdum ornare. Nullam dolor quam, aliquam sit amet'.
'lacinia vel, rutrum et lacus. Aenean condimentum, massa a consectetur feugiat, massa augue accumsan tellus, ac'.
'fringilla turpis velit a velit. Nunc ut tincidunt nisi. Ut pretium laoreet nisi, quis commodo lectus porta'.
'vulputate. Vestibulum ullamcorper sed sapien ut venenatis. Morbi ut nulla eget dolor mattis dictum. Suspendisse'.
'ut rutrum quam. Praesent id mi id justo maximus tristique.'
));

$zone->addResourceRecord($txt);

$alignedBuilder = new Badcow\DNS\AlignedBuilder();
$alignedBuilder->addRdataFormatter('TXT', 'specialTxtFormatter');

echo $alignedBuilder->build($zone);
```
####Output
```
$ORIGIN example.com.
$TTL 3600
; TXT RECORDS
txt.example.com. IN TXT (
"Lorem ipsum dolor sit amet, consectetur adipiscing"
" elit. Quisque ac suscipit risus. Curabitur ac urn"
"a et quamporttitor bibendum ut ac ipsum. Duis cong"
"ue diam sed velit interdum ornare. Nullam dolor qu"
"am, aliquam sit ametlacinia vel, rutrum et lacus. "
"Aenean condimentum, massa a consectetur feugiat, m"
"assa augue accumsan tellus, acfringilla turpis vel"
"it a velit. Nunc ut tincidunt nisi. Ut pretium lao"
"reet nisi, quis commodo lectus portavulputate. Ves"
"tibulum ullamcorper sed sapien ut venenatis. Morbi"
" ut nulla eget dolor mattis dictum. Suspendisseut "
"rutrum quam. Praesent id mi id justo maximus trist"
"ique."
)
```
75 changes: 65 additions & 10 deletions lib/AlignedBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class AlignedBuilder
*
* @var array
*/
private static $order = [
private $order = [
SOA::TYPE,
NS::TYPE,
A::TYPE,
Expand All @@ -56,10 +56,54 @@ class AlignedBuilder
RRSIG::TYPE,
];

/**
* @var callable[] array of Rdata type indexed, callables that handle the output formatting of Rdata
*/
private $rdataFormatters = [];

public function __construct()
{
$this->rdataFormatters = AlignedRdataFormatters::$rdataFormatters;
}

/**
* Adds or changes an Rdata output formatter.
*
* @param string $type the Rdata type to be handled by the $formatter
* @param callable $formatter callable that will handle the output formatting of the Rdata
*/
public function addRdataFormatter(string $type, callable $formatter): void
{
$this->rdataFormatters[$type] = $formatter;
}

public function getRdataFormatters(): array
{
return $this->rdataFormatters;
}

/**
* @return string[]
*/
public function getOrder(): array
{
return $this->order;
}

/**
* Set the order in which Resource Records should appear in a zone..
*
* @param string[] $order Simple string array of Rdata types
*/
public function setOrder(array $order): void
{
$this->order = $order;
}

/**
* Build an aligned BIND zone file.
*/
public static function build(Zone $zone): string
public function build(Zone $zone): string
{
$master = self::generateControlEntries($zone);
$resourceRecords = $zone->getResourceRecords();
Expand All @@ -84,7 +128,7 @@ public static function build(Zone $zone): string
str_pad((string) $resourceRecord->getTtl(), $ttlPadding, Tokens::SPACE, STR_PAD_RIGHT),
str_pad((string) $resourceRecord->getClass(), $classPadding, Tokens::SPACE, STR_PAD_RIGHT),
str_pad($rdata->getType(), $typePadding, Tokens::SPACE, STR_PAD_RIGHT),
self::generateRdataOutput($rdata, $rdataPadding)
$this->generateRdataOutput($rdata, $rdataPadding)
);

$master .= self::generateComment($resourceRecord);
Expand Down Expand Up @@ -121,8 +165,15 @@ private static function generateComment(ResourceRecord $resourceRecord): string

/**
* Compares two ResourceRecords to determine which is the higher order. Used with the usort() function.
*
* @param ResourceRecord $a The first ResourceRecord
* @param ResourceRecord $b The second ResourceRecord
*
* @return int $a is higher precedence than $b if return value is less than 0.
* $b is higher precedence than $a if return value is greater than 0.
* $a and $b have the same precedence if the return value is 0.
*/
public static function compareResourceRecords(ResourceRecord $a, ResourceRecord $b): int
public function compareResourceRecords(ResourceRecord $a, ResourceRecord $b): int
{
$a_rdata = (null === $a->getRdata()) ? '' : $a->getRdata()->toText();
$b_rdata = (null === $b->getRdata()) ? '' : $b->getRdata()->toText();
Expand All @@ -133,8 +184,8 @@ public static function compareResourceRecords(ResourceRecord $a, ResourceRecord
}

//Find the precedence (if any) for the two types.
$_a = array_search($a->getType(), self::$order);
$_b = array_search($b->getType(), self::$order);
$_a = array_search($a->getType(), $this->order);
$_b = array_search($b->getType(), $this->order);

//If neither types have defined precedence.
if (!is_int($_a) && !is_int($_b)) {
Expand All @@ -157,12 +208,14 @@ public static function compareResourceRecords(ResourceRecord $a, ResourceRecord

/**
* Composes the RDATA of the Resource Record.
*
* @param RdataInterface $rdata the Rdata to be formatted
* @param int $padding the number of spaces before the Rdata column
*/
private static function generateRdataOutput(RdataInterface $rdata, int $padding): string
private function generateRdataOutput(RdataInterface $rdata, int $padding): string
{
$rdataFormatters = AlignedRdataFormatters::getRdataFormatters();
if (array_key_exists($rdata->getType(), $rdataFormatters)) {
return call_user_func($rdataFormatters[$rdata->getType()], $rdata, $padding);
if (array_key_exists($rdata->getType(), $this->rdataFormatters)) {
return call_user_func($this->rdataFormatters[$rdata->getType()], $rdata, $padding);
}

return $rdata->toText();
Expand All @@ -171,6 +224,8 @@ private static function generateRdataOutput(RdataInterface $rdata, int $padding)
/**
* Get the padding required for a zone.
*
* @param Zone $zone the DNS Zone being processed
*
* @return int[] Array order: [name, ttl, type, class, rdata]
*/
private static function getPadding(Zone $zone): array
Expand Down
24 changes: 13 additions & 11 deletions tests/AlignedBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,20 +187,22 @@ public function testCompareResourceRecords(): void
$spf = new ResourceRecord();
$spf->setRdata(Factory::SPF('skjdfskjasdfjh'));

$this->assertTrue(AlignedBuilder::compareResourceRecords($soa, $ns1) < 0);
$this->assertTrue(AlignedBuilder::compareResourceRecords($aaaa, $cname) < 0);
$this->assertTrue(AlignedBuilder::compareResourceRecords($mx1, $mx2) < 0);
$this->assertTrue(AlignedBuilder::compareResourceRecords($mx1, $mx2) < 0);
$this->assertTrue(AlignedBuilder::compareResourceRecords($mx1, $spf) < 0);
$alignedBuilder = new AlignedBuilder();

$this->assertTrue(AlignedBuilder::compareResourceRecords($mx1, $a) > 0);
$this->assertTrue(AlignedBuilder::compareResourceRecords($ns2, $ns1) > 0);
$this->assertTrue(AlignedBuilder::compareResourceRecords($spf, $txt) > 0);
$this->assertTrue($alignedBuilder->compareResourceRecords($soa, $ns1) < 0);
$this->assertTrue($alignedBuilder->compareResourceRecords($aaaa, $cname) < 0);
$this->assertTrue($alignedBuilder->compareResourceRecords($mx1, $mx2) < 0);
$this->assertTrue($alignedBuilder->compareResourceRecords($mx1, $mx2) < 0);
$this->assertTrue($alignedBuilder->compareResourceRecords($mx1, $spf) < 0);

$this->assertTrue(AlignedBuilder::compareResourceRecords($nsec3, $rrsig) < 0);
$this->assertTrue(AlignedBuilder::compareResourceRecords($rrsig, $nsec3) > 0);
$this->assertTrue($alignedBuilder->compareResourceRecords($mx1, $a) > 0);
$this->assertTrue($alignedBuilder->compareResourceRecords($ns2, $ns1) > 0);
$this->assertTrue($alignedBuilder->compareResourceRecords($spf, $txt) > 0);

$this->assertTrue(AlignedBuilder::compareResourceRecords($rp, $spf) < 0);
$this->assertTrue($alignedBuilder->compareResourceRecords($nsec3, $rrsig) < 0);
$this->assertTrue($alignedBuilder->compareResourceRecords($rrsig, $nsec3) > 0);

$this->assertTrue($alignedBuilder->compareResourceRecords($rp, $spf) < 0);
}

public function testBuild(): void
Expand Down
Loading

0 comments on commit 94ecc1c

Please sign in to comment.