@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.
- package/.codeclimate.yml +25 -0
- package/CHANGELOG.md +253 -0
- package/DEVELOP.md +23 -0
- package/LICENSE +29 -0
- package/README.md +267 -0
- package/index.js +67 -0
- package/lib/tinydns.js +186 -0
- package/package.json +40 -0
- package/rr/a.js +65 -0
- package/rr/aaaa.js +137 -0
- package/rr/caa.js +129 -0
- package/rr/cert.js +75 -0
- package/rr/cname.js +76 -0
- package/rr/dname.js +74 -0
- package/rr/dnskey.js +117 -0
- package/rr/ds.js +104 -0
- package/rr/hinfo.js +84 -0
- package/rr/ipseckey.js +159 -0
- package/rr/key.js +73 -0
- package/rr/loc.js +216 -0
- package/rr/mx.js +86 -0
- package/rr/naptr.js +146 -0
- package/rr/ns.js +74 -0
- package/rr/nsec.js +61 -0
- package/rr/nsec3.js +99 -0
- package/rr/nsec3param.js +84 -0
- package/rr/openpgpkey.js +44 -0
- package/rr/ptr.js +65 -0
- package/rr/rrsig.js +101 -0
- package/rr/sig.js +100 -0
- package/rr/smimea.js +73 -0
- package/rr/soa.js +140 -0
- package/rr/spf.js +54 -0
- package/rr/srv.js +122 -0
- package/rr/sshfp.js +86 -0
- package/rr/tlsa.js +106 -0
- package/rr/txt.js +122 -0
- package/rr/uri.js +95 -0
- package/rr.js +291 -0
- package/test/a.js +76 -0
- package/test/aaaa.js +79 -0
- package/test/base.js +138 -0
- package/test/caa.js +104 -0
- package/test/cert.js +48 -0
- package/test/cname.js +41 -0
- package/test/dname.js +53 -0
- package/test/dnskey.js +44 -0
- package/test/ds.js +44 -0
- package/test/fake.js +26 -0
- package/test/hinfo.js +75 -0
- package/test/ipseckey.js +115 -0
- package/test/key.js +37 -0
- package/test/loc.js +71 -0
- package/test/mx.js +75 -0
- package/test/naptr.js +40 -0
- package/test/ns.js +53 -0
- package/test/nsec.js +38 -0
- package/test/nsec3.js +26 -0
- package/test/nsec3param.js +26 -0
- package/test/openpgpkey.js +35 -0
- package/test/ptr.js +54 -0
- package/test/rr.js +196 -0
- package/test/smimea.js +45 -0
- package/test/soa.js +77 -0
- package/test/spf.js +48 -0
- package/test/srv.js +81 -0
- package/test/sshfp.js +62 -0
- package/test/tinydns.js +140 -0
- package/test/tlsa.js +54 -0
- package/test/txt.js +70 -0
- 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
|
+
})
|