;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;                                                                 ;;
;; Copyright (C) KolibriOS team 2010-2014. All rights reserved.    ;;
;; Distributed under terms of the GNU General Public License       ;;
;;                                                                 ;;
;;  zeroconfig.asm - Zeroconfig service for KolibriOS              ;;
;;                                                                 ;;
;;  Written by hidnplayr@kolibrios.org                             ;;
;;    Some code contributed by Derpenguin                          ;;
;;                                                                 ;;
;;  DHCP code is based on that by Mike Hibbet                      ;;
;;      (DHCP client for menuetos)                                 ;;
;;                                                                 ;;
;;          GNU GENERAL PUBLIC LICENSE                             ;;
;;             Version 2, June 1991                                ;;
;;                                                                 ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

format binary as ""

; CONFIGURATION

TIMEOUT                 = 3             ; in seconds
BUFFER                  = 1024          ; in bytes
DHCP_TRIES              = 3             ; number of times to try contacting DHCP server
__DEBUG__               = 1             ; enable/disable
__DEBUG_LEVEL__         = 2             ; 1 = all, 2 = errors

; CONFIGURATION FOR LINK-LOCAL

PROBE_WAIT              = 1             ; second  (initial random delay)
PROBE_MIN               = 1             ; second  (minimum delay till repeated probe)
PROBE_MAX               = 2             ; seconds (maximum delay till repeated probe)
PROBE_NUM               = 3             ;         (number of probe packets)

ANNOUNCE_NUM            = 2             ;         (number of announcement packets)
ANNOUNCE_INTERVAL       = 2             ; seconds (time between announcement packets)
ANNOUNCE_WAIT           = 2             ; seconds (delay before announcing)

MAX_CONFLICTS           = 10            ;         (max conflicts before rate limiting)

RATE_LIMIT_INTERVAL     = 60            ; seconds (delay between successive attempts)

DEFEND_INTERVAL         = 10            ; seconds (min. wait between defensive ARPs)

use32
        org     0x0

        db      'MENUET01'              ; 8 byte id
        dd      0x01                    ; header version
        dd      START                   ; start of code
        dd      IM_END                  ; size of image
        dd      (I_END+0x100)           ; memory for app
        dd      (I_END+0x100)           ; esp
        dd      0, 0                    ; I_Param, I_Path


include '../../proc32.inc'
include '../../macros.inc'
include '../../debug-fdo.inc'
include '../../network.inc'
include 'dhcp.inc'
include '../../dll.inc'


Ip2dword:
    push    edx

    ; This code validates if the query is an IP containing 4 numbers and 3 dots

    xor     al, al            ; make al (dot count) zero

   @@:
    cmp     byte[edx],'0'     ; check if this byte is a number, if not jump to no_IP
    jl      no_IP             ;
    cmp     byte[edx],'9'     ;
    jg      no_IP             ;

    inc     edx               ; the byte was a number, so lets check the next byte

    cmp     byte[edx],0       ; is this byte zero? (have we reached end of query?)
    jz      @f                ; jump to next @@ then
    cmp     byte[edx],':'
    jz      @f

    cmp     byte[edx],'.'     ; is this byte a dot?
    jne     @r                ; if not, jump to previous @@

    inc     al                ; the byte was a dot so increment al(dot count)
    inc     edx               ; next byte
    jmp     @r                ; lets check for numbers again (jump to previous @@)

   @@:                        ; we reach this when end of query reached
    cmp     al,3              ; check if there where 3 dots
    jnz     no_IP             ; if not, jump to no_IP

    ; The following code will convert this IP into a dword and output it in eax
    ; If there is also a port number specified, this will be returned in ebx, otherwise ebx is -1

    pop     esi               ; edx (query address) was pushed onto stack and is now popped in esi

    xor     edx, edx          ; result
    xor     eax, eax          ; current character
    xor     ebx, ebx          ; current byte

  .outer_loop:
    shl     edx, 8
    add     edx, ebx
    xor     ebx, ebx
  .inner_loop:
    lodsb
    test    eax, eax
    jz      .finish
    cmp     al, '.'
    jz      .outer_loop
    sub     eax, '0'
    imul    ebx, 10
    add     ebx, eax
    jmp     .inner_loop
  .finish:
    shl     edx, 8
    add     edx, ebx

    bswap   edx               ; we want little endian order

    ret

