@nictool/dns-resource-record 1.7.0 → 1.8.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/CHANGELOG.md +14 -0
- package/README.md +45 -12
- package/dist/dns-rr.cjs +7050 -0
- package/dist/dns-rr.min.js +14 -14
- package/dist/dns-rr.min.js.map +3 -3
- package/lib/bind.js +7 -5
- package/lib/wire.js +112 -2
- package/package.json +8 -5
- package/rr/cert.js +8 -0
- package/rr/cname.js +5 -0
- package/rr/dname.js +5 -0
- package/rr/ds.js +20 -1
- package/rr/mx.js +4 -0
- package/rr/rrsig.js +39 -1
- package/rr/spf.js +5 -1
- package/rr/srv.js +17 -0
- package/rr/sshfp.js +15 -0
- package/rr/txt.js +37 -9
package/lib/bind.js
CHANGED
|
@@ -61,15 +61,17 @@ export function fromBind(rrInstance, opts) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
const fields = rrInstance.getFields('rdata')
|
|
64
|
+
const rdataDefs = rrInstance.constructor.rdataFields ?? []
|
|
64
65
|
for (let i = 0; i < fields.length; i++) {
|
|
65
66
|
const isLastField = i === fields.length - 1
|
|
66
67
|
if (isLastField && rrInstance.isQuotedField(fields[i])) {
|
|
67
68
|
// Collect all remaining tokens for TXT/etc
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
const tokens = rdata.slice(i).map((s) => s.replace(/^"|"$/g, ''))
|
|
70
|
+
// For `charstrs` (TXT), preserve multi-string boundaries (RFC 1035 §3.3.14).
|
|
71
|
+
// For `qstr`/`qcharstr` (single character-string), join into one string.
|
|
72
|
+
const def = rdataDefs[i]
|
|
73
|
+
const fieldType = Array.isArray(def) ? def[1] : null
|
|
74
|
+
result[fields[i]] = fieldType === 'charstrs' && tokens.length > 1 ? tokens : tokens.join('')
|
|
73
75
|
break
|
|
74
76
|
}
|
|
75
77
|
|
package/lib/wire.js
CHANGED
|
@@ -15,29 +15,59 @@ export function encodeName(name) {
|
|
|
15
15
|
/**
|
|
16
16
|
* Read a (possibly pointer-compressed) DNS name from a packet.
|
|
17
17
|
* Returns { bytes: Buffer (uncompressed wire name), end: offset after name }.
|
|
18
|
+
*
|
|
19
|
+
* Bounded to defend against malicious or truncated packets:
|
|
20
|
+
* - rejects pointer cycles via a visited-offset set
|
|
21
|
+
* - caps total pointer dereferences at 128
|
|
22
|
+
* - caps the decoded name at 255 bytes (RFC 1035 §3.1)
|
|
23
|
+
* - rejects truncated pointers, pointer targets past the packet end,
|
|
24
|
+
* truncated labels, and unterminated names
|
|
18
25
|
*/
|
|
19
26
|
export function readWireName(packet, offset) {
|
|
27
|
+
if (offset < 0 || offset >= packet.length) {
|
|
28
|
+
throw new Error('readWireName: offset out of bounds')
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
const labels = []
|
|
32
|
+
const visited = new Set()
|
|
21
33
|
let pos = offset
|
|
22
34
|
let end = -1
|
|
35
|
+
let nameLen = 0
|
|
36
|
+
let terminated = false
|
|
23
37
|
|
|
24
38
|
while (pos < packet.length) {
|
|
25
39
|
const len = packet[pos]
|
|
26
40
|
if (len === 0) {
|
|
27
41
|
if (end === -1) end = pos + 1
|
|
28
42
|
labels.push(Buffer.from([0]))
|
|
43
|
+
terminated = true
|
|
29
44
|
break
|
|
30
45
|
}
|
|
31
46
|
if ((len & 0xc0) === 0xc0) {
|
|
47
|
+
if (pos + 1 >= packet.length) throw new Error('readWireName: truncated pointer')
|
|
32
48
|
if (end === -1) end = pos + 2
|
|
33
|
-
|
|
49
|
+
if (visited.has(pos)) throw new Error('readWireName: pointer cycle detected')
|
|
50
|
+
visited.add(pos)
|
|
51
|
+
if (visited.size > 128) throw new Error('readWireName: pointer chain too long')
|
|
52
|
+
const target = ((len & 0x3f) << 8) | packet[pos + 1]
|
|
53
|
+
if (target >= packet.length) throw new Error('readWireName: pointer target out of bounds')
|
|
54
|
+
pos = target
|
|
34
55
|
continue
|
|
35
56
|
}
|
|
57
|
+
// Top two bits not 00 → reserved (RFC 1035 §4.1.4 / RFC 6891)
|
|
58
|
+
if ((len & 0xc0) !== 0) throw new Error('readWireName: reserved label type')
|
|
59
|
+
if (len > 63) throw new Error('readWireName: label exceeds 63 bytes')
|
|
60
|
+
if (pos + 1 + len > packet.length) throw new Error('readWireName: truncated label')
|
|
36
61
|
const label = packet.slice(pos, pos + 1 + len)
|
|
37
62
|
labels.push(label)
|
|
63
|
+
// RFC 1035 §3.1: octets of the full domain name (labels + length octets +
|
|
64
|
+
// final zero) MUST NOT exceed 255 — count the root terminator too.
|
|
65
|
+
nameLen += 1 + len
|
|
66
|
+
if (nameLen + 1 > 255) throw new Error('readWireName: name exceeds 255 bytes')
|
|
38
67
|
pos += 1 + len
|
|
39
68
|
}
|
|
40
69
|
|
|
70
|
+
if (!terminated) throw new Error('readWireName: unterminated name')
|
|
41
71
|
return { bytes: Buffer.concat(labels), end }
|
|
42
72
|
}
|
|
43
73
|
|
|
@@ -207,6 +237,81 @@ export function wirePackDomain(fqdn) {
|
|
|
207
237
|
return buf
|
|
208
238
|
}
|
|
209
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Pack a fully qualified domain name into wire format with RFC 1035 §4.1.4
|
|
242
|
+
* name compression. `dict` is a `Map<suffix, offset>` shared across all names
|
|
243
|
+
* emitted into a single outbound message; `currentOffset` is the absolute
|
|
244
|
+
* offset at which the caller intends to place the returned bytes.
|
|
245
|
+
*
|
|
246
|
+
* Walks labels from longest suffix to shortest: when a suffix already has an
|
|
247
|
+
* offset in `dict`, emits a 2-byte pointer and stops; otherwise emits the label
|
|
248
|
+
* verbatim and records the new suffix offset for later reuse.
|
|
249
|
+
*/
|
|
250
|
+
export function wirePackDomainCompressed(fqdn, dict, currentOffset) {
|
|
251
|
+
if (fqdn === '.') return new Uint8Array([0])
|
|
252
|
+
const enc = new TextEncoder()
|
|
253
|
+
const labels = fqdn
|
|
254
|
+
.split('.')
|
|
255
|
+
.filter((p) => p.length > 0)
|
|
256
|
+
.map((p) => {
|
|
257
|
+
const b = enc.encode(p)
|
|
258
|
+
if (b.length > 63) throw new Error(`DNS label exceeds 63 bytes: ${p}`)
|
|
259
|
+
return b
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// Pre-compute the absolute write offset of each label and the suffix it begins.
|
|
263
|
+
const suffixes = []
|
|
264
|
+
let prefixBytes = 0
|
|
265
|
+
for (let i = 0; i < labels.length; i++) {
|
|
266
|
+
suffixes.push({
|
|
267
|
+
key:
|
|
268
|
+
labels
|
|
269
|
+
.slice(i)
|
|
270
|
+
.map((b) => new TextDecoder().decode(b))
|
|
271
|
+
.join('.') + '.',
|
|
272
|
+
writeOffset: currentOffset + prefixBytes,
|
|
273
|
+
})
|
|
274
|
+
prefixBytes += 1 + labels[i].length
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Find the longest existing suffix in the dict; everything to its right gets
|
|
278
|
+
// replaced by a single pointer.
|
|
279
|
+
let pointerAt = labels.length
|
|
280
|
+
let pointerOffset = -1
|
|
281
|
+
for (let i = 0; i < suffixes.length; i++) {
|
|
282
|
+
const hit = dict.get(suffixes[i].key)
|
|
283
|
+
if (hit !== undefined && hit < 0x4000) {
|
|
284
|
+
pointerAt = i
|
|
285
|
+
pointerOffset = hit
|
|
286
|
+
break
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Compute output size and allocate.
|
|
291
|
+
let outSize = 0
|
|
292
|
+
for (let i = 0; i < pointerAt; i++) outSize += 1 + labels[i].length
|
|
293
|
+
outSize += pointerAt < labels.length ? 2 : 1 // pointer (2 bytes) or root terminator (1)
|
|
294
|
+
const buf = new Uint8Array(outSize)
|
|
295
|
+
|
|
296
|
+
let off = 0
|
|
297
|
+
for (let i = 0; i < pointerAt; i++) {
|
|
298
|
+
// Record this suffix for future reuse before writing past it.
|
|
299
|
+
if (suffixes[i].writeOffset < 0x4000 && !dict.has(suffixes[i].key)) {
|
|
300
|
+
dict.set(suffixes[i].key, suffixes[i].writeOffset)
|
|
301
|
+
}
|
|
302
|
+
buf[off++] = labels[i].length
|
|
303
|
+
buf.set(labels[i], off)
|
|
304
|
+
off += labels[i].length
|
|
305
|
+
}
|
|
306
|
+
if (pointerAt < labels.length) {
|
|
307
|
+
buf[off++] = 0xc0 | ((pointerOffset >> 8) & 0x3f)
|
|
308
|
+
buf[off] = pointerOffset & 0xff
|
|
309
|
+
} else {
|
|
310
|
+
buf[off] = 0
|
|
311
|
+
}
|
|
312
|
+
return buf
|
|
313
|
+
}
|
|
314
|
+
|
|
210
315
|
/**
|
|
211
316
|
* Get wire format rdata for an RR instance.
|
|
212
317
|
* Derived classes should override this with direct RFC 1035 wire encoding.
|
|
@@ -332,13 +437,18 @@ export function fromWireGeneric(rrInstance, { owner, cls, ttl, rdata }) {
|
|
|
332
437
|
break
|
|
333
438
|
}
|
|
334
439
|
case 'charstrs': {
|
|
440
|
+
// RFC 1035 §3.3.14: each <character-string> on the wire is len-prefixed.
|
|
441
|
+
// Preserve multi-string boundaries by returning an array when >1.
|
|
335
442
|
const parts = []
|
|
336
443
|
while (pos < rdata.length) {
|
|
337
444
|
const strLen = rdata[pos++]
|
|
445
|
+
if (pos + strLen > rdata.length) {
|
|
446
|
+
throw new Error('fromWireGeneric: truncated character-string in rdata')
|
|
447
|
+
}
|
|
338
448
|
parts.push(new TextDecoder().decode(rdata.subarray(pos, pos + strLen)))
|
|
339
449
|
pos += strLen
|
|
340
450
|
}
|
|
341
|
-
result[fieldName] = parts.
|
|
451
|
+
result[fieldName] = parts.length > 1 ? parts : (parts[0] ?? '')
|
|
342
452
|
break
|
|
343
453
|
}
|
|
344
454
|
case 'svcparams':
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nictool/dns-resource-record",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "DNS Resource Records",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
8
|
"browser": "./dist/dns-rr.min.js",
|
|
9
|
+
"import": "./index.js",
|
|
10
|
+
"require": "./dist/dns-rr.cjs",
|
|
9
11
|
"default": "./index.js"
|
|
10
12
|
}
|
|
11
13
|
},
|
|
@@ -18,8 +20,9 @@
|
|
|
18
20
|
],
|
|
19
21
|
"type": "module",
|
|
20
22
|
"scripts": {
|
|
21
|
-
"build": "npm run build:browser",
|
|
23
|
+
"build": "npm run build:browser && npm run build:cjs",
|
|
22
24
|
"build:browser": "npx rollup -i index.js -o dist/dns-rr.js -f es && esbuild dist/dns-rr.js --minify --sourcemap --outfile=dist/dns-rr.min.js && rm dist/dns-rr.js",
|
|
25
|
+
"build:cjs": "npx rollup -i index.js -o dist/dns-rr.cjs -f cjs --exports named",
|
|
23
26
|
"clean": "rm -rf coverage dist node_modules package-lock.json",
|
|
24
27
|
"format:check": "npm run prettier; npm run lint",
|
|
25
28
|
"format": "npm run prettier -- --write && npm run lint:fix",
|
|
@@ -58,7 +61,7 @@
|
|
|
58
61
|
},
|
|
59
62
|
"homepage": "https://github.com/NicTool/dns-resource-record#readme",
|
|
60
63
|
"engines": {
|
|
61
|
-
"node": ">=
|
|
64
|
+
"node": ">=20"
|
|
62
65
|
},
|
|
63
66
|
"browserslist": [
|
|
64
67
|
"chrome >= 74",
|
|
@@ -68,8 +71,8 @@
|
|
|
68
71
|
],
|
|
69
72
|
"devDependencies": {
|
|
70
73
|
"@eslint/js": "^10.0.1",
|
|
71
|
-
"eslint": "^10.
|
|
72
|
-
"globals": "^17.
|
|
74
|
+
"eslint": "^10.4.0",
|
|
75
|
+
"globals": "^17.6.0"
|
|
73
76
|
},
|
|
74
77
|
"prettier": {
|
|
75
78
|
"printWidth": 110,
|
package/rr/cert.js
CHANGED
|
@@ -58,6 +58,14 @@ export default class CERT extends RR {
|
|
|
58
58
|
this.throwHelp(`CERT: unknown cert type mnemonic: ${val}`)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
setKeyTag(val) {
|
|
62
|
+
this.setTypedValue('u16', 'key tag', val)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setAlgorithm(val) {
|
|
66
|
+
this.setTypedValue('u8', 'algorithm', val)
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
setCertificate(val) {
|
|
62
70
|
// certificate/CRL portion is represented in base 64 [16] and may be
|
|
63
71
|
// divided into any number of white-space-separated substrings
|
package/rr/cname.js
CHANGED
package/rr/dname.js
CHANGED
package/rr/ds.js
CHANGED
|
@@ -15,6 +15,14 @@ export default class DS extends RR {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/****** Resource record specific setters *******/
|
|
18
|
+
setKeyTag(val) {
|
|
19
|
+
this.setTypedValue('u16', 'key tag', val)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setDigest(val) {
|
|
23
|
+
this.setTypedValue('str', 'digest', val)
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
setAlgorithm(val) {
|
|
19
27
|
if (!this.getAlgorithmOptions().has(val)) this.throwHelp(`DS: algorithm invalid`)
|
|
20
28
|
|
|
@@ -22,19 +30,30 @@ export default class DS extends RR {
|
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
getAlgorithmOptions() {
|
|
33
|
+
// IANA DNSSEC Algorithm Numbers
|
|
34
|
+
// https://www.iana.org/assignments/dns-sec-alg-numbers/
|
|
25
35
|
return new Map([
|
|
26
36
|
[1, 'RSA/MD5'],
|
|
27
37
|
[2, 'DH'],
|
|
28
38
|
[3, 'DSA/SHA-1'],
|
|
29
39
|
[4, 'EC'],
|
|
30
40
|
[5, 'RSA/SHA-1'],
|
|
41
|
+
[6, 'DSA-NSEC3-SHA1'],
|
|
42
|
+
[7, 'RSASHA1-NSEC3-SHA1'],
|
|
43
|
+
[8, 'RSA/SHA-256'],
|
|
44
|
+
[10, 'RSA/SHA-512'],
|
|
45
|
+
[13, 'ECDSA P-256/SHA-256'],
|
|
46
|
+
[14, 'ECDSA P-384/SHA-384'],
|
|
47
|
+
[15, 'Ed25519'],
|
|
48
|
+
[16, 'Ed448'],
|
|
31
49
|
[253, ''],
|
|
32
50
|
[254, ''],
|
|
33
51
|
])
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
setDigestType(val) {
|
|
37
|
-
|
|
55
|
+
// 1=SHA-1 (RFC 4034), 2=SHA-256 (RFC 4509), 4=SHA-384 (RFC 6605)
|
|
56
|
+
if (![1, 2, 4].includes(val)) this.throwHelp(`DS: digest type invalid`)
|
|
38
57
|
|
|
39
58
|
this.set('digest type', val)
|
|
40
59
|
}
|
package/rr/mx.js
CHANGED
package/rr/rrsig.js
CHANGED
|
@@ -49,13 +49,51 @@ export default class RRSIG extends RR {
|
|
|
49
49
|
this.set('algorithm', val)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
setLabels(val) {
|
|
53
|
+
this.setTypedValue('u8', 'labels', val)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setOriginalTtl(val) {
|
|
57
|
+
this.setTypedValue('u32', 'original ttl', val)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setSignatureExpiration(val) {
|
|
61
|
+
this.setTypedValue('u32', 'signature expiration', val)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setSignatureInception(val) {
|
|
65
|
+
this.setTypedValue('u32', 'signature inception', val)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setKeyTag(val) {
|
|
69
|
+
this.setTypedValue('u16', 'key tag', val)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setSignersName(val) {
|
|
73
|
+
this.setTypedValue('fqdn', 'signers name', val)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setSignature(val) {
|
|
77
|
+
this.setTypedValue('str', 'signature', val)
|
|
78
|
+
}
|
|
79
|
+
|
|
52
80
|
getAlgorithmOptions() {
|
|
81
|
+
// IANA DNSSEC Algorithm Numbers
|
|
82
|
+
// https://www.iana.org/assignments/dns-sec-alg-numbers/
|
|
53
83
|
return new Map([
|
|
54
84
|
[1, 'RSA/MD5'],
|
|
55
85
|
[2, 'DH'],
|
|
56
|
-
[3, '
|
|
86
|
+
[3, 'DSA/SHA-1'],
|
|
57
87
|
[4, 'EC'],
|
|
58
88
|
[5, 'RSA/SHA-1'],
|
|
89
|
+
[6, 'DSA-NSEC3-SHA1'],
|
|
90
|
+
[7, 'RSASHA1-NSEC3-SHA1'],
|
|
91
|
+
[8, 'RSA/SHA-256'],
|
|
92
|
+
[10, 'RSA/SHA-512'],
|
|
93
|
+
[13, 'ECDSA P-256/SHA-256'],
|
|
94
|
+
[14, 'ECDSA P-384/SHA-384'],
|
|
95
|
+
[15, 'Ed25519'],
|
|
96
|
+
[16, 'Ed448'],
|
|
59
97
|
[253],
|
|
60
98
|
[254],
|
|
61
99
|
])
|
package/rr/spf.js
CHANGED
|
@@ -55,7 +55,11 @@ export default class SPF extends TXT {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
toTinydns() {
|
|
58
|
-
|
|
58
|
+
// `data` may be a string or an array of <character-string>s (RFC 1035 §3.3.14).
|
|
59
|
+
// tinydns generic format stores rdata as a flat byte stream, so join here.
|
|
60
|
+
let data = this.get('data')
|
|
61
|
+
if (Array.isArray(data)) data = data.join('')
|
|
62
|
+
const rdata = TINYDNS.escapeOctal(new RegExp(/[\r\n\t:\\/]/, 'g'), data)
|
|
59
63
|
return this.getTinydnsGeneric(rdata)
|
|
60
64
|
}
|
|
61
65
|
}
|
package/rr/srv.js
CHANGED
|
@@ -17,6 +17,23 @@ export default class SRV extends RR {
|
|
|
17
17
|
super(opts)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/****** Resource record specific setters *******/
|
|
21
|
+
setPriority(val) {
|
|
22
|
+
this.setTypedValue('u16', 'priority', val)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setWeight(val) {
|
|
26
|
+
this.setTypedValue('u16', 'weight', val)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setPort(val) {
|
|
30
|
+
this.setTypedValue('u16', 'port', val)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setTarget(val) {
|
|
34
|
+
this.setTypedValue('fqdn', 'target', val)
|
|
35
|
+
}
|
|
36
|
+
|
|
20
37
|
getDescription() {
|
|
21
38
|
return 'Service'
|
|
22
39
|
}
|
package/rr/sshfp.js
CHANGED
|
@@ -17,6 +17,21 @@ export default class SSHFP extends RR {
|
|
|
17
17
|
super(opts)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/****** Resource record specific setters *******/
|
|
21
|
+
setAlgorithm(val) {
|
|
22
|
+
if (!this.getAlgorithmOptions().has(val)) this.throwHelp(`SSHFP: algorithm invalid`)
|
|
23
|
+
this.setTypedValue('u8', 'algorithm', val)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setFptype(val) {
|
|
27
|
+
if (!this.getFptypeOptions().has(val)) this.throwHelp(`SSHFP: fptype invalid`)
|
|
28
|
+
this.setTypedValue('u8', 'fptype', val)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setFingerprint(val) {
|
|
32
|
+
this.setTypedValue('hex', 'fingerprint', val)
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
getAlgorithmOptions() {
|
|
21
36
|
return new Map([
|
|
22
37
|
[0, 'reserved'],
|
package/rr/txt.js
CHANGED
|
@@ -62,15 +62,20 @@ export default class TXT extends RR {
|
|
|
62
62
|
if (n != 16) this.throwHelp('TXT fromTinydns, invalid n')
|
|
63
63
|
|
|
64
64
|
rdata = TINYDNS.octalToChar(rdata)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
let pos =
|
|
65
|
+
// Walk RFC 1035 §3.3.14 len-prefixed <character-string> segments.
|
|
66
|
+
const parts = []
|
|
67
|
+
let pos = 0
|
|
68
68
|
while (pos < rdata.length) {
|
|
69
|
-
|
|
70
|
-
pos
|
|
71
|
-
|
|
69
|
+
const len = rdata.charCodeAt(pos)
|
|
70
|
+
pos += 1
|
|
71
|
+
if (pos + len > rdata.length) {
|
|
72
|
+
this.throwHelp('TXT fromTinydnsGeneric: truncated character-string in rdata')
|
|
73
|
+
}
|
|
74
|
+
parts.push(rdata.slice(pos, pos + len))
|
|
75
|
+
pos += len
|
|
72
76
|
}
|
|
73
|
-
|
|
77
|
+
const data = parts.length > 1 ? parts : (parts[0] ?? '')
|
|
78
|
+
return [fqdn, data, ttl, ts, loc]
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
/****** EXPORTERS *******/
|
|
@@ -84,8 +89,31 @@ export default class TXT extends RR {
|
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
getWireRdata() {
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
// RFC 1035 §3.3.14: TXT rdata is one or more <character-string>s, each up
|
|
93
|
+
// to 255 bytes. An array preserves explicit boundaries between strings;
|
|
94
|
+
// each element MUST be <= 255 UTF-8 bytes, otherwise we would silently
|
|
95
|
+
// split it and the boundary the caller asked us to preserve would be lost.
|
|
96
|
+
// A single string is auto-chunked at 255-byte UTF-8 boundaries.
|
|
97
|
+
const data = this.get('data')
|
|
98
|
+
if (Array.isArray(data)) {
|
|
99
|
+
const enc = new TextEncoder()
|
|
100
|
+
const buffers = data.map((s, i) => {
|
|
101
|
+
if (enc.encode(s).length > 255) {
|
|
102
|
+
this.throwHelp(
|
|
103
|
+
`TXT: array element ${i} exceeds 255 bytes; split it yourself or pass a single string to auto-chunk`,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
return packStringWire(s)
|
|
107
|
+
})
|
|
108
|
+
const total = buffers.reduce((n, b) => n + b.length, 0)
|
|
109
|
+
const out = new Uint8Array(total)
|
|
110
|
+
let off = 0
|
|
111
|
+
for (const b of buffers) {
|
|
112
|
+
out.set(b, off)
|
|
113
|
+
off += b.length
|
|
114
|
+
}
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
89
117
|
return packStringWire(data)
|
|
90
118
|
}
|
|
91
119
|
|