; DNS Client, only MX records are supported
; #########################################################################

.data
        bDNSLoaded      db      0

.const

;============
; Iphlpapi.h
;============

MAX_HOSTNAME_LEN        equ     128 
MAX_DOMAIN_NAME_LEN     equ     128 
MAX_SCOPE_ID_LEN        equ     256 


IP_ADDR_STRING struct
        Next            DWORD   ?
        IpAddress       BYTE    16 dup(?)
        IpMask          BYTE    16 dup(?)
        Context         DWORD   ?
IP_ADDR_STRING ends



;===========
; DNS types
;===========

; RR types
TYPE_A          equ 1   ; Host address
TYPE_NS         equ 2   ; Authorative name server
TYPE_MD         equ 3   ; Mail destination
TYPE_MF         equ 4   ; Mail forwarder
TYPE_CNAME      equ 5   ; Canonical name of an alias
TYPE_SOA        equ 6   ; Marks the start of a zone of authority
TYPE_MB         equ 7   ; Mailbox domain name (Experimental)
TYPE_MG         equ 8   ; Mail group member (Experimental)
TYPE_MR         equ 9   ; Mail rename domain name (Experimental)
TYPE_NULL       equ 10  ; Null RR (Experimental)
TYPE_WKS        equ 11  ; Will known service description
TYPE_PTR        equ 12  ; Domain name pointer
TYPE_HINFO      equ 13  ; Host information
TYPE_MINFO      equ 14  ; Mailbox or mail list information
TYPE_MX         equ 15  ; Mail exchange
TYPE_TXT        equ 16  ; Text strings
TYPE_RP         equ 17  ; Responsible person (Experimental)
TYPE_AFSDB      equ 18  ; Andrew File System Data Base (Experimental)
TYPE_X25        equ 19  ; X.25 address (Experimental)
TYPE_ISDN       equ 20  ; Integrated Services Digital Network addresses (Experimental)
TYPE_RT         equ 21  ; Route Through (Experimental)

; Classes
CLASS_IN        equ 1   ; Internet class
CLASS_CS        equ 2   ; CSNET class
CLASS_CH        equ 3   ; CHAOS class
CLASS_HS        equ 4   ; Hesiod

RECURSIZE_REQUEST equ 256

DNS_HEADER struct
        ID      WORD    ?
        OPCODE  WORD    ?
        QDCOUNT WORD    ?
        ANCOUNT WORD    ?
        NSCOUNT WORD    ?
        ARCOUNT WORD    ?
DNS_HEADER ends

FIXED_INFO struct
        HostName         BYTE   MAX_HOSTNAME_LEN+4 dup(?)       ; Host name for the local computer.
        DomainName       BYTE   MAX_DOMAIN_NAME_LEN+4 dup(?)    ; Domain in which the local computer is registered.
        CurrentDnsServer DWORD  ?                               ; Reserved. Use the DnsServerList member to obtain the DNS servers for the local computer.
        DnsServerList    IP_ADDR_STRING <>                      ; Linked list of IP_ADDR_STRING structures that specify the set of DNS servers used by the local computer.
        NodeType         DWORD  ?                               ; Node type of the local computer.
        ScopeId          BYTE   MAX_SCOPE_ID_LEN+4 dup(?)       ; DHCP scope name.
        EnableRouting    DWORD  ?                               ; Specifies whether routing is enabled on the local computer.
        EnableProxy      DWORD  ?                               ; Specifies whether the local computer is acting as an ARP proxy.
        EnableDns 	 DWORD  ?  

       DWORD  ?                               ; Specifies whether DNS is enabled on the local computer.
FIXED_INFO ends

.code

; Get localy saved DNS server
DNSGetServer proc
        LOCAL   pFixedInfo, pOutBufLen: DWORD

        invoke  GlobalAlloc, GPTR, sizeof FIXED_INFO
        mov     pFixedInfo, eax
        mov     pOutBufLen, sizeof FIXED_INFO

        lea     eax, pOutBufLen
        push    eax
        push    pFixedInfo
        call    dwGetNetworkParams
        .IF     eax == ERROR_BUFFER_OVERFLOW
                invoke  GlobalFree, pFixedInfo
                invoke  GlobalAlloc, GPTR, pOutBufLen
                mov     pFixedInfo, eax
        .ENDIF

        lea     eax, pOutBufLen
        push    eax
        push    pFixedInfo
        call    dwGetNetworkParams
        .IF     !eax
                mov     eax, pFixedInfo
                assume  eax: ptr FIXED_INFO
                invoke  lstrcpy, offset szDNSHost, addr [eax].DnsServerList.IpAddress
        .ENDIF
        invoke  GlobalFree, pFixedInfo
        ret