no_IP:
    pop     edx
    xor     edx, edx

    ret






START:
        mcall   40, EVM_STACK2

        DEBUGF  2,"Zero-config service loaded\n"

  .wait:
        mov     ebx, API_ETH + 0
        mov     bh, [device]
        mcall   76                              ; get MAC of ethernet interface 1
        cmp     eax, -1
        jne     .start

        mcall   10
        jmp     .wait

  .start:
        mov     word[MAC], bx
        mov     dword[MAC+2], eax
        DEBUGF  1,"MAC: %x-%x-%x-%x-%x-%x\n", [MAC+0]:2, [MAC+1]:2, [MAC+2]:2, [MAC+3]:2, [MAC+4]:2, [MAC+5]:2

        mcall   40, EVM_STACK

        mcall   68, 11

        stdcall dll.Load,@IMPORT
        or      eax, eax
        jnz     try_dhcp

        invoke  ini.get_str, path, str_ipconfig, str_type, inibuf, 16, 0

        cmp     dword[inibuf], 'stat'
        jne     try_dhcp

        invoke  ini.get_str, path, str_ipconfig, str_ip, inibuf, 16, 0
        mov     edx, inibuf
        call    Ip2dword
        mov     ecx, edx
        mov     ebx, API_IPv4 + 3       ; set IP
        mov     bh, [device]
        mcall   76

        invoke  ini.get_str, path, str_ipconfig, str_gateway, inibuf, 16, 0
        mov     edx, inibuf
        call    Ip2dword
        mov     ecx, edx
        mov     ebx, API_IPv4 + 9       ; set gateway
        mov     bh, [device]
        mcall   76

        invoke  ini.get_str, path, str_ipconfig, str_dns, inibuf, 16, 0
        mov     edx, inibuf
        call    Ip2dword
        mov     ecx, edx
        mov     ebx, API_IPv4 + 5       ; set DNS
        mov     bh, [device]
        mcall   76

        invoke  ini.get_str, path, str_ipconfig, str_subnet, inibuf, 16, 0
        mov     edx, inibuf
        call    Ip2dword
        mov     ecx, edx
        mov     ebx, API_IPv4 + 7       ; set subnet
        mov     bh, [device]
        mcall   76


        mcall   -1


try_dhcp:

        DEBUGF  2,"Trying to contact DHCP server\n"

        mcall   75, 0, AF_INET4, SOCK_DGRAM, 0          ; open socket (parameters: domain, type, reserved)
        cmp     eax, -1
        je      error
        mov     [socketNum], eax

        DEBUGF  1,"Socket %x opened\n", eax

        mcall   75, 2, [socketNum], sockaddr1, 18       ; bind socket to local port 68
        cmp     eax, -1
        je      error

        DEBUGF  1,"Socket Bound to local port 68\n"

        mcall   75, 4, [socketNum], sockaddr2, 18       ; connect to 255.255.255.255 on port 67
        cmp     eax, -1
        je      error

        DEBUGF  1,"Connected to 255.255.255.255 on port 67\n"

        mov     [dhcpMsgType], 0x01                     ; DHCP discover
        mov     [dhcpLease], esi                        ; esi is still -1 (-1 = forever)

        mcall   26, 9                                   ; Get system time
        imul    eax, 100
        mov     [currTime], eax

