#-
# Copyright (c) 2008-2010 Joe Marcus Clarke <jclarke@cisco.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
#
#
# Modified 2010 Jakob Pfeiffer <code@pfeiffer.ws>
# * UDP support added. Use '-u' as first parameter.
# * Looks in 'show host' for name server too.
#

namespace eval MarcusHost {
    variable A_RECORD    0x0001
    variable AAAA_RECORD 0x001c
    variable PTR_RECORD  0x000c
    variable CLASS_IN    0x0001
    variable REQ_FLAGS   0x0100

    variable RCODES [list        \
        {No error}                 \
        {Format error}                \
        {Server failure}        \
        {No such name}                \
        {Not implemented}        \
        {Refused}                \
        {Name exists}                \
        {RRset exists}                \
        {RRset does not exist}        \
        {Not authoritative}        \
        {Name out of zone}        \
    ]

    proc connect { server proto } {
        if { $proto == "tcp" } {
            if { [catch {set fd [socket $server 53]} result] } {
                return -code error $result
            }
            fconfigure $fd -translation binary

        } else {
            if { [catch {set fd [udp_open]} result] } {
                return -code error $result
            }
            fconfigure $fd -remote [list $server 53] -translation binary -buffering none
        }

        return $fd
    }

    proc getTid { } {
        expr srand([expr [clock seconds] ^ [pid]])
        return [expr int([expr rand() * 65535])]
    }

    proc getPtr6Req { addr } {
	set shorts [split $addr ":"]
	set zeros 0
	set ptraddr [list]
	if { [llength $shorts] < 8 } {
	    set zeros [expr (8 - [llength $shorts] + 1) * 4]
	}
	for { set i [expr [llength $shorts] - 1] } { $i >= 0 } { incr i -1 } {
	    if { [lindex $shorts $i] == "" } {
		set ptraddr [concat $ptraddr [split [string repeat "0" $zeros] ""]]
	    } else {
		set l [split [lindex $shorts $i] ""]
	        set l [concat [string repeat "0" [expr 4 - [string length [lindex $shorts $i]]]] $l]
		for { set j [expr [llength $l] - 1] } { $j >= 0 } { incr j -1 } {
		    lappend ptraddr [lindex $l $j]
		}
	    }
	}

	lappend ptraddr {ip6} {arpa}

	return [join $ptraddr "."]
    }

    proc getPtrReq { addr } {
	if { [isIP6Addr $addr] } {
	    return [getPtr6Req $addr]
	}
        set octets [split $addr "."]
        for { set i [expr [llength $octets] - 1] } { $i >= 0 } { incr i -1 } {
            lappend ptraddr [lindex $octets $i]
        }

        lappend ptraddr {in-addr} {arpa}

        return [join $ptraddr "."]
    }

    proc isIP6Addr { host } {
	set shorts [split $host ":"]
	if { [llength $shorts] > 8 } {
	    return 0
	}

	foreach short $shorts {
	    if { $short == "" } {
		continue
	    }
	    if { [string length $short] > 4 } {
		return 0
	    }
	    if { ![regexp {^[A-Fa-f0-9]+$} $short] } {
		return 0
	    }
	}

	return 1
    }

    proc isIPAddr { host } {
	if { [isIP6Addr $host] } {
	    return 1
	}
        set octets [split $host "."]
        if { [llength $octets] != 4 } {
            return 0
        }

        foreach octet $octets {
            if { ![regexp {\d+} $octet] } {
                return 0
            }
        }

        return 1
    }

    proc udp_event { fd } {
        global inData
        global host_event

        set inData [read $fd]
        set host_event "reply"
    }

    proc arrToV6 { addr } {
	set j 0
	set start -1
	set end -1
	set tstart -1
	for { set i 0 } { $i < [llength $addr] } { incr i 2 } {
	    set o1 [lindex $addr $i]
	    set o2 [lindex $addr [expr $i + 1]]
	    if { $o1 == 0 && $o2 == 0 } {
		lappend res 0
		if { $tstart == -1 } {
		    set tstart $j
		}
	    } else {
		if { $o1 == 0 } {
		    lappend res [format "%x" $o2]
		} else {
	            lappend res [format "%x%02x" $o1 $o2]
		}
		if { $tstart != -1 && ($start == -1 || [expr $end - $start] < [expr $j - $tstart]) } {
		    set start $tstart
		    set tstart -1
		    set end $j
		}
	    }
	    incr j
	}

	if { $tstart != -1 && [expr $end - $start] < [expr $j - $tstart] } {
	    set start $start
	    set end $j
	}

	if { [expr $end - $start] > 1 } {
	    set res [lreplace $res $start [expr $end - 1]]
	    set res [linsert $res $start ""]
	}
	return [join $res ":"]
    }

