@nictool/dns-resource-record 1.4.0 → 1.5.0

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/rr/svcb.js CHANGED
@@ -46,11 +46,11 @@ export default class SVCB extends RR {
46
46
 
47
47
  /****** IMPORTERS *******/
48
48
 
49
- fromBind(opts) {
49
+ fromBind({ bindline }) {
50
50
  // test.example.com 3600 IN SVCB Priority TargetName Params
51
51
  // _8443._foo.api.example.com. 7200 IN SVCB 0 svc4.example.net.
52
52
  // svc4.example.net. 7200 IN SVCB 3 svc4.example.net. ( alpn="bar" port="8004" )
53
- const [owner, ttl, c, type, pri, fqdn] = opts.bindline.split(/\s+/)
53
+ const [owner, ttl, c, type, pri, fqdn] = bindline.split(/\s+/)
54
54
  return new SVCB({
55
55
  owner,
56
56
  ttl: parseInt(ttl, 10),
@@ -58,22 +58,35 @@ export default class SVCB extends RR {
58
58
  type,
59
59
  priority: parseInt(pri, 10),
60
60
  'target name': fqdn,
61
- params: opts.bindline.split(/\s+/).slice(6).join(' ').trim(),
61
+ params: bindline.split(/\s+/).slice(6).join(' ').trim(),
62
62
  })
63
63
  }
64
64
 
65
- fromTinydns({ rd, owner, ttl }) {
65
+ fromTinydns({ tinyline }) {
66
+ const [owner, _typeId, rd, ttl, ts, loc] = tinyline.slice(1).split(':')
67
+
66
68
  if (rd.length < 6) {
67
69
  this.throwHelp(`SVCB: RDATA too short: ${rd}`)
68
70
  }
69
71
 
70
- const priority = TINYDNS.octalToUInt16(rd.substring(0, 6))
71
- const remainingRdata = rd.slice(6)
72
+ // Convert escaped octal RDATA into a binary buffer for reliable parsing
73
+ const binary = Buffer.from(TINYDNS.octalToChar(rd), 'binary')
72
74
 
73
- const [targetName, consumedLength] = TINYDNS.unpackDomainName(remainingRdata)
75
+ const priority = binary.readUInt16BE(0)
74
76
 
75
- const params = remainingRdata.slice(consumedLength)
76
- const unescapedParams = TINYDNS.unescapeOctal(params)
77
+ // parse domain name from binary starting at offset 2
78
+ let pos = 2
79
+ const labels = []
80
+ while (true) {
81
+ const len = binary.readUInt8(pos)
82
+ pos += 1
83
+ if (len === 0) break
84
+ labels.push(binary.slice(pos, pos + len).toString())
85
+ pos += len
86
+ }
87
+ const targetName = `${labels.join('.')}.`
88
+ // remaining params are ASCII text after the domain
89
+ const params = binary.slice(pos).toString()
77
90
 
78
91
  return new SVCB({
79
92
  owner: this.fullyQualify(owner),
@@ -81,7 +94,9 @@ export default class SVCB extends RR {
81
94
  type: 'SVCB',
82
95
  priority: priority,
83
96
  'target name': targetName,
84
- params: unescapedParams,
97
+ params: params,
98
+ timestamp: ts,
99
+ location: loc?.trim() ?? '',
85
100
  })
86
101
  }
87
102
 
package/rr/tlsa.js CHANGED
@@ -76,12 +76,12 @@ export default class TLSA extends RR {
76
76
 
77
77
  /****** IMPORTERS *******/
78
78
 
79
- fromBind(opts) {
79
+ fromBind({ bindline }) {
80
80
  // test.example.com 3600 IN TLSA, usage, selector, match, data
81
81
  const regex =
82
- /^(?<owner>\S+)\s+(?<ttl>\d{1,10})\s+(?<cls>IN)\s+(?<type>TLSA)\s+(?<usage>\d+)\s+(?<selector>\d+)\s+(?<matchtype>\d+)\s+(?<cad>.+)$/i
83
- const match = opts.bindline.trim().match(regex)
84
- if (!match) this.throwHelp(`unable to parse TLSA: ${opts.bindline}`)
82
+ /^(?<owner>\S+)\s+(?<ttl>\d{1,10})\s+(?<cls>IN)\s+(?<type>TLSA)\s+(?<usage>\d+)\s+(?<selector>\d+)\s+(?<matchtype>\d+)\s+(?<cad>\S.*)$/i
83
+ const match = bindline.trim().match(regex)
84
+ if (!match) this.throwHelp(`unable to parse TLSA: ${bindline}`)
85
85
  const { owner, ttl, cls, type, usage, selector, matchtype, cad } = match.groups
86
86
 
87
87
  return new TLSA({
@@ -92,12 +92,12 @@ export default class TLSA extends RR {
92
92
  'certificate usage': parseInt(usage, 10),
93
93
  selector: parseInt(selector, 10),
94
94
  'matching type': parseInt(matchtype, 10),
95
- 'certificate association data': cad,
95
+ 'certificate association data': cad.trim(),
96
96
  })
97
97
  }
98
98
 
99
- fromTinydns(opts) {
100
- const [fqdn, n, rdata, ttl, ts, loc] = opts.tinyline.substring(1).split(':')
99
+ fromTinydns({ tinyline }) {
100
+ const [fqdn, n, rdata, ttl, ts, loc] = tinyline.slice(1).split(':')
101
101
  if (n != 52) this.throwHelp('TLSA fromTinydns, invalid n')
102
102
 
103
103
  const bytes = Buffer.from(TINYDNS.octalToChar(rdata), 'binary')
@@ -111,7 +111,7 @@ export default class TLSA extends RR {
111
111
  'matching type': bytes.readUInt8(2),
112
112
  'certificate association data': bytes.slice(3).toString(),
113
113
  timestamp: ts,
114
- location: loc.trim() || '',
114
+ location: loc?.trim() ?? '',
115
115
  })
116
116
  }
117
117
 
package/rr/tsig.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import RR from '../rr.js'
2
+ import * as TINYDNS from '../lib/tinydns.js'
2
3
 
3
4
  export default class TSIG extends RR {
4
5
  constructor(opts) {
@@ -17,32 +18,157 @@ export default class TSIG extends RR {
17
18
  }
18
19
 
19
20
  getRFCs() {
20
- return [2845]
21
+ return [2845, 8945]
21
22
  }
22
23
 
23
24
  getTypeId() {
24
25
  return 250
25
26
  }
26
27
 
28
+ setClass(t) {
29
+ if (t !== 'ANY') this.throwHelp('TSIG: Class is required to be ANY')
30
+ this.set('class', t)
31
+ }
32
+
33
+ setTtl(t) {
34
+ if (t !== 0) this.throwHelp('TSIG: TTL is required to be 0')
35
+ this.set('ttl', t)
36
+ }
37
+
38
+ setAlgorithmName(val) {
39
+ if (!val) this.throwHelp(`TSIG: 'algorithm name' is required`)
40
+ this.set('algorithm name', val)
41
+ }
42
+
43
+ setTimeSigned(val) {
44
+ // a 48-bit unsigned integer, as seconds since the UNIX epoch
45
+ if (val === undefined) this.throwHelp(`TSIG: 'time signed' is required`)
46
+ this.set('time signed', val)
47
+ }
48
+
49
+ setFudge(val) {
50
+ // 16-bit unsigned
51
+ this.is16bitInt('TSIG', 'fudge', val)
52
+ this.set('fudge', val)
53
+ }
54
+
55
+ setMac(val) {
56
+ this.set('mac', val ?? '')
57
+ }
58
+
59
+ setOriginalId(val) {
60
+ this.is16bitInt('TSIG', 'original id', val)
61
+ this.set('original id', val)
62
+ }
63
+
64
+ setError(val) {
65
+ this.is16bitInt('TSIG', 'error', val)
66
+ this.set('error', val)
67
+ }
68
+
69
+ setOther(val) {
70
+ this.set('other', val ?? '')
71
+ }
72
+
27
73
  /****** IMPORTERS *******/
28
74
 
29
- fromBind(opts) {
30
- // test.example.com 3600 IN TSIG SAMPLE-ALG.EXAMPLE. 853804800 300 0 0 0
31
- const [owner, ttl, c, type, algorithm] = opts.bindline.split(/\s+/)
75
+ fromBind({ bindline }) {
76
+ // owner ttl ANY TSIG alg time fudge mac_size mac original_id error other_len
77
+ const parts = bindline.trimEnd().split('\t')
78
+ const [owner, ttl, cls, type, alg, time, fudge, , mac, origId, error] = parts
79
+
32
80
  return new TSIG({
33
81
  owner,
34
82
  ttl: parseInt(ttl, 10),
35
- class: c,
36
- type: type,
37
- algorithm: algorithm,
38
- // 'time signed': opts.bindline,
39
- // fudge
40
- // mac
41
- // original id
42
- // error
43
- // other
83
+ class: cls,
84
+ type: type.toUpperCase(),
85
+ 'algorithm name': alg,
86
+ 'time signed': parseInt(time, 10),
87
+ fudge: parseInt(fudge, 10),
88
+ mac: mac || '',
89
+ 'original id': parseInt(origId, 10),
90
+ error: parseInt(error, 10),
91
+ other: '',
92
+ })
93
+ }
94
+
95
+ fromTinydns({ tinyline }) {
96
+ const [owner, _typeId, rdata, ttl, ts, loc] = tinyline.slice(1).split(':')
97
+
98
+ const algUnpacked = TINYDNS.unpackDomainName(rdata)
99
+ const algBinaryLen = algUnpacked[2]
100
+
101
+ const bytes = Buffer.from(TINYDNS.octalToChar(rdata), 'binary')
102
+ let bpos = algBinaryLen
103
+
104
+ const timeSigned = bytes.readUInt32BE(bpos)
105
+ bpos += 4
106
+ const fudge = bytes.readUInt16BE(bpos)
107
+ bpos += 2
108
+ const macSize = bytes.readUInt16BE(bpos)
109
+ bpos += 2
110
+ const mac = macSize > 0 ? bytes.slice(bpos, bpos + macSize).toString('hex') : ''
111
+ bpos += macSize
112
+ const originalId = bytes.readUInt16BE(bpos)
113
+ bpos += 2
114
+ const error = bytes.readUInt16BE(bpos)
115
+ bpos += 2
116
+ const other = bpos < bytes.length ? bytes.slice(bpos).toString() : ''
117
+
118
+ return new TSIG({
119
+ owner: this.fullyQualify(owner),
120
+ ttl: parseInt(ttl, 10),
121
+ class: 'ANY',
122
+ type: 'TSIG',
123
+ 'algorithm name': algUnpacked[0],
124
+ 'time signed': timeSigned,
125
+ fudge,
126
+ mac,
127
+ 'original id': originalId,
128
+ error,
129
+ other,
130
+ timestamp: ts,
131
+ location: loc?.trim() ?? '',
44
132
  })
45
133
  }
46
134
 
47
135
  /****** EXPORTERS *******/
136
+ toBind(zone_opts) {
137
+ const mac = this.get('mac') ?? ''
138
+ const macSize = mac.length > 0 ? mac.length : ''
139
+ const other = this.get('other') ?? ''
140
+ const otherLen = other.length > 0 ? other.length : 0
141
+ return (
142
+ [
143
+ this.getFQDN('owner', zone_opts),
144
+ this.get('ttl'),
145
+ this.get('class'),
146
+ this.get('type'),
147
+ this.get('algorithm name'),
148
+ this.get('time signed'),
149
+ this.get('fudge'),
150
+ macSize,
151
+ mac,
152
+ this.get('original id'),
153
+ this.get('error'),
154
+ otherLen,
155
+ ].join('\t') + '\n'
156
+ )
157
+ }
158
+
159
+ toTinydns() {
160
+ const dataRe = new RegExp(/[\r\n\t:\\/]/, 'g')
161
+ const alg = this.get('algorithm name') || ''
162
+
163
+ return this.getTinydnsGeneric(
164
+ TINYDNS.packDomainName(alg) +
165
+ TINYDNS.UInt32toOctal(this.get('time signed') ?? 0) +
166
+ TINYDNS.UInt16toOctal(this.get('fudge')) +
167
+ TINYDNS.UInt16toOctal(this.get('mac size') ?? 0) +
168
+ (this.get('mac size') > 0 ? TINYDNS.escapeOctal(dataRe, this.get('mac')) : '') +
169
+ TINYDNS.UInt16toOctal(this.get('original id') ?? 0) +
170
+ TINYDNS.UInt16toOctal(this.get('error') ?? 0) +
171
+ (this.get('other').length > 0 ? TINYDNS.escapeOctal(dataRe, this.get('other')) : ''),
172
+ )
173
+ }
48
174
  }
package/rr/txt.js CHANGED
@@ -29,12 +29,12 @@ export default class TXT extends RR {
29
29
  }
30
30
 
31
31
  /****** IMPORTERS *******/
32
- fromTinydns(opts) {
33
- const str = opts.tinyline
32
+ fromTinydns({ tinyline }) {
33
+ const str = tinyline
34
34
  let fqdn, rdata, s, ttl, ts, loc
35
35
  // 'fqdn:s:ttl:timestamp:lo
36
36
  if (str[0] === "'") {
37
- ;[fqdn, s, ttl, ts, loc] = str.substring(1).split(':')
37
+ ;[fqdn, s, ttl, ts, loc] = str.slice(1).split(':')
38
38
  rdata = TINYDNS.octalToChar(s)
39
39
  } else {
40
40
  ;[fqdn, rdata, ttl, ts, loc] = this.fromTinydnsGeneric(str)
@@ -46,14 +46,14 @@ export default class TXT extends RR {
46
46
  type: 'TXT',
47
47
  data: rdata,
48
48
  timestamp: ts,
49
- location: loc !== '' && loc !== '\n' ? loc : '',
49
+ location: loc?.trim() || '',
50
50
  })
51
51
  }
52
52
 
53
53
  fromTinydnsGeneric(str) {
54
54
  // generic: :fqdn:n:rdata:ttl:timestamp:location
55
55
  // eslint-disable-next-line prefer-const
56
- let [fqdn, n, rdata, ttl, ts, loc] = str.substring(1).split(':')
56
+ let [fqdn, n, rdata, ttl, ts, loc] = str.slice(1).split(':')
57
57
  if (n != 16) this.throwHelp('TXT fromTinydns, invalid n')
58
58
 
59
59
  rdata = TINYDNS.octalToChar(rdata)
@@ -61,19 +61,18 @@ export default class TXT extends RR {
61
61
  let len = rdata[0].charCodeAt(0)
62
62
  let pos = 1
63
63
  while (pos < rdata.length) {
64
- s += rdata.substring(pos, +(len + pos))
64
+ s += rdata.slice(pos, +(len + pos))
65
65
  pos = len + pos
66
66
  len = rdata.charCodeAt(pos + 1)
67
67
  }
68
68
  return [fqdn, s, ttl, ts, loc]
69
69
  }
70
70
 
71
- fromBind(opts) {
71
+ fromBind({ bindline }) {
72
72
  // test.example.com 3600 IN TXT "..."
73
- const regex =
74
- /^(?<owner>[\S]{1,255})\s+(?<ttl>\d{1,10})\s+(?<cls>IN)\s+(?<type>\w{3})\s+?\s*(?<rdata>.+?)$/i
75
- const match = opts.bindline.trim().match(regex)
76
- if (!match) this.throwHelp(`unable to parse TXT: ${opts.bindline}`)
73
+ const regex = /^(?<owner>\S{1,255})\s+(?<ttl>\d{1,10})\s+(?<cls>IN)\s+(?<type>\w{3})\s+(?<rdata>\S.*)$/i
74
+ const match = bindline.trim().match(regex)
75
+ if (!match) this.throwHelp(`unable to parse TXT: ${bindline}`)
77
76
 
78
77
  const { owner, ttl, cls, type, rdata } = match.groups
79
78
 
package/rr/uri.js CHANGED
@@ -27,26 +27,26 @@ export default class URI extends RR {
27
27
  }
28
28
 
29
29
  /****** IMPORTERS *******/
30
- fromTinydns(opts) {
30
+ fromTinydns({ tinyline }) {
31
31
  // URI via generic, :fqdn:n:rdata:ttl:timestamp:lo
32
- const [fqdn, n, rdata, ttl, ts, loc] = opts.tinyline.substring(1).split(':')
32
+ const [fqdn, n, rdata, ttl, ts, loc] = tinyline.slice(1).split(':')
33
33
  if (n != 256) this.throwHelp('URI fromTinydns, invalid n')
34
34
 
35
35
  return new URI({
36
36
  type: 'URI',
37
37
  owner: this.fullyQualify(fqdn),
38
- priority: TINYDNS.octalToUInt16(rdata.substring(0, 8)),
39
- weight: TINYDNS.octalToUInt16(rdata.substring(8, 16)),
40
- target: TINYDNS.octalToChar(rdata.substring(16)),
38
+ priority: TINYDNS.octalToUInt16(rdata.slice(0, 8)),
39
+ weight: TINYDNS.octalToUInt16(rdata.slice(8, 16)),
40
+ target: TINYDNS.octalToChar(rdata.slice(16)),
41
41
  ttl: parseInt(ttl, 10),
42
42
  timestamp: ts,
43
- location: loc.trim() || '',
43
+ location: loc?.trim() ?? '',
44
44
  })
45
45
  }
46
46
 
47
- fromBind(opts) {
47
+ fromBind({ bindline }) {
48
48
  // test.example.com 3600 IN URI priority, weight, target
49
- const [owner, ttl, c, type, priority, weight, target] = opts.bindline.split(/\s+/)
49
+ const [owner, ttl, c, type, priority, weight, target] = bindline.split(/\s+/)
50
50
  return new URI({
51
51
  class: c,
52
52
  type: type,
package/rr/wks.js CHANGED
@@ -43,9 +43,9 @@ export default class WKS extends RR {
43
43
 
44
44
  /****** IMPORTERS *******/
45
45
 
46
- fromBind(opts) {
46
+ fromBind({ bindline }) {
47
47
  // test.example.com 3600 IN WKS 192.168.1.1 TCP ftp smtp
48
- const parts = opts.bindline.split(/\s+/)
48
+ const parts = bindline.split(/\s+/)
49
49
  const [owner, ttl, c, type, address, protocol] = parts
50
50
  return new WKS({
51
51
  owner,
@@ -58,6 +58,30 @@ export default class WKS extends RR {
58
58
  })
59
59
  }
60
60
 
61
+ fromTinydns({ tinyline }) {
62
+ const [owner, _typeId, rdata, ttl, ts, loc] = tinyline.slice(1).split(':')
63
+
64
+ const binary = Buffer.from(TINYDNS.octalToChar(rdata), 'binary')
65
+ const address = [binary.readUInt8(0), binary.readUInt8(1), binary.readUInt8(2), binary.readUInt8(3)].join(
66
+ '.',
67
+ )
68
+ const protoNum = binary.readUInt8(4)
69
+ const protoMap = { 6: 'TCP', 17: 'UDP' }
70
+ const protocol = protoMap[protoNum] ?? protoNum
71
+ const bitmap = binary.slice(5).toString()
72
+
73
+ return new WKS({
74
+ owner: this.fullyQualify(owner),
75
+ ttl: parseInt(ttl, 10),
76
+ type: 'WKS',
77
+ address,
78
+ protocol,
79
+ 'bit map': bitmap,
80
+ timestamp: ts,
81
+ location: loc?.trim() ?? '',
82
+ })
83
+ }
84
+
61
85
  /****** EXPORTERS *******/
62
86
 
63
87
  toTinydns() {
package/rr.js CHANGED
@@ -4,10 +4,10 @@ export default class RR extends Map {
4
4
 
5
5
  if (opts === null) return
6
6
 
7
- if (opts.default) this.default = opts.default
7
+ if (opts?.default) this.default = opts.default
8
8
 
9
- if (opts.bindline) return this.fromBind(opts)
10
- if (opts.tinyline) return this.fromTinydns(opts)
9
+ if (opts?.bindline) return this.fromBind(opts)
10
+ if (opts?.tinyline) return this.fromTinydns(opts)
11
11
 
12
12
  // tinydns specific
13
13
  this.setLocation(opts?.location)
@@ -21,10 +21,10 @@ export default class RR extends Map {
21
21
  for (const f of this.getFields('rdata')) {
22
22
  const fnName = `set${this.ucFirst(f)}`
23
23
  if (this[fnName] === undefined) this.throwHelp(`Missing ${fnName} in class ${this.get('type')}`)
24
- this[fnName](opts[f])
24
+ this[fnName](opts?.[f])
25
25
  }
26
26
 
27
- if (opts.comment) this.set('comment', opts.comment)
27
+ if (opts?.comment) this.set('comment', opts.comment)
28
28
  }
29
29
 
30
30
  ucFirst(str) {
@@ -91,7 +91,7 @@ export default class RR extends Map {
91
91
  }
92
92
 
93
93
  setTtl(t) {
94
- if (t === undefined) t = this?.default?.ttl
94
+ t = t ?? this.default?.ttl
95
95
  if (t === undefined) {
96
96
  if (['SOA', 'SSHPF'].includes(this.get('type'))) return
97
97
  this.throwHelp('TTL is required, no default available')
@@ -157,7 +157,7 @@ export default class RR extends Map {
157
157
  }
158
158
 
159
159
  getEmpty(prop) {
160
- return this.get(prop) === undefined ? '' : this.get(prop)
160
+ return this.get(prop) ?? ''
161
161
  }
162
162
 
163
163
  getComment(prop) {
@@ -307,6 +307,51 @@ export default class RR extends Map {
307
307
  )
308
308
  }
309
309
 
310
+ compressIPv6(val) {
311
+ // * RFC 5952
312
+ // * 4.1. Leading zeros MUST be suppressed...A single 16-bit 0000 field MUST be represented as 0.
313
+ // * 4.2.1 The use of the symbol "::" MUST be used to its maximum capability.
314
+ // * 4.2.2 The symbol "::" MUST NOT be used to shorten just one 16-bit 0 field.
315
+ // * 4.2.3 When choosing placement of a "::", the longest run...MUST be shortened
316
+ // * 4.3 The characters a-f in an IPv6 address MUST be represented in lowercase.
317
+
318
+ // 4.3 Lowercase and 4.1 remove leading zeros per segment
319
+ const segments = val
320
+ .toLowerCase()
321
+ .split(':')
322
+ .map((s) => s.replace(/^0+/, '') || '0')
323
+
324
+ let bestStart = -1
325
+ let bestLen = 0
326
+ let curStart = -1
327
+ let curLen = 0
328
+
329
+ // 4.2.1 & 4.2.3 Find the longest consecutive run of '0'
330
+ for (let i = 0; i < segments.length; i++) {
331
+ if (segments[i] === '0') {
332
+ if (curStart === -1) curStart = i
333
+ curLen++
334
+ if (curLen > bestLen) {
335
+ bestLen = curLen
336
+ bestStart = curStart
337
+ }
338
+ } else {
339
+ curStart = -1
340
+ curLen = 0
341
+ }
342
+ }
343
+
344
+ // 4.2.2 Don't shorten a single 16-bit 0 field
345
+ if (bestLen < 2) {
346
+ return segments.join(':')
347
+ }
348
+
349
+ const head = segments.slice(0, bestStart).join(':')
350
+ const tail = segments.slice(bestStart + bestLen).join(':')
351
+
352
+ return `${head}::${tail}`
353
+ }
354
+
310
355
  toBind(zone_opts) {
311
356
  return `${this.getPrefix(zone_opts)}\t${this.getRdataFields()
312
357
  .map((f) => this.getQuoted(f))