build_request:                                          ; Creates a DHCP request packet.

        mov     [tries], DHCP_TRIES

        DEBUGF  1,"Building request\n"

        stdcall mem.Alloc, BUFFER
        mov     [dhcpMsg], eax
        test    eax, eax
        jz      dhcp_error

            ;;; todo: skip this bullcrap

        mov     edi, eax
        mov     ecx, BUFFER
        xor     eax, eax
        rep     stosb

            ;; todo: put this in a buffer instead of writing bytes and words!

        mov     edx, [dhcpMsg]

        ; Boot protocol legacy
        mov     [edx], byte 0x01                ; Boot request
        mov     [edx+1], byte 0x01              ; Ethernet
        mov     [edx+2], byte 0x06              ; Ethernet h/w len
        mov     [edx+4], dword 0x11223344       ; xid                 ;;;;;;;
        mov     eax, [currTime]
        mov     [edx+8], eax                    ; secs, our uptime
        mov     [edx+10], byte 0x80             ; broadcast flag set
        mov     eax, dword [MAC]                ; first 4 bytes of MAC
        mov     [edx+28],dword eax
        mov     ax, word [MAC+4]                ; last 2 bytes of MAC
        mov     [edx+32],word ax

        ; DHCP extension
        mov     [edx+236], dword 0x63538263     ; magic cookie
        mov     [edx+240], word 0x0135          ; option DHCP msg type
        mov     al, [dhcpMsgType]
        mov     [edx+240+2], al
        mov     [edx+240+3], word 0x0433        ; option Lease time = infinity
        mov     eax, [dhcpLease]
        mov     [edx+240+5], eax
        mov     [edx+240+9], word 0x0432        ; option requested IP address
        mov     eax, [dhcp.ip]
        mov     [edx+240+11], eax
        mov     [edx+240+15], word 0x0437       ; option request list
        mov     [edx+240+17], dword 0x0f060301

        cmp     [dhcpMsgType], 0x01             ; Check which msg we are sending
        jne     request_options

        mov     [edx+240+21], byte 0xff         ; end of options marker

        mov     [dhcpMsgLen], 262               ; length
        jmp     send_dhcpmsg

request_options:
        mov     [edx+240+21], word 0x0436       ; server IP
        mov     eax, [dhcpServerIP]
        mov     [edx+240+23], eax

        mov     [edx+240+27], byte 0xff         ; end of options marker

        mov     [dhcpMsgLen], 268               ; length

send_dhcpmsg:
        DEBUGF  1,"Sending DHCP discover/request\n"
        mcall   75, 6, [socketNum], [dhcpMsg], [dhcpMsgLen]     ; write to socket ( send broadcast request )
        mcall   26, 9
        add     eax, TIMEOUT*100
        mov     [timeout], eax
  .wait:
        mcall   23, TIMEOUT                                             ; wait for data

read_data:                                                              ; we have data - this will be the response
        mcall   75, 7, [socketNum], [dhcpMsg], BUFFER, MSG_DONTWAIT     ; read data from socket
        cmp     eax, -1
        jne     @f

        mcall   26, 9
        cmp     eax, [timeout]
        jb      send_dhcpmsg.wait

        DEBUGF  2,"No answer from DHCP server\n"
        dec     [tries]
        jnz     send_dhcpmsg                    ; try again
        jmp     dhcp_error                      ; fail

  @@:
        DEBUGF  1,"%d bytes received\n", eax
        mov     [dhcpMsgLen], eax

; depending on which msg we sent, handle the response
; accordingly.
; If the response is to a dhcp discover, then:
;  1) If response is DHCP OFFER then
;  1.1) record server IP, lease time & IP address.
;  1.2) send a request packet
; If the response is to a dhcp request, then:
;  1) If the response is DHCP ACK then
;  1.1) extract the DNS & subnet fields. Set them in the stack

        cmp     [dhcpMsgType], 0x01             ; did we send a discover?
        je      discover

        cmp     [dhcpMsgType], 0x03             ; did we send a request?
        je      request

        call    dhcp_end                        ; we should never reach here ;)
        jmp     exit

discover:
        call    parse_response

        cmp     [dhcpMsgType2], 0x02            ; Was the response an offer?
        je      send_request

        call    dhcp_end
        jmp     link_local

send_request:
        DEBUGF  1, "Got offer, making request\n"
        mov     [dhcpMsgType], 0x03             ; make it a request
        jmp     build_request

