#!/usr/bin/perl
# ########################################################################
# keyextract.pl is a feature extractor for keystroke monitoring
# Copyright (C) 2008 Deian Stefan (stefan at cooper dot edu)
#
# 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., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
# ########################################################################

#$|=1;

# TODO: 
# 1. fix the $winbuf_flag hack
# 2. for words such as 'stefan' the info gets printed two times, this
# can be quite annoying and a problem
use strict;
use warnings;
use xWord;
use xKey;
use constant DEBUG => 0; 
select((select(STDOUT), $| = 1)[0]);


my $keylog='../xtrapdei-0.2/xtrapdei'; #keylogger w/ output like xtrapdei
# ########################################################################

use constant BUF_SIZ => 15;             #maximum buffer size
my ($MIN_WIN_SIZ,$MAX_WIN_SIZ)=(BUF_SIZ,0);             #window sizes

require "xtrapdei_special.pl";
our %specialsyms;
our %specialsyms_print;


my $db_fname='./keydd.db'; # database of the words you want to keep track
my %db_words = ();

my @winbuf=();
my $winbuf_siz=undef; #size of circular buffer
my $winbuf_beg=0;
my $winbuf_end=-1;
my $winbuf_flag=-2; #litle hack that should be fixed
my $winbuf_fill=0;  #number of characters in "pipeline"


# reason for using this buffer:
# because the flow of characters is assumed to be continuous and for a 
# very long period of time using push instead of manually managing the
# buffer will use more memory than really necessary.

sub print_all {
   if(DEBUG) {
      print "\n ".("-"x78)."\n";
      printf("%20s\t\tPRESS\t\tRELEASE\n","key");
      print "\n ".("-"x78)."\n";
      for(my $i=0;$i<$winbuf_siz;$i++) {
         if(defined $winbuf[$i]) {
            printf "[%02d]",$i;
            $winbuf[$i]->print;
         }
      }
      print "\n ".("-"x78)."\n";
   }
}

sub get_str {
   my $s="";
   my $len=0;

   for(my ($i,$j)=($winbuf_beg,0);$j<$winbuf_siz;$j++,
         $i=($i+1) % $winbuf_siz) {
      if(defined $winbuf[$i]) {
         if(exists $specialsyms_print{$winbuf[$i]->key}) {
            $s.=$specialsyms_print{$winbuf[$i]->key};
         } else {
            $s.=$winbuf[$i]->key;
         }
         if(!(defined $winbuf[$i]->r)) { last; }
         $len++;
      } else { last; }
   }
   return ($s,$len);
}

sub creat_word {
   my $str=shift;
   my $word=xWord->new;
   my $s="";
   my $len=0;

   for(my ($i,$j)=($winbuf_beg,0);$j<$winbuf_siz;$j++,
         $i=($i+1) % $winbuf_siz) {
      if(defined $winbuf[$i]) {
         my $ch;
         if(exists $specialsyms_print{$winbuf[$i]->key}) {
            $ch=$specialsyms_print{$winbuf[$i]->key};
         } else {
            $ch=$winbuf[$i]->key;
         }
         $s.=$ch;
         $word->add_key($winbuf[$i],$ch);
         $len++;
         if($len == length($str)) { last; }
      } else { last; }
   }
   if($str ne $s) { 
      print STDERR "xWord of string \'$str\' could not be added!\n";
      return undef;
   }
   return $word;
}


sub get_inv_case {
   my $ch=shift;

   if($ch =~ m/^[a-z]$/) { # must be only 1 character
      return uc($ch);
   } elsif($ch =~ m/^[A-Z]$/) { #must be only one character
      return lc($ch);
   } elsif(exists $specialsyms{$ch}) {
      return $specialsyms{$ch};
   } else {
      return $ch;
   }
}

