Skip to content

Commit

Permalink
release 0.9.0 (#12)
Browse files Browse the repository at this point in the history
- feat(bind): add more RR types to zoneRR pattern
- style: move class ZONE from ./index to lib/zone
- test: replace coverage reporter nyc with c8
- bind: when a RR doesn't parse, show it before the error
  • Loading branch information
msimerson authored Apr 20, 2022
1 parent b62eb16 commit 7a89b3e
Show file tree
Hide file tree
Showing 9 changed files with 388 additions and 365 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ jobs:

- name: Coverage Run
run: |
npm install --no-save nyc codecov
npx nyc --reporter=lcovonly npm run test
npm install --no-save c8 codecov
npx c8 --reporter=lcovonly npm run test
- name: Coveralls
uses: coverallsapp/github-action@master
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@

#### 1.N.N - YYYY-MM-DD


#### 0.9.0 - 2022-04-19

- feat(bind): add more RR types to zoneRR pattern
- style: move class ZONE from ./index to lib/zone
- test: replace coverage reporter nyc with c8
- bind: when a RR doesn't parse, show it before the error


#### 0.8.5 - 2022-04-18

- updated to work with dns-rr as ES6 module
Expand Down
150 changes: 4 additions & 146 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,153 +1,11 @@

import fs from 'fs/promises'

export class ZONE extends Map {
constructor (opts = {}) {
super()
import bind from './lib/bind.js'
import maradns from './lib/maradns.js'
import tinydns from './lib/tinydns.js'

this.RR = []
this.SOA = {}

this.setOrigin(opts.origin)
this.setTTL(opts.ttl)

if (opts.RR) {
for (const r of opts.RR) {
this.addRR(r)
}
}
}

addRR (rr) {

if (rr.$TTL) {
this.setTTL(rr.$TTL)
return
}

if (rr.$ORIGIN) {
this.setOrigin(rr.$ORIGIN)
return
}

const type = rr.get('type')

// assure origin is set
if (type !== 'SOA') {
if (!this.SOA.owner) throw new Error('SOA must be set first!')

const c = rr.get('class')
if (c !== this.SOA.class)
throw new Error('All RRs in a file should have the same class')
}

this.isNotDuplicate(rr)
this.itMatchesSetTTL(rr)
this.hasNoConflictingLabels(rr)

switch (type) {
case 'SOA' : return this.setSOA(rr)
case 'CNAME': return this.addCname(rr)
// any types with additional validation go here
default:
}

this.RR.push(rr)
}

addCname (rr) {

const ownerMatches = this.getOwnerMatches(rr)

const bothMatch = ownerMatches.filter(r => r.get('type') === 'CNAME').length
if (bothMatch) throw new Error('multiple CNAME records with the same owner are NOT allowed, RFC 1034')

// RFC 2181: An alias name (label of a CNAME record) may, if DNSSEC is
// in use, have SIG, NXT, and KEY RRs, but may have no other data.
// RFC 4035: If a CNAME RRset is present at a name in a signed zone,
// appropriate RRSIG and NSEC RRsets are REQUIRED at that name.
const compatibleTypes = 'SIG NXT KEY NSEC RRSIG'.split(' ')
const conflicts = ownerMatches.filter(r => {
return !compatibleTypes.includes(r.get('type'))
}).length
if (conflicts) throw new Error(`owner already exists, CNAME not allowed, RFC 1034, 2181, & 4035`)

this.RR.push(rr)
}

getRR (rr) {
const fields = rr.getFields()

return this.RR.filter(r => {

const fieldDiffs = fields.map(f => {
return r.get(f) === rr.get(f)
}).filter(m => m === false).length

if (!fieldDiffs) return r
})
}

hasNoConflictingLabels (rr) {

const ownerMatches = this.getOwnerMatches(rr)
if (ownerMatches.length === 0) return

// CNAME conflicts with almost everything, assure no CNAME at this name
if (!'CNAME SIG NXT KEY NSEC RRSIG'.split(' ').includes(rr.get('type'))) {
const conflicts = ownerMatches.filter(r => {
return r.get('type') === 'CNAME'
}).length
if (conflicts) throw new Error(`owner exists as CNAME, not allowed, RFC 1034, 2181, & 4035`)
}
}

isNotDuplicate (rr) {
if (this.getRR(rr).length)
throw new Error('multiple identical RRs are not allowed, RFC 2181')
}

itMatchesSetTTL (rr) {
// a Resource Record Set exists...with the same label, class, type (different data)
const matches = this.RR.filter(r => {

const diffs = [ 'owner', 'class', 'type' ].map(f => {
return r.get(f) === rr.get(f)
}).filter(m => m === false).length

if (!diffs) return r
})
if (!matches.length) return true
if (matches[0].get('ttl') === rr.get('ttl')) return true
throw new Error('Records with identical label, class, and type must have identical TTL, RFC 2181')
}

getOwnerMatches (rr) {
const owner = rr.get('owner')
return this.RR.filter(r => {
return r.get('owner') === owner
})
}

setOrigin (val) {
if (!val) throw new Error('origin is required!')
this.set('origin', val)
}

setSOA (rr) {
if (this.SOA.owner)
throw new Error('Exactly one SOA RR should be present at the top!, RFC 1035')

rr.getFields().map(f => this.SOA[f] = rr.get(f))
}

setTTL (val) {
if (!val) return
this.set('ttl', val)
}
}

export default { ZONE }
export { bind, maradns, tinydns }

export function valueCleanup (str) {

Expand Down
23 changes: 13 additions & 10 deletions lib/bind.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default { zoneOpts, parseZoneFile }
const re = {
zoneTTL : /^\$TTL\s+([0-9]{1,5})\s*(;.*)?$/,
zoneOrigin: /^\$ORIGIN\s+([^\s]+?)\s*(;.*)?$/,
zoneRR : /^([^\s]+)?\s*([0-9]{1,5})?\s*(IN|CS|CH|HS|NONE|ANY)?\s+(A|AAAA|CAA|CNAME|DNAME|DNSKEY|DS|HINFO|LOC|MX|NAPTR|NS|NSEC|NSEC3|PTR|RRSIG|SMIMEA|SSHFP|SOA|SPF|SRV|TLSA|TXT|URI|TYPE)\s+(.*?)\s*$/,
zoneRR : /^([^\s]+)?\s*([0-9]{1,5})?\s*(IN|CS|CH|HS|NONE|ANY)?\s+(A|AAAA|APL|CAA|CERT|CNAME|DHCID|DNAME|DNSKEY|DS|HINFO|HIP|IPSECKEY|KEY|KX|LOC|MD|MF|MX|NAPTR|NS|NSEC|NSEC3|NSEC3PARAM|NXT|OPENPGPKEY|PTR|RP|RRSIG|SIG|SMIMEA|SOA|SPF|SRV|SSHFP|SVCB|TLSA|TXT|URI|TYPE)\s+(.*?)\s*$/,
blank : /^\s*?$/,
comment : /^\s*(?:\/\/|;)[^\r\n]*?$/,
}
Expand Down Expand Up @@ -148,15 +148,18 @@ function isZoneOrigin (str, res) {
}

function parseRR (rr) {
switch (rr.type) {
case 'SOA' : return parseSOA(rr)
default:
return parseAny(rr.type, rr)
try {
switch (rr.type) {
case 'SOA': return parseSOA(rr)
default:
return new RR[rr.type]({ bindline: `${rr.owner} ${rr.ttl} ${rr.class} ${rr.type} ${rr.rdata}` })
}
}
catch (e) {
console.error(rr)
console.error(e.message)
throw e
}
}

function parseAny (type, rri) {
return new RR[type]({ bindline: `${rri.owner} ${rri.ttl} ${rri.class} ${rri.type} ${rri.rdata}` })
}

function parseSOA (rri) {
Expand All @@ -178,7 +181,7 @@ function parseSOA (rri) {
return rrsoa
}

// TODO: integrate this with parseZone
// TODO: integrate these remnants with parseZone
export async function expandShortcuts (zoneArray) {

const implicitOrigin = rr.fullyQualify(zoneOpts.origin) // zone 'name' in named.conf
Expand Down
2 changes: 2 additions & 0 deletions lib/tinydns.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as RR from 'dns-resource-record'

const rr = new RR.A(null)

export default { parseData }

export async function parseData (str) {
// https://cr.yp.to/djbdns/tinydns-data.html
const rrs = []
Expand Down
146 changes: 146 additions & 0 deletions lib/zone.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@

export default class ZONE extends Map {
constructor (opts = {}) {
super()

this.RR = []
this.SOA = {}

this.setOrigin(opts.origin)
this.setTTL(opts.ttl)

if (opts.RR) {
for (const r of opts.RR) {
this.addRR(r)
}
}
}

addRR (rr) {

if (rr.$TTL) {
this.setTTL(rr.$TTL)
return
}

if (rr.$ORIGIN) {
this.setOrigin(rr.$ORIGIN)
return
}

const type = rr.get('type')

// assure origin is set
if (type !== 'SOA') {
if (!this.SOA.owner) throw new Error('SOA must be set first!')

const c = rr.get('class')
if (c !== this.SOA.class)
throw new Error('All RRs in a file should have the same class')
}

this.isNotDuplicate(rr)
this.itMatchesSetTTL(rr)
this.hasNoConflictingLabels(rr)

switch (type) {
case 'SOA' : return this.setSOA(rr)
case 'CNAME': return this.addCname(rr)
// any types with additional validation go here
default:
}

this.RR.push(rr)
}

addCname (rr) {

const ownerMatches = this.getOwnerMatches(rr)

const bothMatch = ownerMatches.filter(r => r.get('type') === 'CNAME').length
if (bothMatch) throw new Error('multiple CNAME records with the same owner are NOT allowed, RFC 1034')

// RFC 2181: An alias name (label of a CNAME record) may, if DNSSEC is
// in use, have SIG, NXT, and KEY RRs, but may have no other data.
// RFC 4035: If a CNAME RRset is present at a name in a signed zone,
// appropriate RRSIG and NSEC RRsets are REQUIRED at that name.
const compatibleTypes = 'SIG NXT KEY NSEC RRSIG'.split(' ')
const conflicts = ownerMatches.filter(r => {
return !compatibleTypes.includes(r.get('type'))
}).length
if (conflicts) throw new Error(`owner already exists, CNAME not allowed, RFC 1034, 2181, & 4035`)

this.RR.push(rr)
}

getRR (rr) {
const fields = rr.getFields()

return this.RR.filter(r => {

const fieldDiffs = fields.map(f => {
return r.get(f) === rr.get(f)
}).filter(m => m === false).length

if (!fieldDiffs) return r
})
}

hasNoConflictingLabels (rr) {

const ownerMatches = this.getOwnerMatches(rr)
if (ownerMatches.length === 0) return

// CNAME conflicts with almost everything, assure no CNAME at this name
if (!'CNAME SIG NXT KEY NSEC RRSIG'.split(' ').includes(rr.get('type'))) {
const conflicts = ownerMatches.filter(r => {
return r.get('type') === 'CNAME'
}).length
if (conflicts) throw new Error(`owner exists as CNAME, not allowed, RFC 1034, 2181, & 4035`)
}
}

isNotDuplicate (rr) {
if (this.getRR(rr).length)
throw new Error('multiple identical RRs are not allowed, RFC 2181')
}

itMatchesSetTTL (rr) {
// a Resource Record Set exists...with the same label, class, type (different data)
const matches = this.RR.filter(r => {

const diffs = [ 'owner', 'class', 'type' ].map(f => {
return r.get(f) === rr.get(f)
}).filter(m => m === false).length

if (!diffs) return r
})
if (!matches.length) return true
if (matches[0].get('ttl') === rr.get('ttl')) return true
throw new Error('Records with identical label, class, and type must have identical TTL, RFC 2181')
}

getOwnerMatches (rr) {
const owner = rr.get('owner')
return this.RR.filter(r => {
return r.get('owner') === owner
})
}

setOrigin (val) {
if (!val) throw new Error('origin is required!')
this.set('origin', val)
}

setSOA (rr) {
if (this.SOA.owner)
throw new Error('Exactly one SOA RR should be present at the top!, RFC 1035')

rr.getFields().map(f => this.SOA[f] = rr.get(f))
}

setTTL (val) {
if (!val) return
this.set('ttl', val)
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dns-zone-validator",
"version": "0.8.5",
"version": "0.9.0",
"description": "DNS Zone",
"main": "index.js",
"type": "module",
Expand Down
Loading

0 comments on commit 7a89b3e

Please sign in to comment.