Compare commits

..

4 Commits

Author SHA1 Message Date
ff49ac8f24 Fix SSHFP 2026-04-08 22:23:07 +08:00
a16cd9dd21 Update types 2026-04-07 23:23:42 +08:00
d95b456ddf Bye buffer 2026-04-07 23:01:07 +08:00
5c37d07fcf Fix ESLint error 2026-04-07 22:50:33 +08:00
9 changed files with 1528 additions and 319 deletions

204
buffer.js Normal file
View File

@ -0,0 +1,204 @@
'use strict'
// Allocate a new Uint8Array buffer
function allocBuffer (size) {
return new Uint8Array(size)
}
// Allocate an uninitialized Uint8Array (same as allocBuffer for Uint8Array)
function allocUnsafeBuffer (size) {
return new Uint8Array(size)
}
// Check if an object is a Uint8Array
function isBuffer (obj) {
return obj instanceof Uint8Array
}
// Create Uint8Array from data with optional encoding
function fromBuffer (data, encoding) {
if (data instanceof Uint8Array) {
return new Uint8Array(data)
}
if (Array.isArray(data)) {
return new Uint8Array(data)
}
if (typeof data === 'string') {
if (encoding === 'base64') {
return base64ToBuffer(data)
} else if (encoding === 'hex') {
return hexToBuffer(data)
} else {
return stringToBuffer(data)
}
}
return new Uint8Array(data)
}
// Convert string to Uint8Array (UTF-8 encoding)
function stringToBuffer (str) {
const encoder = new TextEncoder()
return encoder.encode(str)
}
// Convert Base64 string to Uint8Array
function base64ToBuffer (base64Str) {
if (typeof window !== 'undefined' && window.atob) {
// Browser environment
const binaryString = window.atob(base64Str)
const bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes
} else {
// Node.js environment
return new Uint8Array(Buffer.from(base64Str, 'base64'))
}
}
// Convert hex string to Uint8Array
function hexToBuffer (hexStr) {
const bytes = new Uint8Array(hexStr.length / 2)
for (let i = 0; i < hexStr.length; i += 2) {
bytes[i / 2] = parseInt(hexStr.substr(i, 2), 16)
}
return bytes
}
// Convert Uint8Array to hex string
function bufferToHex (buf) {
let hex = ''
for (let i = 0; i < buf.length; i++) {
const byte = buf[i]
hex += (byte < 16 ? '0' : '') + byte.toString(16)
}
return hex.toUpperCase()
}
// Convert Uint8Array to string
function bufferToString (buf, encoding, start, end) {
encoding = encoding || 'utf-8'
start = start || 0
end = end || buf.length
if (encoding !== 'utf-8' && encoding !== 'utf8') {
throw new Error('Unsupported encoding: ' + encoding)
}
const slice = buf.slice(start, end)
const decoder = new TextDecoder('utf-8')
return decoder.decode(slice)
}
// Write string to buffer, return number of bytes written
function writeString (buf, str, offset) {
offset = offset || 0
const encoded = stringToBuffer(str)
for (let i = 0; i < encoded.length && offset + i < buf.length; i++) {
buf[offset + i] = encoded[i]
}
return encoded.length
}
// Get byte length of a string
function byteLength (str) {
const encoder = new TextEncoder()
return encoder.encode(str).length
}
// Read big-endian 16-bit unsigned integer
function readUInt16BE (buf, offset) {
offset = offset || 0
return (buf[offset] << 8) | buf[offset + 1]
}
// Write big-endian 16-bit unsigned integer
function writeUInt16BE (buf, value, offset) {
offset = offset || 0
buf[offset] = (value >> 8) & 0xff
buf[offset + 1] = value & 0xff
}
// Read big-endian 32-bit unsigned integer
function readUInt32BE (buf, offset) {
offset = offset || 0
return ((buf[offset] << 24) | (buf[offset + 1] << 16) | (buf[offset + 2] << 8) | buf[offset + 3]) >>> 0
}
// Write big-endian 32-bit unsigned integer
function writeUInt32BE (buf, value, offset) {
offset = offset || 0
buf[offset] = (value >> 24) & 0xff
buf[offset + 1] = (value >> 16) & 0xff
buf[offset + 2] = (value >> 8) & 0xff
buf[offset + 3] = value & 0xff
}
// Read 8-bit unsigned integer
function readUInt8 (buf, offset) {
offset = offset || 0
return buf[offset]
}
// Write 8-bit unsigned integer
function writeUInt8 (buf, value, offset) {
offset = offset || 0
buf[offset] = value & 0xff
}
// Copy buffer contents
function bufferCopy (source, target, targetStart, sourceStart, sourceEnd) {
targetStart = targetStart || 0
sourceStart = sourceStart || 0
sourceEnd = sourceEnd !== undefined ? sourceEnd : source.length
for (let i = sourceStart; i < sourceEnd; i++) {
target[targetStart + (i - sourceStart)] = source[i]
}
}
// Concatenate multiple buffers
function bufferConcat (buffers, totalLength) {
if (!Array.isArray(buffers)) {
throw new Error('buffers must be an Array')
}
if (buffers.length === 0) {
return new Uint8Array(0)
}
totalLength = totalLength || buffers.reduce((sum, buf) => sum + buf.length, 0)
const result = new Uint8Array(totalLength)
let offset = 0
for (let i = 0; i < buffers.length; i++) {
const buf = buffers[i]
result.set(buf, offset)
offset += buf.length
}
return result
}
module.exports = {
allocBuffer,
allocUnsafeBuffer,
isBuffer,
fromBuffer,
stringToBuffer,
base64ToBuffer,
hexToBuffer,
bufferToHex,
bufferToString,
writeString,
byteLength,
readUInt8,
writeUInt8,
readUInt16BE,
writeUInt16BE,
readUInt32BE,
writeUInt32BE,
bufferCopy,
bufferConcat
}