sub track_key {
   my ($win,$ch,$t,$event) = @_;

# print "DEBUG: $win\t$t\t$ch\t$event\n";

   if($event=~/KeyPress/) {
      my $k=xKey->new;
      $k->ch($ch);
      $k->key_act($event,$t);
      $winbuf_end=($winbuf_end+1) % $winbuf_siz;
      $winbuf[$winbuf_end]=$k;
      $winbuf_flag++;
      if($winbuf_flag>=0
            && $winbuf_end==$winbuf_beg) { # the end and beg chase
         $winbuf_beg=($winbuf_beg+1) % $winbuf_siz;
      }


      print "beg=$winbuf_beg\tend=$winbuf_end\n" if DEBUG;
   } elsif($event=~/KeyRelease/) {
# want to find the last addded letter to modify the release time
      my $lasti=undef;

      $winbuf_fill++;

      for(my ($i,$j)=($winbuf_end,0); $j<$winbuf_siz; $j++,$i--) {
# can check for $i<$winbuf_beg, but not really necessary

         if($i<0) { $i=$winbuf_siz-1; }

         if(defined $winbuf[$i]) {
            if(!(defined $winbuf[$i]->r)) {
               if(($winbuf[$i]->key eq $ch) or
                     ($winbuf[$i]->key eq get_inv_case($ch))) {
# The second part is true when you press say Shift+O and first release 
# the Shift key and then 'O' (so the release would look like 'o')
                  $lasti=$i;
                  last;
               }
            }
         }
      }

      if(defined $lasti) {
         my ($s,$len);
         $winbuf[$lasti]->key_act($event,$t);

         if($winbuf_fill>=$MAX_WIN_SIZ) {
            $winbuf_fill=$MAX_WIN_SIZ; # not really necessary

            ($s,$len)=get_str();
            print "CURRENT=$s\n" if DEBUG;
            if($len) {
               for(my $l=$MAX_WIN_SIZ;$l>=$MIN_WIN_SIZ;$l--) {
                  if($l<=$len) {
                     my $word=substr($s,0,$l);
#                     printf("CURRENT_%02d = $word\n",$l);
# lookup word in our current database
                     if(exists $db_words{$word}) {
                        my $tmp_word=creat_word($word);

                        print "$word is in the database,"
                           ."getting dynamics vector..." if DEBUG;

                        if(!(defined $db_words{$word})) {
                           $db_words{$word}=$tmp_word;
                        }
                        if(defined $tmp_word) {
                           print $tmp_word->get_str_vec;
#                           print CLASS $tmp_word->get_str_vec;
                           print "OK!\n" if DEBUG;
                        } else {
                           print "FAILED!\n" if DEBUG;
                        }
# first windows will have the same entry matching, kind of like
# filling a pipeline -- so those few can be ignored
                        last;
                     }
                  }
               }
            }
         }

      } else {
         if(DEBUG) {
            print STDERR "Could not find the pressed key to release it!\n";
         }
      }
   }


}
# ########################################################################

sub db_import {
   my $fname=shift;

   open(DB,"<$fname") or die "Could not open database \'$fname\'";

   while(my $entry=<DB>) {
      chomp($entry);
      if(DEBUG) {
         print "Importing \'$entry\' ..."
      }
      if(length($entry)<BUF_SIZ) {
         if(length($entry)>$MAX_WIN_SIZ) { $MAX_WIN_SIZ=length($entry); }
         if(length($entry)<$MIN_WIN_SIZ) { $MIN_WIN_SIZ=length($entry); }
         if(!(exists $db_words{$entry})) {
            $db_words{$entry}=undef;
            print "OK!\n" if DEBUG;
         }
      } else {
         my $e=substr($entry,0,$MAX_WIN_SIZ);
         print "FAILED: " if DEBUG;
         if(!(exists $db_words{$e})) {
            $db_words{$e}=undef;
            print "added substring \'$e\' instead!\n" if DEBUG;
         }
      }
   }
   if(DEBUG) {
      print "MAX=$MAX_WIN_SIZ\tMIN=$MIN_WIN_SIZ\n";
   }
   $winbuf_siz=$MAX_WIN_SIZ;
   close(DB);
}

sub db_print {
   print "Current entries in the database: \n";
   print "\n ".("-"x78)."\n";
   foreach my $key (keys %db_words) {
         print "$key\n";
   }
   print "\n ".("-"x78)."\n";
}

# ########################################################################

#foreach  my $key (%specialsyms) {
#   print "$key => $specialsyms{$key}\n";
#}

db_import($db_fname);
db_print() if DEBUG;

open(KL,"$keylog|") or die "Coud not open pipe to $keylog\n";
#open(CLASS,">>classifier") or die "Coud not open to write to classifier\n";

my $ln=0; #line number
while(my $line=<KL>) {
   $ln++;
   chomp($line);

   my @keylog_line=split(/\|/, $line);
   if($keylog_line[1] =~ m/Key/) {
      print "from keylogger=>$line\n" if DEBUG;
      my ($win,$event,$type,$kc,$ks,$ch,
            $screen,$rootXY,$root,$state,$t) = @keylog_line;

      if($win =~ /Window=(.*)/) { $win=$1; }
      if($ch =~ /char=(.*)/) { $ch=$1; }
      if($t =~ /time=(.*)]/) { $t=$1; }
      if($event =~ /Event=(.*)/) { $event=$1; }

      track_key($win,$ch,$t,$event);
      print_all();
   }

}

close(KL);
#close(CLASS);


