UNPKG

@stacksjs/dnsx

Version:

A command-line & library DNS client. Like dig & dog, but for TypeScript.

355 lines (293 loc) 8.76 kB
import type { DnsAnswer, DnsQuery, DnsResponse, EDNSMode, ProtocolTweaks } from './types'; declare const DNS_HEADER_SIZE: unknown; export declare class DnsFlags { response = false opcode = 0 authoritative = false truncated = false recursionDesired = false recursionAvailable = false authenticData = false checkingDisabled = false responseCode = 0 static fromBuffer(buffer: Buffer): DnsFlags { const flags = new DnsFlags() const rawFlags = buffer.readUInt16BE(0) flags.response = (rawFlags & 0x8000) !== 0 flags.opcode = (rawFlags & 0x7800) >>> 11 flags.authoritative = (rawFlags & 0x0400) !== 0 flags.truncated = (rawFlags & 0x0200) !== 0 flags.recursionDesired = (rawFlags & 0x0100) !== 0 flags.recursionAvailable = (rawFlags & 0x0080) !== 0 flags.authenticData = (rawFlags & 0x0020) !== 0 flags.checkingDisabled = (rawFlags & 0x0010) !== 0 flags.responseCode = rawFlags & 0x000F return flags } toBuffer(): Buffer { const buffer = Buffer.alloc(2) let flags = 0 if (this.response) flags |= 0x8000 flags |= (this.opcode & 0x0F) << 11 if (this.authoritative) flags |= 0x0400 if (this.truncated) flags |= 0x0200 if (this.recursionDesired) flags |= 0x0100 if (this.recursionAvailable) flags |= 0x0080 if (this.authenticData) flags |= 0x0020 if (this.checkingDisabled) flags |= 0x0010 flags |= this.responseCode & 0x000F buffer.writeUInt16BE(flags, 0) return buffer } } export declare class DnsEncoder { private buffer: Buffer private offset: number private nameOffsets: Map<string, number> private verbose?: boolean constructor(size = 512, verbose?: boolean) { this.buffer = Buffer.alloc(size) this.offset = 0 this.nameOffsets = new Map() this.verbose = verbose } writeHeader(id: number, flags: DnsFlags, counts: { qdcount: number ancount: number nscount: number arcount: number }): void { debugLog('encoder', `Writing DNS header: ${JSON.stringify({ id, flags: { response: flags.response, opcode: flags.opcode, recursionDesired: flags.recursionDesired, }, counts, })}`, this.verbose) this.writeUint16(id) const flagsBuffer = flags.toBuffer() this.buffer.set(flagsBuffer, this.offset) this.offset += 2 this.writeUint16(counts.qdcount) this.writeUint16(counts.ancount) this.writeUint16(counts.nscount) this.writeUint16(counts.arcount) } writeName(name: string): void { debugLog('encoder', `Writing DNS name: ${name}`, this.verbose) const labels = name.split('.') for (const label of labels) { if (label.length > MAX_LABEL_LENGTH) { throw new Error(`Label too long: ${label}`) } this.buffer[this.offset++] = label.length this.buffer.write(label, this.offset) this.offset += label.length } this.buffer[this.offset++] = 0 } writeQuestion(query: DnsQuery): void { this.writeName(query.name) this.writeUint16(query.type) this.writeUint16(query.class) } private writeUint16(value: number): void { this.buffer.writeUInt16BE(value, this.offset) this.offset += 2 } private writeUint32(value: number): void { this.buffer.writeUInt32BE(value, this.offset) this.offset += 4 } getBuffer(): Buffer { return this.buffer.slice(0, this.offset) } } export declare class DnsDecoder { private buffer: Buffer private offset: number constructor(buffer: Buffer) { this.buffer = buffer this.offset = 0 } private canRead(bytes: number): boolean { return this.offset + bytes <= this.buffer.length } readName(): string { const labels: string[] = [] let jumping = false let jumpOffset = this.offset for (let jumps = 0; jumps < 5; jumps++) { if (jumpOffset >= this.buffer.length) { throw new Error('Out of bounds access') } const len = this.buffer[jumpOffset] if (len === 0) { if (!jumping) { this.offset = jumpOffset + 1 } break } if ((len & 0xC0) === 0xC0) { if (!this.canRead(2)) { throw new Error('Out of bounds access') } if (!jumping) { this.offset = jumpOffset + 2 jumping = true } jumpOffset = ((len & 0x3F) << 8) | this.buffer[jumpOffset + 1] continue } jumpOffset++ if (jumpOffset + len > this.buffer.length) { throw new Error('Out of bounds access') } const label = this.buffer.slice(jumpOffset, jumpOffset + len).toString('ascii') labels.push(label) jumpOffset += len } return labels.join('.') } readHeader(): { id: number, flags: DnsFlags, counts: { qdcount: number, ancount: number, nscount: number, arcount: number } } { if (!this.canRead(DNS_HEADER_SIZE)) { throw new Error(`Invalid DNS header size: ${this.buffer.length} bytes`) } const id = this.readUint16() const flags = DnsFlags.fromBuffer(this.buffer.slice(this.offset, this.offset + 2)) this.offset += 2 const counts = { qdcount: this.readUint16(), ancount: this.readUint16(), nscount: this.readUint16(), arcount: this.readUint16(), } return { id, flags, counts } } readQuestion(): DnsQuery { const name = this.readName() if (!this.canRead(4)) { throw new Error('Out of bounds access') } const type = this.readUint16() const qclass = this.readUint16() return { name, type, class: qclass } } readAnswer(): DnsAnswer { const name = this.readName() if (!this.canRead(10)) { throw new Error('Out of bounds access') } const type = this.readUint16() const qclass = this.readUint16() const ttl = this.readUint32() const rdlength = this.readUint16() if (!this.canRead(rdlength)) { throw new Error('Out of bounds access') } const data = this.readRData(type, rdlength) return { name, type, class: qclass, ttl, data, } } private readRData(type: number, length: number): any { const startOffset = this.offset let data: any switch (type) { case RecordType.A: if (length !== 4) throw new Error('Invalid IPv4 length') data = this.readIPv4() break case RecordType.AAAA: if (length !== 16) throw new Error('Invalid IPv6 length') data = this.readIPv6() break case RecordType.MX: if (length < 3) throw new Error('Invalid MX record length') data = { preference: this.readUint16(), exchange: this.readName(), } break case RecordType.TXT: data = this.readString(length) break case RecordType.CNAME: case RecordType.NS: case RecordType.PTR: data = this.readName() break default: data = this.buffer.slice(this.offset, this.offset + length).toString('hex') this.offset += length } const bytesRead = this.offset - startOffset if (bytesRead !== length) { this.offset = startOffset + length } return data } private readUint16(): number { if (!this.canRead(2)) { throw new Error('Out of bounds access') } const value = this.buffer.readUInt16BE(this.offset) this.offset += 2 return value } private readUint32(): number { if (!this.canRead(4)) { throw new Error('Out of bounds access') } const value = this.buffer.readUInt32BE(this.offset) this.offset += 4 return value } private readIPv4(): string { if (!this.canRead(4)) { throw new Error('Out of bounds access') } const parts = [] for (let i = 0; i < 4; i++) { parts.push(this.buffer[this.offset++]) } return parts.join('.') } private readIPv6(): string { if (!this.canRead(16)) { throw new Error('Out of bounds access') } const parts = [] for (let i = 0; i < 8; i++) { parts.push(this.buffer.readUInt16BE(this.offset).toString(16)) this.offset += 2 } return parts.join(':') } private readString(length: number): string { if (!this.canRead(length)) { throw new Error('Out of bounds access') } const str = this.buffer.slice(this.offset, this.offset + length).toString() this.offset += length return str } } export declare function buildQuery(options: { id?: number, txid?: number, edns?: EDNSMode, tweaks?: ProtocolTweaks, verbose?: boolean }): Buffer; export declare function parseResponse(buffer: Buffer): DnsResponse;