View File

@ -13,7 +13,7 @@ exports.toString = function (klass) {
exports.toClass = function (name) { exports.toClass = function (name) {
if (typeof name === 'number') { if (typeof name === 'number') {
return name; return name
} }
switch (name.toUpperCase()) { switch (name.toUpperCase()) {
case 'IN': return 1 case 'IN': return 1

277
dns-packet-tests.ts Normal file
View File

@ -0,0 +1,277 @@
import {
Answer,
decode,
DecodedPacket,
encode,
encodingLength,
Packet,
Question,
RECURSION_DESIRED,
streamDecode,
streamEncode,
} from "dns-packet";
const answer: Answer = {
type: "A",
name: "localhost",
ttl: 3600,
data: "127.0.0.1",
class: "ANY",
flush: true,
};
const question: Question = {
type: "A",
name: "localhost",
class: "IN",
};
const inPacket: Packet = {
additionals: [answer],
authorities: [answer],
answers: [answer],
flags: 0,
id: 0,
questions: [question],
type: "query",
};
const inputBuf = Buffer.alloc(0);
const length: number = encodingLength(inPacket);
const out: Buffer = encode(inPacket, inputBuf, length - length);
const outPacket: DecodedPacket = decode(out, 0);
const flag_qr: boolean = outPacket.flag_qr;
const flag_aa: boolean = outPacket.flag_aa;
const flag_tc: boolean = outPacket.flag_tc;
const flag_rd: boolean = outPacket.flag_rd;
const flag_ra: boolean = outPacket.flag_ra;
const flag_z: boolean = outPacket.flag_z;
const flag_ad: boolean = outPacket.flag_ad;
const flag_cd: boolean = outPacket.flag_cd;
encode(outPacket);
const records: Answer[] = [
{
type: "A",
name: "localhost",
data: "127.0.0.1",
},
{
type: "AAAA",
name: "localhost",
data: "::1",
},
{
type: "CNAME",
name: "localhost",
data: "example.com",
},
{
type: "DNSKEY",
name: "localhost",
data: {
algorithm: 1,
flags: 257,
key: Buffer.from("test"),
},
},
{
type: "DS",
name: "localhost",
data: {
keyTag: 12345,
algorithm: 8,
digestType: 1,
digest: Buffer.from("test"),
},
},
{
type: "NAPTR",
name: "localhost",
data: {
order: 100,
preference: 10,
flags: "s",
services: "SIP+D2U",
regexp: "!^.*$!sip:customer-service@example.com!",
replacement: "_sip._udp.example.com",
},
},
{
type: "NS",
name: "localhost",
data: "ns1.localhost",
},
{
type: "NSEC",
name: "localhost",
data: {
nextDomain: "a.domain",
rrtypes: ["A", "TXT", "RRSIG"],
},
},
{
type: "NSEC3",
name: "localhost",
data: {
algorithm: 1,
flags: 0,
iterations: 2,
salt: Buffer.from("test"),
nextDomain: Buffer.from("test"), // Hashed per RFC5155
rrtypes: ["A", "TXT", "RRSIG"],
},
},
{
type: "MX",
name: "localhost",
data: {
preference: 10,
exchange: "mx.localhost",
},
},
{
type: "TXT",
name: "localhost",
data: "test",
},
{
type: "TXT",
name: "localhost",
data: Buffer.from("test"),
},
{
type: "TXT",
name: "localhost",
data: ["foo", "bar"],
},
{
type: "SRV",
name: "_imap._tcp.localhost",
data: {
priority: 10,
weight: 60,
port: 5060,
target: "imap.example.com",
},
},
{
type: "SOA",
name: "localhost",
data: {
mname: "localhost",
rname: "hostmaster.localhost",
serial: 2021122101,
},
},
{
type: "CAA",
name: "localhost",
data: {
issuerCritical: false,
tag: "issue",
value: "ca.example.com",
},
},
{
type: "TXT",
name: "version.bind",
class: "CH",
data: "1.2.3",
},
{
type: "OPT",
name: ".",
udpPayloadSize: 65535,
extendedRcode: 255,
ednsVersion: 255,
flags: 65535,
flag_do: true,
options: [
{
code: 8,
type: "CLIENT_SUBNET",
sourcePrefixLength: 0,
scopePrefixLength: 0,
ip: "127.0.0.1",
},
{
code: 8,
ip: "127.0.0.1",
},
{
code: 11,
type: "TCP_KEEPALIVE",
},
{
code: 11,
timeout: 2468,
},
{
code: 12,
length: 13,
},
{
code: 14,
tags: [],
},
{
code: 14,
tags: [256],
},
],
},
{
type: "RP",
name: "localhost",
data: {
mbox: "admin.example.com",
txt: "txt.example.com",
},
},
{
type: "RRSIG",
name: "localhost",
data: {
typeCovered: "A",
algorithm: 8,
labels: 1,
originalTTL: 3600,
expiration: Date.now(),
inception: Date.now(),
keyTag: 12345,
signersName: "a.name",
signature: Buffer.from("test"),
},
},
{
type: "SSHFP",
name: "localhost",
data: {
algorithm: 1,
hash: 1,
fingerprint: "A108C9F834354D5B37AF988141C9294822F5BC00",
},
},
];
encode({ answers: records });
// https://github.com/mafintosh/dns-packet/blob/5aebb85c3221292e994d01b68cadf067e78efabf/examples/tcp.js
function getRandomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
let buf = streamEncode({
type: "query",
id: getRandomInt(1, 65534),
flags: RECURSION_DESIRED,
questions: [
{
type: "A",
name: "google.com",
},
],
});
streamDecode(buf);
buf = buf.slice(2 + streamDecode.bytes);

View File

@ -1,7 +1,16 @@
'use strict' 'use strict'
const wout = (s) => { const wout = (s) => {
process.stdout.write(s) if (typeof process !== 'undefined' && process.stdout) {
process.stdout.write(s)
} else if (typeof console !== 'undefined') {
if (s === '\n') {
console.log('')
} else {
process.stdout = process.stdout || ''
process.stdout += s
}
}
} }
const padStr = (s, len, ch) => { const padStr = (s, len, ch) => {
@ -55,7 +64,7 @@ const hexdump = (buf, len) => {
} }
/* /*
let buf = Buffer.alloc(500) let buf = new Uint8Array(500)
for (let i = 0; i < buf.length; i++) { for (let i = 0; i < buf.length; i++) {
buf[i] = i & 0xff buf[i] = i & 0xff
} }

668
index.d.ts vendored Normal file
View File

@ -0,0 +1,668 @@
/// <reference types="node" />
/**
* The currently defined set of DNS record types.
*/
export type RecordType =
| "A"
| "AAAA"
| "AFSDB"
| "APL"
| "AXFR"
| "CAA"
| "CDNSKEY"
| "CDS"
| "CERT"
| "CNAME"
| "DNAME"
| "DHCID"
| "DLV"
| "DNSKEY"
| "DS"
| "HINFO"
| "HIP"
| "HTTPS"
| "IXFR"
| "IPSECKEY"
| "KEY"
| "KX"
| "LOC"
| "MX"
| "NAPTR"
| "NS"
| "NSEC"
| "NSEC3"
| "NSEC3PARAM"
| "NULL"
| "OPT"
| "PTR"
| "RRSIG"
| "RP"
| "SIG"
| "SOA"
| "SRV"
| "SSHFP"
| "SVCB"
| "TA"
| "TKEY"
| "TLSA"
| "TSIG"
| "TXT"
| "URI";
export type RecordClass = "IN" | "CS" | "CH" | "HS" | "ANY";
export interface Question {
type: RecordType;
name: string;
class?: RecordClass | undefined;
qu_bit?: boolean | undefined;
}
// Data interfaces for various record types
export interface CaaData {
issuerCritical?: boolean | undefined;
flags?: number | undefined;
tag: "issue" | "issuewild" | "iodef";
value: string;
}
export interface DnskeyData {
flags: number;
algorithm: number;
key: Buffer;
}
export interface DsData {
keyTag: number;
algorithm: number;
digestType: number;
digest: Buffer;
}
export interface HInfoData {
cpu: string;
os: string;
}
export interface MxData {
preference?: number | undefined;
exchange: string;
}
export interface NaptrData {
order: number;
preference: number;
flags: string;
services: string;
regexp: string;
replacement: string;
}
export interface NsecData {
nextDomain: string;
rrtypes: string[];
}
export interface Nsec3Data {
algorithm: number;
flags: number;
iterations: number;
salt: Buffer;
nextDomain: Buffer;
rrtypes: string[];
}
export interface RpData {
mbox: string;
txt: string;
}
export interface RrsigData {
typeCovered: string;
algorithm: number;
labels: number;
originalTTL: number;
expiration: number;
inception: number;
keyTag: number;
signersName: string;
signature: Buffer;
}
export interface SrvData {
port: number;
target: string;
priority?: number | undefined;
weight?: number | undefined;
}
export interface SoaData {
mname: string;
rname: string;
serial?: number | undefined;
refresh?: number | undefined;
retry?: number | undefined;
expire?: number | undefined;
minimum?: number | undefined;
}
export interface SshfpData {
algorithm: number;
hash: number;
fingerprint: string;
}
export interface TlsaData {
usage: number;
selector: number;
matchingType: number;
certificate: Buffer;
}
export type TxtData = string | Buffer | Array<string | Buffer>;
// SVCB/HTTPS Parameter types
export interface SvcParamMandatory {
key: "mandatory" | 0;
value: Array<string | number>;
}
export interface SvcParamAlpn {
key: "alpn" | 1;
value: string | string[];
}
export interface SvcParamNoDefaultAlpn {
key: "no-default-alpn" | 2;
value?: number | undefined;
}
export interface SvcParamPort {
key: "port" | 3;
value: number;
}
export interface SvcParamIpv4Hint {
key: "ipv4hint" | 4;
value: string | string[];
}
export interface SvcParamEchConfig {
key: "echconfig" | 5;
value: string;
needBase64Decode?: boolean | undefined;
}
export interface SvcParamIpv6Hint {
key: "ipv6hint" | 6;
value: string | string[];
}
export interface SvcParamDohPath {
key: "dohpath" | 7;
value: string;
}
export interface SvcParamOdoh {
key: "odoh" | 32769;
value: string;
needBase64Decode?: boolean | undefined;
}
export interface SvcParamUnknown {
key: string | number;
value?: unknown | undefined;
data?: Buffer | undefined;
}
export type SvcParam =
| SvcParamMandatory
| SvcParamAlpn
| SvcParamNoDefaultAlpn
| SvcParamPort
| SvcParamIpv4Hint
| SvcParamEchConfig
| SvcParamIpv6Hint
| SvcParamDohPath
| SvcParamOdoh
| SvcParamUnknown;
export interface HttpsData {
priority: number;
name: string;
values?: Record<string, unknown> | undefined;
}
export interface SvcbData {
priority: number;
name: string;
values?: Record<string, unknown> | undefined;
}
// Generic answer types
export interface GenericAnswer<T> {
type: T;
name: string;
}
export interface BaseAnswer<T, D> extends GenericAnswer<T> {
ttl?: number | undefined;
class?: RecordClass | undefined;
flush?: boolean | undefined;
data: D;
}
// Answer type groupings
export type StringRecordType = "A" | "AAAA" | "CNAME" | "DNAME" | "NS" | "PTR";
export type OtherRecordType =
| "AFSDB"
| "APL"
| "AXFR"
| "CDNSKEY"
| "CDS"
| "CERT"
| "DHCID"
| "DLV"
| "HIP"
| "IPSECKEY"
| "IXFR"
| "KEY"
| "KX"
| "LOC"
| "NSEC3PARAM"
| "NULL"
| "SIG"
| "TA"
| "TKEY"
| "TSIG"
| "URI";
// Specific answer types
export type StringAnswer = BaseAnswer<StringRecordType, string>;
export type BufferAnswer = BaseAnswer<OtherRecordType, Buffer>;
export type CaaAnswer = BaseAnswer<"CAA", CaaData>;
export type DnskeyAnswer = BaseAnswer<"DNSKEY", DnskeyData>;
export type DSAnswer = BaseAnswer<"DS", DsData>;
export type HInfoAnswer = BaseAnswer<"HINFO", HInfoData>;
export type MxAnswer = BaseAnswer<"MX", MxData>;
export type NaptrAnswer = BaseAnswer<"NAPTR", NaptrData>;
export type Nsec3Answer = BaseAnswer<"NSEC3", Nsec3Data>;
export type NsecAnswer = BaseAnswer<"NSEC", NsecData>;
export type RpAnswer = BaseAnswer<"RP", RpData>;
export type RrsigAnswer = BaseAnswer<"RRSIG", RrsigData>;
export type SoaAnswer = BaseAnswer<"SOA", SoaData>;
export type SrvAnswer = BaseAnswer<"SRV", SrvData>;
export type SshfpAnswer = BaseAnswer<"SSHFP", SshfpData>;
export type TlsaAnswer = BaseAnswer<"TLSA", TlsaData>;
export type TxtAnswer = BaseAnswer<"TXT", TxtData>;
export type SvcbAnswer = BaseAnswer<"SVCB", SvcbData>;
export type HttpsAnswer = BaseAnswer<"HTTPS", HttpsData>;
// OPT record
interface OptCodes {
OPTION_0: 0;
LLQ: 1;
UL: 2;
NSID: 3;
OPTION_4: 4;
DAU: 5;
DHU: 6;
N3U: 7;
CLIENT_SUBNET: 8;
EXPIRE: 9;
COOKIE: 10;
TCP_KEEPALIVE: 11;
PADDING: 12;
CHAIN: 13;
KEY_TAG: 14;
DEVICEID: 26946;
OPTION_65535: 65535;
}
type OptCodeType = keyof OptCodes;
type OptCode<K extends OptCodeType> = OptCodes[K];
interface GenericOpt<T extends OptCodeType> {
code: OptCode<T>;
type?: T | undefined;
data?: Buffer | undefined;
}
interface ClientSubnetOpt extends GenericOpt<"CLIENT_SUBNET"> {
family?: number | undefined;
sourcePrefixLength?: number | undefined;
scopePrefixLength?: number | undefined;
ip: string | undefined;
}
interface KeepAliveOpt extends GenericOpt<"TCP_KEEPALIVE"> {
timeout?: number | undefined;
}
interface PaddingOpt extends GenericOpt<"PADDING"> {
length?: number | undefined;
}
interface TagOpt extends GenericOpt<"KEY_TAG"> {
tags: number[];
}
export type PacketOpt = ClientSubnetOpt | KeepAliveOpt | PaddingOpt | TagOpt;
export interface OptAnswer extends GenericAnswer<"OPT"> {
udpPayloadSize: number;
extendedRcode: number;
ednsVersion: number;
flags: number;
/**
* Whether or not the DNS DO bit is set
*/
flag_do: boolean;
options: PacketOpt[];
}
// Complete answer union type
export type Answer =
| StringAnswer
| BufferAnswer
| CaaAnswer
| DnskeyAnswer
| DSAnswer
| HInfoAnswer
| HttpsAnswer
| MxAnswer
| NaptrAnswer
| Nsec3Answer
| NsecAnswer
| OptAnswer
| RpAnswer
| RrsigAnswer
| SoaAnswer
| SrvAnswer
| SshfpAnswer
| SvcbAnswer
| TlsaAnswer
| TxtAnswer;
export interface Packet {
/**
* Whether the packet is a query or a response. This field may be
* omitted if it is clear from the context of usage what type of packet
* it is.
*/
type?: "query" | "response" | undefined;
id?: number | undefined;
/**
* A bit-mask combination of zero or more of:
* {@link AUTHORITATIVE_ANSWER},
* {@link TRUNCATED_RESPONSE},
* {@link RECURSION_DESIRED},
* {@link RECURSION_AVAILABLE},
* {@link AUTHENTIC_DATA},
* {@link CHECKING_DISABLED},
* {@link DNSSEC_OK}.
*/
flags?: number | undefined;
opcode?: string | undefined;
rcode?: string | undefined;
questions?: Question[] | undefined;
answers?: Answer[] | undefined;
additionals?: Answer[] | undefined;
authorities?: Answer[] | undefined;
}
/**
* Decoded packet with individual flag bits extracted.
*/
export interface DecodedPacket extends Packet {
flag_qr: boolean;
flag_aa: boolean;
flag_tc: boolean;
flag_rd: boolean;
flag_ra: boolean;
flag_z: boolean;
flag_ad: boolean;
flag_cd: boolean;
}
// Constants
export const AUTHORITATIVE_ANSWER: number;
export const TRUNCATED_RESPONSE: number;
export const RECURSION_DESIRED: number;
export const RECURSION_AVAILABLE: number;
export const AUTHENTIC_DATA: number;
export const CHECKING_DISABLED: number;
export const DNSSEC_OK: number;
export const NXDOMAIN: number;
// Main functions
export function encode(packet: Packet, buf?: Buffer, offset?: number): Buffer;
export namespace encode {
let bytes: number;
}
export function decode(buf: Buffer, offset?: number): DecodedPacket;
export namespace decode {
let bytes: number;
}
export function encodingLength(packet: Packet): number;
export function streamEncode(packet: Packet): Buffer;
export namespace streamEncode {
let bytes: number;
}
export function streamDecode(buf: Buffer): DecodedPacket | null;
export namespace streamDecode {
let bytes: number;
}
// Utility/Helper exports
export const name: {
encode(
str: string,
buf?: Buffer,
offset?: number,
options?: { mail?: boolean }
): Buffer;
decode(buf: Buffer, offset?: number, options?: { mail?: boolean }): string;
encodingLength(name: string): number;
};
export const question: {
encode(q: Question, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): Question;
encodingLength(q: Question): number;
};
export const answer: {
encode(a: Answer, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): Answer;
encodingLength(a: Answer): number;
};
export const svcparam: {
encode(param: SvcParam, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): SvcParam;
encodingLength(param: SvcParam): number;
keyToNumber(keyName: string): number;
numberToKeyName(number: number): string;
};
export const svcb: {
encode(data: SvcbData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): SvcbData;
encodingLength(data: SvcbData): number;
};
export const httpssvc: {
encode(data: HttpsData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): HttpsData;
encodingLength(data: HttpsData): number;
};
export const a: {
encode(host: string, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): string;
encodingLength(): number;
};
export const aaaa: {
encode(host: string, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): string;
encodingLength(): number;
};
export const cname: {
encode(data: string, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): string;
encodingLength(data: string): number;
};
export const dname: {
encode(data: string, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): string;
encodingLength(data: string): number;
};
export const ptr: {
encode(data: string, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): string;
encodingLength(data: string): number;
};
export const ns: {
encode(data: string, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): string;
encodingLength(data: string): number;
};
export const mx: {
encode(data: MxData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): MxData;
encodingLength(data: MxData): number;
};
export const srv: {
encode(data: SrvData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): SrvData;
encodingLength(data: SrvData): number;
};
export const caa: {
encode(data: CaaData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): CaaData;
encodingLength(data: CaaData): number;
ISSUER_CRITICAL: number;
};
export const txt: {
encode(data: TxtData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): Buffer[];
encodingLength(data: TxtData): number;
};
export const null_: {
encode(data: Buffer | string, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): Buffer;
encodingLength(data?: Buffer | string): number;
};
export const hinfo: {
encode(data: HInfoData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): HInfoData;
encodingLength(data: HInfoData): number;
};
export const soa: {
encode(data: SoaData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): SoaData;
encodingLength(data: SoaData): number;
};
export const naptr: {
encode(data: NaptrData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): NaptrData;
encodingLength(data: NaptrData): number;
};
export const dnskey: {
encode(key: DnskeyData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): DnskeyData;
encodingLength(key: DnskeyData): number;
PROTOCOL_DNSSEC: number;
ZONE_KEY: number;
SECURE_ENTRYPOINT: number;
};
export const rrsig: {
encode(sig: RrsigData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): RrsigData;
encodingLength(sig: RrsigData): number;
};
export const rp: {
encode(data: RpData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): RpData;
encodingLength(data: RpData): number;
};
export const nsec: {
encode(record: NsecData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): NsecData;
encodingLength(record: NsecData): number;
};
export const nsec3: {
encode(record: Nsec3Data, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): Nsec3Data;
encodingLength(record: Nsec3Data): number;
};
export const ds: {
encode(digest: DsData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): DsData;
encodingLength(digest: DsData): number;
};
export const sshfp: {
encode(record: SshfpData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): SshfpData;
encodingLength(record: SshfpData): number;
getFingerprintLengthForHashType(hashType: number): number | undefined;
};
export const tlsa: {
encode(cert: TlsaData, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): TlsaData;
encodingLength(cert: TlsaData): number;
};
export const opt: {
encode(options: PacketOpt[], buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): PacketOpt[];
encodingLength(options: PacketOpt[]): number;
};
export const unknown: {
encode(data: Buffer, buf?: Buffer, offset?: number): Buffer;
decode(buf: Buffer, offset?: number): Buffer;
encodingLength(data: Buffer): number;
};
export function record(type: RecordType): any;
export {};

564
index.js

File diff suppressed because it is too large Load Diff

View File

@ -17,17 +17,19 @@
"coverage": "nyc -r html npm test" "coverage": "nyc -r html npm test"
}, },
"dependencies": { "dependencies": {
"@leichtgewicht/ip-codec": "^2.0.1", "@leichtgewicht/ip-codec": "^2.0.1"
"buffer": "^6.0.3"
}, },
"types": "index.d.ts",
"devDependencies": { "devDependencies": {
"@types/node": "*",
"eslint": "^5.14.1", "eslint": "^5.14.1",
"eslint-config-standard": "^12.0.0", "eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.16.0", "eslint-plugin-import": "^2.16.0",
"eslint-plugin-node": "^8.0.1", "eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.0.1", "eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0", "eslint-plugin-standard": "^4.0.0",
"tape": "^4.10.1" "tape": "^4.10.1",
"typescript": "^6.0.2"
}, },
"keywords": [ "keywords": [
"dns", "dns",
@ -39,6 +41,7 @@
], ],
"files": [ "files": [
"index.js", "index.js",
"index.d.ts",
"types.js", "types.js",
"rcodes.js", "rcodes.js",
"opcodes.js", "opcodes.js",

86
test.js
View File

@ -1,6 +1,5 @@
'use strict' 'use strict'
const Buffer = require('buffer/').Buffer
const tape = require('tape') const tape = require('tape')
const packet = require('./') const packet = require('./')
const rcodes = require('./rcodes') const rcodes = require('./rcodes')
@ -12,7 +11,7 @@ const ip = require('@leichtgewicht/ip-codec')
const hexdump = require('./hexdump') const hexdump = require('./hexdump')
tape('unknown', function (t) { tape('unknown', function (t) {
testEncoder(t, packet.unknown, Buffer.from('hello world')) testEncoder(t, packet.unknown, new Uint8Array('hello world'))
t.end() t.end()
}) })
@ -20,9 +19,9 @@ tape('txt', function (t) {
testEncoder(t, packet.txt, []) testEncoder(t, packet.txt, [])
testEncoder(t, packet.txt, ['hello world']) testEncoder(t, packet.txt, ['hello world'])
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, [new Uint8Array([0, 1, 2, 3, 4, 5])])
testEncoder(t, packet.txt, ['a', 'b', Buffer.from([0, 1, 2, 3, 4, 5])]) testEncoder(t, packet.txt, ['a', 'b', new Uint8Array([0, 1, 2, 3, 4, 5])])
testEncoder(t, packet.txt, ['', Buffer.allocUnsafe(0)]) testEncoder(t, packet.txt, ['', new Uint8Array(0)])
t.end() t.end()
}) })
@ -30,16 +29,17 @@ tape('txt-scalar-string', function (t) {
const buf = packet.txt.encode('hi') const buf = packet.txt.encode('hi')
const val = packet.txt.decode(buf) const val = packet.txt.decode(buf)
t.ok(val.length === 1, 'array length') t.ok(val.length === 1, 'array length')
t.ok(val[0].toString() === 'hi', 'data') const decoded = val[0] instanceof Uint8Array ? new TextDecoder().decode(val[0]) : val[0].toString()
t.ok(decoded === 'hi', 'data')
t.end() t.end()
}) })
tape('txt-scalar-buffer', function (t) { tape('txt-scalar-buffer', function (t) {
const data = Buffer.from([0, 1, 2, 3, 4, 5]) const data = new Uint8Array([0, 1, 2, 3, 4, 5])
const buf = packet.txt.encode(data) const buf = packet.txt.encode(data)
const val = packet.txt.decode(buf) const val = packet.txt.decode(buf)
t.ok(val.length === 1, 'array length') t.ok(val.length === 1, 'array length')
t.ok(val[0].equals(data), 'data') t.ok(compare(t, val[0], data), 'data')
t.end() t.end()
}) })
@ -51,7 +51,7 @@ tape('txt-invalid-data', function (t) {
}) })
tape('null', function (t) { tape('null', function (t) {
testEncoder(t, packet.null, Buffer.from([0, 1, 2, 3, 4, 5])) testEncoder(t, packet.null, new Uint8Array([0, 1, 2, 3, 4, 5]))
t.end() t.end()
}) })
@ -260,7 +260,7 @@ tape('response', function (t) {
answers: [{ answers: [{
type: 'NULL', type: 'NULL',
name: 'hello.null.com', name: 'hello.null.com',
data: Buffer.from([1, 2, 3, 4, 5]) data: new Uint8Array([1, 2, 3, 4, 5])
}] }]
}) })
@ -316,7 +316,7 @@ tape('rcode', function (t) {
tape('name_encoding', function (t) { tape('name_encoding', function (t) {
let data = 'foo.example.com' let data = 'foo.example.com'
const buf = Buffer.allocUnsafe(255) const buf = new Uint8Array(255)
let offset = 0 let offset = 0
packet.name.encode(data, buf, offset) packet.name.encode(data, buf, offset)
t.ok(packet.name.encode.bytes === 17, 'name encoding length matches') t.ok(packet.name.encode.bytes === 17, 'name encoding length matches')
@ -385,40 +385,40 @@ tape('name_encoding', function (t) {
tape('name_decoding', function (t) { tape('name_decoding', function (t) {
// The two most significant bits of a valid label header must be either both zero or both one // 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(new Uint8Array([0x80])) }, /Cannot decode name \(bad label\)$/)
t.throws(function () { packet.name.decode(Buffer.from([0xb0])) }, /Cannot decode name \(bad label\)$/) t.throws(function () { packet.name.decode(new Uint8Array([0xb0])) }, /Cannot decode name \(bad label\)$/)
// Ensure there's enough buffer to read // 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(new Uint8Array([])) }, /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(new Uint8Array([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(new Uint8Array([0x01])) }, /Cannot decode name \(buffer overflow\)$/)
t.throws(function () { packet.name.decode(Buffer.from([0xc0])) }, /Cannot decode name \(buffer overflow\)$/) t.throws(function () { packet.name.decode(new Uint8Array([0xc0])) }, /Cannot decode name \(buffer overflow\)$/)
// Allow only pointers backwards // 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(new Uint8Array([0xc0, 0x00])) }, /Cannot decode name \(bad pointer\)$/)
t.throws(function () { packet.name.decode(Buffer.from([0xc0, 0x01])) }, /Cannot decode name \(bad pointer\)$/) t.throws(function () { packet.name.decode(new Uint8Array([0xc0, 0x01])) }, /Cannot decode name \(bad pointer\)$/)
// A name can be only 253 characters (when connected with dots) // A name can be only 253 characters (when connected with dots)
const maxLength = Buffer.alloc(255) const maxLength = new Uint8Array(255)
maxLength.fill(Buffer.from([0x01, 0x61]), 0, 254) maxLength.fill(new Uint8Array([0x01, 0x61]), 0, 254)
t.ok(packet.name.decode(maxLength) === new Array(127).fill('a').join('.')) t.ok(packet.name.decode(maxLength) === new Array(127).fill('a').join('.'))
const tooLong = Buffer.alloc(256) const tooLong = new Uint8Array(256)
tooLong.fill(Buffer.from([0x01, 0x61])) tooLong.fill(new Uint8Array([0x01, 0x61]))
t.throws(function () { packet.name.decode(tooLong) }, /Cannot decode name \(name too long\)$/) t.throws(function () { packet.name.decode(tooLong) }, /Cannot decode name \(name too long\)$/)
// Ensure jumps don't reset the total length counter // Ensure jumps don't reset the total length counter
const tooLongWithJump = Buffer.alloc(403) const tooLongWithJump = new Uint8Array(403)
tooLongWithJump.fill(Buffer.from([0x01, 0x61]), 0, 200) tooLongWithJump.fill(new Uint8Array([0x01, 0x61]), 0, 200)
tooLongWithJump.fill(Buffer.from([0x01, 0x61]), 201, 401) tooLongWithJump.fill(new Uint8Array([0x01, 0x61]), 201, 401)
tooLongWithJump.set([0xc0, 0x00], 401) tooLongWithJump.set([0xc0, 0x00], 401)
t.throws(function () { packet.name.decode(tooLongWithJump, 201) }, /Cannot decode name \(name too long\)$/) 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 // 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') t.ok(packet.name.decode(new Uint8Array([0x00, 0x01, 0x61, 0xc0, 0x00]), 1) === 'a')
// Ensure deeply nested pointers don't cause "Maximum call stack size exceeded" errors // Ensure deeply nested pointers don't cause "Maximum call stack size exceeded" errors
const buf = Buffer.alloc(16386) const buf = new Uint8Array(16386)
for (let i = 0; i < 16384; i += 2) { for (let i = 0; i < 16384; i += 2) {
buf.writeUInt16BE(0xc000 | i, i + 2) buf.writeUInt16BE(0xc000 | i, i + 2)
} }
@ -520,7 +520,7 @@ tape('dnskey', function (t) {
testEncoder(t, packet.dnskey, { testEncoder(t, packet.dnskey, {
flags: packet.dnskey.SECURE_ENTRYPOINT | packet.dnskey.ZONE_KEY, flags: packet.dnskey.SECURE_ENTRYPOINT | packet.dnskey.ZONE_KEY,
algorithm: 1, algorithm: 1,
key: Buffer.from([0, 1, 2, 3, 4, 5]) key: new Uint8Array([0, 1, 2, 3, 4, 5])
}) })
t.end() t.end()
}) })
@ -535,12 +535,12 @@ tape('rrsig', function (t) {
inception: 1233, inception: 1233,
keyTag: 2345, keyTag: 2345,
signersName: 'foo.com', signersName: 'foo.com',
signature: Buffer.from([0, 1, 2, 3, 4, 5]) signature: new Uint8Array([0, 1, 2, 3, 4, 5])
} }
testEncoder(t, packet.rrsig, testRRSIG) testEncoder(t, packet.rrsig, testRRSIG)
// Check the signature length is correct with extra junk at the end // Check the signature length is correct with extra junk at the end
const buf = Buffer.allocUnsafe(packet.rrsig.encodingLength(testRRSIG) + 4) const buf = new Uint8Array(packet.rrsig.encodingLength(testRRSIG) + 4)
packet.rrsig.encode(testRRSIG, buf) packet.rrsig.encode(testRRSIG, buf)
const val2 = packet.rrsig.decode(buf) const val2 = packet.rrsig.decode(buf)
t.ok(compare(t, testRRSIG, val2)) t.ok(compare(t, testRRSIG, val2))
@ -590,7 +590,7 @@ tape('nsec', function (t) {
}) })
// Test with the sample NSEC from https://tools.ietf.org/html/rfc4034#section-4.3 // Test with the sample NSEC from https://tools.ietf.org/html/rfc4034#section-4.3
var sampleNSEC = Buffer.from('003704686f7374076578616d706c6503636f6d00' + var sampleNSEC = new Uint8Array('003704686f7374076578616d706c6503636f6d00' +
'0006400100000003041b000000000000000000000000000000000000000000000' + '0006400100000003041b000000000000000000000000000000000000000000000' +
'000000020', 'hex') '000000020', 'hex')
var decoded = packet.nsec.decode(sampleNSEC) var decoded = packet.nsec.decode(sampleNSEC)
@ -609,8 +609,8 @@ tape('nsec3', function (t) {
algorithm: 1, algorithm: 1,
flags: 0, flags: 0,
iterations: 257, iterations: 257,
salt: Buffer.from([42, 42, 42]), salt: new Uint8Array([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]), nextDomain: new Uint8Array([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'] rrtypes: ['A', 'DNSKEY', 'CAA', 'DLV']
}) })
t.end() t.end()
@ -621,7 +621,7 @@ tape('ds', function (t) {
keyTag: 1234, keyTag: 1234,
algorithm: 1, algorithm: 1,
digestType: 1, digestType: 1,
digest: Buffer.from([0, 1, 2, 3, 4, 5]) digest: new Uint8Array([0, 1, 2, 3, 4, 5])
}) })
t.end() t.end()
}) })
@ -643,13 +643,13 @@ tape('tlsa', function (t) {
usage: 3, usage: 3,
selector: 1, selector: 1,
matchingType: 1, matchingType: 1,
certificate: Buffer.from([0, 1, 2, 3, 4, 5]) certificate: new Uint8Array([0, 1, 2, 3, 4, 5])
}) })
t.end() t.end()
}) })
const unhexlify = (hex) => { const unhexlify = (hex) => {
return Buffer.from(hex.match(/[\da-f]{2}/gi).map(function (h) { return parseInt(h, 16) })) return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) { return parseInt(h, 16) }))
} }
// <HTTPS SVCB test cases> // <HTTPS SVCB test cases>
@ -1177,7 +1177,7 @@ tape('google resolver SVCB real world', function (t) {
// </HTTPS SVCB test cases> // </HTTPS SVCB test cases>
tape('unpack', function (t) { tape('unpack', function (t) {
const buf = Buffer.from([ const buf = new Uint8Array([
0x00, 0x79, 0x00, 0x79,
0xde, 0xad, 0x85, 0x00, 0x00, 0x01, 0x00, 0x01, 0xde, 0xad, 0x85, 0x00, 0x00, 0x01, 0x00, 0x01,
0x00, 0x02, 0x00, 0x02, 0x02, 0x6f, 0x6a, 0x05, 0x00, 0x02, 0x00, 0x02, 0x02, 0x6f, 0x6a, 0x05,
@ -1257,7 +1257,7 @@ function testEncoder (t, rpacket, val) {
t.ok(compare(t, val, val3), 'decoded object match 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') t.ok(compare(t, val2, val3), 're-encoded decoded object match on re-encode')
const bigger = Buffer.allocUnsafe(buf2.length + 10) const bigger = new Uint8Array(buf2.length + 10)
const buf3 = rpacket.encode(val, bigger, 10) const buf3 = rpacket.encode(val, bigger, 10)
const val4 = rpacket.decode(buf3, 10) const val4 = rpacket.decode(buf3, 10)
@ -1268,7 +1268,13 @@ function testEncoder (t, rpacket, val) {
} }
function compare (t, a, b) { function compare (t, a, b) {
if (Buffer.isBuffer(a)) return a.toString('hex') === b.toString('hex') if (a instanceof Uint8Array && b instanceof Uint8Array) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false
}
return true
}
if (typeof a === 'object' && a && b) { if (typeof a === 'object' && a && b) {
const keys = Object.keys(a) const keys = Object.keys(a)
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {

24
tsconfig.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"module": "node16",
"lib": [
"es6"
],
"noImplicitAny": true,
"noImplicitThis": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"types": ["node"],
"noEmit": true,
"forceConsistentCasingInFileNames": true,
"ignoreDeprecations": "6.0",
"baseUrl": ".",
"paths": {
"dns-packet": ["./index.d.ts"]
}
},
"files": [
"index.d.ts",
"dns-packet-tests.ts"
]
}