#!/usr/bin/perl
#################################################################Modules
use strict;
use warnings;
use threads;
use threads::shared;
use Net::DNS;
use Net::Whois::IANA;
use Tk::ProgressBar;
use Tk::Dialog;
use Tk::ROText;
use Tk::Menu;
use Tk;
########################################################Optional Modules
if ($^O eq 'MSWin32') {
  eval    { require Win32::Console; Win32::Console::Free()   };
  if ($@) { warn "Win32::Console is not installed.\n[$@]\n"; }
}
############################################################Declarations
our $VERSION = 4.00;
my ($mw, %threads, %shash,);
####################################################################Main
init();
gui();
Tk::MainLoop();
####################################################################Exit
foreach my $k (sort keys %threads) {
  warn "Destroying Thread [$k]\n";
  $shash{$k}{fin} = 1;
  sleep (1);
}
warn "IPLU is exiting \t(" . (localtime) . ")\n";
close STDERR;
exit;
#############################################################Subroutines
sub init #--------------------------------------------------------------
{
  #start logging
  open (STDERR, '>', 'error.log') || warn "Cant create logfile!\n[$!]\n";
  warn "IPLU is starting\t(" . (localtime) . ")\n";
  
  #start threads
  warn "Initializing shared memory\n";
  foreach my $l qw (NS MX AX WI progress fin returnCSV optionCSV) {
    share ($shash{1}{$l});
    $shash{1}{$l} = 0;
  }
  warn "Launching thread\n";
  $threads{1} = threads->new(\&worker, 1);
  warn "Thread 1 is active\n";
  
  return (1);
}
sub gui #---------------------------------------------------------------
{
  #Main Window
  $mw = MainWindow->new(
    -title    => 'IP Lookup',
    -relief   => 'groove',
    -colormap => 'new',
    -bd       => 2,
  );
  $mw->setPalette(
    background       => '#a1a1a1',
    activebackground => '#a1a1a1',
    activeforeground => '#000fff',
  );
  my ($f1, $f2, $f3,);
  $f1 = $mw->Frame()->grid(
    -in     => $mw,    -column => 1,
    -sticky => 'news', -row    => 1,
  );
  $mw->gridRowconfigure   (1, -minsize => 8, -weight => 1,);
  $mw->gridColumnconfigure(1, -minsize => 8, -weight => 1,);
  $f1->gridRowconfigure   (1, -minsize => 8,);
  $f1->gridRowconfigure   (2, -minsize => 8,);
  $f1->gridRowconfigure   (3, -minsize => 8, -weight => 1,);
  $f1->gridRowconfigure   (4, -minsize => 8,);
  $f1->gridColumnconfigure(1, -minsize => 8,);
  $f1->gridColumnconfigure(2, -minsize => 8, -weight => 1,);
  $f1->gridColumnconfigure(3, -minsize => 8,);
  $f2 = $f1->Frame()->grid(
    -in     => $f1,    -column => 2,
    -sticky => 'news', -row    => 2,
  );
  $f2->gridRowconfigure   (2,  -minsize => 8,);
  $f2->gridRowconfigure   (8,  -minsize => 8, -weight => 1,);
  $f2->gridColumnconfigure(2,  -minsize => 8, -weight => 1,);
  $f2->gridColumnconfigure(4,  -minsize => 8,);
  $f3 = $f1->Frame(-relief => 'solid', -bd => .5,)->grid(
    -in     => $f2,    -column => 1, -columnspan => 3,
    -sticky => 'news', -row    => 3, -rowspan    => 6,
  );
  $f3->gridRowconfigure   (1,  -minsize => 8, -weight => 1,);
  $f3->gridColumnconfigure(1,  -minsize => 8, -weight => 1,);
  
  
  my ($l1,);
  $l1 = $f2->Label(-text => 'Host:  ')->grid(
    -in     => $f2,    -column => 1,
    -sticky => 'news', -row    => 1,
  );

  my ($e1,);
  $e1 = $f2->Entry(
    -textvariable => \our $host,
    -width        => 80,
    -bd           => .5,
    -relief       => 'solid',
    -bg           => 'white',
    -fg           => 'black',
  )->grid(
    -in     => $f2,    -column => 2,
    -sticky => 'news', -row    => 1,
  );

  my ($b1, $b2, $b3, $b4, $b5, $b6, $b7,);
  $b1 = $f2->Button(
    -activeforeground => '#fff000',
    -activebackground => '#a1a1a1',
    -background       => '#a1a1a1',
    -bitmap           => '@' . Tk->findINC('cbxarrow.xbm'),
    -command          => sub { history('SHOW'); }
  )->grid(
    -in     => $f2,    -column => 3,
    -sticky => 'news', -row    => 1,
  );
  $b2 = $f2->Button(
    -text             => 'Lookup',
    -activeforeground => '#000fff',
    -activebackground => '#a1a1a1',
    -background       => '#a1a1a1',
    -foreground       => '#000000',
    -width            => 8,
    -command          => \&lookup
  )->grid(
    -in     => $f2,    -column => 5,
    -sticky => 'news', -row    => 1,
  );
  $b3 = $f2->Button(
    -text             => 'View Log',
    -activeforeground => '#000fff',
    -activebackground => '#a1a1a1',
    -background       => '#a1a1a1',
    -foreground       => '#000000',
    -width            => 8,
    -command          => sub {logfile('VIEW');},
  )->grid(
    -in     => $f2,    -column => 5,
    -sticky => 'news', -row    => 3,
  );
  $b3 = $f2->Button(
    -text             => 'Save Log',
    -activeforeground => '#000fff',
    -activebackground => '#a1a1a1',
    -background       => '#a1a1a1',
    -foreground       => '#000000',
    -width            => 8,
    -command          => sub {logfile('SAVE');},
  )->grid(
    -in     => $f2,    -column => 5,
    -sticky => 'news', -row    => 4,
  );
  $b4 = $f2->Button(
    -text             => 'Clear Log',
    -activeforeground => '#000fff',
    -activebackground => '#a1a1a1',
    -background       => '#a1a1a1',
    -foreground       => '#000000',
    -width            => 8,
    -command          => sub {logfile('CLEAR');},
  )->grid(
    -in     => $f2,    -column => 5,
    -sticky => 'news', -row    => 5,
  );
  $b5 = $f2->Button(
    -text             => 'Help',
    -activeforeground => '#000fff',
    -activebackground => '#a1a1a1',
    -background       => '#a1a1a1',
    -foreground       => '#000000',
    -width            => 8,
    -command          => \&help
  )->grid(
    -in     => $f2,    -column => 5,
    -sticky => 'news', -row    => 6,
  );
  $b3 = $f2->Button(
    -text             => 'Exit',
    -activeforeground => '#000fff',
    -activebackground => '#a1a1a1',
    -background       => '#a1a1a1',
    -foreground       => '#000000',
    -width            => 8,
    -command          => \&quit
  )->grid(
    -in     => $f2,    -column => 5,
    -sticky => 'news', -row    => 7,
  );

  our ($t1,);
  $t1 = $f3->Scrolled(
    'ROText',
    -scrollbars       => 'e',
    -bg               => '#ffffff',
    -fg               => '#000000',
    -insertbackground => '#ffffff',
    -relief           => 'flat',
    -wrap             => 'word',
    -bd               => 2,
    -height           => 20,
    -width            => 80,
  )->grid(
    -in     => $f3,    -column => 1,
    -sticky => 'news', -row    => 1,
  );
  $t1->menu(undef);
  
  #History window
  our ($tl2, $f1_hst, $lb_hst, $loadhistory,);
  $tl2 = $mw ->Toplevel(-relief => 'flat',);
  $tl2->overrideredirect(1);
  $tl2->resizable(0, 0);
  $tl2->transient($mw);
  $tl2->withdraw;
  
  $f1_hst = $tl2->Frame(
    -relief    => 'groove',
    -bd        => 2,
    -takefocus => 1,
  )->grid(
    -in     => $tl2,   -column => 1,
    -sticky => 'news', -row    => 1,
  );
  $tl2->gridRowconfigure      (1, -minsize => 8, -weight => 1,);
  $tl2->gridColumnconfigure   (1, -minsize => 8, -weight => 1,);
  $f1_hst->gridRowconfigure   (1, -minsize => 8, -weight => 1,);
  $f1_hst->gridColumnconfigure(1, -minsize => 8, -weight => 1,);
  
  $lb_hst = $tl2->Scrolled(
    'Listbox',
    -width            => 80,
    -height           => 8,
    -scrollbars       => 'ose',
    -selectmode       => 'single',
    -bg               => '#000000',
    -fg               => '#ffffff',
    -selectforeground => '#000000',
    -selectbackground => '#fff000',
  )->grid(
    -in     => $f1_hst, -column => 1,
    -sticky => 'news',  -row    => 1,
  );
  
  #Bindings
  $mw->bind    ('<F1>'            => \&help);
  $e1->bind    ('<Return>'        => \&lookup);
  $lb_hst->bind('<ButtonPress-1>' => sub {history('SELECT');});
  $f1_hst->bind('<FocusOut>'      => sub {
    $lb_hst->selectionClear(0, "end");
    $tl2   ->withdraw;
  });
  
  #Defaults
  $e1->focus;
  
  #Callbacks
  sub lookup #----------------------------------------------------------
  {
    my (@RHostList,);
    
    #Gather target from entry widget
    if (!defined $host) {
      help();
      return (0);
    }
    else {
      $t1->delete('1.0', 'end');
      $mw->Busy(-recurse => 1,);
      @RHostList = split(' ', $host);
    }
    
    foreach my $target (@RHostList) {
      my ($ip, $name, $aliases, $addrtype, $length, @addrs,);
      my ($NS_out, $MX_out, $AXfr_out, $WhoIs_out,);
      
      #Record lookup attempt in history
      history('SAVE', $target);
      
      #Check to see if an IP address or Hostname was supplied
      if ($target =~ m#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#) {
        my ($a, $b, $c, $d,);
        
        #Determine target
        ($a,$b,$c,$d) = split /\./, "$target";
        if ($a > 255 || $a < 1 || $b > 255 || $c > 255 || $d > 255) {
          warn "IP: [$target] is not a valid IP address\n";
          return (0);
        }
        else {
          my ($packed_ip) = pack 'C4',$a,$b,$c,$d;
          if (($name, $aliases, $addrtype, $length, @addrs,)
               =gethostbyaddr($packed_ip, 1)) {
            $target = $name;
          }
          else {
            warn "IP: [$target] is not resolvable through DNS\n";
            return (0);
          }
        }
      }
      else {
        if (($name, $aliases, $addrtype, $length, @addrs,)
            =gethostbyname($target)) {
          $target = $name;
        }
        else {
          warn "Host: [$target] is not a valid DNS hostname\n";
          return (0);
        }
      }
      
      #Target is now the FQDN, determine the IP
      {
        my ($a,$b,$c,$d) = unpack('C4', $addrs[0]);
        $ip = join('.', $a,$b,$c,$d);
      }
      
      #Perform lookups
      {
        $NS_out    = NS_lookup($target);
        $MX_out    = MX_lookup($target);
        $AXfr_out  = AXfr_lookup($target);
        $WhoIs_out = WhoIs_lookup($ip);
      }
      
      #Display Output
      {
        my $address_counter = 0;
        my $address_out     = '';
        my @alias_out       = (split ' ', $aliases);
        $aliases = (join ', ', @alias_out);
        foreach my $address_dry (@addrs) {
          my $address_wet = join(".",unpack("C4",$address_dry));
          if ($address_counter == 0) {
            $address_out = "$address_out \t$address_wet\n";
          }
          else{
            $address_out = "$address_out \t\t$address_wet\n";
          }
          $address_counter = 1;
        }
        $t1->insert('end', 'Target:'."\t\t$target ($ip)\n\n");
        $t1->insert('end', 'Name:'."\t\t$name\n\n");
        $t1->insert('end', 'Aliases:'."\t$aliases\n\n");
        $t1->insert('end', 'Addresses:'."$address_out\n");
        $t1->insert('end', 'NS Info:'."\t$NS_out\n\n");
        $t1->insert('end', 'MX Info:'."$MX_out\n");
        $t1->insert('end', 'AXfr Info:'."$AXfr_out\n");
        $t1->insert('end', 'WhoIs Info:'."$WhoIs_out\n\n\n");
        $t1->insert('end', '='x80 . "\n\n");
      }
    }
    #Append to logfile.
    {
      my ($logtxt,);
      open (IPLULOG, ">>iplu_log.txt")
        || warn "Can't create or append to logfile\n[$!]";
      $logtxt = $t1->get('1.0', 'end');
      print IPLULOG $logtxt;
      close IPLULOG
        || warn "Can't close logfile\n[$!]";
    }
    $mw->Unbusy;
    $t1->focus;
    $mw->update;
    return (1);
  }
  sub NS_lookup #-------------------------------------------------------
  {
    my $target = $_[0] || return (0);
    my (@dnssrv, $NS_out, $res, $query,);
    
    $res   = Net::DNS::Resolver->new;
    $query = $res->query($target, 'NS');
    
    #perform name server lookup
    if ($query) {
      foreach my $rr (grep {$_->type eq 'NS'} $query->answer) {
        push (@dnssrv, $rr->nsdname);
      }
      $NS_out = join("\n\t\t", @dnssrv);
    }
    else{
      $NS_out = 'query failed.';
    }
    
    #return data
    return ($NS_out);
  }
  sub MX_lookup #-------------------------------------------------------
  {
    my $target = $_[0] || return (0);
    my (@mx, $res, $MX_counter, $MX_out,);
    my (@mx_info, $rr, $rr_error, $rr_pref, $rr_exch,);
    
    $res = Net::DNS::Resolver->new;
    @mx = mx($res, $target);
    $MX_counter = 0;
    
    #perform mail exchanger lookup
    if (@mx) {
      foreach my $rr (@mx) {
        $rr_pref = $rr->preference;
        $rr_exch = $rr->exchange,;
        push @mx_info, ("\t","\t", 'Preference:', $rr_pref,
                        ' ', "Exchange: ", $rr_exch, "\n");
        if ($MX_counter == 0) {
          shift @mx_info;
        }
        $MX_counter = 1;
      }
    }
    else{
      $rr_error = "\tCan't find MX records for $target\n";
    }
    
    #return data
    if ($rr_error) { $MX_out = $rr_error;           }
    else           { $MX_out = (join '', @mx_info); }
    return ($MX_out);
  }
  sub AXfr_lookup #-----------------------------------------------------
  {
    my $target = $_[0] || return (0);
    my ($treturn, $AXfr_out,);
    
    #invoke thread
    $shash{1}{AX} = 1;
    $shash{1}{optionCSV} = $target;
    $mw->after(500);
    while ($shash{1}{AX} == 1) {
      #wait for the thread, update gui
      $mw->update;
      $mw->after(50);
    }
    #check return data
    if ($shash{1}{returnCSV}) {
      $AXfr_out = "\t" . $shash{1}{returnCSV} . "\n";
    }
    else {
      $AXfr_out = "\tZone transfer not available\n";
    }
    undef $shash{1}{returnCSV};
    
    return ($AXfr_out);
  }
  sub WhoIs_lookup #----------------------------------------------------
  {
    my $ip = $_[0] || return (0);
    my ($iana, $WhoIs_out,);
    
    #perform whois lookup
    $iana = new Net::Whois::IANA;
    $iana->whois_query(-ip => $ip,);
    $WhoIs_out = "\n" . $iana->fullinfo();
    
    #return data
    return ($WhoIs_out);
  }
  sub logfile #---------------------------------------------------------
  {
    my $opt1 = uc ($_[0]) || return (0);
    
    if ($opt1 eq 'VIEW') {
      $t1->delete('1.0', 'end');
      if (-e 'iplu_log.txt') {
        open (IPLULOG, '<', 'iplu_log.txt')
          || warn "Can't open log.\n[$!]\n";
        while (my $line = (<IPLULOG>)) {
          $t1->insert('end', $line);
          $mw->update;
        }
        close IPLULOG;
      }
      else {
        return (0);
      }
      $t1->focus;
      $mw->update;
      return (1);
    }
    elsif ($opt1 eq 'SAVE') {
      my $types = [
        ['Text Files',       ['.txt', '.text']],
        ['All Files',        '*',             ],
      ];
      my $sfile = $mw->getSaveFile(
        -title            => 'Save Log',
        -defaultextension => 'txt',
        -filetypes        => $types,
      );
      if (defined ($sfile)) {
        open (IPLU_LOG,  '<', 'iplu_log.txt')
          || warn 'Cant read iplu_log.txt' . "\n[$!]\n";
        open (IPLU_SAVE, '>', $sfile)
          || warn 'Cant create ' . $sfile . "\n[$!]\n";
        my @iplu_copy = (<IPLU_LOG>);
        while (my $line = (<IPLU_LOG>)) {
          print IPLU_SAVE $line;
        }
        close IPLU_LOG
          || warn 'Cant close iplu_log.txt' . "\n[$!]\n";
        close IPLU_SAVE
          || warn 'Cant close' . $sfile . "\n[$!]\n";
      }
      else {
        return (0);
      }
      return (1);
    }
    elsif ($opt1 eq 'CLEAR') {
      my $confirm = $mw->Dialog(
        -text    => 'Are you sure you want to clear the log?',
        -title   => 'Confirm Clear Log',
        -buttons => [ 'Yes', 'No'],
        -bitmap  => 'question',
        -default_button => 'Yes',)->Show(
      );
      if ($confirm eq 'Yes') {
        if (-e 'iplu_log.txt') {
          unlink 'iplu_log.txt'
            || warn "Cant delete iplu_log.txt\n[$!]\n";
        }
        open (IPLULOG, ">>iplu_log.txt")
          || warn "Cant create or append to log\n[$!]\n";
        close IPLULOG
          || warn "Cant close log\n[$!]\n";
      }
      return (1);
    }
    return (0);
  }
  sub quit #------------------------------------------------------------
  {
    $mw->Busy(-recurse => 1);
    $mw->destroy;
    return (1);
  }
  sub help #------------------------------------------------------------
  {
      $t1->delete('1.0', 'end');
      $t1->insert('end', 'IPLU - Lists IP addresses, aliases, MX, '  .
                         "and AXfr info for the targets.\n\n"        .
                         "Version:\t4.00\nAuthor:\t\t"               .
                         "Jason McManus\nContact:\t".'QoS@cpan.org'  .
                         "\n\n");
      $t1->insert('end', "Usage:\t\t<target1> <target2> <etc..>\n\n" .
                         "Examples:\t"                               .
                         "17.254.0.91\n\t\t"                         .
                         "www.perlmonks.org\n\t\t"                   .
                         "www.cpan.org www.perl.org 10.5.2.1\n\n");
      $mw->update;
      return (1);
  }
  sub history #---------------------------------------------------------
  {
    my $cmd = uc ($_[0]) || return (0);
    my $in1 = $_[1]      || 0;
    
    if ($cmd eq 'SHOW') {
      my ($x, $y) = $mw->pointerxy;
      $f1_hst->focus;
      $lb_hst->see('end');
      $x -= 505;
      $y += 5;
      $tl2->geometry('+'."$x".'+'."$y");
      $tl2->deiconify();
      $tl2->raise();
      
      #read in the history file the first time history is requested
      if (! $loadhistory) {
        if (-e 'iplu.hst') {
          open(HIST_IN, '< iplu.hst')
            || warn "Unable to open iplu.hst\n[$!]"
            && return (0);
          my @hist = (<HIST_IN>);
          close HIST_IN
            || warn "Unable to close iplu.hst\n[$!]";
          while ($#hist >= 9) {
            shift @hist;
          }
          open(HIST_OUT, '> iplu.hst')
            || warn "Unable to open iplu.hst\n[$!]"
            && return (0);
          foreach my $i (@hist) {
            chomp $i;
            print HIST_OUT $i . "\n";
            $lb_hst->insert('end', $i);
          }
          close HIST_OUT
            || warn "Unable to close iplu.hst\n[$!]";
        }
        else {
          open(HIST_OUT, '> iplu.hst')
            || warn "Unable to open iplu.hst\n[$!]"
            && return (0);
          close HIST_OUT
            || warn "Unable to close iplu.hst\n[$!]";
        }
        $loadhistory = 1;
      }
      return (1);
    }
    elsif ($cmd eq 'SELECT') {
      $mw->update;
      $mw->after(400);
      my @sels = $lb_hst->curselection();
      if (defined $sels[0]) {
        $host = $lb_hst->get($sels[0]);
        $mw->focus;
        $mw->update;
      }
      else {
        return (0);
      }
      return (1);
    }
    elsif ($cmd eq 'SAVE') {
      $lb_hst->insert('end', $in1);
      open (HIST_OUT, '>> iplu.hst')
        || warn "Unable to create iplu.hst\n[$!]\n"
        && return (0);
      print HIST_OUT $in1 . "\n";
      close HIST_OUT
        || warn "Unable to close iplu.hst\n[$!]\n";
      return (1);
    }
    else {
      warn "Error found in history, cmd is: [$cmd]\n[$!]\n";
      return (0);
    }
  }
}
#################################################################Workers
sub worker #------------------------------------------------------------
{
  #called from main
  #shash = NS MX AX WI progress die returnCSV optionCSV
  my $TID = $_[0] || 0;
  $| = 1;
  
  while(1) {
    if ($shash{$TID}{fin} == 1) {                                   #Fin
      last;
    }
    elsif ($shash{$TID}{NS} == 1) {                         #Name server
      $shash{$TID}{optionCSV} = 0;
      $shash{$TID}{NS}        = 0;
    }
    elsif ($shash{$TID}{MX} == 1) {                      #Mail exchanger
      $shash{$TID}{optionCSV} = 0;
      $shash{$TID}{MX}        = 0;
    }
    elsif ($shash{$TID}{AX} == 1) {                                #AXfr
      my ($dns, $target, @zone,);
      undef $shash{$TID}{returnCSV};
      
      #perform zone transfer
      $dns = Net::DNS::Resolver->new;
      $dns->tcp_timeout(10);
      $target = $shash{1}{optionCSV} || '127.0.0.1';
      @zone   = $dns->axfr($target);
      
      if (@zone) {
        #print zone transfer to tempfile
        my ($save_fh,);
        open (TEMPFILE_1, '>~iplutmp1.tmp')
          || warn "Unable to create ~tplutmp1.tmp\n[$!]\n"
          && return (0);
        $save_fh = select(TEMPFILE_1);
        $| = 1;
        print "\n\n";
        foreach my $rr (@zone) {
          $rr->print;
        }
        select ($save_fh);
        $| = 0;
        close TEMPFILE_1
          || warn "Can't close ~iplutmp1.tmp\n[$!]\n";
        
        #read tempfile and prepare the return data
        open (TEMPFILE_1, '~iplutmp1.tmp')
          || warn "Can't open ~iplutmp1.tmp\n[$!]";
        foreach my $line ((<TEMPFILE_1>)) {
          $shash{$TID}{returnCSV} .= $line;
        }
        close TEMPFILE_1
          || warn "Can't close ~iplutmp1.tmp\n[$!]\n";
        unlink '~iplutmp1.tmp'
          || warn "Can't delete ~iplutmp1.tmp\n[$!]\n";
      }
      $shash{$TID}{optionCSV} = 0;
      $shash{$TID}{AX}        = 0;
    }
    elsif ($shash{$TID}{WI} == 1) {                               #WhoIs
      $shash{$TID}{optionCSV} = 0;
      $shash{$TID}{WI}        = 0;
    }
    sleep (1);
  }
  warn "Thread [$TID] is closing.\n";
  return (0);
}
###########################################################DOCUMENTATION
=head1 NAME

IPLU - IP Lookup

=head1 DESCRIPTION

A IP Networking tool

=head1 README

IPLU - Displays WhoIs, MX, NS, Axfr, and other information

=head1 PREREQUISITES

Tk
Net::DNS
Net::Whois::IANA

=head1 COREQUISITES

=head1 Copyright

IPLU - Discovers Multi-Homed addresses and other information.
Copyright (C) 2003 - 2008 Jason David McManus

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 2 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, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

=head1 History

v1_0

      Initial release. (Dec. 2003)

v2_0

      It is now possible to use an ip address in the target field.
      Minor GUI enhancements

v2_1

      Added mousewheel support, other GUI enhancements
      Fixed a bug when looking up by ip address

v2_2

      Improved GUI
      Improved callback structure
      Improved memory management
      Various bugfixes when looking up by ip address

v2_3

      Cleaned up code somewhat
      Added a history function

v2_4

      Removed some widgets, cleaned up code and gui
      Removed console window, added an error log
      Fixed up right-click menu

v3_0

      Improved GUI

v4_0

      Faster
      Complete re-write
      Now performs all lookups all the time

=head1 ToDo

Move more functions into the worker thread

=pod OSNAMES

=pod SCRIPT CATEGORIES

Networking

=cut
