diff --git a/src/benchmark.ts b/src/benchmark.ts index 5761cf0..5573218 100644 --- a/src/benchmark.ts +++ b/src/benchmark.ts @@ -1,6 +1,6 @@ import { Request, parseInput } from "./request"; import { RDATARecord, RecourceRecord } from "./record"; -import { MonitoringPlugin, QuestionPlugin, DnsCore, AnswerHandler, ListenerPlugin, StoragePlugin, Record } from "."; +import { MonitoringPlugin, QuestionPlugin, DnsCore, AnswerHandler, ListenerPlugin, StoragePlugin, Record } from "./core"; import assert = require("assert"); import { RecordTypes } from "./types"; import { Question } from "./question"; diff --git a/src/core.ts b/src/core.ts new file mode 100644 index 0000000..ddd66c2 --- /dev/null +++ b/src/core.ts @@ -0,0 +1,309 @@ +import { Request } from "./request"; +import { RecordTypes } from "./types"; +import * as LRU from "lru-cache"; +import { Writable } from "stream"; +import { Question } from "./question"; +import { RecourceRecord } from "./record"; + +// export class AnswerSteam extends Writable { + +// } + +export interface Record { + type: RecordTypes + domain: string + hostname: string + value: string + ttl: number + priority?: number + additional?: string +} + +export interface Plugin { + /** + * This sets the priority of this Plugin. + * + * The lower the value, the higher priority. + * It is recommended to make this value changeable over the cosntructor options field. + */ + Priority: number + + init(core: DnsCore): Promise +} + +export interface ListenerPlugin extends Plugin { + /** + * This method is for registering a callback that is called + * when packet is received and ready for parsing. + * The function returns the Answer data. Errors shouldn't be possible + * by this function + */ + registerCallback(callback: (data: Buffer, sender: string, max_size?: number) => Promise): void + registerCallback(callback: (data: Buffer, sender: string, answer: Writable) => Promise) +} + +export class ListenerManager { + private _listener: ListenerPlugin[] = [] + + add(listener: ListenerPlugin) { + this._listener.push(listener) + } + + setup(core: DnsCore) { + return Promise.all(this._listener.map(e => e.init(core))) + } + + registerCallback(callback: (data: Buffer, sender: string, max_size?: number) => Promise) { + this._listener.forEach(e => e.registerCallback(callback)) + } +} + +export interface MonitoringPlugin extends Plugin { + onRequest(domain: string, hostname: string, type: RecordTypes, additionalInformations): void +} + +export class MonitoringManager { + private _monitoring: MonitoringPlugin[] = [] + + add(monitoring: MonitoringPlugin) { + this._monitoring.push(monitoring) + } + + setup(core: DnsCore) { + return Promise.all(this._monitoring.map(e => e.init(core))) + } + + onRequest(domain: string, hostname: string, type: RecordTypes, additionalInformations) { + this._monitoring.forEach(e => e.onRequest(domain, hostname, type, additionalInformations)) + } +} + +export interface StoragePlugin extends Plugin { + /** + * Here you can define wich record types this storage plugin is capable of. + */ + RecordTypes: RecordTypes[] + + /** + * Disables record caching for this Storage + */ + NoCache: boolean; + + /** + * Returns if storage plugin is responsible for this domain. + * This check is synchroneus because it should be quick. + * + * @param domain The domain for check + */ + isResponsible(domain: string): boolean + getRecords(domain: string, hostname: string, type: RecordTypes): Promise + getAllRecordsForDomain(domain: string): Promise +} + +type SortedStorage = { + [P in keyof any]: StoragePlugin[] | undefined +} + +const cacheoptions: LRU.Options = { + length: () => 1, + max: 500, + maxAge: 10 * 60 * 1000 +} + +class RecordCache { + private cache = new LRU(cacheoptions); + + getRecords(domain: string, hostname: string, type: RecordTypes) { + let key = `${domain};;${hostname};;${RecordTypes[type]}`; + let rec = this.cache.get(key) + if (!rec) { + return undefined; + } + return rec; + } + + addRecords(domain: string, hostname: string, type: RecordTypes, records: Record[], ttl: number) { + let key = `${domain};;${hostname};;${RecordTypes[type]}`; + this.cache.set(key, records, ttl); + } +} + +export class StorageManager { + private _storages: StoragePlugin[] = [] + private _sorted: SortedStorage; + private _cache = new RecordCache(); + + add(storage: StoragePlugin) { + this._storages.push(storage) + } + + private presort() { + this._storages = this._storages.sort((e1, e2) => e1.Priority - e2.Priority) + this._sorted = {} + for (let key in RecordTypes) { + let key_n = Number(key) + if (key_n !== Number.NaN) { + //The _storages list is sorted with priority. + this._sorted[key_n] = this._storages.filter(e => e.RecordTypes.find(t => t === key_n) !== undefined) + } + } + } + + setup(core: DnsCore) { + this.presort() + return Promise.all(this._storages.map(e => e.init(core))) + } + + async getAllRecordsForDomain(domain: string) { + return this._storages.find(e => e.isResponsible(domain)).getAllRecordsForDomain(domain); + } + + async getRecords(domain: string, hostname: string, type: RecordTypes) { + // First check if record is cached. Since the dns entries should not chacnge quickly + // using the cach for quicker response times is the way to go. + let records = this._cache.getRecords(domain, hostname, type); + if (records) return records; + + let storages = this._sorted[type].filter(s => s.isResponsible(domain)); + if (!storages || storages.length <= 0) return undefined + + let nocache = false; + for (let storage of storages) { + nocache = storage.NoCache || nocache; + records = await storage.getRecords(domain, hostname, type) + if (records) { + if (storage.NoCache) this._cache.addRecords(domain, hostname, type, records, records.length <= 0 ? 3600 : records[0].ttl) + return records; + } + } + + // If any of the checked storage engines that is + // responsible for the domain has the noCache flag, + // caching will be disabled for all. + if (!nocache) + this._cache.addRecords(domain, hostname, type, [], 0); + return []; + } +} + +export interface AnswerHandler { + addQuestion: (question: Question) => void; + addAnswer: (record: RecourceRecord) => void; + addAdditional: (record: RecourceRecord) => void; + addAutority: (record: RecourceRecord) => void +} + +export interface QuestionPlugin extends Plugin { + /** + * This field defines wich question types this plugin is capable of handling. + */ + QuestionTypes: RecordTypes[]; + init(core: DnsCore): Promise + + /** + * + * @param question The question this plugin should solve + * @param request This gives the plugin access to relevant + * functions for queing depending questions or adding answers etc. + * @param next This function should be called if the plugin is not able to resolve + * the request. This will trigger other handlers. + */ + handleQuestion(question: Question, request: AnswerHandler, next: () => void): Promise +} + +export class QuestionManager { + private _questions: QuestionPlugin[] = []; + private _sorted = {}; + add(question: QuestionPlugin) { + this._questions.push(question); + } + + private sort() { + this._questions = this._questions.sort((e1, e2) => e1.Priority - e2.Priority); + this._sorted = {}; + for (let key in RecordTypes) { + let key_n = Number(key) + if (key_n !== Number.NaN) { + this._sorted[key_n] = this._questions.filter(e => e.QuestionTypes.find(t => t === key_n) !== undefined) + } + } + } + + setup(core: DnsCore) { + this.sort(); + return Promise.all(this._questions.map(e => e.init(core))) + } + + async handleQuestion(question: Question, request: AnswerHandler) { + let handlers: QuestionPlugin[] = this._sorted[question.QTYPE]; + if (!handlers || handlers.length <= 0) return; + let index = 0; + while (index < handlers.length) { + let i = index; + await handlers[i].handleQuestion(question, request, () => { + index++; + }) + if (i === index) break; + } + } +} + +export class DnsCore { + storageManager: StorageManager; + addStorage(plugin: StoragePlugin) { + this.storageManager.add(plugin); + } + monitoringManager: MonitoringManager; + addMonitoring(plugin: MonitoringPlugin) { + this.monitoringManager.add(plugin); + } + listenerManager: ListenerManager; + addListener(plugin: ListenerPlugin) { + this.listenerManager.add(plugin); + } + + questionManager: QuestionManager; + addQuestion(plugin: QuestionPlugin) { + this.questionManager.add(plugin); + } + + constructor() { + this.listenerManager = new ListenerManager() + this.monitoringManager = new MonitoringManager() + this.storageManager = new StorageManager() + this.questionManager = new QuestionManager() + } + + async start() { + await this.storageManager.setup(this); + await this.monitoringManager.setup(this); + await this.listenerManager.setup(this); + await this.questionManager.setup(this); + this.listenerManager.registerCallback(this.onMessage.bind(this)); + } + + private async onMessage(data: Buffer, sender: string, max_size: number = 0) { + let request = new Request(data, sender, max_size); + let questionqueue: Promise[] = []; + let handleQuestion = question => { + let prom = this.questionManager.handleQuestion(question, { + addAdditional: record => { + request.addAdditionals(record) + }, + addAnswer: record => { + request.addAnswer(record); + }, + addAutority: record => { + request.addAuthorities(record); + }, + addQuestion: question => { + handleQuestion(question) + } + }) + questionqueue.push(prom); + } + request.questions.map(q => handleQuestion(q)); + await Promise.all(questionqueue); + return request.serialize(); + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index ddd66c2..d886623 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,309 +1,4 @@ -import { Request } from "./request"; -import { RecordTypes } from "./types"; -import * as LRU from "lru-cache"; -import { Writable } from "stream"; -import { Question } from "./question"; -import { RecourceRecord } from "./record"; - -// export class AnswerSteam extends Writable { - -// } - -export interface Record { - type: RecordTypes - domain: string - hostname: string - value: string - ttl: number - priority?: number - additional?: string -} - -export interface Plugin { - /** - * This sets the priority of this Plugin. - * - * The lower the value, the higher priority. - * It is recommended to make this value changeable over the cosntructor options field. - */ - Priority: number - - init(core: DnsCore): Promise -} - -export interface ListenerPlugin extends Plugin { - /** - * This method is for registering a callback that is called - * when packet is received and ready for parsing. - * The function returns the Answer data. Errors shouldn't be possible - * by this function - */ - registerCallback(callback: (data: Buffer, sender: string, max_size?: number) => Promise): void - registerCallback(callback: (data: Buffer, sender: string, answer: Writable) => Promise) -} - -export class ListenerManager { - private _listener: ListenerPlugin[] = [] - - add(listener: ListenerPlugin) { - this._listener.push(listener) - } - - setup(core: DnsCore) { - return Promise.all(this._listener.map(e => e.init(core))) - } - - registerCallback(callback: (data: Buffer, sender: string, max_size?: number) => Promise) { - this._listener.forEach(e => e.registerCallback(callback)) - } -} - -export interface MonitoringPlugin extends Plugin { - onRequest(domain: string, hostname: string, type: RecordTypes, additionalInformations): void -} - -export class MonitoringManager { - private _monitoring: MonitoringPlugin[] = [] - - add(monitoring: MonitoringPlugin) { - this._monitoring.push(monitoring) - } - - setup(core: DnsCore) { - return Promise.all(this._monitoring.map(e => e.init(core))) - } - - onRequest(domain: string, hostname: string, type: RecordTypes, additionalInformations) { - this._monitoring.forEach(e => e.onRequest(domain, hostname, type, additionalInformations)) - } -} - -export interface StoragePlugin extends Plugin { - /** - * Here you can define wich record types this storage plugin is capable of. - */ - RecordTypes: RecordTypes[] - - /** - * Disables record caching for this Storage - */ - NoCache: boolean; - - /** - * Returns if storage plugin is responsible for this domain. - * This check is synchroneus because it should be quick. - * - * @param domain The domain for check - */ - isResponsible(domain: string): boolean - getRecords(domain: string, hostname: string, type: RecordTypes): Promise - getAllRecordsForDomain(domain: string): Promise -} - -type SortedStorage = { - [P in keyof any]: StoragePlugin[] | undefined -} - -const cacheoptions: LRU.Options = { - length: () => 1, - max: 500, - maxAge: 10 * 60 * 1000 -} - -class RecordCache { - private cache = new LRU(cacheoptions); - - getRecords(domain: string, hostname: string, type: RecordTypes) { - let key = `${domain};;${hostname};;${RecordTypes[type]}`; - let rec = this.cache.get(key) - if (!rec) { - return undefined; - } - return rec; - } - - addRecords(domain: string, hostname: string, type: RecordTypes, records: Record[], ttl: number) { - let key = `${domain};;${hostname};;${RecordTypes[type]}`; - this.cache.set(key, records, ttl); - } -} - -export class StorageManager { - private _storages: StoragePlugin[] = [] - private _sorted: SortedStorage; - private _cache = new RecordCache(); - - add(storage: StoragePlugin) { - this._storages.push(storage) - } - - private presort() { - this._storages = this._storages.sort((e1, e2) => e1.Priority - e2.Priority) - this._sorted = {} - for (let key in RecordTypes) { - let key_n = Number(key) - if (key_n !== Number.NaN) { - //The _storages list is sorted with priority. - this._sorted[key_n] = this._storages.filter(e => e.RecordTypes.find(t => t === key_n) !== undefined) - } - } - } - - setup(core: DnsCore) { - this.presort() - return Promise.all(this._storages.map(e => e.init(core))) - } - - async getAllRecordsForDomain(domain: string) { - return this._storages.find(e => e.isResponsible(domain)).getAllRecordsForDomain(domain); - } - - async getRecords(domain: string, hostname: string, type: RecordTypes) { - // First check if record is cached. Since the dns entries should not chacnge quickly - // using the cach for quicker response times is the way to go. - let records = this._cache.getRecords(domain, hostname, type); - if (records) return records; - - let storages = this._sorted[type].filter(s => s.isResponsible(domain)); - if (!storages || storages.length <= 0) return undefined - - let nocache = false; - for (let storage of storages) { - nocache = storage.NoCache || nocache; - records = await storage.getRecords(domain, hostname, type) - if (records) { - if (storage.NoCache) this._cache.addRecords(domain, hostname, type, records, records.length <= 0 ? 3600 : records[0].ttl) - return records; - } - } - - // If any of the checked storage engines that is - // responsible for the domain has the noCache flag, - // caching will be disabled for all. - if (!nocache) - this._cache.addRecords(domain, hostname, type, [], 0); - return []; - } -} - -export interface AnswerHandler { - addQuestion: (question: Question) => void; - addAnswer: (record: RecourceRecord) => void; - addAdditional: (record: RecourceRecord) => void; - addAutority: (record: RecourceRecord) => void -} - -export interface QuestionPlugin extends Plugin { - /** - * This field defines wich question types this plugin is capable of handling. - */ - QuestionTypes: RecordTypes[]; - init(core: DnsCore): Promise - - /** - * - * @param question The question this plugin should solve - * @param request This gives the plugin access to relevant - * functions for queing depending questions or adding answers etc. - * @param next This function should be called if the plugin is not able to resolve - * the request. This will trigger other handlers. - */ - handleQuestion(question: Question, request: AnswerHandler, next: () => void): Promise -} - -export class QuestionManager { - private _questions: QuestionPlugin[] = []; - private _sorted = {}; - add(question: QuestionPlugin) { - this._questions.push(question); - } - - private sort() { - this._questions = this._questions.sort((e1, e2) => e1.Priority - e2.Priority); - this._sorted = {}; - for (let key in RecordTypes) { - let key_n = Number(key) - if (key_n !== Number.NaN) { - this._sorted[key_n] = this._questions.filter(e => e.QuestionTypes.find(t => t === key_n) !== undefined) - } - } - } - - setup(core: DnsCore) { - this.sort(); - return Promise.all(this._questions.map(e => e.init(core))) - } - - async handleQuestion(question: Question, request: AnswerHandler) { - let handlers: QuestionPlugin[] = this._sorted[question.QTYPE]; - if (!handlers || handlers.length <= 0) return; - let index = 0; - while (index < handlers.length) { - let i = index; - await handlers[i].handleQuestion(question, request, () => { - index++; - }) - if (i === index) break; - } - } -} - -export class DnsCore { - storageManager: StorageManager; - addStorage(plugin: StoragePlugin) { - this.storageManager.add(plugin); - } - monitoringManager: MonitoringManager; - addMonitoring(plugin: MonitoringPlugin) { - this.monitoringManager.add(plugin); - } - listenerManager: ListenerManager; - addListener(plugin: ListenerPlugin) { - this.listenerManager.add(plugin); - } - - questionManager: QuestionManager; - addQuestion(plugin: QuestionPlugin) { - this.questionManager.add(plugin); - } - - constructor() { - this.listenerManager = new ListenerManager() - this.monitoringManager = new MonitoringManager() - this.storageManager = new StorageManager() - this.questionManager = new QuestionManager() - } - - async start() { - await this.storageManager.setup(this); - await this.monitoringManager.setup(this); - await this.listenerManager.setup(this); - await this.questionManager.setup(this); - this.listenerManager.registerCallback(this.onMessage.bind(this)); - } - - private async onMessage(data: Buffer, sender: string, max_size: number = 0) { - let request = new Request(data, sender, max_size); - let questionqueue: Promise[] = []; - let handleQuestion = question => { - let prom = this.questionManager.handleQuestion(question, { - addAdditional: record => { - request.addAdditionals(record) - }, - addAnswer: record => { - request.addAnswer(record); - }, - addAutority: record => { - request.addAuthorities(record); - }, - addQuestion: question => { - handleQuestion(question) - } - }) - questionqueue.push(prom); - } - request.questions.map(q => handleQuestion(q)); - await Promise.all(questionqueue); - return request.serialize(); - } -} \ No newline at end of file +export { DnsCore, ListenerPlugin, MonitoringPlugin, StoragePlugin, QuestionPlugin, AnswerHandler, Record } from "./core" +export { RecourceRecord, RDATARecord } from "./record" +export { Request } from "./request" +export { Label } from "./label" \ No newline at end of file diff --git a/src/test.ts b/src/test.ts index 746bf12..d7b12b1 100644 --- a/src/test.ts +++ b/src/test.ts @@ -338,7 +338,7 @@ describe("parser", function () { }) }) -import { DnsCore, StoragePlugin, Record, MonitoringPlugin, QuestionPlugin, AnswerHandler, ListenerPlugin } from "./index" +import { DnsCore, StoragePlugin, Record, MonitoringPlugin, QuestionPlugin, AnswerHandler, ListenerPlugin } from "./core" describe("DNS Core", function () { it("Initialization", () => {