#! /usr/bin/wish -f
#
# pingi.tcl: nmap GUI for ping scanning a bunch of hosts
# Copyright (C) 2009 Thomas Henlich
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

proc scanHosts {parm} {
	global nmapParms progname errorInfo
  set status {}
	if {[catch {set dot [open "|nmap --no-stylesheet $parm [join $nmapParms] -oX - 2>NUL" r]}]} {
		tk_messageBox -title $progname \
			-message "Error executing command\n$errorInfo"
		exit 1
	}
	set scanResult [read $dot]
	
  if {[catch {close $dot}]} {
    tk_messageBox -title $progname \
      -message "Error on pipe\n$errorInfo"
		exit 1
	}
	
  return [dom parse -simple $scanResult]
}

proc readList {filename} {
  global sHostList sHostComment
  set fp [open $filename r]
  
  while 1 {
    gets $fp line
    if {[regexp "^\[\t \]*#" $line] == 0} {
      if {[regexp "(\[^\t \]+)(\[\t \]+(.*))?" $line match hostname rest comment]} {
        lappend sHostList $hostname
        if {$comment != ""} {set sHostComment($hostname) $comment}
      }
    } 
    if [eof $fp] {
      break
    }
  }
  close $fp
}

proc getLocalTargets {} {
	global progname errorInfo
	if {[catch {set nmap [open "|nmap --iflist - 2>NUL" r]}]} {
		tk_messageBox -title $progname \
			-message "Error executing command\n$errorInfo"
		exit 1
	}

  set interfaces 0
  while 1 {
    gets $nmap line
    if {$interfaces} {
      if {[regexp "^(\[^\t \]+)\[\t \]+(\[^\t \]+)\[\t \]+(\[^/]+)/(\[^\t \]+)\[\t \]+ethernet\[\t \]+up" $line match dev short ip mask type]} {
        if {$mask < 24} {
          set mask 24
        }
        lappend localTargets "$ip/$mask"
      }
      if {$line == ""} {
        break
      }
    } else {
      if {[regexp "INTERFACES" $line]} {
        set interfaces 1
      }
    }

    if [eof $nmap] {
      break
    }
  }
  
  if {[catch {close $nmap}]} {
    tk_messageBox -title $progname \
      -message "Error on pipe\n$errorInfo"
		exit 1
	}

  return $localTargets
}

proc statcol {status} {
  switch $status {
    up {return "green"}
    down {return "red"}
    default {return "lightgrey"}
  }
}

proc dimcol {color} {
  switch $color {
    green {return "darkgreen"}
    red {return "darkred"}
    default {return $color}
  }
}

proc initStatus {} {
  global count_buttons
  set count_buttons 0
  set hostlist [scanHosts "-sL"]
  updateButtons $hostlist
  button .upd -text "Update" -command {updateStatus}
  #label .last -text "Last Update"
  pack .upd -pady 5
}

proc dim {} {
  global count_buttons
  for {set i 0} {$i < $count_buttons} {incr i} {
      .f.b$i configure -background [dimcol [.f.b$i cget -background]]
    }
}

proc updateStatus {} {
  global count_buttons last_update
  catch {after cancel $last_update}
  set hostResult [scanHosts "-sP"]
  updateButtons $hostResult
  set last_update [after 10000 dim]
}  

proc updateButtons {hostResult} { 
  global count_buttons sHostList sHostComment progname
set hostlist [$hostResult selectNodes /nmaprun/host]
set initButtons 0

set nmapversion [[$hostResult selectNodes /nmaprun] getAttribute version]
.vers configure -text "Powered by Nmap $nmapversion & TCL [info patchlevel]"

if {[llength $sHostList] > 0 && [llength $sHostList] != [llength $hostlist]} {
  if {[set miss [expr [llength $sHostList] - [llength $hostlist]]] > 0} {
    if {$miss == 1} {set msg "$miss error resolving hostlist"} {set msg "$miss errors resolving hostlist"}
  } else {
    set msg "Error in hostlist"
  }
  tk_messageBox -title $progname \
    -message $msg
	exit 1
}

if {[llength $hostlist] != $count_buttons} {
  for {set i 0} {$i < $count_buttons} {incr i} {
    destroy .f.b$i
  }
  set initButtons 1
  set count_buttons [llength $hostlist]
}
set nRows [expr int(ceil(2*sqrt($count_buttons)))]
for {set i 0} {$i < $count_buttons} {incr i} {
  set host [lindex $hostlist $i]
  set addr [[$host selectNodes address\[@addrtype='ipv4'\]] getAttribute addr]
  set mac [$host selectNodes address\[@addrtype='mac'\]]
  set vendor ""
  if {$mac != {}} {set vendor [$mac getAttribute vendor ""]}
  set name [lindex [$host selectNodes hostnames/hostname] 0]
  set hostname ""
  if {$name != {}} {set hostname [$name getAttribute name]}
  set status [[$host selectNodes status] getAttribute state]
  set callname [lindex $sHostList $i]
  if {[info exists sHostComment($callname)]} {
    set label $sHostComment($callname)
    if {$callname != $addr} {
      set tip "$callname ($addr)"
    } elseif {$hostname != ""} {
      set tip "$hostname ($addr)"
    } else {
      set tip "$addr"
    }
  } elseif {$callname != ""} {
    set label $callname
    if {$hostname != ""} {
      set tip "$hostname ($addr)"
    } else {
      set tip "$addr"
    }
  } else {
    if {$hostname != ""} {
      set label $hostname
      set tip "$addr"
    } else {
      set label $addr
      set tip ""
    }
  }
  if {$initButtons == 1} {
    label .f.b$i -text "$label" -borderwidth 2 -relief groove -background [statcol $status]
    grid .f.b$i -sticky "we" -padx 3 -pady 3 -ipadx 2 -column [expr $i / $nRows] -row [expr $i % $nRows]
  } else {
    .f.b$i configure -text "$label" -background [statcol $status]
  }
    if {$vendor != ""} {
      if {$tip != ""} {
        append tip "\n"
      }
      append tip $vendor
    }
    tooltip::tooltip .f.b$i $tip
  }
  $hostResult delete
}

encoding system utf-8
set progname "pingi"
set pingiVersion "2009.6.10"

if {$argc <= 0} {
    wm withdraw .
		tk_messageBox -title $progname \
			-message "Usage: $progname inputfilename\n\
\twhere inputfile is a list of targets, one per line.\n\
\tTargets may be followed by a label, separated by whitespace.\n\
\tComment lines begin with #."
    exit 1
}

package require tooltip
package require tdom
wm title . "$progname v$pingiVersion"
label .vers
pack .vers -pady 5
frame .f
pack .f -padx 10 -pady 10

	if {$tcl_platform(platform) == "windows" && $env(ProgramFiles) != ""} {
		set nmapPath [file join $env(ProgramFiles) "Nmap"]
		if {[file isdirectory $nmapPath]} {
			append env(PATH) ";" $nmapPath
		}
	}


set count_buttons 0

set sHostList {}

if {$argc > 0} {
  readList [lindex $argv 0]
	set nmapParms [list "-n -T2"]
  lappend nmapParms $sHostList
}

initStatus
#update