    proc lookup { host server proto {ts "A"} } {
        set fd [MarcusHost::connect $server $proto]
        if { [catch {set fd [MarcusHost::connect $server $proto]} result] } {
            return -code error $result
        }

        set tid [MarcusHost::getTid]
        set flags $MarcusHost::REQ_FLAGS
        set class $MarcusHost::CLASS_IN
	if { $ts == "A" } {
            set type $MarcusHost::A_RECORD
            set reqTypeStr "A"
	} elseif { $ts == "AAAA" } {
	    set type $MarcusHost::AAAA_RECORD
	    set reqTypeStr "AAAA"
	}
        if { [MarcusHost::isIPAddr $host] } {
            set type $MarcusHost::PTR_RECORD
            set host [MarcusHost::getPtrReq $host]
            set reqTypeStr "PTR"
        }

        set qs "\x00\x01"
        set as "\x00\x00"
        set aurrs "\x00\x00"
        set adrrs "\x00\x00"

        set hparts [split $host "."]
        set lkup ""
        foreach part $hparts {
            set len [format "%02x" [string length $part]]
            append lkup [binary format H2 $len]
            append lkup $part
        }

        set query ""
        append query [binary format H4 [format "%04x" $tid]]
        append query [binary format H4 [format "%04x" $flags]]
        append query $qs
        append query $as
        append query $aurrs
        append query $adrrs
        append query $lkup
        append query "\x00"
        append query [binary format H4 [format "%04x" $type]]
        append query [binary format H4 [format "%04x" $class]]
        set reqlen [string length $query]
        if { $proto == "tcp" } {
            set query [binary format H4 [format "%04x" $reqlen]]$query
        }

        puts -nonewline $fd $query
        flush $fd

        if { $proto == "tcp" } {
            set inData [read $fd 2]
            if { $inData == "" || $inData == 0 || ![binary scan $inData S readLen]} {
                close $fd
                return -code error "Error reading reply length from server"
            }

            set readLen [expr $readLen & 0xFFFF]

            set inData [read $fd $readLen]
            close $fd

        } else {
            global inData
	    global host_event

            set host_event ""
	    # XXX This hardcoded value should be configurable.
            after 5000 set host_event "timeout"
            fileevent $fd readable [list MarcusHost::udp_event $fd]
            vwait host_event
            close $fd

            if { $host_event == "timeout" } {
                return -code error "Timeout didn't recieved answer"
            }
        }

        if { $inData == "" || $inData == 0 } {
            return -code error "Error reading reply from server"
        }

        if { ! [binary scan $inData SSSS repTid repFlags repQs repAs] } {
            return -code error "Error parsing reply header"
        }

        set repTid [expr $repTid & 0xFFFF]
        set repFlags [expr $repFlags & 0xFFFF]
        set repAs [expr $repAs & 0xFFFF]

        if { $repTid != $tid } {
            return -code error "Transaction ID mismatch"
        }

        set repError [expr $repFlags & 0x000F]
        if { $repAs == 0 } {
            return -code error [lindex $MarcusHost::RCODES $repError]
        }

        set answers [string range $inData $reqlen end]

        set aaddrs [list $host $reqTypeStr]
        for { set i 0 } { $i < $repAs } { incr i } {
            if { ! [binary scan $answers SSSIS respName respType respClass respTTL respLen] } {
                return -code error "Error parsing reply body"
            }
            set respType [expr $respType & 0xFFFF]
            set respClass [expr $respClass & 0xFFFF]
            set respLen [expr $respLen & 0xFFFF]
            set answers [string range $answers 12 end]

            if { $respType != $type  || $respClass != $class } {
                set answers [string range $answers $respLen end]
                continue
            }

            set aaddr [list]
            if { $reqTypeStr == "PTR" } {
                binary scan $answers c elen
                set elen [expr $elen & 0xFF]
                while { $elen > 0 } {
                    set answers [string range $answers 1 end]
                    binary scan $answers a$elen elem
                    lappend aaddr $elem
                    set answers [string range $answers $elen end]
                    binary scan $answers c elen
                    set elen [expr $elen & 0xFF]
                }
            } else {
                for { set j 0 } { $j < $respLen } { incr j } {
                    binary scan $answers c octet
                    set octet [expr $octet & 0xFF]
                    lappend aaddr $octet
                    set answers [string range $answers 1 end]
                }
            }

	    if { $reqTypeStr == "AAAA" } {
		lappend aaddrs [arrToV6 $aaddr]
	    } else {
                lappend aaddrs [join $aaddr "."]
	    }
        }

        return -code ok $aaddrs
    }
}

proc get_server { } {
    set cmd "show run | inc ^ip name-server"
    set output [split [exec $cmd] "\n"]
    foreach line $output {
        if { [regexp {([\d\.]+)} $line -> server] } {
            return $server
        }
    }

    set cmd "show host | inc ^Name servers are"
    set output [split [exec $cmd] "\n"]
    foreach line $output {
        if { [regexp {([\d\.]+)} $line -> server] } {
            return $server
        }
    }

    return ""
}

proc host { args } {
    set args [join $args " "]

    set proto "tcp"
    set ts "A"
    set i 0
    for { } { $i < [llength $args] } { incr i } {
	switch -glob -- [lindex $args $i] {
	    -u { set proto "udp" }
	    -6 { set ts "AAAA" }
	    -* { puts "Unknown option, [lindex $args $i]"
		 return -code error
	    }
	    * { break }
	}
    }

    if { $i < [llength $args] } {
	set host [lindex $args $i]
	incr i
	if { $i < [llength $args] } {
	    set dns [lindex $args $i]
	} else {
	    set dns [get_server]
	}
    }

    if { $dns == "" } {
        puts "Failed to find a valid DNS server."
        return -code error
    }

    if { $host == "" } {
        puts {usage: host [-u] [-6] address [dns_server]}
        return -code error
    }

    if { [catch {set addrs [MarcusHost::lookup $host $dns $proto $ts]} result] } {
        puts "ERROR: $result"
        return -code error
    }

    set host [lindex $addrs 0]
    set type [lindex $addrs 1]

    for { set i 2 } { $i < [llength $addrs] } { incr i } {
        if { $type == "A" || $type == "AAAA" } {
            puts "$host has address [lindex $addrs $i]"
        } elseif { $type == "PTR" } {
            puts "$host domain name pointer [lindex $addrs $i]"
        }
    }
}

if { $argv0 == "tclsh" } {
    puts "Loaded [info script] successfully."
} else {
    set rc [catch { host $argv } result]
    return -code $rc $result
}