DNSGetServer endp

DNSHandleRequest proto :DWORD ; forward

; Prepare MX record query
DNSQuery proc uses esi edi ebx stream, szHost: DWORD
        LOCAL   header: DNS_HEADER
        LOCAL   pbyte: DWORD
        LOCAL   bword: WORD
        
        invoke  ZeroMemory, addr header, sizeof header

        mov     header.ID, 0202h
        mov     header.OPCODE, RECURSIZE_REQUEST
        ror     header.OPCODE, 8
        mov     header.QDCOUNT, 1
        ror     header.QDCOUNT, 8
        coinvoke stream, IStream, Write, addr header, sizeof header, 0

        ; Write host
        invoke  lstrlen, szHost
        mov     ecx, eax
        mov     edi, szHost

@l:
        mov     edx, edi
        mov     al, '.'
        cld
        repnz scasb
        mov     ebx, edi
        sub     ebx, edx
        .IF     byte ptr[edi-1] == '.'
                dec     ebx
        .ENDIF
        mov     pbyte, ebx

        push    ecx
        push    edx

        coinvoke stream, IStream, Write, addr pbyte, 1, 0
        pop     ecx
        coinvoke stream, IStream, Write, ecx, pbyte, 0
        mov     pbyte, 0

        pop     ecx
        test    ecx, ecx
        jnz     @l

        ; Zero terminator
        coinvoke stream, IStream, Write, addr pbyte, 1, 0

        ; Type
        mov     bword, TYPE_MX
        ror     bword, 8
        coinvoke stream, IStream, Write, addr bword, 2, 0

        ; Class
        mov     bword, CLASS_IN
        ror     bword, 8
        coinvoke stream, IStream, Write, addr bword, 2, 0

        ret
DNSQuery endp

; Send request, receive response, returns MX server on success, otherwise NULL
DNSRequest proc uses ebx stream, szDNSServer: DWORD
        LOCAL   len: DWORD
        LOCAL   lpbuf[128]: BYTE

        mov     ecx, DNSPort
        xchg    cl, ch
        invoke  NetConnect, szDNSServer, 0, ecx
        .IF     eax
                mov     ebx, eax
                invoke  StreamGetLength, stream
                xchg    al, ah
                mov     len, eax

                invoke  send, ebx, addr len, 2, 0
                invoke  StreamGotoBegin, stream
@l:
                coinvoke stream, IStream, Read, addr lpbuf, 128, addr len
                .IF     len
                        invoke  send, ebx, addr lpbuf, len, 0
                        jmp     @l
                .ENDIF

                invoke  StreamClear, stream
                invoke  NetRecvUntilBytes, ebx, stream, 2, 4, 0
                test    eax, eax
                jz      @close_sock

                invoke  StreamGotoBegin, stream
                mov     len, 0
                coinvoke stream, IStream, Read, addr len, 2, 0
                invoke  StreamClear, stream

                mov     eax, len
                xchg    al, ah
                invoke  NetRecvUntilBytes, ebx, stream, eax, 4, 0
                test    eax, eax
                jz      @close_sock
                invoke  closesocket, ebx

                invoke  DNSHandleRequest, stream
                mov     ebx, eax
                invoke  lstrlen, eax
                .IF     !eax
                        ; Empty string
                        invoke  GlobalFree, ebx
                        xor     eax, eax
                .ELSE
                        mov     eax, ebx
                .ENDIF
                ret
@close_sock:
                invoke  closesocket, ebx
        .ENDIF
        xor     eax, eax
        ret
DNSRequest endp

; Read compressed string chunk and append it to strmem
DNSReadChunk proc uses esi edi basemem, mem, strmem: DWORD
        mov     esi, mem