request:
        call    parse_response

        cmp     [dhcpMsgType2], 0x05            ; Was the response an ACK? It should be
        jne     read_data                       ; NO - read next packets

        DEBUGF  2, "Setting IP using DHCP\n"

        mov     [notify_struct.msg], str_connected
        mcall   70, notify_struct
        call    dhcp_end

        mov     ebx, API_IPv4 + 3
        mov     bh, [device]
        mcall   76, , [dhcp.ip]                 ; ip
        mov     bl, 5
        mcall   76, , [dhcp.dns]                ; dns
        mov     bl, 7
        mcall   76, , [dhcp.subnet]             ; subnet
        mov     bl, 9
        mcall   76, , [dhcp.gateway]            ; gateway

        jmp     exit

dhcp_end:
        mcall   close, [socketNum]
        stdcall mem.Free, [dhcpMsg]

        ret

;***************************************************************************
;   Function
;      parseResponse
;
;   Description
;      extracts the fields ( client IP address and options ) from
;      a DHCP response
;      The values go into
;       dhcpMsgType,dhcpLease,dhcpClientIP,dhcpServerIP,
;       dhcpDNSIP, dhcpSubnet
;      The message is stored in dhcpMsg
;
;***************************************************************************
parse_response:

        DEBUGF  1,"Data received, parsing response\n"
        mov     edx, [dhcpMsg]
        mov     [dhcpMsgType2], 0

        push    dword [edx+16]
        pop     [dhcp.ip]
        DEBUGF  1,"Client: %u.%u.%u.%u\n", [edx+16]:1, [edx+17]:1, [edx+18]:1, [edx+19]:1

; TODO: check if there really are options

        mov     al, 240                         ; Point to first option
        movzx   ecx, al

  .next_option:
        add     edx, ecx

        mov     al, [edx]                       ; get message identifier

        cmp     al, 0xff                        ; End of options?
        je      .done

        cmp     al, 0
        je      .pad

; TODO: check if we still are inside the buffer

        inc     edx
        movzx   ecx, byte [edx]                 ; get data length
        inc     edx                             ; point to data

        cmp     al, dhcp_msg_type               ; Msg type is a single byte option
        je      .msgtype

        cmp     al, dhcp_dhcp_server_id
        je      .server

        cmp     al, dhcp_address_time
        je      .lease

        cmp     al, dhcp_subnet_mask
        je      .subnet

        cmp     al, dhcp_router
        je      .router

        cmp     al, dhcp_domain_server
        je      .dns

        DEBUGF  1,"Unsupported DHCP option: %u\n", al

        jmp     .next_option

  .pad:
        xor     ecx, ecx
        inc     ecx
        jmp     .next_option

  .msgtype:
        mov     al, [edx]
        mov     [dhcpMsgType2], al

        DEBUGF  1,"DHCP Msg type: %u\n", al
        jmp     .next_option                    ; Get next option

  .server:
        mov     eax, [edx]
        mov     [dhcpServerIP], eax
        DEBUGF  1,"Server: %u.%u.%u.%u\n",[edx]:1,[edx+1]:1,[edx+2]:1,[edx+3]:1
        jmp     .next_option

  .lease:
        pusha
        mov     eax,[edx]
        bswap   eax
        mov     [dhcpLease],eax
        DEBUGF  1,"lease: %d\n",eax
        popa
        jmp     .next_option

  .subnet:
        push    dword [edx]
        pop     [dhcp.subnet]
        DEBUGF  1,"Subnet: %u.%u.%u.%u\n",[edx]:1,[edx+1]:1,[edx+2]:1,[edx+3]:1
        jmp     .next_option

  .router:
        push    dword [edx]
        pop     [dhcp.gateway]
        DEBUGF  1,"Gateway: %u.%u.%u.%u\n",[edx]:1,[edx+1]:1,[edx+2]:1,[edx+3]:1
        jmp     .next_option

  .dns:
        push    dword [edx]
        pop     [dhcp.dns]
        DEBUGF  1,"DNS: %u.%u.%u.%u\n",[edx]:1,[edx+1]:1,[edx+2]:1,[edx+3]:1
        jmp     .next_option

  .done:
        ret



dhcp_error:
        call    dhcp_end

