@nictool/dns-resource-record 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.codeclimate.yml +25 -0
  2. package/CHANGELOG.md +253 -0
  3. package/DEVELOP.md +23 -0
  4. package/LICENSE +29 -0
  5. package/README.md +267 -0
  6. package/index.js +67 -0
  7. package/lib/tinydns.js +186 -0
  8. package/package.json +40 -0
  9. package/rr/a.js +65 -0
  10. package/rr/aaaa.js +137 -0
  11. package/rr/caa.js +129 -0
  12. package/rr/cert.js +75 -0
  13. package/rr/cname.js +76 -0
  14. package/rr/dname.js +74 -0
  15. package/rr/dnskey.js +117 -0
  16. package/rr/ds.js +104 -0
  17. package/rr/hinfo.js +84 -0
  18. package/rr/ipseckey.js +159 -0
  19. package/rr/key.js +73 -0
  20. package/rr/loc.js +216 -0
  21. package/rr/mx.js +86 -0
  22. package/rr/naptr.js +146 -0
  23. package/rr/ns.js +74 -0
  24. package/rr/nsec.js +61 -0
  25. package/rr/nsec3.js +99 -0
  26. package/rr/nsec3param.js +84 -0
  27. package/rr/openpgpkey.js +44 -0
  28. package/rr/ptr.js +65 -0
  29. package/rr/rrsig.js +101 -0
  30. package/rr/sig.js +100 -0
  31. package/rr/smimea.js +73 -0
  32. package/rr/soa.js +140 -0
  33. package/rr/spf.js +54 -0
  34. package/rr/srv.js +122 -0
  35. package/rr/sshfp.js +86 -0
  36. package/rr/tlsa.js +106 -0
  37. package/rr/txt.js +122 -0
  38. package/rr/uri.js +95 -0
  39. package/rr.js +291 -0
  40. package/test/a.js +76 -0
  41. package/test/aaaa.js +79 -0
  42. package/test/base.js +138 -0
  43. package/test/caa.js +104 -0
  44. package/test/cert.js +48 -0
  45. package/test/cname.js +41 -0
  46. package/test/dname.js +53 -0
  47. package/test/dnskey.js +44 -0
  48. package/test/ds.js +44 -0
  49. package/test/fake.js +26 -0
  50. package/test/hinfo.js +75 -0
  51. package/test/ipseckey.js +115 -0
  52. package/test/key.js +37 -0
  53. package/test/loc.js +71 -0
  54. package/test/mx.js +75 -0
  55. package/test/naptr.js +40 -0
  56. package/test/ns.js +53 -0
  57. package/test/nsec.js +38 -0
  58. package/test/nsec3.js +26 -0
  59. package/test/nsec3param.js +26 -0
  60. package/test/openpgpkey.js +35 -0
  61. package/test/ptr.js +54 -0
  62. package/test/rr.js +196 -0
  63. package/test/smimea.js +45 -0
  64. package/test/soa.js +77 -0
  65. package/test/spf.js +48 -0
  66. package/test/srv.js +81 -0
  67. package/test/sshfp.js +62 -0
  68. package/test/tinydns.js +140 -0
  69. package/test/tlsa.js +54 -0
  70. package/test/txt.js +70 -0
  71. package/test/uri.js +61 -0
package/rr.js ADDED
@@ -0,0 +1,291 @@
1
+
2
+ export default class RR extends Map {
3
+ constructor (opts) {
4
+ super()
5
+
6
+ if (opts === null) return
7
+
8
+ if (opts.default) this.default = opts.default
9
+
10
+ if (opts.bindline) return this.fromBind(opts)
11
+ if (opts.tinyline) return this.fromTinydns(opts)
12
+
13
+ // tinydns specific
14
+ this.setLocation(opts?.location)
15
+ this.setTimestamp(opts?.timestamp)
16
+
17
+ this.setOwner(opts?.owner)
18
+ this.setType (opts?.type)
19
+ this.setTtl (opts?.ttl)
20
+ this.setClass(opts?.class)
21
+
22
+ for (const f of this.getFields('rdata')) {
23
+ const fnName = `set${this.ucfirst(f)}`
24
+ if (this[fnName] === undefined) throw new Error(`Missing ${fnName} in class ${this.get('type')}`)
25
+ this[fnName](opts[f])
26
+ }
27
+
28
+ if (opts.comment) this.set('comment', opts.comment)
29
+ }
30
+
31
+ ucfirst (str) {
32
+ return str.split(/\s/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('')
33
+ }
34
+
35
+ setClass (c) {
36
+ switch (c) {
37
+ case 'IN': // 1
38
+ case undefined:
39
+ case null:
40
+ case '':
41
+ this.set('class', 'IN')
42
+ break
43
+ case 'CS': // 2
44
+ case 'CH': // 3
45
+ case 'HS': // 4
46
+ case 'NONE': // 254
47
+ case 'ANY': // 255
48
+ this.set('class', c)
49
+ break
50
+ default:
51
+ throw new Error(`invalid class ${c}`)
52
+ }
53
+ }
54
+
55
+ setLocation (l) {
56
+ switch (l) {
57
+ case undefined:
58
+ return
59
+ default:
60
+ this.set('location', l)
61
+ }
62
+ }
63
+
64
+ setTimestamp (l) {
65
+ switch (l) {
66
+ case undefined:
67
+ return
68
+ default:
69
+ this.set('timestamp', l)
70
+ }
71
+ }
72
+
73
+ setOwner (n) {
74
+ if (n === undefined) throw new Error(`owner is required`)
75
+
76
+ if (n.length < 1 || n.length > 255)
77
+ throw new Error('Domain names must have 1-255 octets (characters): RFC 2181')
78
+
79
+ this.isFullyQualified('', 'owner', n)
80
+ this.hasValidLabels(n)
81
+
82
+ // wildcard records: RFC 1034, 4592
83
+ if (/\*/.test(n)) {
84
+ if (!/^\*\./.test(n) && !/\.\*\./.test(n)) throw new Error('only *.something or * (by itself) is a valid wildcard')
85
+ }
86
+
87
+ this.set('owner', n.toLowerCase())
88
+ }
89
+
90
+ setTtl (t) {
91
+
92
+ if (t === undefined) t = this?.default?.ttl
93
+ if (t === undefined) {
94
+ if ([ 'SOA', 'SSHPF' ].includes(this.get('type'))) return
95
+ throw new Error('TTL is required, no default available')
96
+ }
97
+
98
+ if (typeof t !== 'number') throw new Error(`TTL must be numeric (${typeof t})`)
99
+
100
+ // RFC 1035, 2181
101
+ this.is32bitInt(this.owner, 'TTL', t)
102
+
103
+ this.set('ttl', t)
104
+ }
105
+
106
+ setType (t) {
107
+ if ([ undefined, '' ].includes(t))
108
+ throw new Error(`type ${t} not supported (yet)`)
109
+
110
+ if (t.toUpperCase() !== this.constructor.name)
111
+ throw new Error(`type ${t} doesn't match ${this.constructor.name}`)
112
+
113
+ this.set('type', t.toUpperCase())
114
+ }
115
+
116
+ citeRFC () {
117
+ return `see RFC ${this.getRFCs()}`
118
+ }
119
+
120
+ fullyQualify (hostname, origin) {
121
+ if (!hostname) return hostname
122
+ if (hostname === '@' && origin) hostname = origin
123
+ if (hostname.endsWith('.')) return hostname.toLowerCase()
124
+ if (origin) return `${hostname}.${origin}`.toLowerCase()
125
+ return `${hostname}.`
126
+ }
127
+
128
+ getPrefix (zone_opts = {}) {
129
+ const classVal = zone_opts.hide?.class ? '' : this.get('class')
130
+
131
+ let rrTTL = this.get('ttl')
132
+ if (zone_opts.hide?.ttl && rrTTL === zone_opts.ttl) rrTTL = ''
133
+
134
+ let owner = this.get('owner')
135
+ if (zone_opts.hide?.sameOwner && zone_opts.previousOwner === owner) {
136
+ owner = ''
137
+ }
138
+ else {
139
+ owner = this.getFQDN('owner', zone_opts)
140
+ }
141
+
142
+ return `${owner}\t${rrTTL}\t${classVal}\t${this.get('type')}`
143
+ }
144
+
145
+ getEmpty (prop) {
146
+ return this.get(prop) === undefined ? '' : this.get(prop)
147
+ }
148
+
149
+ getComment (prop) {
150
+ const c = this.get('comment')
151
+ if (!c || !c[prop]) return ''
152
+ return c[prop]
153
+ }
154
+
155
+ getQuoted (prop) {
156
+ // if prop is not in quoted list, return bare
157
+ if (!this.getQuotedFields().includes(prop)) return this.get(prop)
158
+
159
+ // if it's already quoted, return as-is
160
+ if (/['"]/.test(this.get(prop)[0])) return this.get(prop)
161
+
162
+ return `"${this.get(prop)}"` // add double quotes
163
+ }
164
+
165
+ getQuotedFields () {
166
+ return []
167
+ }
168
+
169
+ getRdataFields () {
170
+ return []
171
+ }
172
+
173
+ getFields (arg) {
174
+ const commonFields = [ 'owner', 'ttl', 'class', 'type' ]
175
+ Object.freeze(commonFields)
176
+
177
+ switch (arg) {
178
+ case 'common':
179
+ return commonFields
180
+ case 'rdata':
181
+ return this.getRdataFields()
182
+ default:
183
+ return commonFields.concat(this.getRdataFields())
184
+ }
185
+ }
186
+
187
+ getFQDN (field, zone_opts = {}) {
188
+ let fqdn = this.get(field)
189
+ if (!fqdn) throw new Error(`empty value for field ${field}`)
190
+ if (!fqdn.endsWith('.')) fqdn += '.'
191
+
192
+ if (zone_opts.hide?.origin && zone_opts.origin) {
193
+ if (fqdn === zone_opts.origin) return '@'
194
+ if (fqdn.endsWith(zone_opts.origin)) return fqdn.slice(0, fqdn.length - zone_opts.origin.length - 1)
195
+ }
196
+
197
+ return fqdn
198
+ }
199
+
200
+ getTinyFQDN (field) {
201
+ const val = this.get(field)
202
+ if (val === '') return val // empty
203
+ if (val === '.') return val // null MX
204
+
205
+ // strip off trailing ., tinydns doesn't require it for FQDN
206
+ if (val.endsWith('.')) return val.slice(0, -1)
207
+
208
+ return val
209
+ }
210
+
211
+ getTinydnsGeneric (rdata) {
212
+ return `:${this.getTinyFQDN('owner')}:${this.getTypeId()}:${rdata}:${this.getTinydnsPostamble()}\n`
213
+ }
214
+
215
+ getTinydnsPostamble () {
216
+ return [ 'ttl', 'timestamp', 'location' ].map(f => this.getEmpty(f)).join(':')
217
+ }
218
+
219
+ hasValidLabels (hostname) {
220
+ // RFC 952 defined valid hostnames
221
+ // RFC 1035 limited domain label chars to letters, digits, and hyphen
222
+ // RFC 1123 allowed hostnames to start with a digit
223
+ // RFC 2181 'any binary string can be used as the label'
224
+ const fq = hostname.endsWith('.') ? hostname.slice(0, -1) : hostname
225
+ for (const label of fq.split('.')) {
226
+ if (label.length < 1 || label.length > 63)
227
+ throw new Error('Labels must have 1-63 octets (characters), RFC 2181')
228
+ }
229
+ }
230
+
231
+ is8bitInt (type, field, value) {
232
+ if (typeof value === 'number'
233
+ && parseInt(value, 10) === value // assure integer
234
+ && value >= 0
235
+ && value <= 255) return true
236
+
237
+ throw new Error(`${type} ${field} must be a 8-bit integer (in the range 0-255)`)
238
+ }
239
+
240
+ is16bitInt (type, field, value) {
241
+ if (typeof value === 'number'
242
+ && parseInt(value, 10) === value // assure integer
243
+ && value >= 0
244
+ && value <= 65535) return true
245
+
246
+ throw new Error(`${type} ${field} must be a 16-bit integer (in the range 0-65535)`)
247
+ }
248
+
249
+ is32bitInt (type, field, value) {
250
+ if (typeof value === 'number'
251
+ && parseInt(value, 10) === value // assure integer
252
+ && value >= 0
253
+ && value <= 2147483647) return true
254
+
255
+ throw new Error(`${type} ${field} must be a 32-bit integer (in the range 0-2147483647)`)
256
+ }
257
+
258
+ isQuoted (val) {
259
+ return /^["']/.test(val) && /["']$/.test(val)
260
+ }
261
+
262
+ isFullyQualified (type, blah, hostname) {
263
+ if (hostname.endsWith('.')) return true
264
+
265
+ throw new Error(`${type}: ${blah} must be fully qualified`)
266
+ }
267
+
268
+ isValidHostname (type, field, hostname) {
269
+ const allowed = new RegExp(/[^a-zA-Z0-9\-._/\\]/)
270
+ if (!allowed.test(hostname)) return true
271
+
272
+ const matches = allowed.exec(hostname)
273
+ throw new Error(`${type}, ${field} has invalid hostname character (${matches[0]})`)
274
+ }
275
+
276
+ toBind (zone_opts) {
277
+ return `${this.getPrefix(zone_opts)}\t${this.getRdataFields().map(f => this.getQuoted(f)).join('\t')}\n`
278
+ }
279
+
280
+ toMaraDNS () {
281
+ const type = this.get('type')
282
+ const supportedTypes = 'A PTR MX AAAA SRV NAPTR NS SOA TXT SPF RAW FQDN4 FQDN6 CNAME HINFO WKS LOC'.split(/\s+/g)
283
+ if (!supportedTypes.includes(type)) return this.toMaraGeneric()
284
+ return `${this.get('owner')}\t+${this.get('ttl')}\t${type}\t${this.getRdataFields().map(f => this.getQuoted(f)).join('\t')} ~\n`
285
+ }
286
+
287
+ toMaraGeneric () {
288
+ // throw new Error(`\nMaraDNS does not support ${type} records yet and this package does not support MaraDNS generic records. Yet.\n`)
289
+ return `${this.get('owner')}\t+${this.get('ttl')}\tRAW ${this.getTypeId()}\t'${this.getRdataFields().map(f => this.getQuoted(f)).join(' ')}' ~\n`
290
+ }
291
+ }
package/test/a.js ADDED
@@ -0,0 +1,76 @@
1
+
2
+ import A from '../rr/a.js'
3
+ import * as base from './base.js'
4
+
5
+ const defaults = { class: 'IN', ttl: 3600, type: 'A', address: '192.0.2.127' }
6
+
7
+ const validRecords = [
8
+ {
9
+ ...defaults,
10
+ owner: 'test.example.com.',
11
+ testB: 'test.example.com.\t3600\tIN\tA\t192.0.2.127\n',
12
+ testT: '+test.example.com:192.0.2.127:3600::\n',
13
+ },
14
+ {
15
+ ...defaults,
16
+ owner: 'test.example.com.',
17
+ ttl : 2147483647,
18
+ testB: 'test.example.com.\t2147483647\tIN\tA\t192.0.2.127\n',
19
+ testT: '+test.example.com:192.0.2.127:2147483647::\n',
20
+ },
21
+ {
22
+ ...defaults,
23
+ owner: 'a.',
24
+ ttl : 86400,
25
+ testB: 'a.\t86400\tIN\tA\t192.0.2.127\n',
26
+ testT: '+a:192.0.2.127:86400::\n',
27
+ },
28
+ {
29
+ ...defaults,
30
+ owner: '*.example.com.',
31
+ testB: '*.example.com.\t3600\tIN\tA\t192.0.2.127\n',
32
+ testT: '+*.example.com:192.0.2.127:3600::\n',
33
+ },
34
+ ]
35
+
36
+ const invalidRecords = [
37
+ { ...defaults, owner: '', msg: /RFC/ },
38
+ { ...defaults, owner: 'something*', msg: /fully/ },
39
+ { ...defaults, owner: 'some*thing', msg: /fully/ },
40
+ { ...defaults, owner: '*something', msg: /fully/ },
41
+ { ...defaults, owner: 'something.*', msg: /fully/ },
42
+ { ...defaults, address: 'hosts.not.valid.here', msg: /address must be IPv4/ },
43
+ { ...defaults, address: '', msg: /address is required/ },
44
+ { ...defaults, address: undefined, msg: /address is required/ },
45
+ { ...defaults, address: '1.x.2.3', msg: /address must be IPv4/ },
46
+ { ...defaults, address: '.1.2.3', msg: /address must be IPv4/ },
47
+ { ...defaults, type: '', msg: /not supported/ },
48
+ { ...defaults, type: undefined, msg: /type undefined not supported/ },
49
+ { ...defaults, ttl: '', msg: /TTL must be numeric/ },
50
+ { ...defaults, ttl: -299, msg: /TTL must be a 32-bit integer/ },
51
+ { ...defaults, ttl: 2147483648, msg: /TTL must be a 32-bit integer/ },
52
+ ]
53
+
54
+ // copy invalid properties to a valid object
55
+ for (let i = 0; i < invalidRecords.length; i++) {
56
+ const temp = JSON.parse(JSON.stringify(validRecords[0]))
57
+ Object.assign(temp, invalidRecords[i])
58
+ invalidRecords[i] = temp
59
+ }
60
+
61
+ describe('A record', function () {
62
+ base.valid(A, validRecords)
63
+ base.invalid(A, invalidRecords)
64
+
65
+ base.getDescription(A)
66
+ base.getRFCs(A, validRecords[0])
67
+ base.getRdataFields(A, [ 'address' ])
68
+ base.getFields(A, [ 'address' ])
69
+ base.getTypeId(A, 1)
70
+
71
+ base.toBind(A, validRecords)
72
+ base.toTinydns(A, validRecords)
73
+
74
+ base.fromBind(A, validRecords)
75
+ base.fromTinydns(A, validRecords)
76
+ })
package/test/aaaa.js ADDED
@@ -0,0 +1,79 @@
1
+
2
+ import assert from 'assert'
3
+
4
+ import * as base from './base.js'
5
+ import AAAA from '../rr/aaaa.js'
6
+
7
+ const defaults = { class: 'IN', ttl: 3600, type: 'AAAA' }
8
+
9
+ const validRecords = [
10
+ {
11
+ ...defaults,
12
+ owner : 'test.example.com.',
13
+ address: '2001:0db8:0020:000a:0000:0000:0000:0004',
14
+ testB : 'test.example.com.\t3600\tIN\tAAAA\t2001:db8:20:a::4\n',
15
+ testT : ':test.example.com:28:\\040\\001\\015\\270\\000\\040\\000\\012\\000\\000\\000\\000\\000\\000\\000\\004:3600::\n',
16
+ },
17
+ ]
18
+
19
+ const invalidRecords = [
20
+ {
21
+ ...defaults,
22
+ owner : 'test.example.com.',
23
+ address: '192.0.2.204',
24
+ msg : /address must be IPv6/,
25
+ },
26
+ ]
27
+
28
+ describe('AAAA record', function () {
29
+ base.valid(AAAA, validRecords)
30
+ base.invalid(AAAA, invalidRecords)
31
+
32
+ base.getDescription(AAAA)
33
+ base.getRFCs(AAAA, validRecords[0])
34
+ base.getFields(AAAA, [ 'address' ])
35
+ base.getTypeId(AAAA, 28)
36
+
37
+ base.toBind(AAAA, validRecords)
38
+ base.toTinydns(AAAA, validRecords)
39
+
40
+ base.fromBind(AAAA, validRecords)
41
+ base.fromTinydns(AAAA, validRecords)
42
+
43
+ for (const val of validRecords) {
44
+ it(`imports tinydns AAAA (generic) record (${val.owner})`, async function () {
45
+ const r = new AAAA({ tinyline: val.testT })
46
+ if (process.env.DEBUG) console.dir(r)
47
+ for (const f of [ 'owner', 'address', 'ttl' ]) {
48
+ assert.deepStrictEqual(r.get(f), val[f], `${f}: ${r.get(f)} !== ${val[f]}`)
49
+ }
50
+ })
51
+ }
52
+
53
+ const tests = [
54
+ { e: '2001:0db8:0020:000a:0000:0000:0000:0004', c: '2001:db8:20:a::4' },
55
+ { e: '0000:0000:0000:0000:0000:0000:0000:0000', c: '::' },
56
+ { e: '0000:0000:0000:0000:0000:0000:0000:0001', c: '::1' },
57
+ { e: '2001:0db8:0000:0000:0000:0000:0002:0001', c: '2001:db8::2:1' },
58
+ { e: '2001:0db8:0000:0001:0001:0001:0001:0001', c: '2001:db8:0:1:1:1:1:1' },
59
+ { e: '2001:0DB8:0000:0000:0008:0800:200C:417A', c: '2001:DB8::8:800:200C:417A' },
60
+ ]
61
+
62
+ describe('compress', function () {
63
+ const r = new AAAA(null)
64
+ for (const t of tests) {
65
+ it(`compresses IPv6 address (${t.e})`, function () {
66
+ assert.equal(r.compress(t.e), t.c)
67
+ })
68
+ }
69
+ })
70
+
71
+ describe('expand', function () {
72
+ const r = new AAAA(null)
73
+ for (const t of tests) {
74
+ it(`expands IPv6 address (${t.c})`, function () {
75
+ assert.equal(r.expand(t.c), t.e)
76
+ })
77
+ }
78
+ })
79
+ })
package/test/base.js ADDED
@@ -0,0 +1,138 @@
1
+
2
+ import assert from 'assert'
3
+
4
+ export function valid (type, validRecords, defaults) {
5
+ describe('valid', function () {
6
+ for (const val of validRecords) {
7
+ // console.log(val)
8
+ it(`parses record: ${val.owner}`, async function () {
9
+ if (defaults) val.default = defaults
10
+ const r = new type(val)
11
+ if (defaults) delete val.default
12
+ if (process.env.DEBUG) console.dir(r)
13
+
14
+ for (const k of Object.keys(val)) {
15
+ if (/^test/.test(k)) continue
16
+ assert.strictEqual(r.get(k), val[k], `${type.name} ${k} ${r.get(k)} !== ${val[k]}`)
17
+ }
18
+ })
19
+ }
20
+ })
21
+ }
22
+
23
+ export function invalid (type, invalidRecords, defaults) {
24
+ describe('invalid', function () {
25
+ for (const inv of invalidRecords) {
26
+ if (defaults) inv.default = defaults
27
+ it(`throws on record (${inv.owner})`, async function () {
28
+ assert.throws(() => {
29
+ new type(inv)
30
+ },
31
+ {
32
+ message: inv.msg,
33
+ })
34
+ })
35
+ }
36
+ })
37
+ }
38
+
39
+ export function toBind (type, validRecords) {
40
+ describe('toBind', function () {
41
+ for (const val of validRecords) {
42
+ it(`exports to BIND: ${val.owner}`, async function () {
43
+ const r = new type(val).toBind()
44
+ if (process.env.DEBUG) console.dir(r)
45
+ assert.strictEqual(r, val.testB)
46
+ })
47
+ }
48
+ })
49
+ }
50
+
51
+ export function toTinydns (type, validRecords) {
52
+ describe('toTinydns', function () {
53
+ for (const val of validRecords) {
54
+ if (val.testT === undefined) continue
55
+ it(`exports to tinydns: ${val.owner}`, async function () {
56
+ const r = new type(val).toTinydns()
57
+ if (process.env.DEBUG) console.dir(r)
58
+ assert.strictEqual(r, val.testT)
59
+ })
60
+ }
61
+ })
62
+ }
63
+
64
+ export function getDescription (type) {
65
+ describe('getDescription', function () {
66
+ const desc = new type(null).getDescription()
67
+ it(`gets description: ${desc}`, async function () {
68
+ assert.ok(desc)
69
+ })
70
+ })
71
+ }
72
+
73
+ export function getRFCs (type, valid) {
74
+ describe('getRFCs', function () {
75
+ const r = new type(null)
76
+ const rfcs = r.getRFCs()
77
+ it(`can retrieve RFCs: ${rfcs.join(',')}`, async function () {
78
+ assert.ok(rfcs.length)
79
+ })
80
+ })
81
+ }
82
+
83
+ function checkFromNS (type, validRecords, nsName, nsLineName) {
84
+ for (const val of validRecords) {
85
+ const testLine = nsLineName === 'bindline' ? val.testB : val.testT
86
+ if (testLine == undefined) continue
87
+ it(`imports ${nsName} record: ${val.owner}`, async function () {
88
+ const r = new type({ [nsLineName]: testLine })
89
+ if (process.env.DEBUG) console.dir(r)
90
+ for (const f of r.getFields()) {
91
+ if (f === 'class') continue
92
+ let expected = val[f]
93
+ if (f === 'data' && Array.isArray(expected)) expected = expected.join('') // TXT
94
+ assert.deepStrictEqual(r.get(f), expected, `${f}: ${r.get(f)} !== ${expected}`)
95
+ }
96
+ })
97
+ }
98
+ }
99
+
100
+ export function fromTinydns (type, validRecords) {
101
+ describe('fromTinydns', function () {
102
+ checkFromNS(type, validRecords, 'tinydns', 'tinyline')
103
+ })
104
+ }
105
+
106
+ export function fromBind (type, validRecords) {
107
+ describe('fromBind', function () {
108
+ checkFromNS(type, validRecords, 'BIND', 'bindline')
109
+ })
110
+ }
111
+
112
+ export function getRdataFields (type, rdataFields) {
113
+ describe('getRdataFields', function () {
114
+ const r = new type(null)
115
+ it(`can retrieve rdata fields: (${r.getRdataFields('rdata')})`, async function () {
116
+ assert.deepEqual(r.getRdataFields('rdata'), rdataFields)
117
+ })
118
+ })
119
+ }
120
+
121
+ export function getFields (type, rdataFields) {
122
+ describe('getFields', function () {
123
+ const r = new type(null)
124
+ it(`can retrieve record fields`, async function () {
125
+ assert.deepEqual(r.getFields('rdata'), rdataFields)
126
+ assert.deepEqual(r.getFields(), r.getFields('common').concat(rdataFields))
127
+ })
128
+ })
129
+ }
130
+
131
+ export function getTypeId (type, val) {
132
+ describe('getTypeId', function () {
133
+ const r = new type(null)
134
+ it(`can retrieve record type ID (${r.getTypeId()})`, async function () {
135
+ assert.deepEqual(r.getTypeId(), val)
136
+ })
137
+ })
138
+ }
package/test/caa.js ADDED
@@ -0,0 +1,104 @@
1
+
2
+ import assert from 'assert'
3
+
4
+ import * as base from './base.js'
5
+
6
+ import CAA from '../rr/caa.js'
7
+
8
+ const validRecords = [
9
+ {
10
+ owner: 'ns1.example.com.',
11
+ ttl : 3600,
12
+ class: 'IN',
13
+ type : 'CAA',
14
+ flags: 0,
15
+ tag : 'issue',
16
+ value: 'http://letsencrypt.org',
17
+ testB: `ns1.example.com.\t3600\tIN\tCAA\t0\tissue\t"http://letsencrypt.org"\n`,
18
+ testT: ':ns1.example.com:257:\\000\\005issue"http\\072\\057\\057letsencrypt.org":3600::\n',
19
+ },
20
+ {
21
+ owner: 'ns2.example.com.',
22
+ ttl : 3600,
23
+ class: 'IN',
24
+ type : 'CAA',
25
+ flags: 0,
26
+ tag : 'issue',
27
+ value: 'mailto:lets-crypt.org',
28
+ testB: `ns2.example.com.\t3600\tIN\tCAA\t0\tissue\t"mailto:lets-crypt.org"\n`,
29
+ testT: ':ns2.example.com:257:\\000\\005issue"mailto\\072lets-crypt.org":3600::\n',
30
+ },
31
+ {
32
+ owner: 'example.net.',
33
+ ttl : 86400,
34
+ type : 'CAA',
35
+ flags: 0,
36
+ tag : 'issuewild',
37
+ value: 'https://letsencrypt.org',
38
+ testB: 'example.net.\t86400\tIN\tCAA\t0\tissuewild\t"https://letsencrypt.org"\n',
39
+ testT: ':example.net:257:\\000\\011issuewild"https\\072\\057\\057letsencrypt.org":86400::\n',
40
+ },
41
+ {
42
+ owner: 'certs.example.com.',
43
+ ttl : 86400,
44
+ type : 'CAA',
45
+ flags: 0,
46
+ tag : 'issue',
47
+ value: 'ca1.example.net',
48
+ testB: 'certs.example.com.\t86400\tIN\tCAA\t0\tissue\t"ca1.example.net"\n',
49
+ testT: ':certs.example.com:257:\\000\\005issue"ca1.example.net":86400::\n',
50
+ },
51
+ ]
52
+
53
+ const invalidRecords = [
54
+ {
55
+ owner: 'example.com.',
56
+ type : 'CAA',
57
+ flags: 128,
58
+ tag : 'iodef',
59
+ value: 'letsencrypt.org', // missing iodef prefix
60
+ msg : /RFC/,
61
+ },
62
+ {
63
+ owner: 'example.com.',
64
+ type : 'CAA',
65
+ flags: 128,
66
+ tag : 'invalid', // invalid
67
+ value: 'http://letsencrypt.org',
68
+ msg : /RFC/,
69
+ },
70
+ {
71
+ owner: 'example.com.',
72
+ type : 'CAA',
73
+ flags: 15, // invalid
74
+ tag : 'issue',
75
+ value: 'http://letsencrypt.org',
76
+ msg : /RFC/,
77
+ },
78
+ ]
79
+
80
+ describe('CAA record', function () {
81
+ base.valid(CAA, validRecords)
82
+ base.invalid(CAA, invalidRecords, { ttl: 3600 })
83
+
84
+ base.getDescription(CAA)
85
+ base.getRFCs(CAA, validRecords[0])
86
+ base.getFields(CAA, [ 'flags', 'tag', 'value' ])
87
+ base.getTypeId(CAA, 257)
88
+
89
+ base.toBind(CAA, validRecords)
90
+ base.toTinydns(CAA, validRecords)
91
+
92
+ base.fromBind(CAA, validRecords)
93
+ base.fromTinydns(CAA, validRecords)
94
+
95
+ for (const val of validRecords) {
96
+ it(`imports tinydns CAA (generic) record`, async function () {
97
+ const r = new CAA({ tinyline: val.testT })
98
+ if (process.env.DEBUG) console.dir(r)
99
+ for (const f of [ 'owner', 'flags', 'tag', 'value', 'ttl' ]) {
100
+ assert.deepStrictEqual(r.get(f), val[f], `${f}: ${r.get(f)} !== ${val[f]}`)
101
+ }
102
+ })
103
+ }
104
+ })