@next_read:
        cld
        xor     eax, eax
        lodsb

        test    al, 11000000b
        .IF     !ZERO?
                and     al, 00111111b
                shl     ax, 8
                ; Second octet of the offset
                lodsb
                push    esi
                mov     esi, basemem
                add     esi, eax
                invoke  DNSReadChunk, basemem, esi, strmem
                pop     esi
        .ELSEIF al
                push    eax
                invoke  lstrlen, strmem
                mov     edi, strmem
                add     edi, eax
                pop     ecx
                jecxz   @no_cpe_str

                cld
                rep movsb

        @no_cpe_str:
                mov     al, '.'
                stosb
                xor     eax, eax
                stosb

                jmp     @next_read
        .ENDIF

        mov     eax, esi        
        ret
DNSReadChunk endp

; Read compressed string
DNSReadString proc basemem, mem, strmem: DWORD
        szText  DNSTrimChars, "."
        mov     eax, strmem
        mov     byte ptr[eax], 0

        invoke  DNSReadChunk, basemem, mem, strmem
        push    eax
        invoke  StrTrim, strmem, offset DNSTrimChars
        pop     eax
        ret
DNSReadString endp

; Handles only MX records, returns string with MX server on success
; Otherwise returned string consists from NULL terminator only
DNSHandleRequest proc uses esi edi ebx stream: DWORD
        LOCAL   hMem, hStr, hStr2: DWORD
        LOCAL   pref: WORD

        mov     pref, 65535     ; MX PREFERENCE

        invoke  GlobalAlloc, GMEM_FIXED, 65536
        mov     hStr, eax

        invoke  GlobalAlloc, GMEM_FIXED, 65536
        mov     byte ptr[eax], 0
        mov     hStr2, eax

        invoke  StreamGetLength, stream
        mov     ebx, eax
        invoke  GlobalAlloc, GMEM_FIXED, eax
        mov     hMem, eax
        invoke  StreamGotoBegin, stream
        coinvoke stream, IStream, Read, hMem, ebx, 0

        mov     esi, hMem
        assume  esi: ptr DNS_HEADER
        ror     [esi].ANCOUNT, 8
        ror     [esi].OPCODE, 8
        test    [esi].OPCODE, 1111b
        .IF     !ZERO?
                jmp     @yep_the_error
        .ENDIF

        movzx   ebx, [esi].ANCOUNT
        add     esi, sizeof DNS_HEADER

        ; Skip question
        invoke  DNSReadString, hMem, esi, hStr
        mov     esi, eax
        lodsd   ; Type & Class
        .IF     eax != 01000f00h
                jmp     @yep_the_error
        .ENDIF

        ; Read answer
        .IF     ebx
                @read_answer:
                invoke  DNSReadString, hMem, esi, hStr
                mov     esi, eax
                lodsd   ; Type, Class
                push    eax
                lodsd   ; TTL
                xor     eax, eax
                lodsw   ; RDLENGTH
                
                pop     edx
                .IF     edx != 01000f00h
                        ; RDATA(Unknown), skip
                        xchg    al, ah
                        add     esi, eax
                .ELSE
                        ; RDATA(MX)
                        lodsw   ; PREFERENCE
                        push    ax
                        invoke  DNSReadString, hMem, esi, hStr
                        mov     esi, eax
                        pop     dx

                        ; Store MX host
                        .IF     dx < pref
                                mov     pref, dx
                                invoke  lstrcpy, hStr2, hStr
                        .ENDIF
                .ENDIF
                dec     ebx
                jnz     @read_answer
        .ENDIF

@yep_the_error:
        invoke  GlobalFree, hMem
        invoke  GlobalFree, hStr
        mov     eax, hStr2
        ret
DNSHandleRequest endp

; Retrieves MX record for szBaseHost, returns null terminated 
; host on success, otherwise null
DNSGetMXHost proc szBaseHost: DWORD
        LOCAL   stream: DWORD

        .IF     !bDNSLoaded
                mov     bDNSLoaded, 1
                .IF     dwGetNetworkParams
                        invoke  DNSGetServer
                .ENDIF
        .ENDIF

        invoke  StreamCreate, addr stream
        invoke  DNSQuery, stream, szBaseHost
        invoke  DNSRequest, stream, offset szDNSHost
        push    eax
        invoke  StreamFree, stream
        pop     eax
        ret
DNSGetMXHost endp
