import assert from "assert" ; import { DecodedData } from "./DNSPacket" ; interface WrittenName { name : string ; writtenLabels : number [ ] ; } interface NameLength { name : string ; length : number ; labelLengths : number [ ] ; } export class DNSLabelCoder { static DISABLE_COMPRESSION = false ; private static readonly POINTER_MASK = 0xC000 ; private static readonly POINTER_MASK_ONE_BYTE = 0xC0 ; private static readonly LOCAL_COMPRESSION_ONE_BYTE = 0x80 ; private static readonly EXTENDED_LABEL_TYPE_ONE_BYTE = 0x40 ; private static readonly NOT_POINTER_MASK = 0x3FFF ; private static readonly NOT_POINTER_MASK_ONE_BYTE = 0x3F ; private buffer ? : Buffer ; readonly legacyUnicastEncoding : boolean ; private startOfRR ? : number ; private startOfRData ? : number ; private rDataLength ? : number ; private readonly trackedLengths : NameLength [ ] = [ ] ; private readonly writtenNames : WrittenName [ ] = [ ] ; constructor ( legacyUnicastEncoding ? ) { this . legacyUnicastEncoding = legacyUnicastEncoding || false ; } public initBuf ( buffer ? ) { this . buffer = buffer ; } public initRRLocation ( recordOffset , rDataOffset , rDataLength ) { this . startOfRR = recordOffset ; this . startOfRData = rDataOffset ; this . rDataLength = rDataLength ; } public clearRRLocation ( ) { this . startOfRR = undefined ; this . startOfRData = undefined ; this . rDataLength = undefined ; } public getUncompressedNameLength ( name ) { if ( name === "." ) { return 1 ; } assert ( name . endsWith ( "." ) , "Supplied illegal name which doesn't end with the root label!" ) ; let length = 0 ; const labels = name . split ( "." ) ; for ( let i = 0 ; i < labels . length ; i ++ ) { const label = labels [ i ] ; if ( ! label && i < labels . length - 1 ) { assert . fail ( "Label " + i + " in name '" + name + "' was empty" ) ; } length += DNSLabelCoder . getLabelLength ( label ) ; } return length ; } public getNameLength ( name ) { if ( DNSLabelCoder . DISABLE_COMPRESSION ) { return this . getUncompressedNameLength ( name ) ; } if ( name === "." ) { return 1 ; } assert ( name . endsWith ( "." ) , "Supplied illegal name which doesn't end with the root label!" ) ; const labelLengths = name . split ( "." ) . map ( label => DNSLabelCoder . getLabelLength ( label ) ) ; const nameLength = { name : name , length : 0 , labelLengths : labelLengths , } ; let candidateSharingLongestSuffix = undefined ; let longestSuffixLength = 0 ; for ( let i = 0 ; i < this . trackedLengths . length ; i ++ ) { const element = this . trackedLengths [ i ] ; const suffixLength = DNSLabelCoder . computeLabelSuffixLength ( element . name , name ) ; if ( suffixLength > longestSuffixLength ) { candidateSharingLongestSuffix = element ; longestSuffixLength = suffixLength ; } } let length = 0 ; if ( candidateSharingLongestSuffix ) { const pointingFromIndex = labelLengths . length - 1 - longestSuffixLength ; for ( let i = 0 ; i < pointingFromIndex ; i ++ ) { length += labelLengths [ i ] ; } length += 2 ; } else { for ( let i = 0 ; i < labelLengths . length ; i ++ ) { length += labelLengths [ i ] ; } } nameLength . length = length ; this . trackedLengths . push ( nameLength ) ; return nameLength . length ; } public encodeUncompressedName ( name , offset ) { if ( ! this . buffer ) { assert . fail ( "Illegal state. Buffer not initialized!" ) ; } return DNSLabelCoder . encodeUncompressedName ( name , this . buffer , offset ) ; } public static encodeUncompressedName ( name , buffer , offset ) { assert ( name . endsWith ( "." ) , "Name does not end with the root label" ) ; const oldOffset = offset ; const labels = name === "." ? [ "" ] : name . split ( "." ) ; for ( let i = 0 ; i < labels . length ; i ++ ) { const label = labels [ i ] ; if ( label === "" ) { assert ( i === labels . length - 1 , "Encountered root label being not at the end of the domain name" ) ; buffer . writeUInt8 ( 0 , offset ++ ) ; break ; } const length = buffer . write ( label , offset + 1 ) ; buffer . writeUInt8 ( length , offset ) ; offset += length + 1 ; } return offset - oldOffset ; } public encodeName ( name , offset ) { if ( DNSLabelCoder . DISABLE_COMPRESSION ) { return this . encodeUncompressedName ( name , offset ) ; } if ( ! this . buffer ) { assert . fail ( "Illegal state. Buffer not initialized!" ) ; } if ( name === "." ) { this . buffer . writeUInt8 ( 0 , offset ) ; return 1 ; } const oldOffset = offset ; const labels = name . split ( "." ) ; const writtenName = { name : name , writtenLabels : new Array ( labels . length ) . fill ( - 1 ) , } ; let candidateSharingLongestSuffix = undefined ; let longestSuffixLength = 0 ; for ( let i = 0 ; i < this . writtenNames . length ; i ++ ) { const element = this . writtenNames [ i ] ; const suffixLength = DNSLabelCoder . computeLabelSuffixLength ( element . name , name ) ; if ( suffixLength > longestSuffixLength ) { candidateSharingLongestSuffix = element ; longestSuffixLength = suffixLength ; } } if ( candidateSharingLongestSuffix ) { const pointingFromIndex = labels . length - 1 - longestSuffixLength ; const pointingToIndex = candidateSharingLongestSuffix . writtenLabels . length - 1 - longestSuffixLength ; for ( let i = 0 ; i < pointingFromIndex ; i ++ ) { writtenName . writtenLabels [ i ] = offset ; offset += DNSLabelCoder . writeLabel ( labels [ i ] , this . buffer , offset ) ; } const pointerDestination = candidateSharingLongestSuffix . writtenLabels [ pointingToIndex ] ; assert ( pointerDestination !== - 1 , "Label which was pointed at wasn't yet written to the buffer!" ) ; assert ( pointerDestination <= DNSLabelCoder . NOT_POINTER_MASK , "Pointer exceeds to length of a maximum of 14 bits" ) ; assert ( pointerDestination < offset , "Pointer can only point to a prior location" ) ; const pointer = DNSLabelCoder . POINTER_MASK | pointerDestination ; this . buffer . writeUInt16BE ( pointer , offset ) ; offset += 2 ; } else { for ( let i = 0 ; i < labels . length ; i ++ ) { writtenName . writtenLabels [ i ] = offset ; offset += DNSLabelCoder . writeLabel ( labels [ i ] , this . buffer , offset ) ; } } this . writtenNames . push ( writtenName ) ; return offset - oldOffset ; } public decodeName ( offset , resolvePointers = true ) { if ( ! this . buffer ) { assert . fail ( "Illegal state. Buffer not initialized!" ) ; } const oldOffset = offset ; let name = "" ; for ( ; ; ) { const length = this . buffer . readUInt8 ( offset ++ ) ; if ( length === 0 ) { name += "." ; break ; } const labelTypePattern = length & DNSLabelCoder . POINTER_MASK_ONE_BYTE ; if ( labelTypePattern ) { if ( labelTypePattern === DNSLabelCoder . POINTER_MASK_ONE_BYTE ) { const pointer = this . buffer . readUInt16BE ( offset - 1 ) & DNSLabelCoder . NOT_POINTER_MASK ; offset ++ ; if ( ! resolvePointers ) { name += name ? ".~" : "~" ; break ; } assert ( pointer < oldOffset , "Pointer at " + ( offset - 2 ) + " MUST point to a prior location!" ) ; name += ( name ? "." : "" ) + this . decodeName ( pointer ) . data ; break ; } else if ( labelTypePattern === DNSLabelCoder . LOCAL_COMPRESSION_ONE_BYTE ) { let localPointer = this . buffer . readUInt16BE ( offset - 1 ) & DNSLabelCoder . NOT_POINTER_MASK ; offset ++ ; if ( ! resolvePointers ) { name += name ? ".~" : "~" ; break ; } if ( localPointer >= 0 && localPointer < 255 ) { assert ( this . startOfRR !== undefined , "Cannot decompress locally compressed name as record is not initialized!" ) ; localPointer += this . startOfRR ! ; assert ( localPointer < oldOffset , "LocalPointer <255 at " + ( offset - 2 ) + " MUST point to a prior location!" ) ; name += ( name ? "." : "" ) + this . decodeName ( localPointer ) . data ; } else if ( localPointer >= 256 ) { assert ( this . startOfRData !== undefined && this . rDataLength !== undefined , "Cannot decompress locally compressed name as record is not initialized!" ) ; localPointer -= - 256 ; localPointer += this . startOfRData ! ; assert ( localPointer < oldOffset , "LocationPoint >265 at " + ( offset + 2 ) + " MUST point to a prior location!" ) ; name += ( name ? "." : "" ) + this . decodeName ( localPointer ) . data ; } else { assert . fail ( "Encountered unknown pointer range " + localPointer ) ; } break ; } else if ( labelTypePattern === DNSLabelCoder . EXTENDED_LABEL_TYPE_ONE_BYTE ) { const extendedLabelType = length & DNSLabelCoder . NOT_POINTER_MASK_ONE_BYTE ; assert . fail ( "Received extended label type " + extendedLabelType + " at " + ( offset - 1 ) ) ; } else { assert . fail ( "Encountered unknown pointer type: " + Buffer . from ( [ labelTypePattern >> 6 ] ) . toString ( "hex" ) + " (with original byte " + Buffer . from ( [ length ] ) . toString ( "hex" ) + ")" ) ; } } const label = this . buffer . toString ( "utf-8" , offset , offset + length ) ; offset += length ; name += ( name ? "." : "" ) + label ; } return { data : name , readBytes : offset - oldOffset , } ; } private static getLabelLength ( label ) { if ( ! label ) { return 1 ; } else { const byteLength = Buffer . byteLength ( label ) ; assert ( byteLength <= 63 , "Label cannot be longer than 63 bytes (" + label + ")" ) ; return 1 + byteLength ; } } private static writeLabel ( label , buffer , offset ) { if ( ! label ) { buffer . writeUInt8 ( 0 , offset ) ; return 1 ; } else { const length = buffer . write ( label , offset + 1 ) ; buffer . writeUInt8 ( length , offset ) ; return length + 1 ; } } private static computeLabelSuffixLength ( a , b ) { assert ( a . length !== 0 && b . length !== 0 , "Encountered empty name when comparing suffixes!" ) ; const lastAIndex = a . length - 1 ; const lastBIndex = b . length - 1 ; let equalLabels = 0 ; let exitByBreak = false ; for ( let i = 1 ; i <= lastAIndex && i <= lastBIndex ; i ++ ) { const aChar = a . charAt ( lastAIndex - i ) ; const bChar = b . charAt ( lastBIndex - i ) ; assert ( ! ! aChar && ! ! bChar , "Seemingly encountered out of bounds trying to calculate suffixes" ) ; if ( aChar !== bChar ) { exitByBreak = true ; break ; } else if ( aChar === "." ) { equalLabels ++ ; } } if ( ! exitByBreak ) { equalLabels ++ ; } return equalLabels ; } } export class NonCompressionLabelCoder extends DNSLabelCoder { public static readonly INSTANCE = new NonCompressionLabelCoder ( ) ; public getNameLength ( name ) { return this . getUncompressedNameLength ( name ) ; } public encodeName ( name , offset ) { return this . encodeUncompressedName ( name , offset ) ; } }