link_local:
        call    random
        mov     cx, ax
        shl     ecx, 16
        mov     cx, 0xfea9                              ; IP 169.254.0.0 link local net, see RFC3927
        mov     ebx, API_IPv4 + 3
        mov     bh, [device]
        mcall   76, , ecx                   ; mask is 255.255.0.0
        DEBUGF  2,"Link Local IP assigned: 169.254.%u.%u\n", [generator+0]:1, [generator+1]:1
        mov     bl, 7
        mcall   76, , 0xffff
        mov     bl, 9
        mcall   76, , 0x0
        mov     bl, 5
        mcall   76, , 0x0

        mcall   5, PROBE_WAIT*100

        xor     esi, esi
   probe_loop:
        call    random                                  ; create a pseudo random number in eax (seeded by MAC)

        cmp     al, PROBE_MIN*100                       ; check if al is bigger then PROBE_MIN
        jae     @f                                      ; all ok
        add     al, (PROBE_MAX-PROBE_MIN)*100           ; al is too small
   @@:

        cmp     al, PROBE_MAX*100
        jbe     @f
        sub     al, (PROBE_MAX-PROBE_MIN)*100
   @@:

        movzx   ebx,al
        DEBUGF  1,"Waiting %u0ms\n",ebx
        mcall   5

        DEBUGF  1,"Sending Probe\n"
        mov     ebx, API_ARP + 6
        mov     bh, [device]
        mcall   76
        inc     esi

        cmp     esi, PROBE_NUM
        jb      probe_loop

; now we wait further ANNOUNCE_WAIT seconds and send ANNOUNCE_NUM ARP announces. If any other host has assingned
; IP within this time, we should create another adress, that have to be done later

        DEBUGF  1,"Waiting %us\n", ANNOUNCE_WAIT
        mcall   5, ANNOUNCE_WAIT*100
        xor   esi, esi
   announce_loop:

        DEBUGF  1,"Sending Announce\n"
        mov     ebx, API_ARP + 6
        mov     bh, [device]
        mcall   76

        inc     esi
        cmp     esi,ANNOUNCE_NUM
        je      @f

        DEBUGF  1,"Waiting %us\n", ANNOUNCE_INTERVAL
        mcall   5, ANNOUNCE_INTERVAL*100
        jmp     announce_loop
   @@:
        jmp     exit


error:
        DEBUGF  2,"Socket error\n"
exit:   ; we should, instead of closing, detect ARP conflicts and detect if cable keeps connected ;)
        DEBUGF  2,"Exiting\n"
        mcall   -1


random:  ; Pseudo random actually

        mov     eax, [generator]
        add     eax, -43ab45b5h
        ror     eax, 1
        bswap   eax
        xor     eax, dword[MAC]
        ror     eax, 1
        xor     eax, dword[MAC+2]
        mov     [generator], eax

        ret

; DATA AREA

align 16
@IMPORT:

library \
        libini,'libini.obj'

import  libini, \
        ini.get_str,'ini_get_str'

include_debug_strings

str_ip          db 'ip', 0
str_subnet      db 'subnet', 0
str_gateway     db 'gateway', 0
str_dns         db 'dns', 0
str_ipconfig    db 'ipconfig', 0
str_type        db 'type', 0


sockaddr1:

        dw AF_INET4
        dw 68 shl 8     ; local port
        dd 0            ; local IP

        rb 10


sockaddr2:

        dw AF_INET4
        dw 67 shl 8     ; destination port
        dd -1           ; destination IP

        rb 10

notify_struct:
        dd 7            ; run application
        dd 0
 .msg   dd 0
        dd 0
        dd 0
        db '/sys/@notify', 0

str_connected   db '"You are now connected to the network." -N', 0
path            db '/sys/settings/network.ini',0

IM_END:

device          db 1
inibuf          rb 16
tries           db ?

dhcpMsgType     db ?    ; sent
dhcpMsgType2    db ?    ; received
dhcpLease       dd ?
dhcpServerIP    dd ?

dhcp:
.ip             dd ?
.subnet         dd ?
.dns            dd ?
.gateway        dd ?


dhcpMsgLen      dd ?
socketNum       dd ?

MAC             dp ?

currTime        dd ?
generator       dd ?

dhcpMsg         dd ?

timeout         dd ?

I_END: