Justin Fisher ab3c5ca3f5 feat: add support for HTTPS SVCB (type 65)
* This commit adds support for HTTPS SVCB record, both encoding and decoding.
2026-04-06 17:17:03 +08:00

996 lines
30 KiB
JavaScript

'use strict'
const tape = require('tape')
const packet = require('./')
const rcodes = require('./rcodes')
const opcodes = require('./opcodes')
const optioncodes = require('./optioncodes')
const ip = require('@leichtgewicht/ip-codec')
const hexdump = require('./hexdump')
tape('unknown', function (t) {
testEncoder(t, packet.unknown, Buffer.from('hello world'))
t.end()
})
tape('txt', function (t) {
testEncoder(t, packet.txt, [])
testEncoder(t, packet.txt, ['hello world'])
testEncoder(t, packet.txt, ['hello', 'world'])
testEncoder(t, packet.txt, [Buffer.from([0, 1, 2, 3, 4, 5])])
testEncoder(t, packet.txt, ['a', 'b', Buffer.from([0, 1, 2, 3, 4, 5])])
testEncoder(t, packet.txt, ['', Buffer.allocUnsafe(0)])
t.end()
})
tape('txt-scalar-string', function (t) {
const buf = packet.txt.encode('hi')
const val = packet.txt.decode(buf)
t.ok(val.length === 1, 'array length')
t.ok(val[0].toString() === 'hi', 'data')
t.end()
})
tape('txt-scalar-buffer', function (t) {
const data = Buffer.from([0, 1, 2, 3, 4, 5])
const buf = packet.txt.encode(data)
const val = packet.txt.decode(buf)
t.ok(val.length === 1, 'array length')
t.ok(val[0].equals(data), 'data')
t.end()
})
tape('txt-invalid-data', function (t) {
t.throws(function () { packet.txt.encode(null) }, 'null')
t.throws(function () { packet.txt.encode(undefined) }, 'undefined')
t.throws(function () { packet.txt.encode(10) }, 'number')
t.end()
})
tape('null', function (t) {
testEncoder(t, packet.null, Buffer.from([0, 1, 2, 3, 4, 5]))
t.end()
})
tape('hinfo', function (t) {
testEncoder(t, packet.hinfo, { cpu: 'intel', os: 'best one' })
t.end()
})
tape('ptr', function (t) {
testEncoder(t, packet.ptr, 'hello.world.com')
t.end()
})
tape('cname', function (t) {
testEncoder(t, packet.cname, 'hello.cname.world.com')
t.end()
})
tape('dname', function (t) {
testEncoder(t, packet.dname, 'hello.dname.world.com')
t.end()
})
tape('srv', function (t) {
testEncoder(t, packet.srv, { port: 9999, target: 'hello.world.com' })
testEncoder(t, packet.srv, { port: 9999, target: 'hello.world.com', priority: 42, weight: 10 })
t.end()
})
tape('caa', function (t) {
testEncoder(t, packet.caa, { flags: 128, tag: 'issue', value: 'letsencrypt.org', issuerCritical: true })
testEncoder(t, packet.caa, { tag: 'issue', value: 'letsencrypt.org', issuerCritical: true })
testEncoder(t, packet.caa, { tag: 'issue', value: 'letsencrypt.org' })
t.end()
})
tape('mx', function (t) {
testEncoder(t, packet.mx, { preference: 10, exchange: 'mx.hello.world.com' })
testEncoder(t, packet.mx, { exchange: 'mx.hello.world.com' })
t.end()
})
tape('ns', function (t) {
testEncoder(t, packet.ns, 'ns.world.com')
t.end()
})
tape('soa', function (t) {
testEncoder(t, packet.soa, {
mname: 'hello.world.com',
rname: 'root.hello.world.com',
serial: 2018010400,
refresh: 14400,
retry: 3600,
expire: 604800,
minimum: 3600
})
t.end()
})
tape('sshfp', function (t) {
testEncoder(t, packet.sshfp, {
algorithm: 1,
hash: 1,
fingerprint: 'A108C9F834354D5B37AF988141C9294822F5BC00'
})
t.end()
})
tape('a', function (t) {
testEncoder(t, packet.a, '127.0.0.1')
t.end()
})
tape('aaaa', function (t) {
testEncoder(t, packet.aaaa, 'fe80::1')
t.end()
})
tape('query', function (t) {
testEncoder(t, packet, {
type: 'query',
questions: [{
type: 'A',
name: 'hello.a.com'
}, {
type: 'SRV',
name: 'hello.srv.com'
}]
})
testEncoder(t, packet, {
type: 'query',
id: 42,
questions: [{
type: 'A',
class: 'IN',
name: 'hello.a.com'
}, {
type: 'SRV',
name: 'hello.srv.com'
}]
})
testEncoder(t, packet, {
type: 'query',
id: 42,
questions: [{
type: 'A',
class: 'CH',
name: 'hello.a.com'
}, {
type: 'SRV',
name: 'hello.srv.com'
}]
})
testEncoder(t, packet, {
type: 'query',
questions: [{
type: 'A',
class: 'IN',
name: 'hello.a.com',
qu_bit: false
}, {
type: 'SRV',
name: 'hello.srv.com',
qu_bit: false
}]
})
testEncoder(t, packet, {
type: 'query',
questions: [{
type: 'A',
class: 'IN',
name: 'hello.a.com',
qu_bit: true
}, {
type: 'SRV',
name: 'hello.srv.com',
qu_bit: true
}]
})
t.end()
})
tape('response', function (t) {
testEncoder(t, packet, {
type: 'response',
answers: [{
type: 'A',
class: 'IN',
flush: true,
name: 'hello.a.com',
data: '127.0.0.1'
}]
})
testEncoder(t, packet, {
type: 'response',
flags: packet.TRUNCATED_RESPONSE,
answers: [{
type: 'A',
class: 'IN',
name: 'hello.a.com',
data: '127.0.0.1'
}, {
type: 'SRV',
class: 'IN',
name: 'hello.srv.com',
data: {
port: 9090,
target: 'hello.target.com'
}
}, {
type: 'CNAME',
class: 'IN',
name: 'hello.cname.com',
data: 'hello.other.domain.com'
}]
})
testEncoder(t, packet, {
type: 'response',
id: 100,
flags: 0,
additionals: [{
type: 'AAAA',
name: 'hello.a.com',
data: 'fe80::1'
}, {
type: 'PTR',
name: 'hello.ptr.com',
data: 'hello.other.ptr.com'
}, {
type: 'SRV',
name: 'hello.srv.com',
ttl: 42,
data: {
port: 9090,
target: 'hello.target.com'
}
}],
answers: [{
type: 'NULL',
name: 'hello.null.com',
data: Buffer.from([1, 2, 3, 4, 5])
}]
})
testEncoder(t, packet, {
type: 'response',
answers: [{
type: 'TXT',
name: 'emptytxt.com',
data: ''
}]
})
t.end()
})
tape('rcode', function (t) {
const errors = ['NOERROR', 'FORMERR', 'SERVFAIL', 'NXDOMAIN', 'NOTIMP', 'REFUSED', 'YXDOMAIN', 'YXRRSET', 'NXRRSET', 'NOTAUTH', 'NOTZONE', 'RCODE_11', 'RCODE_12', 'RCODE_13', 'RCODE_14', 'RCODE_15']
for (const i in errors) {
const code = rcodes.toRcode(errors[i])
t.ok(errors[i] === rcodes.toString(code), 'rcode conversion from/to string matches: ' + rcodes.toString(code))
}
const ops = ['QUERY', 'IQUERY', 'STATUS', 'OPCODE_3', 'NOTIFY', 'UPDATE', 'OPCODE_6', 'OPCODE_7', 'OPCODE_8', 'OPCODE_9', 'OPCODE_10', 'OPCODE_11', 'OPCODE_12', 'OPCODE_13', 'OPCODE_14', 'OPCODE_15']
for (const j in ops) {
const ocode = opcodes.toOpcode(ops[j])
t.ok(ops[j] === opcodes.toString(ocode), 'opcode conversion from/to string matches: ' + opcodes.toString(ocode))
}
const buf = packet.encode({
type: 'response',
id: 45632,
flags: 0x8480,
answers: [{
type: 'A',
name: 'hello.example.net',
data: '127.0.0.1'
}]
})
const val = packet.decode(buf)
t.ok(val.type === 'response', 'decode type')
t.ok(val.opcode === 'QUERY', 'decode opcode')
t.ok(val.flag_qr === true, 'decode flag_qr')
t.ok(val.flag_aa === true, 'decode flag_aa')
t.ok(val.flag_tc === false, 'decode flag_tc')
t.ok(val.flag_rd === false, 'decode flag_rd')
t.ok(val.flag_ra === true, 'decode flag_ra')
t.ok(val.flag_z === false, 'decode flag_z')
t.ok(val.flag_ad === false, 'decode flag_ad')
t.ok(val.flag_cd === false, 'decode flag_cd')
t.ok(val.rcode === 'NOERROR', 'decode rcode')
t.end()
})
tape('name_encoding', function (t) {
let data = 'foo.example.com'
const buf = Buffer.allocUnsafe(255)
let offset = 0
packet.name.encode(data, buf, offset)
t.ok(packet.name.encode.bytes === 17, 'name encoding length matches')
let dd = packet.name.decode(buf, offset)
t.ok(data === dd, 'encode/decode matches')
offset += packet.name.encode.bytes
data = 'com'
packet.name.encode(data, buf, offset)
t.ok(packet.name.encode.bytes === 5, 'name encoding length matches')
dd = packet.name.decode(buf, offset)
t.ok(data === dd, 'encode/decode matches')
offset += packet.name.encode.bytes
data = 'example.com.'
packet.name.encode(data, buf, offset)
t.ok(packet.name.encode.bytes === 13, 'name encoding length matches')
dd = packet.name.decode(buf, offset)
t.ok(data.slice(0, -1) === dd, 'encode/decode matches')
offset += packet.name.encode.bytes
data = '.'
packet.name.encode(data, buf, offset)
t.ok(packet.name.encode.bytes === 1, 'name encoding length matches')
dd = packet.name.decode(buf, offset)
t.ok(data === dd, 'encode/decode matches')
offset += packet.name.encode.bytes
data = 'a.b.c.d.example.com'
packet.name.encode(data, buf, offset, { mail: false })
t.ok(packet.name.encode.bytes === 21, 'name (mail) encoding length matches')
dd = packet.name.decode(buf, offset)
t.ok(data === dd, 'encode/decode matches')
offset += packet.name.encode.bytes
data = 'a.b.c.d.example.com'
packet.name.encode(data, buf, offset, { mail: true })
t.ok(packet.name.encode.bytes === 21, 'name (mail) encoding length matches')
dd = packet.name.decode(buf, offset)
t.ok(data === dd, 'encode/decode matches')
offset += packet.name.encode.bytes
data = 'a\\.b.c.d.example.com'
packet.name.encode(data, buf, offset, { mail: true })
t.ok(packet.name.encode.bytes === 21, 'name (mail) encoding length matches')
dd = packet.name.decode(buf, offset, { mail: true })
t.ok(data === dd, 'encode/decode matches')
offset += packet.name.encode.bytes
data = 'a\\.b\\.c.d.example.com'
packet.name.encode(data, buf, offset, { mail: true })
t.ok(packet.name.encode.bytes === 21, 'name (mail) encoding length matches')
dd = packet.name.decode(buf, offset, { mail: true })
t.ok(data === dd, 'encode/decode matches')
offset += packet.name.encode.bytes
data = 'root\\.mail'
packet.name.encode(data, buf, offset, { mail: true })
t.ok(packet.name.encode.bytes === 11, 'name (mail) encoding length matches')
dd = packet.name.decode(buf, offset, { mail: true })
t.ok(data === dd, 'encode/decode matches')
offset += packet.name.encode.bytes
t.end()
})
tape('name_decoding', function (t) {
// The two most significant bits of a valid label header must be either both zero or both one
t.throws(function () { packet.name.decode(Buffer.from([0x80])) }, /Cannot decode name \(bad label\)$/)
t.throws(function () { packet.name.decode(Buffer.from([0xb0])) }, /Cannot decode name \(bad label\)$/)
// Ensure there's enough buffer to read
t.throws(function () { packet.name.decode(Buffer.from([])) }, /Cannot decode name \(buffer overflow\)$/)
t.throws(function () { packet.name.decode(Buffer.from([0x01, 0x00])) }, /Cannot decode name \(buffer overflow\)$/)
t.throws(function () { packet.name.decode(Buffer.from([0x01])) }, /Cannot decode name \(buffer overflow\)$/)
t.throws(function () { packet.name.decode(Buffer.from([0xc0])) }, /Cannot decode name \(buffer overflow\)$/)
// Allow only pointers backwards
t.throws(function () { packet.name.decode(Buffer.from([0xc0, 0x00])) }, /Cannot decode name \(bad pointer\)$/)
t.throws(function () { packet.name.decode(Buffer.from([0xc0, 0x01])) }, /Cannot decode name \(bad pointer\)$/)
// A name can be only 253 characters (when connected with dots)
const maxLength = Buffer.alloc(255)
maxLength.fill(Buffer.from([0x01, 0x61]), 0, 254)
t.ok(packet.name.decode(maxLength) === new Array(127).fill('a').join('.'))
const tooLong = Buffer.alloc(256)
tooLong.fill(Buffer.from([0x01, 0x61]))
t.throws(function () { packet.name.decode(tooLong) }, /Cannot decode name \(name too long\)$/)
// Ensure jumps don't reset the total length counter
const tooLongWithJump = Buffer.alloc(403)
tooLongWithJump.fill(Buffer.from([0x01, 0x61]), 0, 200)
tooLongWithJump.fill(Buffer.from([0x01, 0x61]), 201, 401)
tooLongWithJump.set([0xc0, 0x00], 401)
t.throws(function () { packet.name.decode(tooLongWithJump, 201) }, /Cannot decode name \(name too long\)$/)
// Ensure a jump to a null byte doesn't add extra dots
t.ok(packet.name.decode(Buffer.from([0x00, 0x01, 0x61, 0xc0, 0x00]), 1) === 'a')
// Ensure deeply nested pointers don't cause "Maximum call stack size exceeded" errors
const buf = Buffer.alloc(16386)
for (let i = 0; i < 16384; i += 2) {
buf.writeUInt16BE(0xc000 | i, i + 2)
}
t.ok(packet.name.decode(buf, 16384) === '.')
t.end()
})
tape('stream', function (t) {
const val = {
type: 'query',
id: 45632,
flags: 0x8480,
answers: [{
type: 'A',
name: 'test2.example.net',
data: '198.51.100.1'
}]
}
const buf = packet.streamEncode(val)
const val2 = packet.streamDecode(buf)
t.same(buf.length, packet.streamEncode.bytes, 'streamEncode.bytes was set correctly')
t.ok(compare(t, val2.type, val.type), 'streamDecoded type match')
t.ok(compare(t, val2.id, val.id), 'streamDecoded id match')
t.ok(parseInt(val2.flags) === parseInt(val.flags & 0x7FFF), 'streamDecoded flags match')
const answer = val.answers[0]
const answer2 = val2.answers[0]
t.ok(compare(t, answer.type, answer2.type), 'streamDecoded RR type match')
t.ok(compare(t, answer.name, answer2.name), 'streamDecoded RR name match')
t.ok(compare(t, answer.data, answer2.data), 'streamDecoded RR rdata match')
t.end()
})
tape('opt', function (t) {
const val = {
type: 'query',
questions: [{
type: 'A',
name: 'hello.a.com'
}],
additionals: [{
type: 'OPT',
name: '.',
udpPayloadSize: 1024
}]
}
testEncoder(t, packet, val)
let buf = packet.encode(val)
let val2 = packet.decode(buf)
const additional1 = val.additionals[0]
let additional2 = val2.additionals[0]
t.ok(compare(t, additional1.name, additional2.name), 'name matches')
t.ok(compare(t, additional1.udpPayloadSize, additional2.udpPayloadSize), 'udp payload size matches')
t.ok(compare(t, 0, additional2.flags), 'flags match')
additional1.flags = packet.DNSSEC_OK
additional1.extendedRcode = 0x80
additional1.options = [ {
code: 'CLIENT_SUBNET', // edns-client-subnet, see RFC 7871
ip: 'fe80::',
sourcePrefixLength: 64
}, {
code: 8, // still ECS
ip: '5.6.0.0',
sourcePrefixLength: 16,
scopePrefixLength: 16
}, {
code: 'padding',
length: 31
}, {
code: 'TCP_KEEPALIVE'
}, {
code: 'tcp_keepalive',
timeout: 150
}, {
code: 'KEY_TAG',
tags: [1, 82, 987]
}]
buf = packet.encode(val)
val2 = packet.decode(buf)
additional2 = val2.additionals[0]
t.ok(compare(t, 1 << 15, additional2.flags), 'DO bit set in flags')
t.ok(compare(t, true, additional2.flag_do), 'DO bit set')
t.ok(compare(t, additional1.extendedRcode, additional2.extendedRcode), 'extended rcode matches')
t.ok(compare(t, 8, additional2.options[0].code))
t.ok(compare(t, 'fe80::', additional2.options[0].ip))
t.ok(compare(t, 64, additional2.options[0].sourcePrefixLength))
t.ok(compare(t, '5.6.0.0', additional2.options[1].ip))
t.ok(compare(t, 16, additional2.options[1].sourcePrefixLength))
t.ok(compare(t, 16, additional2.options[1].scopePrefixLength))
t.ok(compare(t, additional1.options[2].length, additional2.options[2].data.length))
t.ok(compare(t, additional1.options[3].timeout, undefined))
t.ok(compare(t, additional1.options[4].timeout, additional2.options[4].timeout))
t.ok(compare(t, additional1.options[5].tags, additional2.options[5].tags))
t.end()
})
tape('dnskey', function (t) {
testEncoder(t, packet.dnskey, {
flags: packet.dnskey.SECURE_ENTRYPOINT | packet.dnskey.ZONE_KEY,
algorithm: 1,
key: Buffer.from([0, 1, 2, 3, 4, 5])
})
t.end()
})
tape('rrsig', function (t) {
const testRRSIG = {
typeCovered: 'A',
algorithm: 1,
labels: 2,
originalTTL: 3600,
expiration: 1234,
inception: 1233,
keyTag: 2345,
signersName: 'foo.com',
signature: Buffer.from([0, 1, 2, 3, 4, 5])
}
testEncoder(t, packet.rrsig, testRRSIG)
// Check the signature length is correct with extra junk at the end
const buf = Buffer.allocUnsafe(packet.rrsig.encodingLength(testRRSIG) + 4)
packet.rrsig.encode(testRRSIG, buf)
const val2 = packet.rrsig.decode(buf)
t.ok(compare(t, testRRSIG, val2))
t.end()
})
tape('rrp', function (t) {
testEncoder(t, packet.rp, {
mbox: 'foo.bar.com',
txt: 'baz.bar.com'
})
testEncoder(t, packet.rp, {
mbox: 'foo.bar.com'
})
testEncoder(t, packet.rp, {
txt: 'baz.bar.com'
})
testEncoder(t, packet.rp, {})
t.end()
})
tape('nsec', function (t) {
testEncoder(t, packet.nsec, {
nextDomain: 'foo.com',
rrtypes: ['A', 'DNSKEY', 'CAA', 'DLV']
})
testEncoder(t, packet.nsec, {
nextDomain: 'foo.com',
rrtypes: ['TXT'] // 16
})
testEncoder(t, packet.nsec, {
nextDomain: 'foo.com',
rrtypes: ['TKEY'] // 249
})
testEncoder(t, packet.nsec, {
nextDomain: 'foo.com',
rrtypes: ['RRSIG', 'NSEC']
})
testEncoder(t, packet.nsec, {
nextDomain: 'foo.com',
rrtypes: ['TXT', 'RRSIG']
})
testEncoder(t, packet.nsec, {
nextDomain: 'foo.com',
rrtypes: ['TXT', 'NSEC']
})
// Test with the sample NSEC from https://tools.ietf.org/html/rfc4034#section-4.3
var sampleNSEC = Buffer.from('003704686f7374076578616d706c6503636f6d00' +
'0006400100000003041b000000000000000000000000000000000000000000000' +
'000000020', 'hex')
var decoded = packet.nsec.decode(sampleNSEC)
t.ok(compare(t, decoded, {
nextDomain: 'host.example.com',
rrtypes: ['A', 'MX', 'RRSIG', 'NSEC', 'UNKNOWN_1234']
}))
var reencoded = packet.nsec.encode(decoded)
t.same(sampleNSEC.length, reencoded.length)
t.same(sampleNSEC, reencoded)
t.end()
})
tape('nsec3', function (t) {
testEncoder(t, packet.nsec3, {
algorithm: 1,
flags: 0,
iterations: 257,
salt: Buffer.from([42, 42, 42]),
nextDomain: Buffer.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]),
rrtypes: ['A', 'DNSKEY', 'CAA', 'DLV']
})
t.end()
})
tape('ds', function (t) {
testEncoder(t, packet.ds, {
keyTag: 1234,
algorithm: 1,
digestType: 1,
digest: Buffer.from([0, 1, 2, 3, 4, 5])
})
t.end()
})
tape('naptr', function (t) {
testEncoder(t, packet.naptr, {
order: 1,
preference: 1,
flags: 'S',
services: 'SIP+D2T',
regexp: '!^.*$!sip:customer-service@xuexample.com!',
replacement: '_sip._udp.xuexample.com'
})
t.end()
})
tape('tlsa', function (t) {
testEncoder(t, packet.tlsa, {
usage: 3,
selector: 1,
matchingType: 1,
certificate: Buffer.from([0, 1, 2, 3, 4, 5])
})
t.end()
})
const unhexlify = (hex) => {
return Buffer.from(hex.match(/[\da-f]{2}/gi).map(function (h) {return parseInt(h, 16)}))
}
// <HTTPS SVCB test cases>
const debug_https = false
const test_http_decode_encode = (t, testname, packetbuf, expected, skip_encode=false, skip_memcmp_bufs=false) => {
const decoded = packet.httpssvc.decode(packetbuf, 0)
if (debug_https) {
console.log(`${testname}: decode:`)
console.log(JSON.stringify(decoded, null, 2))
}
t.ok(compare(t, decoded, expected), testname + ' decode')
const encoded = packet.httpssvc.encode(expected)
if (!skip_encode) {
if (debug_https) {
console.log(`${testname}: encode:`)
hexdump(encoded)
}
if (!skip_memcmp_bufs) {
t.ok(compare(t, packetbuf, encoded), testname + ' encode memcmp')
}
// now decode the encoded buffer and check for sameness
const recoded = packet.httpssvc.decode(encoded, 0)
if (debug_https) {
console.log(`${testname}: recode`)
console.log(JSON.stringify(decoded, null, 2))
}
t.ok(compare(t, recoded, expected), testname + ' recode')
}
return encoded
}
tape('https svcb', function (t) {
// see https://datatracker.ietf.org/doc/rfc9460/ for the test vectors
// https AliasMode
test_http_decode_encode(t, 'https rfc9460 case1 (AliasMode)',
unhexlify(
"00 13" + // rdata len
"00 00" + // priority
"03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00" //target
),
{
priority: 0,
name: 'foo.example.com'
}
)
// https target name is "."
test_http_decode_encode(t, 'https rfc9460 case2 (target name ".")',
unhexlify(
"00 03" + // rdata len
"00 01" + // priority
"00" // target (root label)
),
{
priority: 1,
name: '.'
}
)
// https port
test_http_decode_encode(t, 'https rfc9460 case3 (port)',
unhexlify(
"00 19" + // rdata len
"00 10" + // priority
"03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00" + // target
"00 03" + // key 3
"00 02" + // length 2
"00 35" // value - target (root label)
),
{
priority: 16,
name: 'foo.example.com',
values: {
port: 53
}
}
)
// https generic key and value
test_http_decode_encode(t, 'https rfc9460 case4 (generic key,val)',
unhexlify(
"00 1c" + // rdata len
"00 01" + // priority
"03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00" + // target
"02 9b" + // key 667
"00 05" + // length 5
"68 65 6c 6c 6f" // value
),
{
priority: 1,
name: 'foo.example.com',
values: {
key667: 'hello'
}
},
true //skip_encode, we cannot encode an unknown key
)
// https generic key and value with decimal escape
test_http_decode_encode(t, 'https rfc9460 case5 (generic key,val with escape)',
unhexlify(
"00 20" + // rdata len
"00 01" + // priority
"03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00" + // target
"02 9b" + // key 667
"00 09" + // length 9
"68 65 6c 6c 6f d2 71 6f 6f" // value
),
{
priority: 1,
name: 'foo.example.com',
values: {
key667: unhexlify("68 65 6c 6c 6f d2 71 6f 6f").toString("utf-8")
},
},
true // skip_encode, we cannot encode an unknown key
)
// https two quoted ipv6 hints
test_http_decode_encode(t, 'https rfc9460 case6 (ipv6hint)',
unhexlify(
"00 37" + // rdata len
"00 01" + // priority
"03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00" + // target
"00 06" + // key 6
"00 20" + // length 32
"20 01 0d b8 00 00 00 00 00 00 00 00 00 00 00 01" + // first address
"20 01 0d b8 00 00 00 00 00 00 00 00 00 53 00 01" // second address
),
{
priority: 1,
name: "foo.example.com",
values: {
ipv6hint: [
"2001:db8::1",
"2001:db8::53:1"
]
}
}
)
// https ipv6 hint using embedded ipv4 syntax [2001:db8:122:344::192.0.2.33]
test_http_decode_encode(t, 'https rfc9460 case7 (ipv6hint v4 syntax)',
unhexlify(
"00 23" + // rdata len
"00 01" + // priority
"07 65 78 61 6d 70 6c 65 03 63 6f 6d 00" + // target
"00 06" + // key 6
"00 10" + // length 16
"20 01 0d b8 01 22 03 44 00 00 00 00 c0 00 02 21" // address
),
{
priority: 1,
name: "example.com",
values: {
ipv6hint: [ ip.v6.decode(ip.v6.encode("2001:db8:122:344::192.0.2.33")) ]
}
}
)
// case 8
// https 16 foo.example.org. (
// alpn=h2,h3-19 mandatory=ipv4hint,alpn
// ipv4hint=192.0.2.1
// )
//
// note: this may not encode to the same buffer due to internal js
// ordering of the `values` object storing the params
test_http_decode_encode(t, 'https rfc9460 case8 (alpn,mandatory,ipv4hint)',
unhexlify(
"00 30" + // rdata len
"00 10" + // priority
"03 66 6f 6f 07 65 78 61 6d 70 6c 65 03 6f 72 67 00" + // target
"00 00" + // key 0
"00 04" + // param length 4
"00 01" + // value: key 1
"00 04" + // value: key 4
"00 01" + // key 1
"00 09" + // param length 9
"02" + // alpn length 2
"68 32" + // alpn value
"05" + // alpn length 5
"68 33 2d 31 39" + // alpn value
"00 04" + // key 4
"00 04" + // param length 4
"c0 00 02 01" // param value
),
{
priority: 16,
name: "foo.example.org",
values: {
mandatory: [
"alpn",
"ipv4hint"
],
alpn: [
"h2",
"h3-19"
],
ipv4hint: [
"192.0.2.1"
]
}
},
false, // skip_encode: false
true, // skip_memcmp_bufs: true, do not directly memcmp the resulting bufs, see comment above
)
testEncoder(t, packet.httpssvc, {
priority: 16,
name: "foo.example.org",
values: {
mandatory: [
"alpn",
"ipv4hint"
],
alpn: [
"h2",
"h3-19"
],
ipv4hint: [
"192.0.2.1"
]
}
}
)
t.end()
})
// </HTTPS SVCB test cases>
tape('unpack', function (t) {
const buf = Buffer.from([
0x00, 0x79,
0xde, 0xad, 0x85, 0x00, 0x00, 0x01, 0x00, 0x01,
0x00, 0x02, 0x00, 0x02, 0x02, 0x6f, 0x6a, 0x05,
0x62, 0x61, 0x6e, 0x67, 0x6a, 0x03, 0x63, 0x6f,
0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c,
0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10,
0x00, 0x04, 0x81, 0xfa, 0x0b, 0xaa, 0xc0, 0x0f,
0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10,
0x00, 0x05, 0x02, 0x63, 0x6a, 0xc0, 0x0f, 0xc0,
0x0f, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x0e,
0x10, 0x00, 0x02, 0xc0, 0x0c, 0xc0, 0x3a, 0x00,
0x01, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10, 0x00,
0x04, 0x45, 0x4d, 0x9b, 0x9c, 0xc0, 0x0c, 0x00,
0x1c, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10, 0x00,
0x10, 0x20, 0x01, 0x04, 0x18, 0x00, 0x00, 0x50,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf9
])
const val = packet.streamDecode(buf)
const answer = val.answers[0]
const authority = val.authorities[1]
t.ok(val.rcode === 'NOERROR', 'decode rcode')
t.ok(compare(t, answer.type, 'A'), 'streamDecoded RR type match')
t.ok(compare(t, answer.name, 'oj.bangj.com'), 'streamDecoded RR name match')
t.ok(compare(t, answer.data, '129.250.11.170'), 'streamDecoded RR rdata match')
t.ok(compare(t, authority.type, 'NS'), 'streamDecoded RR type match')
t.ok(compare(t, authority.name, 'bangj.com'), 'streamDecoded RR name match')
t.ok(compare(t, authority.data, 'oj.bangj.com'), 'streamDecoded RR rdata match')
t.end()
})
tape('optioncodes', function (t) {
const opts = [
[0, 'OPTION_0'],
[1, 'LLQ'],
[2, 'UL'],
[3, 'NSID'],
[4, 'OPTION_4'],
[5, 'DAU'],
[6, 'DHU'],
[7, 'N3U'],
[8, 'CLIENT_SUBNET'],
[9, 'EXPIRE'],
[10, 'COOKIE'],
[11, 'TCP_KEEPALIVE'],
[12, 'PADDING'],
[13, 'CHAIN'],
[14, 'KEY_TAG'],
[26946, 'DEVICEID'],
[65535, 'OPTION_65535'],
[64000, 'OPTION_64000'],
[65002, 'OPTION_65002'],
[-1, null]
]
for (const [code, str] of opts) {
const s = optioncodes.toString(code)
t.ok(compare(t, s, str), `${code} => ${str}`)
t.ok(compare(t, optioncodes.toCode(s), code), `${str} => ${code}`)
}
t.ok(compare(t, optioncodes.toCode('INVALIDINVALID'), -1))
t.end()
})
function testEncoder (t, rpacket, val) {
const buf = rpacket.encode(val)
const val2 = rpacket.decode(buf)
t.same(buf.length, rpacket.encode.bytes, 'encode.bytes was set correctly')
t.same(buf.length, rpacket.encodingLength(val), 'encoding length matches')
t.ok(compare(t, val, val2), 'decoded object match')
const buf2 = rpacket.encode(val2)
const val3 = rpacket.decode(buf2)
t.same(buf2.length, rpacket.encode.bytes, 'encode.bytes was set correctly on re-encode')
t.same(buf2.length, rpacket.encodingLength(val), 'encoding length matches on re-encode')
t.ok(compare(t, val, val3), 'decoded object match on re-encode')
t.ok(compare(t, val2, val3), 're-encoded decoded object match on re-encode')
const bigger = Buffer.allocUnsafe(buf2.length + 10)
const buf3 = rpacket.encode(val, bigger, 10)
const val4 = rpacket.decode(buf3, 10)
t.ok(buf3 === bigger, 'echoes buffer on external buffer')
t.same(rpacket.encode.bytes, buf.length, 'encode.bytes is the same on external buffer')
t.ok(compare(t, val, val4), 'decoded object match on external buffer')
}
function compare (t, a, b) {
if (Buffer.isBuffer(a)) return a.toString('hex') === b.toString('hex')
if (typeof a === 'object' && a && b) {
const keys = Object.keys(a)
for (let i = 0; i < keys.length; i++) {
if (!compare(t, a[keys[i]], b[keys[i]])) {
return false
}
}
} else if (Array.isArray(b) && !Array.isArray(a)) {
// TXT always decode as array
return a.toString() === b[0].toString()
} else {
return a === b
}
return true
}