Skip to content

Commit

Permalink
release 0.9.4 (#16)
Browse files Browse the repository at this point in the history
- add: getComment
- zone_opts, for influencing output of toBind
- normalize hostnames to lower case
- add tests: fullyQualify, getFQDN
- AAAA
    - compress: rewrote per RFC 5952, added tests
    - internally store address in expanded notation
- fromTinydns: apply correct semantics for 'x' handling
- fullyQualify
    - special handling for @
    - consider $ORIGIN
- add uc hex chars to ip6 compress
  • Loading branch information
msimerson authored Mar 24, 2022
1 parent f58c7b0 commit a466357
Show file tree
Hide file tree
Showing 15 changed files with 199 additions and 65 deletions.
16 changes: 16 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@
#### 1.N.N - YYYY-MM-DD


#### 0.9.4 - 2022-03-24

- add: getComment
- zone_opts, for influencing output of toBind
- normalize hostnames to lower case
- add tests: fullyQualify, getFQDN
- AAAA
- compress: rewrote per RFC 5952, added tests
- internally store address in expanded notation
- fromTinydns: apply correct semantics for 'x' handling
- fullyQualify
- special handling for @
- consider $ORIGIN
- add uc hex chars to ip6 compress


#### 0.9.3 - 2022-03-22

- hasValidLabels: remove trailing dot, else split returns empty string
Expand Down
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ try {
}
}
catch (e) {
// invalid RRs will throw
console.error(e)
console.error(e.message) // invalid RRs throw
}
```

Expand All @@ -100,11 +99,18 @@ console.log(validatedA.toBind())
test.example.com 3600 IN A 192.0.2.128
```

The setter functions are named: `set` + `Field`, where field is the resource record field name to modify. Multi-word names are camel cased, so a field named `Certificate Usage` would have a setter named `setCertificateUsage`. You can get the field names with `getFields()`:
The setter functions are named: `set` + `Field`, where field is the resource record field name to modify. Multi-word names are camel cased, so a field named `Certificate Usage` would have a setter named `setCertificateUsage`. You can get the field names for each RR type with `getFields()`:

```js
> validatedA.getFields()
> const RR = require('dns-resource-record')
> new RR.A(null).getFields()
[ 'name', 'ttl', 'class', 'type', 'address' ]

> new RR.PTR(null).getFields()
[ 'name', 'ttl', 'class', 'type', 'dname' ]

> new RR.SSHFP(null).getFields()
[ 'name', 'ttl', 'class', 'type', 'algorithm', 'fptype', 'fingerprint' ]
```

## FUNCTIONS
Expand Down Expand Up @@ -193,10 +199,10 @@ PRs are welcome, especially PRs with tests.

## TIPS

