import { assert } from "chai"; import { parseInput, Request, questionParser, headerParser } from "./request" import { IMessageHeader, RecordTypes } from "./types"; import { RDATARecord, RecourceRecord } from "./record"; import { Question } from "./question"; import { Header } from "./header"; function fromHex(data: string) { return Buffer.from(data.replace(/\s/g, ""), "hex"); } describe("parser", function () { describe("header", function () { describe("header parser", function () { let should_templ: IMessageHeader = { ID: 0, QR: 0, OPCODE: 0, AA: 0, TC: 0, RD: 0, RA: 0, Z: 0, AD: 0, CD: 0, RCODE: 0, QDCOUNT: 0, ANCOUNT: 0, ARCOUNT: 0, NSCOUNT: 0 } let tests: { name: string, data: string, fields: Partial }[] = [ { name: "Testing ID field", data: "0001 0000 0000 0000 0000 0000", fields: { ID: 1 } }, { name: "Testing ID field with max value", data: "FFFF 0000 0000 0000 0000 0000", fields: { ID: 65535 } }, { name: "Testing QR field", data: "0000 8000 0000 0000 0000 0000", fields: { QR: 1 } }, { name: "Testing OPCODE field value 2", data: "0000 1000 0000 0000 0000 0000", fields: { OPCODE: 2 } }, { name: "Testing OPCODE field value 1", data: "0000 0800 0000 0000 0000 0000", fields: { OPCODE: 1 } }, { name: "Testing AA field", data: "0000 0400 0000 0000 0000 0000", fields: { AA: 1 } }, { name: "Testing TC field", data: "0000 0200 0000 0000 0000 0000", fields: { TC: 1 } }, { name: "Testing RD field", data: "0000 0100 0000 0000 0000 0000", fields: { RD: 1 } }, { name: "Testing RCODE field", data: "0000 0002 0000 0000 0000 0000", fields: { RCODE: 2 } }, { name: "Testing QDCOUNT field max value", data: "0000 0000 FFFF 0000 0000 0000", fields: { QDCOUNT: 65535 } }, { name: "Testing ANCOUNT field max value", data: "0000 0000 0000 FFFF 0000 0000", fields: { ANCOUNT: 65535 } }, { name: "Testing NSCOUNT field max value", data: "0000 0000 0000 0000 FFFF 0000", fields: { NSCOUNT: 65535 } }, { name: "Testing ARCOUNT field max value", data: "0000 0000 0000 0000 0000 FFFF", fields: { ARCOUNT: 65535 } }, { name: "Testing all Flags and Values max", data: "FFFF FFFF FFFF FFFF FFFF FFFF", fields: { ID: 65535, QR: 1, OPCODE: 15, AA: 1, TC: 1, RD: 1, RA: 1, Z: 1, AD: 1, CD: 1, RCODE: 15, QDCOUNT: 65535, ANCOUNT: 65535, NSCOUNT: 65535, ARCOUNT: 65535 } } ] tests.forEach(function (e) { it(e.name, function () { let testdata = fromHex(e.data) let should: IMessageHeader = { ...should_templ, ...e.fields } let header = headerParser.parse(testdata); assert.hasAllKeys(header, Object.keys(should), "Parsed header is missing some fields") assert.deepEqual(header, should, "Parsed header has not expected values!") }) }) }) describe("header serializer", function () { let empty_header: IMessageHeader = { ID: 0, QR: 0, OPCODE: 0, AA: 0, TC: 0, RD: 0, RA: 0, Z: 0, AD: 0, CD: 0, RCODE: 0, QDCOUNT: 0, ANCOUNT: 0, ARCOUNT: 0, NSCOUNT: 0 } let tests: { name: string, result: string, values: Partial }[] = [ { name: "Fill header with 0s", result: "0000 0000 0000 0000 0000 0000", values: {} }, { name: "Set header id to 5", result: "0005 0000 0000 0000 0000 0000", values: { ID: 5 } }, { name: "Set QR", result: "0000 8000 0000 0000 0000 0000", values: { QR: 1 } }, { name: "Set OPCODE", result: "0000 7800 0000 0000 0000 0000", values: { OPCODE: 15 } }, { name: "Set AA", result: "0000 0400 0000 0000 0000 0000", values: { AA: 1 } }, { name: "Set TC", result: "0000 0200 0000 0000 0000 0000", values: { TC: 1 } }, { name: "Set RD", result: "0000 0100 0000 0000 0000 0000", values: { RD: 1 } }, { name: "Set RA", result: "0000 0080 0000 0000 0000 0000", values: { RA: 1 } }, { name: "Set Z", result: "0000 0040 0000 0000 0000 0000", values: { Z: 1 } }, { name: "Set AD", result: "0000 0020 0000 0000 0000 0000", values: { AD: 1 } }, { name: "Set CD", result: "0000 0010 0000 0000 0000 0000", values: { CD: 1 } }, { name: "Set RCODE", result: "0000 000F 0000 0000 0000 0000", values: { RCODE: 15 } }, { name: "Set QDCOUNT", result: "0000 0000 FFFF 0000 0000 0000", values: { QDCOUNT: 65535 } }, { name: "Set ANCOUNT", result: "0000 0000 0000 FFFF 0000 0000", values: { ANCOUNT: 65535 } }, { name: "Set NSCOUNT", result: "0000 0000 0000 0000 FFFF 0000", values: { NSCOUNT: 65535 } }, { name: "Set ARCOUNT", result: "0000 0000 0000 0000 0000 FFFF", values: { ARCOUNT: 65535 } }, ] tests.forEach(function (e) { it(e.name, function () { let header = Object.assign({}, empty_header, e.values) let slz = new Header(header).lock(); assert.equal(slz.length, 12, "Header returns wrong length"); let res = Buffer.alloc(slz.length); assert.equal(slz.serialize(res, 0), 12, "Header returns wrong offset"); assert.equal(res.toString("hex"), e.result.replace(/\s/g, "").toLowerCase(), "Header serialization failed"); }) }) }) }) describe("question", function () { let questionData = fromHex("0474 6573 7407 6578 616d 706c 6503 636f 6d00 0001 0001") let questionObj = { QNAME: "test.example.com", QTYPE: RecordTypes.A, QCLASS: 1 } it("check question parser with one question", function () { let res = questionParser.parse(questionData) assert.deepEqual(res, questionObj, "Question parser does not parse input correctly") }) it("check question serialization", function () { let slz = new Question(questionObj).lock() assert.equal(slz.length, 22, "Question serializer returns wrong length") let res = Buffer.alloc(slz.length); assert.equal(slz.serialize(res, 0), 22, "Question serializer returns wrong offset") assert.equal(res.toString("hex"), questionData.toString("hex"), "Query serializer does not serialite correctly"); }) }) it("parse valid request", () => { let should = { header: { ID: 59445, QR: 0, OPCODE: 0, AA: 0, TC: 0, RD: 1, RA: 0, Z: 0, AD: 0, CD: 0, RCODE: 0, QDCOUNT: 1, ANCOUNT: 0, NSCOUNT: 0, ARCOUNT: 0 }, questions: [{ QNAME: 'example.com', QTYPE: 1, QCLASS: 1 }] } let data = fromHex("E835 0100 0001 0000 0000 0000 07 6578616D706c65 03636F6D 00 0001 0001"); let res = parseInput(data); assert.deepEqual(res, should); }) it("recource record serialization", function () { let should = "07 6578616D706C65 03 636f6D 00 0001 0001 00000640 0004 0A000001" let rr = new RDATARecord() rr.CLASS = 1 rr.NAME = "example.com" rr.TTL = 1600 rr.TYPE = 1 rr.RDATA = fromHex("0A 00 00 01") let srl = rr.lock(); let res = Buffer.alloc(srl.length); srl.serialize(res, 0); assert.equal(res.toString("hex"), should.replace(/\s/g, "").toLowerCase(), "Serialization not working properly") }) it("recource record constructor value assign", function () { let should = "07 6578616D706C65 03 636f6D 00 0001 0001 00000640 0004 0A000001" let rr = new RDATARecord({ CLASS: 1, NAME: "example.com", TTL: 1600, TYPE: 1, RDATA: fromHex("0A 00 00 01") }) let srl = rr.lock(); assert.equal(srl.length, 27, "Record serializer returns wrong length") let res = Buffer.alloc(srl.length); assert.equal(srl.serialize(res, 0), 27, "Record serializer returns wrong offset") assert.equal(res.toString("hex"), should.replace(/\s/g, "").toLowerCase(), "Record serialization not working properly") }) it("full response serialization", function () { let reqData = fromHex("E835 0100 0001 0000 0000 0000 07 6578616D706c65 03636F6D 00 0001 0001"); let should = "E835 8580 0001000100000000 076578616D706C6503636F6D0000010001 07 6578616D706C65 03 636F6D 00 0001 0001 0000 0640 0004 0A000001" let request = new Request(reqData, "") let rr = new RDATARecord() rr.CLASS = 1 rr.NAME = "example.com" rr.TTL = 1600 rr.TYPE = 1 rr.RDATA = fromHex("0A 00 00 01") request.addAnswer(rr); let data = request.serialize() assert.equal(data.toString("hex"), should.replace(/\s/g, "").toLowerCase(), "Whole packet serialization failed") }) }) import { DnsCore, StoragePlugin, Record, MonitoringPlugin, QuestionPlugin, AnswerHandler, ListenerPlugin } from "./core" describe("DNS Core", function () { it("Initialization", () => { let core = new DnsCore(); assert.exists(core, "Core constructor working") }) it("Adding fake plugins and initialize", () => { let core = new DnsCore(); core.addListener({ init: async (c) => { assert.exists(c, "Core not defined on Plugin init") }, Priority: 0, registerCallback: () => { } }) core.addMonitoring({ init: async (c) => { assert.exists(c, "Core not defined on Plugin init") }, Priority: 0, onRequest: () => { } }) core.addQuestion({ init: async (c) => { assert.exists(c, "Core not defined on Plugin init") }, QuestionTypes: [RecordTypes.A], Priority: 0, handleQuestion: async () => { }, }) core.addStorage({ getAllRecordsForDomain: async () => undefined, getRecords: async () => undefined, init: async (c) => { assert.exists(c, "Core not defined on Plugin init") }, isResponsible: () => true, NoCache: false, Priority: 0, RecordTypes: [RecordTypes.A] }) return core.start(); }) it("Test Request flow", async function () { let core = new DnsCore(); let listener = new TestListenerPlugin(); core.addListener(listener) let monitoring = new TestMonitoringPlugin({ domain: "example.com", hostname: "test", type: "A" }); core.addMonitoring(monitoring) let question = new TestQuestionPlugin(); core.addQuestion(question) let storage = new TestStoragePlugin({ domain: "example.com", hostname: "test", type: RecordTypes.A, ttl: 500, value: "10.0.0.1" }) core.addStorage(storage); await core.start(); let reqData = fromHex("E835 0100 0001 0000 0000 0000 04 74657374 07 6578616D706c65 03 636F6D 00 0001 0001"); let should = "E835 8580 0001000100000000 04 74657374 076578616D706C6503636F6D0000010001 04 74657374 07 6578616D706C65 03 636F6D 00 0001 0001 0000 0640 0004 0A000001" let resData = await listener.sendRequest(reqData) assert.equal(resData.toString("hex"), should.replace(/\s/g, "").toLowerCase(), "Whole packet serialization failed") }) }) class TestStoragePlugin implements StoragePlugin { re: Record = { domain: "example.com", hostname: "test", type: RecordTypes.A, value: "10.0.0.1", ttl: 1600 } constructor(record?: Record) { if (record) this.re = record; } Priority = 0 NoCache = false RecordTypes = [RecordTypes.A] isResponsible(domain: string) { // console.log("Is responsible") return true } async init(core) { assert.exists(core, "TestStoragePlugin init no core object") } async getRecords(domain, hostname, type) { if (domain !== this.re.domain || hostname !== this.re.hostname || type != this.re.type) return undefined; return [this.re]; } async getAllRecordsForDomain(domain) { if (domain !== this.re.domain) return undefined; return [this.re]; } } class TestMonitoringPlugin implements MonitoringPlugin { constructor(private should: { domain: string, hostname: string, type: string }) { } async init(core) { assert.exists(core, "TestMonitoringPlugin init no core object") } Priority = 0 onRequest(domain, hostname, type) { assert.equal(this.should.domain, domain, "Domain value wrong") assert.equal(this.should.hostname, hostname, "Domain value wrong") assert.equal(this.should.type, type, "Domain value wrong") } } class ARR extends RecourceRecord { TYPE = RecordTypes.A constructor(domain: string, ip: string) { super(); this.NAME = domain this.TTL = 1600 let data = Buffer.alloc(4) let idx = 0; ip.split(".").forEach(e => { data.writeUInt8(Number(e), idx); idx++; }) this.dataLock = () => { return { length: 4, serialize: (buffer, offset) => { data.copy(buffer, offset); return offset + 4; } } }; // this.RDATA = data; } } class TestQuestionPlugin implements QuestionPlugin { private Core: DnsCore QuestionTypes = [RecordTypes.A] Priority = 0 async init(core) { assert.exists(core, "TestQuestionPlugin init no core object") this.Core = core; } async handleQuestion(question: Question, request: AnswerHandler, next: () => void) { assert(this.QuestionTypes.find(e => e === question.QTYPE), "Handler was called with not supported questin type") let parts = question.QNAME.split(".") let domain = parts.splice(-2, 2).join(".") let hostname = parts.join("."); hostname = hostname !== "" ? hostname : "*" let records = await this.Core.storageManager.getRecords(domain, hostname, question.QTYPE) records.forEach(e => request.addAnswer(new ARR(question.QNAME, e.value))); } } class TestListenerPlugin implements ListenerPlugin { async init(core) { assert.exists(core, "TestListenerPlugin init no core object") } Priority = 0; callback: (data: Buffer, sender: string, max_size?: number) => Promise; registerCallback(callback) { this.callback = callback; } sendRequest(message: Buffer) { return this.callback(message, "localhost") } }