- Domain names are stored fully qualified, absolute, with the trailing dot.
- Domain names are stored fully qualified, aka absolute.
- Master Zone File expansions exist at another level
- fromBIND is regex based and is naive. [dns-zone-validator](https://github.com/msimerson/dns-zone-validator) has a much more robust parser.
-
- toBind output (suppress TTL, class, relative domain names) can be influenced by passing in an options object. See it in `bin/import.js` in the [dns-zone-validator](https://github.com/msimerson/dns-zone-validator) package.


## TODO
Expand All @@ -207,3 +213,4 @@ PRs are welcome, especially PRs with tests.
- [x] add defaults for empty values like TTL
- [x] DNSSEC RRs, except: RRSIG, NSEC, NSEC3, NSEC3PARAM
- [ ] Additional RRs?: KX, CERT, DHCID, TLSA, ...
- [ ] add toWire, exports in wire/network format
2 changes: 1 addition & 1 deletion lib/tinydns.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

const SPECIALCHARS = {
'+': [ 'A' ],
'-': [ undefined ],
'-': [ undefined ], // disabled RR
'%': [ 'location' ],
'.': [ 'SOA', 'NS', 'A' ],
'&': [ 'NS', 'A' ],
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dns-resource-record",
"version": "0.9.3",
"version": "0.9.4",
"description": "DNS Resource Records",
"main": "rr/index.js",
"scripts": {
Expand Down
51 changes: 38 additions & 13 deletions rr/aaaa.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ class AAAA extends RR {
/****** Resource record specific setters *******/
setAddress (val) {
if (!val) throw new Error('AAAA: address is required')
if (!net.isIPv6(val)) throw new Error('AAAA: address must be IPv6')
if (!net.isIPv6(val)) throw new Error(`AAAA: address must be IPv6 (${val})`)

this.set('address', val.toLowerCase()) // IETFs suggest only lower case
this.set('address', this.expand(val.toLowerCase())) // lower case: RFC 5952
}

getCompressed (val) {
Expand Down Expand Up @@ -45,14 +45,13 @@ class AAAA extends RR {
case ':':
// GENERIC => :fqdn:28:rdata:ttl:timestamp:lo
[ fqdn, n, rdata, ttl, ts, loc ] = str.substring(1).split(':')
if (n != 28) throw new Error('SRV fromTinydns, invalid n')
// compressed format is needed for test comparisons
ip = this.compress(TINYDNS.octalToHex(rdata).match(/([0-9a-fA-F]{4})/g).join(':'))
if (n != 28) throw new Error('AAAA fromTinydns, invalid n')
ip = TINYDNS.octalToHex(rdata).match(/([0-9a-fA-F]{4})/g).join(':')
break
case '3':
case '6':
// AAAA => 3 fqdn:rdata:x:ttl:timestamp:lo
// AAAA,PTR => 6 fqdn:rdata:x:ttl:timestamp:lo
// AAAA => 3fqdn:ip:x:ttl:timestamp:lo
// AAAA,PTR => 6fqdn:ip:x:ttl:timestamp:lo
[ fqdn, rdata, ttl, ts, loc ] = str.substring(1).split(':')
ip = rdata.match(/(.{4})/g).join(':')
break
Expand All @@ -75,17 +74,40 @@ class AAAA extends RR {
class : c,
type : type,
name : fqdn,
address: ip,
address: this.expand(ip),
ttl : parseInt(ttl, 10),
})
}

compress (val) {
return val
.replace(/\b(?:0+:){2,}/, ':') // compress all zero
.split(':')
.map(o => o.replace(/\b0+/g, '')) // strip leading zero
.join(':')
/*
* RFC 5952
* 4.1. Leading zeros MUST be suppressed...A single 16-bit 0000 field MUST be represented as 0.
* 4.2.1 The use of the symbol "::" MUST be used to its maximum capability.
* 4.2.2 The symbol "::" MUST NOT be used to shorten just one 16-bit 0 field.
* 4.2.3 When choosing placement of a "::", the longest run...MUST be shortened
* 4.3 The characters a-f in an IPv6 address MUST be represented in lowercase.
*/
let r = val
.replace(/0000/g, '0') // 4.1 0000 -> 0
.replace(/:0+([1-9a-fA-F])/g, ':$1') // 4.1 remove leading zeros

const mostConsecutiveZeros = [
new RegExp(/0?(?::0){6,}:0?/),
new RegExp(/0?(?::0){5,}:0?/),
new RegExp(/0?(?::0){4,}:0?/),
new RegExp(/0?(?::0){3,}:0?/),
new RegExp(/0?(?::0){2,}:0?/),
]

for (const re of mostConsecutiveZeros) {
if (re.test(r)) {
r = r.replace(re, '::')
break
}
}

return r
}

expand (val, delimiter) {
Expand All @@ -102,6 +124,9 @@ class AAAA extends RR {
}

/****** EXPORTERS *******/
toBind (zone_opts) {
return `${this.getPrefix(zone_opts)}\t${this.compress(this.get('address'))}\n`
}

toTinydns () {
// from AAAA notation (8 groups of 4 hex digits) to 16 escaped octals
Expand Down
47 changes: 36 additions & 11 deletions rr/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class RR extends Map {
if (this[fnName] === undefined) throw new Error(`Missing ${fnName} in class ${this.get('type')}`)
this[fnName](opts[f])
}

if (opts.comment) this.set('comment', opts.comment)
}

ucfirst (str) {
Expand Down Expand Up @@ -84,7 +86,7 @@ class RR extends Map {
if (!/^\*\./.test(n) && !/\.\*\./.test(n)) throw new Error('only *.something or * (by itself) is a valid wildcard')
}

this.set('name', n)
this.set('name', n.toLowerCase())
}

setTtl (t) {
Expand All @@ -110,13 +112,21 @@ class RR extends Map {
this.set('type', t)
}

fullyQualify (str) {
if (str.endsWith('.')) return str
return `${str}.`
fullyQualify (hostname, origin) {
if (!hostname) return hostname
if (hostname === '@' && origin) hostname = origin
if (hostname.endsWith('.')) return hostname.toLowerCase()
if (origin) return `${hostname}.${origin}`.toLowerCase()
return `${hostname}.`
}

getPrefix () {
return `${this.getFQDN('name')}\t${this.get('ttl')}\t${this.get('class')}\t${this.get('type')}`
getPrefix (zone_opts = {}) {
const classVal = zone_opts.hide?.class ? '' : this.get('class')

let rrTTL = this.get('ttl')
if (zone_opts.hide?.ttl && rrTTL === zone_opts.ttl) rrTTL = ''

return `${this.getFQDN('name', zone_opts)}\t${rrTTL}\t${classVal}\t${this.get('type')}`
}

getPrefixFields () {
Expand All @@ -129,6 +139,12 @@ class RR extends Map {
return this.get(prop) === undefined ? '' : this.get(prop)
}

getComment (prop) {
const c = this.get('comment')
if (!c || !c[prop]) return ''
return c[prop]
}

getQuoted (prop) {
// if prop is not in quoted list, return bare
if (!this.getQuotedFields().includes(prop)) return this.get(prop)
Expand Down Expand Up @@ -158,9 +174,17 @@ class RR extends Map {
}
}

getFQDN (field) {
if (this.get(field).endsWith('.')) return this.get(field)
return `${this.get(field)}.`
getFQDN (field, zone_opts = {}) {
let fqdn = this.get(field)
if (!fqdn) throw new Error(`empty value for field ${field}`)
if (!fqdn.endsWith('.')) fqdn += '.'

if (zone_opts.hide?.origin && zone_opts.origin) {
if (fqdn === zone_opts.origin) return '@'
if (fqdn.endsWith(zone_opts.origin)) return fqdn.slice(0, fqdn.length - zone_opts.origin.length - 1)
}

return fqdn
}

getTinyFQDN (field) {
Expand Down Expand Up @@ -236,13 +260,14 @@ class RR extends Map {
throw new Error(`${type}, ${field} has invalid hostname characters`)
}

toBind () {
return `${this.getPrefix()}\t${this.getRdataFields().map(f => this.getQuoted(f)).join('\t')}\n`
toBind (zone_opts) {
return `${this.getPrefix(zone_opts)}\t${this.getRdataFields().map(f => this.getQuoted(f)).join('\t')}\n`
}
}

module.exports = {
RR,
TINYDNS: require('../lib/tinydns'),
}

const files = fs.readdirSync(path.join(__dirname))
Expand Down
6 changes: 3 additions & 3 deletions rr/mx.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class MX extends RR {
return new this.constructor({
type : 'MX',
name : this.fullyQualify(fqdn),
exchange : this.fullyQualify(x),
exchange : this.fullyQualify(/\./.test(x) ? x : `${x}.mx.${fqdn}`),
preference: parseInt(preference, 10) || 0,
ttl : parseInt(ttl, 10),
timestamp : ts,
Expand All @@ -75,8 +75,8 @@ class MX extends RR {
}

/****** EXPORTERS *******/
toBind () {
return `${this.getPrefix()}\t${this.get('preference')}\t${this.getFQDN('exchange')}\n`
toBind (zone_opts) {
return `${this.getPrefix(zone_opts)}\t${this.get('preference')}\t${this.getFQDN('exchange', zone_opts)}\n`
}

toTinydns () {
Expand Down
8 changes: 6 additions & 2 deletions rr/ns.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class NS extends RR {
this.isFullyQualified('NS', 'dname', val)
this.isValidHostname('NS', 'dname', val)

this.set('dname', val)
this.set('dname', val.toLowerCase())
}

getDescription () {
Expand Down Expand Up @@ -42,7 +42,7 @@ class NS extends RR {
return new this.constructor({
type : 'NS',
name : this.fullyQualify(fqdn),
dname : this.fullyQualify(dname),
dname : this.fullyQualify(/\./.test(dname) ? dname : `${dname}.ns.${fqdn}`),
ttl : parseInt(ttl, 10),
timestamp: ts,
location : loc !== '' && loc !== '\n' ? loc : '',
Expand All @@ -63,6 +63,10 @@ class NS extends RR {
}

/****** EXPORTERS *******/
toBind (zone_opts) {
return `${this.getPrefix(zone_opts)}\t${this.getFQDN('dname', zone_opts)}\n`
}

toTinydns () {
return `&${this.getTinyFQDN('name')}::${this.getTinyFQDN('dname')}:${this.getTinydnsPostamble()}\n`
}
Expand Down
2 changes: 1 addition & 1 deletion rr/ptr.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class PTR extends RR {
this.isFullyQualified('PTR', 'dname', val)
this.isValidHostname('PTR', 'dname', val)

this.set('dname', val)
this.set('dname', val.toLowerCase())
}

getDescription () {
Expand Down
33 changes: 16 additions & 17 deletions rr/soa.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@ class SOA extends RR {
// MNAME (primary NS)
this.isValidHostname('SOA', 'MNAME', val)
this.isFullyQualified('SOA', 'MNAME', val)
this.set('mname', val)

this.set('mname', val.toLowerCase())
}

setRname (val) {
// RNAME (email of admin) (escape . with \)
this.isValidHostname('SOA', 'RNAME', val)
this.isFullyQualified('SOA', 'RNAME', val)
if (/@/.test(val)) throw new Error(`SOA rname replaces @ with a . (dot), ${this.getRFCs()}`)
this.set('rname', val)

this.set('rname', val.toLowerCase())
}

setSerial (val) {
Expand Down Expand Up @@ -115,28 +117,25 @@ class SOA extends RR {
name : fqdn,
mname : mname,
rname : rname,
serial : parseInt(serial, 10),
serial : parseInt(serial , 10),
refresh: parseInt(refresh, 10),
retry : parseInt(retry, 10),
expire : parseInt(expire, 10),
minimum: parseInt(minimum, 10 ),
ttl : parseInt(ttl, 10),
retry : parseInt(retry , 10),
expire : parseInt(expire , 10),
minimum: parseInt(minimum, 10),
ttl : parseInt(ttl , 10),
}
// console.log(bits)
return new this.constructor(bits)
}

/****** EXPORTERS *******/
toBind () {
return `$TTL\t${this.get('ttl')}
$ORIGIN\t${this.getFQDN('name')}
${this.getFQDN('name')}\t${this.get('class')}\tSOA\t${this.getFQDN('mname')}\t${this.getFQDN('rname')} (
${this.get('serial')}
${this.get('refresh')}
${this.get('retry')}
${this.get('expire')}
${this.get('minimum')}
)\n\n`
toBind (zone_opts) {
const numFields = [ 'serial', 'refresh', 'retry', 'expire', 'minimum' ]
return `$TTL\t${this.get('ttl')}${this.getComment('ttl')}
$ORIGIN\t${this.getFQDN('name')}${this.getComment('origin')}
${this.getFQDN('name', zone_opts)}\t${this.get('class')}\tSOA\t${this.getFQDN('mname', zone_opts)}\t${this.getFQDN('rname', zone_opts)} (
${numFields.map(f => '\t\t' + this.get(f) + this.getComment(f) + '\n').join('')}\t\t)
\n`
}

toTinydns () {
Expand Down
Loading

0 comments on commit a466357

Please sign in to comment.